From 5ab9e060884677b957a5a04a9c8c910e3d1def6f Mon Sep 17 00:00:00 2001 From: Daniel Pacak Date: Mon, 8 Jul 2019 13:24:01 +0200 Subject: [PATCH] feat: Resolving API groups (#40) * test: Add integration test scenario for resolving API group * feat: Resolve resource API groups Kubernetes assigns resources to API grups. So you can have pods in the core group as well as pods in the metrics.k8s.io groups. With the changes introduced the plugin allows you to specify the fully qualified resoruce name at the command line, for example: $ kubect who-can get pods $ kubect who-can get pods.metrics.k8s.io Resolves: #35, #38 --- .github/main.workflow | 18 --- README.md | 9 +- go.mod | 10 +- go.sum | 181 ++++++++++++++++++++++++++++ pkg/cmd/access_checker.go | 1 + pkg/cmd/list.go | 10 +- pkg/cmd/list_test.go | 31 ++--- pkg/cmd/namespace_validator.go | 5 + pkg/cmd/policy_rule_matcher.go | 22 +++- pkg/cmd/policy_rule_matcher_test.go | 124 +++++++++++++++++-- pkg/cmd/resource_resolver.go | 112 +++++++++-------- pkg/cmd/resource_resolver_test.go | 141 ++++++++++++---------- test/integration_test.go | 106 ++++++++++++++-- 13 files changed, 590 insertions(+), 180 deletions(-) delete mode 100644 .github/main.workflow diff --git a/.github/main.workflow b/.github/main.workflow deleted file mode 100644 index c671aa1..0000000 --- a/.github/main.workflow +++ /dev/null @@ -1,18 +0,0 @@ -workflow "Release" { - on = "push" - resolves = ["goreleaser"] -} - -action "is-tag" { - uses = "actions/bin/filter@master" - args = "tag" -} - -action "goreleaser" { - uses = "docker://goreleaser/goreleaser" - secrets = [ - "GITHUB_TOKEN", - ] - args = "release" - needs = ["is-tag"] -} diff --git a/README.md b/README.md index 5072235..e98a471 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ # kubectl-who-can -Shows who has permissions to VERB [TYPE | TYPE/NAME | NONRESOURCEURL] in Kubernetes. +Shows which subjects have RBAC permissions to VERB [TYPE | TYPE/NAME | NONRESOURCEURL] in Kubernetes. -[![asciicast](https://asciinema.org/a/ccqqYwA5L5rMV9kd1tgzyZJ2j.svg)](https://asciinema.org/a/ccqqYwA5L5rMV9kd1tgzyZJ2j) +[![asciicast][asciicast-img]][asciicast] ## Installation @@ -31,7 +31,7 @@ Download a release distribution archive for your operating system, extract it, a executable to your `$PATH`. For example, to manually install `kubectl-who-can` on macOS run the following command: ``` -VERSION="v0.1.0-alpha.1" +VERSION=`git describe --abbrev=0` mkdir -p /tmp/who-can/$VERSION && \ curl -L https://github.com/aquasecurity/kubectl-who-can/releases/download/$VERSION/kubectl-who-can_darwin_x86_64.tar.gz \ @@ -83,3 +83,6 @@ The `kubectl-who-can` binary will be in `/usr/local/bin`. [license-img]: https://img.shields.io/github/license/aquasecurity/kubectl-who-can.svg [license]: https://github.com/aquasecurity/kubectl-who-can/blob/master/LICENSE + +[asciicast-img]: https://asciinema.org/a/ccqqYwA5L5rMV9kd1tgzyZJ2j.svg +[asciicast]: https://asciinema.org/a/ccqqYwA5L5rMV9kd1tgzyZJ2j \ No newline at end of file diff --git a/go.mod b/go.mod index 423525f..8ae664e 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,11 @@ go 1.12 require ( github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b - github.com/spf13/cobra v0.0.0-20180319062004-c439c4fa0937 - github.com/stretchr/objx v0.2.0 // indirect + github.com/spf13/cobra v0.0.4 github.com/stretchr/testify v1.3.0 - k8s.io/api v0.0.0-20190612125737-db0771252981 - k8s.io/apimachinery v0.0.0-20190612125636-6a5db36e93ad + k8s.io/api v0.0.0-20190703205437-39734b2a72fe + k8s.io/apiextensions-apiserver v0.0.0-20190704050600-357b4270afe4 + k8s.io/apimachinery v0.0.0-20190703205208-4cfb76a8bf76 k8s.io/cli-runtime v0.0.0-20190612131021-ced92c4c4749 - k8s.io/client-go v0.0.0-20190612125919-5c45477a8ae7 + k8s.io/client-go v0.0.0-20190704045512-07281898b0f0 ) diff --git a/go.sum b/go.sum index 348c4a3..228d6bd 100644 --- a/go.sum +++ b/go.sum @@ -1,40 +1,102 @@ cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-autorest v11.1.2+incompatible h1:viZ3tV5l4gE2Sw0xrasFHytCGtzYCrT+um/rrSQ1BfA= github.com/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4= github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/coreos/bbolt v1.3.1-coreos.6/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc v0.0.0-20180117170138-065b426bd416/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda h1:NyywMz59neOoVRFDz+ccfKWxn784fiHMDnZSy6T+JXY= github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633 h1:H2pdYOb3KQ1/YsqVWoWNLQO+fusocsw354rqGTZtAgw= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550 h1:mV9jbLoSW/8m4VK16ZkHTozJa8sesK5u5kTMFysTYac= github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.2.0+incompatible h1:fUDGZCv/7iAN7u0puUVhvKCcsR6vRfwrJatElLBEf0I= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4 h1:bRzFpEzvausOAt4va+I/22BZ1vXDtERngp0BNYDKej0= github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.19.0 h1:FTUMcX77w5rQkClIzDtTxvn6Bsa894CcrzNj2MMfeg8= github.com/go-openapi/jsonpointer v0.19.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2 h1:A9+F4Dc/MCNB5jibxf6rRvOvR/iFgQdyNx9eIhnGqq0= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.19.0 h1:BqWKpV1dFd+AuiKlgtddwVIFQsuMpxfBDBHGfM2yNpk= github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2 h1:o20suLFB4Ri0tuzpWtyHlh7E7HnkqTNLq6aR6WVNS1w= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.17.2 h1:eb2NbuCnoe8cWAxhtK6CfMWUYmiFEZJ9Hx3Z2WRwJ5M= github.com/go-openapi/spec v0.17.2/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2 h1:SStNd1jRcYtfKCN7R0laGNs80WYYvn5CbBjM2sOmCrE= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.17.2 h1:K/ycE/XTUDFltNHSO32cGRUhrVGJD64o8WgAIZNyc3k= github.com/go-openapi/swag v0.17.2/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2 h1:jvO6bCMBEilGwMfHhrd61zIID4oIFdwb76V17SM88dE= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415 h1:WSBJMqJbLxsn+bTCPyPYZfqHdJmc8MK4wrBjMft6BAM= github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.0.0 h1:2jyBKDKU/8v3v2xVR2PtiWQviFUyiaGk2rpfyFT8rTM= +github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -46,103 +108,222 @@ github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8 h1:L9JPKrtsHMQ4VCRQfHvbbHBfB2Urn8xf6QZeXZ+OrN4= github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= +github.com/gophercloud/gophercloud v0.1.0 h1:P/nh25+rzXouhytV2pUHBb65fnds26Ghl8/391+sT5o= +github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7 h1:6TSoaYExHper8PYsJu23GWVNOyYRCSnIFyxKgLSZ54w= github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v0.0.0-20170330212424-2500245aa611/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.3.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be h1:AHimNtVIpiBjPUhEF5KNCkrUyqTSA5zWUl8sQ2bfGBE= github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63 h1:nTT4s92Dgz2HlrB2NaMgvlfqHH39OgMhA7z3PK7PGD4= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3 h1:EooPXg51Tn+xmWPXJUGCnJhJSpeuMlBmfJVcqIRmmv8= github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/soheilhy/cmux v0.1.3/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.0-20180319062004-c439c4fa0937 h1:+ryWjMVzFAkEz5zT+Ms49aROZwxlJce3x3zLTFpkz3Y= github.com/spf13/cobra v0.0.0-20180319062004-c439c4fa0937/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.4 h1:S0tLZ3VOKl2Te0hpq8+ke0eSJPfCnNTPiDlsfwi1/NE= +github.com/spf13/cobra v0.0.4/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.uber.org/atomic v0.0.0-20181018215023-8dc6146f7569/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v0.0.0-20180814183419-67bc79d13d15/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20181025213731-e84da0312774 h1:a4tQYYYuK9QdeO/+kEvNYyuR21S+7ve5EANok6hABhI= golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 h1:1wopBVtVdWnn03fZelqdXTqk7U7zPQCb+T4rbU9ZEoU= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 h1:bfLnR+k0tq5Lqt6dflRLcZiz6UaXCMt3vhYJ1l4FQ80= golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA= golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313 h1:pczuHS43Cp2ktBEEmLwScxgjWsBSzdaQiKzUyf3DTTc= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f h1:25KHgbfyiSm6vwQLbM3zZIe1v9p/3ea4Rz+nnM5K/i4= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db h1:6/JqlYfC1CCaLnGceQTI+sDGhC9UBSPAsBqI0Gun6kU= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20161028155119-f51c12702a4d h1:TnM+PKb3ylGmZvyPXmo9m/wktg7Jn/a/fNmr33HSj8g= golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20170731182057-09f6ed296fc6/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.13.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o= gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= k8s.io/api v0.0.0-20190612125737-db0771252981 h1:DN1D/gMpl+h70Ek3Gb2ykCEI0QqIUtJ2e2z9PnAYz+Q= k8s.io/api v0.0.0-20190612125737-db0771252981/go.mod h1:SR4nMi8IQTDnEi4768MsMCoZ9DyfRls7wy+TbRrFicA= +k8s.io/api v0.0.0-20190703205437-39734b2a72fe h1:MFaHtAyhZcfBZocN91muHSqnwiF5yfXx7yGoehneNYg= +k8s.io/api v0.0.0-20190703205437-39734b2a72fe/go.mod h1:J5EZ0KSEjvyKOBy5BDHSF3zn82madLLWg7nUKaOHZKU= +k8s.io/apiextensions-apiserver v0.0.0-20190704050600-357b4270afe4 h1:cSi2zdwa7QZZvnQus4+p0FXSCui7h3GLBHbItcVk01I= +k8s.io/apiextensions-apiserver v0.0.0-20190704050600-357b4270afe4/go.mod h1:nNuYaBGfybrD9XpODH4ucdaoQot7/9061GJeW7/GF4s= k8s.io/apimachinery v0.0.0-20190612125636-6a5db36e93ad h1:x1lITOfDEbnzt8D1cZJsPbdnx/hnv28FxY2GKkxmxgU= k8s.io/apimachinery v0.0.0-20190612125636-6a5db36e93ad/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA= +k8s.io/apimachinery v0.0.0-20190703205208-4cfb76a8bf76 h1:vxMYBaJgczGAIpJAOBco2eHuFYIyDdNIebt60jxLauA= +k8s.io/apimachinery v0.0.0-20190703205208-4cfb76a8bf76/go.mod h1:M2fZgZL9DbLfeJaPBCDqSqNsdsmLN+V29knYJnIXlMA= +k8s.io/apiserver v0.0.0-20190704045957-760aa681203e/go.mod h1:6zvD17QU0+KIk2vCCIRewCPHdEj0Q1KLMOV6WtLkUFU= k8s.io/cli-runtime v0.0.0-20190612131021-ced92c4c4749 h1:b9JPq8kYVRk39BUB7C33RzAVQxbEd7rNSjKHHiZDhVo= k8s.io/cli-runtime v0.0.0-20190612131021-ced92c4c4749/go.mod h1:iRYCccaBcLKVeLqBZZ7GNr7odwdVUIH1m47lt68Pix8= k8s.io/client-go v0.0.0-20190612125919-5c45477a8ae7 h1:LjXh7ChUmcT8ilhmqZ0ZSPQc06zsP4+pqJkKbcQ+g0k= k8s.io/client-go v0.0.0-20190612125919-5c45477a8ae7/go.mod h1:ElCnOBWqvEffJopQHDgJf1jrf7j/f2rNbGv6uUkuHrU= +k8s.io/client-go v0.0.0-20190704045512-07281898b0f0 h1:8nV6JM7RrjI2VP21KEdMZxTCFuPQGXxU3+pUbEcCafQ= +k8s.io/client-go v0.0.0-20190704045512-07281898b0f0/go.mod h1:vn7Y34rpPc8EO7qSbsZ7JCxA3ujt/wnQozW3RfYdT/E= +k8s.io/code-generator v0.0.0-20190703204957-583809a49343/go.mod h1:gZ0mjdzfPz3P+cILxibAJ25xFoZ1Kf0xQrFUHy66AEs= +k8s.io/component-base v0.0.0-20190703210340-65d72cfeb85d/go.mod h1:lD66vYoxfIYwZTZunUYSg/e6fEj0fcgked2esAmsIfA= +k8s.io/gengo v0.0.0-20190116091435-f8a0810f38af/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.1 h1:RVgyDHY/kFKtLqh67NvEWIgkMneNoIrdkN0CxDSQc68= k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30 h1:TRb4wNWoBVrH9plmkp2q86FIDppkbrEXdXlxU3a3BMI= k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= k8s.io/utils v0.0.0-20190221042446-c2654d5206da h1:ElyM7RPonbKnQqOcw7dG2IK5uvQQn3b/WPHqD5mBvP4= k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0= +k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a h1:2jUDc9gJja832Ftp+QbDV0tVhQHMISFn01els+2ZAcw= +k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= +modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= +modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= +modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0= sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= +sigs.k8s.io/structured-merge-diff v0.0.0-20190302045857-e85c7b244fd2/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/pkg/cmd/access_checker.go b/pkg/cmd/access_checker.go index 2d53cb1..913b8a2 100644 --- a/pkg/cmd/access_checker.go +++ b/pkg/cmd/access_checker.go @@ -17,6 +17,7 @@ type accessChecker struct { client clientauthz.SelfSubjectAccessReviewInterface } +// NewAccessChecker constructs the default AccessChecker. func NewAccessChecker(client clientauthz.SelfSubjectAccessReviewInterface) AccessChecker { return &accessChecker{ client: client, diff --git a/pkg/cmd/list.go b/pkg/cmd/list.go index 9c1dbb5..cbf46ec 100644 --- a/pkg/cmd/list.go +++ b/pkg/cmd/list.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/spf13/cobra" core "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime/schema" clioptions "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/kubernetes" clientcore "k8s.io/client-go/kubernetes/typed/core/v1" @@ -24,7 +25,8 @@ const ( whoCanLong = `Shows which users, groups and service accounts can perform a given verb on a given resource type. VERB is a logical Kubernetes API verb like 'get', 'list', 'watch', 'delete', etc. -TYPE is a Kubernetes resource. Shortcuts, such as 'pod' or 'po' will be resolved. NAME is the name of a particular Kubernetes resource. +TYPE is a Kubernetes resource. Shortcuts and API groups will be resolved, e.g. 'po' or 'pods.metrics.k8s.io'. +NAME is the name of a particular Kubernetes resource. NONRESOURCEURL is a partial URL that starts with "/".` whoCanExample = ` # List who can get pods in any namespace kubectl who-can get pods --all-namespaces @@ -32,6 +34,9 @@ NONRESOURCEURL is a partial URL that starts with "/".` # List who can create pods in the current namespace kubectl who-can create pods + # List who can get pods specifying the API group + kubectl who-can get pods.metrics.k8s.io + # List who can create services in namespace "foo" kubectl who-can create services -n foo @@ -63,6 +68,7 @@ type Action struct { nonResourceURL string subResource string resourceName string + gr schema.GroupResource namespace string allNamespaces bool @@ -186,7 +192,7 @@ func (w *whoCan) Complete(args []string) error { } if w.resource != "" { - w.resource, err = w.resourceResolver.Resolve(w.verb, w.resource, w.subResource) + w.gr, err = w.resourceResolver.Resolve(w.verb, w.resource, w.subResource) if err != nil { return fmt.Errorf("resolving resource: %v", err) } diff --git a/pkg/cmd/list_test.go b/pkg/cmd/list_test.go index b944598..23944e9 100644 --- a/pkg/cmd/list_test.go +++ b/pkg/cmd/list_test.go @@ -9,6 +9,7 @@ import ( core "k8s.io/api/core/v1" meta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" clioptions "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/kubernetes/fake" clientTesting "k8s.io/client-go/testing" @@ -40,9 +41,9 @@ type resourceResolverMock struct { mock.Mock } -func (r *resourceResolverMock) Resolve(verb, resource, subResource string) (string, error) { +func (r *resourceResolverMock) Resolve(verb, resource, subResource string) (schema.GroupResource, error) { args := r.Called(verb, resource, subResource) - return args.String(0), args.Error(1) + return args.Get(0).(schema.GroupResource), args.Error(1) } type clientConfigMock struct { @@ -91,8 +92,8 @@ func TestComplete(t *testing.T) { resource string subResource string - result string - err error + gr schema.GroupResource + err error } type expected struct { @@ -106,20 +107,20 @@ func TestComplete(t *testing.T) { data := []struct { scenario string - *currentContext + currentContext *currentContext - flags flags - args []string - *resolution + flags flags + args []string + resolution *resolution - expected + expected expected }{ { scenario: "A", currentContext: ¤tContext{namespace: "foo"}, flags: flags{namespace: "", allNamespaces: false}, args: []string{"list", "pods"}, - resolution: &resolution{verb: "list", resource: "pods", result: "pods"}, + resolution: &resolution{verb: "list", resource: "pods", gr: schema.GroupResource{Resource: "pods"}}, expected: expected{ namespace: "foo", verb: "list", @@ -132,7 +133,7 @@ func TestComplete(t *testing.T) { currentContext: ¤tContext{err: errors.New("cannot open context")}, flags: flags{namespace: "", allNamespaces: false}, args: []string{"list", "pods"}, - resolution: &resolution{verb: "list", resource: "pods", result: "pods"}, + resolution: &resolution{verb: "list", resource: "pods", gr: schema.GroupResource{Resource: "pods"}}, expected: expected{ namespace: "", verb: "list", @@ -145,7 +146,7 @@ func TestComplete(t *testing.T) { scenario: "C", flags: flags{namespace: "", allNamespaces: true}, args: []string{"get", "service/mongodb"}, - resolution: &resolution{verb: "get", resource: "service", result: "services"}, + resolution: &resolution{verb: "get", resource: "service", gr: schema.GroupResource{Resource: "services"}}, expected: expected{ namespace: core.NamespaceAll, verb: "get", @@ -157,7 +158,7 @@ func TestComplete(t *testing.T) { scenario: "D", flags: flags{namespace: "bar", allNamespaces: false}, args: []string{"delete", "pv"}, - resolution: &resolution{verb: "delete", resource: "pv", result: "persistentvolumes"}, + resolution: &resolution{verb: "delete", resource: "pv", gr: schema.GroupResource{Resource: "persistentvolumes"}}, expected: expected{ namespace: "bar", verb: "delete", @@ -211,7 +212,7 @@ func TestComplete(t *testing.T) { if tt.resolution != nil { resourceResolver.On("Resolve", tt.resolution.verb, tt.resolution.resource, tt.resolution.subResource). - Return(tt.resolution.result, tt.resolution.err) + Return(tt.resolution.gr, tt.resolution.err) } if tt.currentContext != nil { clientConfig.On("Namespace").Return(tt.currentContext.namespace, false, tt.currentContext.err) @@ -239,7 +240,7 @@ func TestComplete(t *testing.T) { assert.Equal(t, tt.expected.err, err) assert.Equal(t, tt.expected.namespace, o.namespace) assert.Equal(t, tt.expected.verb, o.verb) - assert.Equal(t, tt.expected.resource, o.resource) + assert.Equal(t, tt.expected.resource, o.gr.Resource) assert.Equal(t, tt.expected.resourceName, o.resourceName) clientConfig.AssertExpectations(t) diff --git a/pkg/cmd/namespace_validator.go b/pkg/cmd/namespace_validator.go index d5b9ec3..c7cfbee 100644 --- a/pkg/cmd/namespace_validator.go +++ b/pkg/cmd/namespace_validator.go @@ -8,6 +8,10 @@ import ( clientcore "k8s.io/client-go/kubernetes/typed/core/v1" ) +// NamespaceValidator wraps the Validate method. +// +// Validate checks whether the given namespace exists or not. +// Returns nil if it exists, an error otherwise. type NamespaceValidator interface { Validate(name string) error } @@ -16,6 +20,7 @@ type namespaceValidator struct { client clientcore.NamespaceInterface } +// NewNamespaceValidator constructs the default NamespaceValidator. func NewNamespaceValidator(client clientcore.NamespaceInterface) NamespaceValidator { return &namespaceValidator{ client: client, diff --git a/pkg/cmd/policy_rule_matcher.go b/pkg/cmd/policy_rule_matcher.go index d406611..d0c8515 100644 --- a/pkg/cmd/policy_rule_matcher.go +++ b/pkg/cmd/policy_rule_matcher.go @@ -8,7 +8,8 @@ import ( // PolicyRuleMatcher wraps the Matches* methods. // // MatchesRole returns `true` if any PolicyRule defined by the given Role matches the specified Action, `false` otherwise. -// MatchesClusterRole returns `true` if any PolicyRule defined by the given ClusterRole matches the specified Action, `false` otherwise. +// +// MatchesClusterRole returns `true` if any PolicyRule defined by the given ClusterRole matches the specified Action, `false` otherwise. type PolicyRuleMatcher interface { MatchesRole(role rbac.Role, action Action) bool MatchesClusterRole(role rbac.ClusterRole, action Action) bool @@ -17,7 +18,7 @@ type PolicyRuleMatcher interface { type matcher struct { } -// NewPolicyRuleMatcher constructs a PolicyRuleMatcher. +// NewPolicyRuleMatcher constructs the default PolicyRuleMatcher. func NewPolicyRuleMatcher() PolicyRuleMatcher { return &matcher{} } @@ -54,11 +55,26 @@ func (m *matcher) matches(rule rbac.PolicyRule, action Action) bool { m.matchesNonResourceURL(rule, action.nonResourceURL) } + resource := action.gr.Resource + if action.subResource != "" { + resource += "/" + action.subResource + } + return m.matchesVerb(rule, action.verb) && - m.matchesResource(rule, action.resource) && + m.matchesResource(rule, resource) && + m.matchesAPIGroup(rule, action.gr.Group) && m.matchesResourceName(rule, action.resourceName) } +func (m *matcher) matchesAPIGroup(rule rbac.PolicyRule, actionGroup string) bool { + for _, group := range rule.APIGroups { + if group == rbac.APIGroupAll || group == actionGroup { + return true + } + } + return false +} + func (m *matcher) matchesVerb(rule rbac.PolicyRule, actionVerb string) bool { for _, verb := range rule.Verbs { if verb == rbac.VerbAll || verb == actionVerb { diff --git a/pkg/cmd/policy_rule_matcher_test.go b/pkg/cmd/policy_rule_matcher_test.go index 9d25018..485b2fc 100644 --- a/pkg/cmd/policy_rule_matcher_test.go +++ b/pkg/cmd/policy_rule_matcher_test.go @@ -4,6 +4,7 @@ import ( "github.com/stretchr/testify/assert" rbac "k8s.io/api/rbac/v1" meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "testing" ) @@ -15,15 +16,23 @@ func TestMatcher_MatchesRole(t *testing.T) { Rules: []rbac.PolicyRule{ { Verbs: []string{"get", "list"}, + APIGroups: []string{""}, Resources: []string{"services"}, }, { Verbs: []string{"get", "list"}, - Resources: []string{"endpoints"}, + APIGroups: []string{"extensions"}, + Resources: []string{"deployments"}, }, }, } - action := Action{verb: "list", resource: "endpoints"} + action := Action{ + verb: "list", + gr: schema.GroupResource{ + Group: "extensions", + Resource: "deployments", + }, + } // then assert.True(t, matcher.MatchesRole(role, action)) @@ -37,21 +46,32 @@ func TestMatcher_MatchesClusterRole(t *testing.T) { Rules: []rbac.PolicyRule{ { Verbs: []string{"update", "patch", "delete"}, + APIGroups: []string{""}, Resources: []string{"deployments"}, }, { Verbs: []string{"update"}, + APIGroups: []string{"extensions"}, Resources: []string{"deployments/scale"}, }, }, } - action := Action{verb: "update", resource: "deployments/scale"} + action := Action{ + verb: "update", + subResource: "scale", + gr: schema.GroupResource{ + Group: "extensions", + Resource: "deployments", + }, + } // then assert.True(t, matcher.MatchesClusterRole(role, action)) } func TestMatcher_matches(t *testing.T) { + servicesGR := schema.GroupResource{Resource: "services"} + data := []struct { scenario string @@ -62,45 +82,67 @@ func TestMatcher_matches(t *testing.T) { }{ { scenario: "A", - action: Action{verb: "get", resource: "services", resourceName: ""}, + action: Action{ + verb: "get", + gr: servicesGR, + }, rule: rbac.PolicyRule{ Verbs: []string{"get", "list"}, + APIGroups: []string{""}, Resources: []string{"services"}, }, matches: true, }, { scenario: "B", - action: Action{verb: "get", resource: "services", resourceName: ""}, + action: Action{ + verb: "get", + gr: servicesGR, + }, rule: rbac.PolicyRule{ Verbs: []string{"get", "list"}, + APIGroups: []string{""}, Resources: []string{"*"}, }, matches: true, }, { scenario: "C", - action: Action{verb: "get", resource: "services", resourceName: ""}, + action: Action{ + verb: "get", + gr: servicesGR, + }, rule: rbac.PolicyRule{ - Verbs: []string{"*"}, + Verbs: []string{rbac.VerbAll}, + APIGroups: []string{""}, Resources: []string{"services"}, }, matches: true, }, { scenario: "D", - action: Action{verb: "get", resource: "services", resourceName: "mongodb"}, + action: Action{ + verb: "get", + resourceName: "mongodb", + gr: servicesGR, + }, rule: rbac.PolicyRule{ Verbs: []string{"get", "list"}, + APIGroups: []string{""}, Resources: []string{"services"}, }, matches: true, }, { scenario: "E", - action: Action{verb: "get", resource: "services", resourceName: "mongodb"}, + action: Action{ + verb: "get", + resourceName: "mongodb", + gr: servicesGR, + }, rule: rbac.PolicyRule{ Verbs: []string{"get", "list"}, + APIGroups: []string{""}, Resources: []string{"services"}, ResourceNames: []string{"mongodb", "nginx"}, }, @@ -108,9 +150,14 @@ func TestMatcher_matches(t *testing.T) { }, { scenario: "F", - action: Action{verb: "get", resource: "services", resourceName: "mongodb"}, + action: Action{ + verb: "get", + resourceName: "mongodb", + gr: servicesGR, + }, rule: rbac.PolicyRule{ Verbs: []string{"get", "list"}, + APIGroups: []string{""}, Resources: []string{"services"}, ResourceNames: []string{"nginx"}, }, @@ -118,9 +165,13 @@ func TestMatcher_matches(t *testing.T) { }, { scenario: "G", - action: Action{verb: "get", resource: "services", resourceName: ""}, + action: Action{ + verb: "get", + gr: servicesGR, + }, rule: rbac.PolicyRule{ Verbs: []string{"get", "list"}, + APIGroups: []string{""}, Resources: []string{"services"}, ResourceNames: []string{"nginx"}, }, @@ -128,18 +179,26 @@ func TestMatcher_matches(t *testing.T) { }, { scenario: "H", - action: Action{verb: "get", resource: "pods", resourceName: ""}, + action: Action{ + verb: "get", + gr: schema.GroupResource{Resource: "pods"}, + }, rule: rbac.PolicyRule{ Verbs: []string{"create"}, + APIGroups: []string{""}, Resources: []string{"pods"}, }, matches: false, }, { scenario: "I", - action: Action{verb: "get", resource: "persistentvolumes", resourceName: ""}, + action: Action{ + verb: "get", + gr: schema.GroupResource{Resource: "persistentvolumes"}, + }, rule: rbac.PolicyRule{ Verbs: []string{"get"}, + APIGroups: []string{""}, Resources: []string{"pods"}, }, matches: false, @@ -171,6 +230,45 @@ func TestMatcher_matches(t *testing.T) { }, matches: false, }, + { + scenario: "Should return true when PolicyRule's APIGroup matches resolved resource's group", + action: Action{ + verb: "get", + gr: schema.GroupResource{Resource: "deployments", Group: "extensions"}, + }, + rule: rbac.PolicyRule{ + Verbs: []string{"get"}, + APIGroups: []string{"extensions"}, + Resources: []string{"deployments"}, + }, + matches: true, + }, + { + scenario: "Should return true when PolicyRule's APIGroup matches all ('*') resource groups", + action: Action{ + verb: "get", + gr: schema.GroupResource{Resource: "pods", Group: "metrics.k8s.io"}, + }, + rule: rbac.PolicyRule{ + Verbs: []string{"get"}, + APIGroups: []string{"*"}, + Resources: []string{"pods"}, + }, + matches: true, + }, + { + scenario: "Should return false when PolicyRule's APIGroup doesn't match resolved resource's Group", + action: Action{ + verb: "get", + gr: schema.GroupResource{Resource: "pods", Group: "metrics.k8s.io"}, + }, + rule: rbac.PolicyRule{ + Verbs: []string{"get"}, + APIGroups: []string{""}, + Resources: []string{"pods"}, + }, + matches: false, + }, } // given diff --git a/pkg/cmd/resource_resolver.go b/pkg/cmd/resource_resolver.go index c9d48e6..c80c6df 100644 --- a/pkg/cmd/resource_resolver.go +++ b/pkg/cmd/resource_resolver.go @@ -7,15 +7,15 @@ import ( apismeta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/discovery" + "strings" ) // ResourceResolver wraps the Resolve method. // -// Resolve attempts to resolve an APIResource's Name by `resource` and `subResource`. -// It then validates that the specified `verb` is supported. -// The returned APIResource's Name may represent a resource (e.g. `pods`) or a sub-resource (e.g. `pods/log`). +// Resolve attempts to resolve a GroupResource by `resource` and `subResource`. +// It also validates that the specified `verb` is supported by the resolved resource. type ResourceResolver interface { - Resolve(verb, resource, subResource string) (string, error) + Resolve(verb, resource, subResource string) (schema.GroupResource, error) } type resourceResolver struct { @@ -23,6 +23,7 @@ type resourceResolver struct { mapper meta.RESTMapper } +// NewResourceResolver constructs the default ResourceResolver. func NewResourceResolver(client discovery.DiscoveryInterface, mapper meta.RESTMapper) ResourceResolver { return &resourceResolver{ client: client, @@ -30,33 +31,62 @@ func NewResourceResolver(client discovery.DiscoveryInterface, mapper meta.RESTMa } } -func (rv *resourceResolver) Resolve(verb, resource, subResource string) (string, error) { +func (rv *resourceResolver) Resolve(verb, resource, subResource string) (schema.GroupResource, error) { if resource == rbac.ResourceAll { - return resource, nil + return schema.GroupResource{Resource: resource}, nil } - apiResource, err := rv.resourceFor(resource, subResource) + + name := resource + if subResource != "" { + name = name + "/" + subResource + } + + gvr, err := rv.resolveGVR(resource) if err != nil { - name := resource - if subResource != "" { - name = name + "/" + subResource - } - return "", fmt.Errorf("the server doesn't have a resource type \"%s\"", name) + return schema.GroupResource{}, fmt.Errorf("the server doesn't have a resource type \"%s\"", name) + } + + apiResource, err := rv.resolveAPIResource(gvr, subResource) + if err != nil { + return schema.GroupResource{}, fmt.Errorf("the server doesn't have a resource type \"%s\"", name) } if !rv.isVerbSupportedBy(verb, apiResource) { - return "", fmt.Errorf("the \"%s\" resource does not support the \"%s\" verb, only %v", apiResource.Name, verb, apiResource.Verbs) + return schema.GroupResource{}, fmt.Errorf("the \"%s\" resource does not support the \"%s\" verb, only %v", apiResource.Name, verb, apiResource.Verbs) + } + + return gvr.GroupResource(), nil +} + +func (rv *resourceResolver) resolveGVR(resource string) (schema.GroupVersionResource, error) { + if resource == rbac.ResourceAll { + return schema.GroupVersionResource{Resource: resource}, nil + } + + fullySpecifiedGVR, groupResource := schema.ParseResourceArg(strings.ToLower(resource)) + gvr := schema.GroupVersionResource{} + if fullySpecifiedGVR != nil { + gvr, _ = rv.mapper.ResourceFor(*fullySpecifiedGVR) } - return apiResource.Name, nil + if gvr.Empty() { + var err error + gvr, err = rv.mapper.ResourceFor(groupResource.WithVersion("")) + if err != nil { + return schema.GroupVersionResource{}, err + } + } + + return gvr, nil } -func (rv *resourceResolver) resourceFor(resourceArg, subResource string) (apismeta.APIResource, error) { - index, err := rv.indexResources() +func (rv *resourceResolver) resolveAPIResource(gvr schema.GroupVersionResource, subResource string) (apismeta.APIResource, error) { + index, err := rv.indexResources(gvr) if err != nil { return apismeta.APIResource{}, err } - apiResource, err := rv.lookupResource(index, resourceArg) + apiResource, err := rv.lookupResource(index, gvr.Resource) if err != nil { return apismeta.APIResource{}, err } @@ -66,25 +96,16 @@ func (rv *resourceResolver) resourceFor(resourceArg, subResource string) (apisme if err != nil { return apismeta.APIResource{}, err } - return apiResource, nil } return apiResource, nil } func (rv *resourceResolver) lookupResource(index map[string]apismeta.APIResource, resourceArg string) (apismeta.APIResource, error) { - resource, ok := index[resourceArg] + apiResource, ok := index[resourceArg] if ok { - return resource, nil + return apiResource, nil } - gvr, err := rv.mapper.ResourceFor(schema.GroupVersionResource{Resource: resourceArg}) - if err != nil { - return apismeta.APIResource{}, err - } - resource, ok = index[gvr.Resource] - if ok { - return resource, nil - } return apismeta.APIResource{}, fmt.Errorf("not found \"%s\"", resourceArg) } @@ -96,36 +117,21 @@ func (rv *resourceResolver) lookupSubResource(index map[string]apismeta.APIResou return apiResource, nil } -// indexResources builds a lookup index for APIResources where the keys are resources names (both plural and short names). -func (rv *resourceResolver) indexResources() (map[string]apismeta.APIResource, error) { - serverResources := make(map[string]apismeta.APIResource) +// indexResources builds a lookup index for APIResources where the keys are plural resources names. +// NB A subresource is also represented by APIResource and the corresponding key is /, +// for example, `pods/log` or `deployments/scale`. +func (rv *resourceResolver) indexResources(gvr schema.GroupVersionResource) (map[string]apismeta.APIResource, error) { + index := make(map[string]apismeta.APIResource) - serverGroups, err := rv.client.ServerGroups() + resourceList, err := rv.client.ServerResourcesForGroupVersion(gvr.GroupVersion().String()) if err != nil { return nil, fmt.Errorf("getting API groups: %v", err) } - for _, sg := range serverGroups.Groups { - for _, version := range sg.Versions { - // Consider only preferred versions - if version.GroupVersion != sg.PreferredVersion.GroupVersion { - continue - } - rsList, err := rv.client.ServerResourcesForGroupVersion(version.GroupVersion) - if err != nil { - return nil, fmt.Errorf("getting resources for API group: %v", err) - } - - for _, res := range rsList.APIResources { - serverResources[res.Name] = res - if len(res.ShortNames) > 0 { - for _, sn := range res.ShortNames { - serverResources[sn] = res - } - } - } - } + for _, res := range resourceList.APIResources { + index[res.Name] = res } - return serverResources, nil + + return index, nil } // isVerbSupportedBy returns `true` if the given verb is supported by the given resource, `false` otherwise. diff --git a/pkg/cmd/resource_resolver_test.go b/pkg/cmd/resource_resolver_test.go index fea37fe..56d31fa 100644 --- a/pkg/cmd/resource_resolver_test.go +++ b/pkg/cmd/resource_resolver_test.go @@ -4,6 +4,7 @@ import ( "errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + rbac "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/meta" apismeta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -23,125 +24,143 @@ func (mm *mapperMock) ResourceFor(resource schema.GroupVersionResource) (schema. func TestResourceResolver_Resolve(t *testing.T) { + podsGVR := schema.GroupVersionResource{Version: "v1", Resource: "pods"} + podsGR := schema.GroupResource{Resource: "pods"} + deploymentsGVR := schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "deployments"} + deploymentsGR := schema.GroupResource{Group: "extensions", Resource: "deployments"} + client := fake.NewSimpleClientset() client.Resources = []*apismeta.APIResourceList{ { GroupVersion: "v1", APIResources: []apismeta.APIResource{ - {Version: "v1", Name: "pods", ShortNames: []string{"po"}, Verbs: []string{"list", "create", "delete"}}, - {Version: "v1", Name: "pods/log", ShortNames: []string{}, Verbs: []string{"get"}}, - {Version: "v1", Name: "services", ShortNames: []string{"svc"}, Verbs: []string{"list", "delete"}}, + {Group: "", Version: "v1", Name: "pods", ShortNames: []string{"po"}, Verbs: []string{"list", "create", "delete"}}, + {Group: "", Version: "v1", Name: "pods/log", ShortNames: []string{}, Verbs: []string{"get"}}, + {Group: "", Version: "v1", Name: "services", ShortNames: []string{"svc"}, Verbs: []string{"list", "delete"}}, + }, + }, + { + GroupVersion: "extensions/v1beta1", + APIResources: []apismeta.APIResource{ + {Group: "extensions", Version: "v1beta1", Name: "deployments", Verbs: []string{"list", "get"}}, + {Group: "extensions", Version: "v1beta1", Name: "deployments/scale", Verbs: []string{"update", "patch"}}, }, }, - } - - type given struct { - verb string - resource string - subResource string } type mappingResult struct { - out string - err error + argGVR schema.GroupVersionResource + + returnGVR schema.GroupVersionResource + returnError error } type expected struct { - resource string - err error + gr schema.GroupResource + err error } data := []struct { - scenario string - given - *mappingResult + scenario string + action Action + mappingResult *mappingResult expected }{ { scenario: "A", - given: given{verb: "list", resource: "pods"}, - expected: expected{resource: "pods"}, + action: Action{verb: "list", resource: "pods"}, + mappingResult: &mappingResult{ + argGVR: schema.GroupVersionResource{Resource: "pods"}, + returnGVR: podsGVR, + }, + expected: expected{gr: podsGR}, }, { scenario: "B", - given: given{verb: "list", resource: "po"}, - expected: expected{resource: "pods"}, + action: Action{verb: "list", resource: "po"}, + mappingResult: &mappingResult{ + argGVR: schema.GroupVersionResource{Resource: "po"}, + returnGVR: podsGVR, + }, + expected: expected{gr: podsGR}, }, { scenario: "C", - given: given{verb: "eat", resource: "pods"}, + action: Action{verb: "eat", resource: "pods"}, + mappingResult: &mappingResult{ + argGVR: schema.GroupVersionResource{Resource: "pods"}, + returnGVR: podsGVR, + }, expected: expected{err: errors.New("the \"pods\" resource does not support the \"eat\" verb, only [list create delete]")}, }, { scenario: "D", - given: given{verb: "list", resource: "services"}, - expected: expected{resource: "services"}, + action: Action{verb: "list", resource: "deployments.extensions"}, + mappingResult: &mappingResult{ + argGVR: schema.GroupVersionResource{Group: "extensions", Version: "", Resource: "deployments"}, + returnGVR: deploymentsGVR, + }, + expected: expected{gr: deploymentsGR}, }, { scenario: "E", - given: given{verb: "list", resource: "svc"}, - expected: expected{resource: "services"}, + action: Action{verb: "get", resource: "pods", subResource: "log"}, + mappingResult: &mappingResult{ + argGVR: schema.GroupVersionResource{Resource: "pods"}, + returnGVR: podsGVR, + }, + expected: expected{gr: podsGR}, }, { scenario: "F", - given: given{verb: "mow", resource: "services"}, - expected: expected{err: errors.New("the \"services\" resource does not support the \"mow\" verb, only [list delete]")}, + action: Action{verb: "get", resource: "pods", subResource: "logz"}, + mappingResult: &mappingResult{ + argGVR: schema.GroupVersionResource{Resource: "pods"}, + returnGVR: podsGVR, + }, + expected: expected{err: errors.New("the server doesn't have a resource type \"pods/logz\"")}, }, { scenario: "G", - given: given{verb: "get", resource: "pods", subResource: "log"}, - expected: expected{resource: "pods/log"}, + action: Action{verb: "list", resource: "bees"}, + mappingResult: &mappingResult{ + argGVR: schema.GroupVersionResource{Resource: "bees"}, + returnError: errors.New("mapping failed"), + }, + expected: expected{err: errors.New("the server doesn't have a resource type \"bees\"")}, }, { scenario: "H", - given: given{verb: "get", resource: "pods", subResource: "logz"}, - expected: expected{err: errors.New("the server doesn't have a resource type \"pods/logz\"")}, - }, - { - scenario: "I", - given: given{verb: "list", resource: "pod"}, - mappingResult: &mappingResult{out: "pods"}, - expected: expected{resource: "pods"}, - }, - { - scenario: "J", - given: given{verb: "get", resource: "pod", subResource: "log"}, - mappingResult: &mappingResult{out: "pods"}, - expected: expected{resource: "pods/log"}, - }, - { - scenario: "K", - given: given{verb: "list", resource: "pod"}, - mappingResult: &mappingResult{err: errors.New("mapping failed")}, - expected: expected{err: errors.New("the server doesn't have a resource type \"pod\"")}, - }, - { - scenario: "L", - given: given{verb: "*", resource: "pods"}, - expected: expected{resource: "pods"}, + action: Action{verb: rbac.VerbAll, resource: "pods"}, + mappingResult: &mappingResult{ + argGVR: schema.GroupVersionResource{Resource: "pods"}, + returnGVR: podsGVR, + }, + expected: expected{gr: podsGR}, }, { - scenario: "M", - given: given{verb: "list", resource: "*"}, - expected: expected{resource: "*"}, + scenario: "I", + action: Action{verb: "list", resource: rbac.ResourceAll}, + expected: expected{gr: schema.GroupResource{Resource: rbac.ResourceAll}}, }, } for _, tt := range data { t.Run(tt.scenario, func(t *testing.T) { mapper := new(mapperMock) + if tt.mappingResult != nil { - mapper.On("ResourceFor", schema.GroupVersionResource{Resource: tt.given.resource}). - Return(schema.GroupVersionResource{Resource: tt.mappingResult.out}, tt.mappingResult.err) + mapper.On("ResourceFor", tt.mappingResult.argGVR). + Return(tt.mappingResult.returnGVR, tt.mappingResult.returnError) } resolver := NewResourceResolver(client.Discovery(), mapper) - resource, err := resolver.Resolve(tt.given.verb, tt.given.resource, tt.given.subResource) + resource, err := resolver.Resolve(tt.action.verb, tt.action.resource, tt.action.subResource) assert.Equal(t, tt.expected.err, err) - assert.Equal(t, tt.expected.resource, resource) + assert.Equal(t, tt.expected.gr, resource) mapper.AssertExpectations(t) }) diff --git a/test/integration_test.go b/test/integration_test.go index e174b3c..9ded935 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -7,9 +7,11 @@ import ( "github.com/stretchr/testify/require" core "k8s.io/api/core/v1" rbac "k8s.io/api/rbac/v1" + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + clientext "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1" meta "k8s.io/apimachinery/pkg/apis/meta/v1" clioptions "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/client-go/kubernetes" + client "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" "os" "strings" @@ -28,10 +30,14 @@ func TestIntegration(t *testing.T) { config, err := clientcmd.BuildConfigFromFlags("", os.Getenv("KUBECONFIG")) require.NoError(t, err) - kubeClient, err := kubernetes.NewForConfig(config) + coreClient, err := client.NewForConfig(config) require.NoError(t, err) - configureRBAC(t, kubeClient) + extClient, err := clientext.NewForConfig(config) + require.NoError(t, err) + + createCRDs(t, extClient.CustomResourceDefinitions()) + configureRBAC(t, coreClient) data := []struct { scenario string @@ -68,6 +74,20 @@ func TestIntegration(t *testing.T) { "devops-can-scale-workloads default devops Group", }, }, + { + scenario: "Should print who can get pod named `pod-xyz` in the namespace `foo`", + args: []string{"get", "pods/pod-xyz", "--namespace=foo"}, + output: []string{ + "batman-can-view-pod-xyz foo Batman User", + }, + }, + { + scenario: "Should print who can list pods in group `metrics.k8s.io`", + args: []string{"list", "pods.metrics.k8s.io"}, + output: []string{ + "spiderman-can-view-pod-metrics Spiderman User", + }, + }, } for _, tt := range data { t.Run(tt.scenario, func(t *testing.T) { @@ -101,10 +121,37 @@ func prettyPrintWhoCanOutput(t *testing.T, args []string, out *bytes.Buffer) { } } -func configureRBAC(t *testing.T, client kubernetes.Interface) { +func createCRDs(t *testing.T, client clientext.CustomResourceDefinitionInterface) { + t.Helper() + _, err := client.Create(&apiext.CustomResourceDefinition{ + ObjectMeta: meta.ObjectMeta{ + Name: "pods.metrics.k8s.io", + }, + Spec: apiext.CustomResourceDefinitionSpec{ + Scope: apiext.NamespaceScoped, + Group: "metrics.k8s.io", + Versions: []apiext.CustomResourceDefinitionVersion{ + { + Name: "v1beta1", + Served: true, + Storage: true, + }, + }, + Names: apiext.CustomResourceDefinitionNames{ + Kind: "PodMetrics", + Singular: "pod", + Plural: "pods", + ShortNames: []string{"po"}, + }, + }, + }) + require.NoError(t, err) +} + +func configureRBAC(t *testing.T, coreClient client.Interface) { t.Helper() - clientRBAC := client.RbacV1() + clientRBAC := coreClient.RbacV1() const namespaceFoo = "foo" @@ -132,6 +179,17 @@ func configureRBAC(t *testing.T, client kubernetes.Interface) { }) require.NoError(t, err) + _, err = clientRBAC.ClusterRoles().Create(&rbac.ClusterRole{ + ObjectMeta: meta.ObjectMeta{Name: "view-pod-metrics"}, + Rules: []rbac.PolicyRule{ + { + Verbs: []string{"get", "list"}, + APIGroups: []string{"metrics.k8s.io"}, + Resources: []string{"pods"}, + }, + }, + }) + _, err = clientRBAC.ClusterRoleBindings().Create(&rbac.ClusterRoleBinding{ ObjectMeta: meta.ObjectMeta{Name: "bob-can-get-logs"}, RoleRef: rbac.RoleRef{ @@ -144,6 +202,17 @@ func configureRBAC(t *testing.T, client kubernetes.Interface) { }) require.NoError(t, err) + _, err = clientRBAC.ClusterRoleBindings().Create(&rbac.ClusterRoleBinding{ + ObjectMeta: meta.ObjectMeta{Name: "spiderman-can-view-pod-metrics"}, + RoleRef: rbac.RoleRef{ + Name: "view-pod-metrics", + Kind: cmd.ClusterRoleKind, + }, + Subjects: []rbac.Subject{ + {Kind: rbac.UserKind, Name: "Spiderman"}, + }, + }) + // Configure default namespace _, err = clientRBAC.Roles(core.NamespaceDefault).Create(&rbac.Role{ ObjectMeta: meta.ObjectMeta{Name: "create-configmaps"}, @@ -185,7 +254,7 @@ func configureRBAC(t *testing.T, client kubernetes.Interface) { ObjectMeta: meta.ObjectMeta{Name: "scale-workloads"}, Rules: []rbac.PolicyRule{ { - APIGroups: []string{""}, + APIGroups: []string{"extensions"}, Verbs: []string{"update"}, Resources: []string{"deployments/scale"}, }, @@ -204,7 +273,7 @@ func configureRBAC(t *testing.T, client kubernetes.Interface) { }) // Configure foo namespace - _, err = client.CoreV1().Namespaces().Create(&core.Namespace{ + _, err = coreClient.CoreV1().Namespaces().Create(&core.Namespace{ ObjectMeta: meta.ObjectMeta{Name: namespaceFoo}, }) require.NoError(t, err) @@ -226,6 +295,18 @@ func configureRBAC(t *testing.T, client kubernetes.Interface) { }) require.NoError(t, err) + _, err = clientRBAC.Roles(namespaceFoo).Create(&rbac.Role{ + ObjectMeta: meta.ObjectMeta{Name: "view-pod-xyz"}, + Rules: []rbac.PolicyRule{ + { + APIGroups: []string{""}, + Verbs: []string{"get"}, + Resources: []string{"pods"}, + ResourceNames: []string{"pod-xyz"}, + }, + }, + }) + _, err = clientRBAC.RoleBindings(namespaceFoo).Create(&rbac.RoleBinding{ ObjectMeta: meta.ObjectMeta{Name: "operator-can-view-services"}, RoleRef: rbac.RoleRef{ @@ -237,4 +318,15 @@ func configureRBAC(t *testing.T, client kubernetes.Interface) { }, }) + _, err = clientRBAC.RoleBindings(namespaceFoo).Create(&rbac.RoleBinding{ + ObjectMeta: meta.ObjectMeta{Name: "batman-can-view-pod-xyz"}, + RoleRef: rbac.RoleRef{ + Name: "view-pod-xyz", + Kind: cmd.RoleKind, + }, + Subjects: []rbac.Subject{ + {Kind: rbac.UserKind, Name: "Batman"}, + }, + }) + }