mirror of
https://github.com/gravitational/teleport
synced 2024-10-19 08:43:58 +00:00
Adds tsh kubectl
support (#20031)
This PR implements a `kubectl` wrapper inside `tsh` that creates resource access requests, waits for their approval and retries the command when it detects that access to a pod was denied due to missing role or Kubernetes RBAC principals permissions. Part of #18434 Updates #19573
This commit is contained in:
parent
2235827c0e
commit
605a7d00f4
12
go.mod
12
go.mod
|
@ -145,6 +145,7 @@ require (
|
|||
k8s.io/apiserver v0.26.0
|
||||
k8s.io/cli-runtime v0.26.0
|
||||
k8s.io/client-go v0.26.0
|
||||
k8s.io/component-base v0.26.0
|
||||
k8s.io/klog/v2 v2.80.1
|
||||
k8s.io/kubectl v0.26.0
|
||||
k8s.io/utils v0.0.0-20221128185143-99ec85e7a448
|
||||
|
@ -207,6 +208,7 @@ require (
|
|||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.0 // indirect
|
||||
github.com/aws/smithy-go v1.13.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/boombuler/barcode v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
|
@ -215,7 +217,9 @@ require (
|
|||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect
|
||||
github.com/danieljoos/wincred v1.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/daviddengcn/go-colortext v1.0.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
|
||||
github.com/elastic/elastic-transport-go/v8 v8.1.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
|
||||
|
@ -226,6 +230,7 @@ require (
|
|||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/fvbommel/sortorder v1.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.1 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
|
||||
github.com/go-errors/errors v1.0.1 // indirect
|
||||
|
@ -277,6 +282,7 @@ require (
|
|||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||
github.com/lithammer/dedent v1.1.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
|
@ -295,7 +301,9 @@ require (
|
|||
github.com/montanaflynn/stats v0.6.6 // indirect
|
||||
github.com/mtibben/percent v0.2.1 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/nsf/termbox-go v1.1.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.17 // indirect
|
||||
github.com/pingcap/errors v0.11.5-0.20201126102027-b0a155152ca3 // indirect
|
||||
|
@ -340,10 +348,12 @@ require (
|
|||
google.golang.org/appengine v1.6.7 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect
|
||||
k8s.io/component-base v0.26.0 // indirect
|
||||
k8s.io/component-helpers v0.26.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
|
||||
k8s.io/metrics v0.26.0 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.12.1 // indirect
|
||||
sigs.k8s.io/kustomize/kustomize/v4 v4.5.7 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||
)
|
||||
|
|
25
go.sum
25
go.sum
|
@ -238,6 +238,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
|
|||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
|
||||
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||
|
@ -310,6 +312,8 @@ github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnG
|
|||
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/daviddengcn/go-colortext v1.0.0 h1:ANqDyC0ys6qCSvuEK7l3g5RaehL/Xck9EX8ATG8oKsE=
|
||||
github.com/daviddengcn/go-colortext v1.0.0/go.mod h1:zDqEI5NVUop5QPpVJUxE9UO10hRnmkD5G4Pmri9+m4c=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
|
@ -317,6 +321,8 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8
|
|||
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
|
||||
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
|
||||
github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
|
@ -376,6 +382,8 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4
|
|||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/fsouza/fake-gcs-server v1.42.2 h1:J7IvZyB2vxxHVRfRd1AHfmtxz8XTMsHWrluYg/gXSGw=
|
||||
github.com/fsouza/fake-gcs-server v1.42.2/go.mod h1:TIot/MGHrgpSCaGcNDK3qVi+vXIiHc6KThR2aXBFSDU=
|
||||
github.com/fvbommel/sortorder v1.0.1 h1:dSnXLt4mJYH25uDDGa3biZNQsozaUWDSWeKJ0qqFfzE=
|
||||
github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
|
||||
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
|
||||
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
|
||||
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
|
||||
|
@ -497,6 +505,11 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8l
|
|||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho=
|
||||
github.com/golangplus/bytes v1.0.0/go.mod h1:AdRaCFwmc/00ZzELMWb01soso6W1R/++O1XL80yAn+A=
|
||||
github.com/golangplus/fmt v1.0.0/go.mod h1:zpM0OfbMCjPtd2qkTD/jX2MgiFCqklhSUFyDW44gVQE=
|
||||
github.com/golangplus/testing v1.0.0 h1:+ZeeiKZENNOMkTTELoSySazi+XaEhVO0mb+eanrSEUQ=
|
||||
github.com/golangplus/testing v1.0.0/go.mod h1:ZDreixUV3YzhoVraIDyOzHrr76p6NUh6k/pPg/Q3gYA=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
|
@ -813,6 +826,8 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhn
|
|||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
|
||||
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
|
||||
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
||||
github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY=
|
||||
github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
|
||||
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mailgun/lemma v0.0.0-20170619173223-4214099fb348 h1:H2VEGfxRuhOR5Dl6guYNTXrHEMh0JkKFDBZqxVCQ3RE=
|
||||
|
@ -905,6 +920,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
|
||||
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
|
||||
|
@ -928,6 +945,8 @@ github.com/onsi/ginkgo/v2 v2.6.0 h1:9t9b9vRUbFq3C4qKFCGkVuq/fIHji802N1nrtkh1mNc=
|
|||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.24.2 h1:J/tulyYK6JwBldPViHJReihxxZ+22FHs0piGjQAvoUE=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
|
||||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
|
@ -1797,12 +1816,16 @@ k8s.io/client-go v0.26.0 h1:lT1D3OfO+wIi9UFolCrifbjUUgu7CpLca0AD8ghRLI8=
|
|||
k8s.io/client-go v0.26.0/go.mod h1:I2Sh57A79EQsDmn7F7ASpmru1cceh3ocVT9KlX2jEZg=
|
||||
k8s.io/component-base v0.26.0 h1:0IkChOCohtDHttmKuz+EP3j3+qKmV55rM9gIFTXA7Vs=
|
||||
k8s.io/component-base v0.26.0/go.mod h1:lqHwlfV1/haa14F/Z5Zizk5QmzaVf23nQzCwVOQpfC8=
|
||||
k8s.io/component-helpers v0.26.0 h1:KNgwqs3EUdK0HLfW4GhnbD+q/Zl9U021VfIU7qoVYFk=
|
||||
k8s.io/component-helpers v0.26.0/go.mod h1:jHN01qS/Jdj95WCbTe9S2VZ9yxpxXNY488WjF+yW4fo=
|
||||
k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4=
|
||||
k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
|
||||
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E=
|
||||
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4=
|
||||
k8s.io/kubectl v0.26.0 h1:xmrzoKR9CyNdzxBmXV7jW9Ln8WMrwRK6hGbbf69o4T0=
|
||||
k8s.io/kubectl v0.26.0/go.mod h1:eInP0b+U9XUJWSYeU9XZnTA+cVYuWyl3iYPGtru0qhQ=
|
||||
k8s.io/metrics v0.26.0 h1:U/NzZHKDrIVGL93AUMRkqqXjOah3wGvjSnKmG/5NVCs=
|
||||
k8s.io/metrics v0.26.0/go.mod h1:cf5MlG4ZgWaEFZrR9+sOImhZ2ICMpIdNurA+D8snIs8=
|
||||
k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 h1:KTgPnR10d5zhztWptI952TNtt/4u5h3IzDXkdIMuo2Y=
|
||||
k8s.io/utils v0.0.0-20221128185143-99ec85e7a448/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
launchpad.net/gocheck v0.0.0-20140225173054-000000000087 h1:Izowp2XBH6Ya6rv+hqbceQyw/gSGoXfH/UPoTGduL54=
|
||||
|
@ -1818,6 +1841,8 @@ sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN
|
|||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/kustomize/api v0.12.1 h1:7YM7gW3kYBwtKvoY216ZzY+8hM+lV53LUayghNRJ0vM=
|
||||
sigs.k8s.io/kustomize/api v0.12.1/go.mod h1:y3JUhimkZkR6sbLNwfJHxvo1TCLwuwm14sCYnkH6S1s=
|
||||
sigs.k8s.io/kustomize/kustomize/v4 v4.5.7 h1:cDW6AVMl6t/SLuQaezMET8hgnadZGIAr8tUrxFVOrpg=
|
||||
sigs.k8s.io/kustomize/kustomize/v4 v4.5.7/go.mod h1:VSNKEH9D9d9bLiWEGbS6Xbg/Ih0tgQalmPvntzRxZ/Q=
|
||||
sigs.k8s.io/kustomize/kyaml v0.13.9 h1:Qz53EAaFFANyNgyOEJbT/yoIHygK40/ZcvU3rgry2Tk=
|
||||
sigs.k8s.io/kustomize/kyaml v0.13.9/go.mod h1:QsRbD0/KcU+wdk0/L0fIp2KLnohkVzs6fQ85/nOXac4=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
|
||||
|
|
|
@ -417,3 +417,17 @@ func searchForSelectedCluster(contexts map[string]*clientcmdapi.Context) string
|
|||
}
|
||||
return selected
|
||||
}
|
||||
|
||||
// SelectedKubeCluster returns the Kubernetes cluster name of the default context
|
||||
// if it belongs to the Teleport cluster provided.
|
||||
func SelectedKubeCluster(path, teleportCluster string) (string, error) {
|
||||
kubeconfig, err := Load(path)
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err)
|
||||
}
|
||||
|
||||
if kubeCluster := KubeClusterFromContext(kubeconfig.CurrentContext, teleportCluster); kubeCluster != "" {
|
||||
return kubeCluster, nil
|
||||
}
|
||||
return "", trace.NotFound("default context does not belong to Teleport")
|
||||
}
|
||||
|
|
277
tool/tsh/kubectl.go
Normal file
277
tool/tsh/kubectl.go
Normal file
|
@ -0,0 +1,277 @@
|
|||
/*
|
||||
Copyright 2023 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
"k8s.io/component-base/cli"
|
||||
"k8s.io/kubectl/pkg/cmd"
|
||||
"k8s.io/kubectl/pkg/cmd/plugin"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/kube/kubeconfig"
|
||||
)
|
||||
|
||||
var (
|
||||
podForbiddenRe = regexp.MustCompile(`(?m)Error from server \(Forbidden\): pods "(.*)" is forbidden: User ".*" cannot get resource "pods" in API group "" in the namespace "(.*)"`)
|
||||
clusterForbidden = "[00] access denied"
|
||||
// clusterObjectDiscoveryFailed is printed when kubectl tries to do API discovery
|
||||
// - calling /apis endpoint - but Teleport denies the request. Since it cannot
|
||||
// discover the resources available in the cluster, it prints this message saying
|
||||
// that the cluster does not have pod(s). Since every Kubernetes cluster supports
|
||||
// pods, it's safe to create a resource access request.
|
||||
clusterObjectDiscoveryFailed = regexp.MustCompile(`(?m)the server doesn't have a resource type "pods?"`)
|
||||
)
|
||||
|
||||
// resourceKind identifies a Kubernetes resource.
|
||||
type resourceKind struct {
|
||||
kind string
|
||||
subResourceName string
|
||||
}
|
||||
|
||||
// onKubectlCommand re-execs itself if env var `tshKubectlRexec` is not set
|
||||
// in order to execute the `kubectl` portion of the code. This is a requirement because
|
||||
// `kubectl` calls `os.Exit()` in every code path, and we need to intercept the
|
||||
// exit code to validate if the request was denied.
|
||||
// When executing `tsh kubectl get pods`, tsh checks if `tshKubectlReexec`. Since
|
||||
// it's the user call and the flag is not present, tsh reexecs the same exact
|
||||
// the user executed and uses an io.MultiWriter to write the os.Stderr output
|
||||
// from the kubectl command into an io.Pipe for analysis. It also sets the env
|
||||
// `tshKubectlReexec` in the exec.Cmd.Env and runs the command. When running the
|
||||
// command, `tsh` will be recalled, and since `tshKubectlReexec` is set only the
|
||||
// kubectl portion of code is executed.
|
||||
// On the caller side, once the callee execution finishes, tsh inspects the stderr
|
||||
// outputs and decides if creating an access request is appropriate.
|
||||
// If the access request is created, tsh waits for the approval and runs the expected
|
||||
// command again.
|
||||
func onKubectlCommand(cf *CLIConf, args []string) error {
|
||||
if os.Getenv(tshKubectlReexecEnvVar) == "" {
|
||||
err := runKubectlAndCollectRun(cf, args)
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
runKubectlCode(args)
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
// tshKubectlReexecEnvVar is the name of the environment variable used to control if
|
||||
// tsh should re-exec or execute a kubectl command.
|
||||
tshKubectlReexecEnvVar = "TSH_KUBE_REEXEC"
|
||||
)
|
||||
|
||||
// runKubectlReexec reexecs itself and copies the `stderr` output into
|
||||
// the provided collector.
|
||||
// It also sets tshKubectlReexec for the command to prevent
|
||||
// an exec loop
|
||||
func runKubectlReexec(selfExec string, args []string, collector io.Writer) error {
|
||||
cmd := exec.Command(selfExec, args...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = io.MultiWriter(os.Stderr, collector)
|
||||
cmd.Env = append(os.Environ(), fmt.Sprintf("%s=yes", tshKubectlReexecEnvVar))
|
||||
return trace.Wrap(cmd.Run())
|
||||
}
|
||||
|
||||
// runKubectlCode runs the actual kubectl package code with the default options.
|
||||
// This code is only executed when `tshKubectlReexec` env is present. This happens
|
||||
// because we need to retry kubectl calls and `kubectl` calls os.Exit in multiple
|
||||
// paths.
|
||||
func runKubectlCode(args []string) {
|
||||
// These values are the defaults used by kubectl and can be found here:
|
||||
// https://github.com/kubernetes/kubectl/blob/3612c18ed86fc0a2f4467ca355b3e21569fabe0a/pkg/cmd/cmd.go#L94
|
||||
defaultConfigFlags := genericclioptions.NewConfigFlags(true).
|
||||
WithDeprecatedPasswordFlag().
|
||||
WithDiscoveryBurst(300).
|
||||
WithDiscoveryQPS(50.0)
|
||||
|
||||
command := cmd.NewDefaultKubectlCommandWithArgs(
|
||||
cmd.KubectlOptions{
|
||||
// init the default plugin handler.
|
||||
PluginHandler: cmd.NewDefaultPluginHandler(plugin.ValidPluginFilenamePrefixes),
|
||||
Arguments: args,
|
||||
ConfigFlags: defaultConfigFlags,
|
||||
// init the IOSStreams.
|
||||
IOStreams: genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr},
|
||||
},
|
||||
)
|
||||
// override args without kubectl to avoid errors.
|
||||
command.SetArgs(args[1:])
|
||||
// run command until it finishes.
|
||||
if err := cli.RunNoErrOutput(command); err != nil {
|
||||
// Pretty-print the error and exit with an error.
|
||||
cmdutil.CheckErr(err)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func runKubectlAndCollectRun(cf *CLIConf, args []string) error {
|
||||
var (
|
||||
alreadyRequestedAccess bool
|
||||
err error
|
||||
exitErr *exec.ExitError
|
||||
)
|
||||
for {
|
||||
// missingKubeResources will include the Kubernetes Resources whose access
|
||||
// was rejected in this kubectl call.
|
||||
missingKubeResources := make([]resourceKind, 0, 50)
|
||||
reader, writer := io.Pipe()
|
||||
group, _ := errgroup.WithContext(cf.Context)
|
||||
group.Go(
|
||||
func() error {
|
||||
// This goroutine scans each line of output emitted to stderr by kubectl
|
||||
// and parses it in order to check if the returned error was a problem with
|
||||
// missing access level. If it's the case, tsh kubectl will create automatically
|
||||
// the access request for the user to access the resource.
|
||||
// Current supported resources:
|
||||
// - pod
|
||||
// - kube_cluster
|
||||
|
||||
scanner := bufio.NewScanner(reader)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// Check if the request targeting a pod endpoint was denied due to
|
||||
// Teleport Pod RBAC or if the operation was denied by Kubernetes RBAC.
|
||||
// In the second case, we should create a Resource Access Request to allow
|
||||
// the user to exec/read logs using different Kubernetes RBAC principals.
|
||||
// using different Kubernetes RBAC principals.
|
||||
if podForbiddenRe.MatchString(line) {
|
||||
results := podForbiddenRe.FindStringSubmatch(line)
|
||||
missingKubeResources = append(missingKubeResources, resourceKind{kind: types.KindKubePod, subResourceName: filepath.Join(results[2], results[1])})
|
||||
// Check if cluster access was denied. If denied we should create
|
||||
// a Resource Access Request for the cluster and not a pod.
|
||||
} else if strings.Contains(line, clusterForbidden) || clusterObjectDiscoveryFailed.MatchString(line) {
|
||||
missingKubeResources = append(missingKubeResources, resourceKind{kind: types.KindKubernetesCluster})
|
||||
}
|
||||
}
|
||||
return trace.Wrap(scanner.Err())
|
||||
},
|
||||
)
|
||||
|
||||
err := runKubectlReexec(cf.executablePath, args, writer)
|
||||
writer.CloseWithError(io.EOF)
|
||||
|
||||
if scanErr := group.Wait(); scanErr != nil {
|
||||
log.WithError(scanErr).Warn("unable to scan stderr payload")
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
break
|
||||
} else if !errors.As(err, &exitErr) {
|
||||
return trace.Wrap(err)
|
||||
} else if errors.As(err, &exitErr) && exitErr.ExitCode() != cmdutil.DefaultErrorExitCode {
|
||||
// if the exit code is not 1, it was emitted by pod exec code and we should
|
||||
// ignore it since the user was allowed to execute the command in the pod.
|
||||
break
|
||||
}
|
||||
|
||||
if len(missingKubeResources) > 0 && !alreadyRequestedAccess {
|
||||
// create the access requests for the user and wait for approval.
|
||||
if err := createKubeAccessRequest(cf, missingKubeResources, args); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
alreadyRequestedAccess = true
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
// exit with the kubectl exit code to keep compatibility.
|
||||
if errors.As(err, &exitErr) {
|
||||
os.Exit(exitErr.ExitCode())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createKubeAccessRequest creates an access request to the denied resources
|
||||
// if the user's roles allow search_as_role.
|
||||
func createKubeAccessRequest(cf *CLIConf, resources []resourceKind, args []string) error {
|
||||
tc, err := makeClient(cf, true)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
kubeName, err := getKubeClusterName(args, tc.SiteName)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
for _, rec := range resources {
|
||||
cf.RequestedResourceIDs = append(
|
||||
cf.RequestedResourceIDs,
|
||||
filepath.Join("/", tc.SiteName, rec.kind, kubeName, rec.subResourceName),
|
||||
)
|
||||
}
|
||||
cf.Reason = fmt.Sprintf("Resource request automatically created for %v", args)
|
||||
if err := executeAccessRequest(cf, tc); err != nil {
|
||||
// TODO(tigrato): intercept the error to validate the origin
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractKubeConfigAndContext parses the args and extracts - if present -
|
||||
// the --kubeconfig flag that overrrides the default kubeconfig location
|
||||
// and the --context flag that overrides the default context to use.
|
||||
func extractKubeConfigAndContext(args []string) (kubeconfig string, context string) {
|
||||
if len(args) <= 2 {
|
||||
return
|
||||
}
|
||||
command := cmd.NewDefaultKubectlCommandWithArgs(
|
||||
cmd.KubectlOptions{
|
||||
Arguments: args[2:],
|
||||
},
|
||||
)
|
||||
|
||||
if err := command.ParseFlags(args[2:]); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
kubeconfig = command.Flag("kubeconfig").Value.String()
|
||||
context = command.Flag("context").Value.String()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// getKubeClusterName extracts the Kubernetes Cluster name if the Kube belongs to
|
||||
// the teleportClusterName cluster. It parses the args to extract the `--kubeconfig`
|
||||
// and `--context` flag values and to use them if any was overriten.
|
||||
func getKubeClusterName(args []string, teleportClusterName string) (string, error) {
|
||||
kubeconfigLocation, selectedContext := extractKubeConfigAndContext(args)
|
||||
if selectedContext == "" {
|
||||
kubeName, err := kubeconfig.SelectedKubeCluster(kubeconfigLocation, teleportClusterName)
|
||||
return kubeName, trace.Wrap(err)
|
||||
}
|
||||
kubeName := kubeconfig.KubeClusterFromContext(selectedContext, teleportClusterName)
|
||||
if kubeName == "" {
|
||||
return "", trace.BadParameter("selected context %q does not belong to Teleport cluster %q", selectedContext, teleportClusterName)
|
||||
}
|
||||
return kubeName, nil
|
||||
}
|
|
@ -888,7 +888,9 @@ func Run(ctx context.Context, args []string, opts ...cliOption) error {
|
|||
|
||||
reqDrop := req.Command("drop", "Drop one more access requests from current identity")
|
||||
reqDrop.Arg("request-id", "IDs of requests to drop (default drops all requests)").Default("*").StringsVar(&cf.RequestIDs)
|
||||
|
||||
kubectl := app.Command("kubectl", "Runs a kubectl command on a Kubernetes cluster").Interspersed(false)
|
||||
// This hack is required in order to accept any args for tsh kubectl.
|
||||
kubectl.Arg("", "").StringsVar(new([]string))
|
||||
// Kubernetes subcommands.
|
||||
kube := newKubeCommand(app)
|
||||
// MFA subcommands.
|
||||
|
@ -1170,6 +1172,9 @@ func Run(ctx context.Context, args []string, opts ...cliOption) error {
|
|||
err = deviceCmd.collect.run(&cf)
|
||||
case deviceCmd.keyget.FullCommand():
|
||||
err = deviceCmd.keyget.run(&cf)
|
||||
case kubectl.FullCommand():
|
||||
idx := slices.Index(args, kubectl.FullCommand())
|
||||
err = onKubectlCommand(&cf, args[idx:])
|
||||
default:
|
||||
// Handle commands that might not be available.
|
||||
switch {
|
||||
|
|
Loading…
Reference in a new issue