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"}, + }, + }) + }