fix pod xray menu + layout

mine
derailed 2020-02-11 15:26:49 -07:00
parent 8299129baf
commit aa58d1063a
38 changed files with 443 additions and 300 deletions

1
go.mod
View File

@ -28,6 +28,7 @@ replace (
)
require (
fyne.io/fyne v1.2.2 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/atotto/clipboard v0.1.2
github.com/derailed/tview v0.3.3

26
go.sum
View File

@ -3,6 +3,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
fyne.io/fyne v1.2.2 h1:mf7EseASp3CAC5vLWVPLnsoKxvp/ARdu3Seh0HvAQak=
fyne.io/fyne v1.2.2/go.mod h1:Ab+3DIB/FVteW0y4DXfmZv4N3JdnCBh2lHkINI02BOU=
github.com/Azure/azure-sdk-for-go v32.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
@ -29,6 +31,7 @@ github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFD
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14=
github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab/go.mod h1:3VYc5hodBMJ5+l/7J4xAyMeuM2PNuepvHlGs8yilUCA=
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E=
github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e h1:eb0Pzkt15Bm7f2FFYv7sjY7NPFi3cPkS3tv1CcrFBWA=
github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E=
@ -59,6 +62,7 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko
github.com/Rican7/retry v0.1.0/go.mod h1:FgOROf8P5bebcC1DS0PdOQiqGUridaZvikzUmkFW6gg=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@ -212,6 +216,10 @@ github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7a
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-acme/lego v2.5.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M=
github.com/go-bindata/go-bindata v3.1.1+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 h1:SCYMcCJ89LjRGwEa0tRluNRiMjZHalQZrVrvTbPh+qw=
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=
github.com/go-gl/glfw v0.0.0-20181213070059-819e8ce5125f h1:7MsFMbSn8Lcw0blK4+NEOf8DuHoOBDhJsHz04yh13pM=
github.com/go-gl/glfw v0.0.0-20181213070059-819e8ce5125f/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
@ -283,6 +291,8 @@ github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09Vjb
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8=
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
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=
@ -366,9 +376,11 @@ github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
github.com/imdario/mergo v0.3.8/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/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc=
github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/josephspurrier/goversioninfo v0.0.0-20190124120936-8611f5a5ff3f/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE=
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
@ -460,6 +472,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
@ -568,6 +582,10 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/srwiley/oksvg v0.0.0-20190829233741-58e08c8fe40e h1:LJUrNHytcMXWKxnULIHPe5SCb1jDpO9o672VB1x2EuQ=
github.com/srwiley/oksvg v0.0.0-20190829233741-58e08c8fe40e/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4=
github.com/srwiley/rasterx v0.0.0-20181219215540-696f7edb7a7e h1:FFotfUvew9Eg02LYRl8YybAnm0HCwjjfY5JlOI1oB00=
github.com/srwiley/rasterx v0.0.0-20181219215540-696f7edb7a7e/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU=
github.com/storageos/go-api v0.0.0-20180912212459-343b3eff91fc/go.mod h1:ZrLn+e0ZuF3Y65PNF6dIwbJPZqfmtCXxFm9ckv0agOY=
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=
@ -577,6 +595,7 @@ github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRci
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/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/syndtr/gocapability v0.0.0-20160928074757-e7cb7fa329f4/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
@ -630,8 +649,12 @@ golang.org/x/crypto v0.0.0-20191028145041-f83a4685e152 h1:ZC1Xn5A1nlpSmQCIva4bZ3
golang.org/x/crypto v0.0.0-20191028145041-f83a4685e152/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495 h1:I6A9Ag9FpEKOjcKrRNjQkPHawoXIhKyTGfvvjFAiiAk=
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/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@ -639,6 +662,8 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -695,6 +720,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191028164358-195ce5e7f934 h1:u/E0NqCIWRDAo9WCFo6Ko49njPFDLSd3z+X1HgWDMpE=
golang.org/x/sys v0.0.0-20191028164358-195ce5e7f934/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -77,6 +77,12 @@ func makeCacheKey(ns, gvr string, vv []string) string {
return ns + ":" + gvr + "::" + strings.Join(vv, ",")
}
func (a *APIClient) clearCache() {
for _, k := range a.cache.Keys() {
a.cache.Remove(k)
}
}
// CanI checks if user has access to a certain resource.
func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error) {
if IsClusterWide(ns) {
@ -131,6 +137,9 @@ func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) {
// BOZO!! No super sure about this approach either??
func (a *APIClient) CheckConnectivity() (status bool) {
defer func() {
if !status {
a.clearCache()
}
if err := recover(); err != nil {
status = false
}
@ -149,10 +158,10 @@ func (a *APIClient) CheckConnectivity() (status bool) {
}
}
if _, err := a.checkClientSet.ServerVersion(); err != nil {
log.Error().Err(err).Msgf("K9s can't connect to cluster")
} else {
if _, err := a.checkClientSet.ServerVersion(); err == nil {
status = true
} else {
log.Error().Err(err).Msgf("K9s can't connect to cluster")
}
return
@ -258,21 +267,24 @@ func (a *APIClient) MXDial() (*versioned.Clientset, error) {
return a.mxsClient, err
}
// SwitchContextOrDie handles kubeconfig context switches.
func (a *APIClient) SwitchContextOrDie(ctx string) {
// SwitchContext handles kubeconfig context switches.
func (a *APIClient) SwitchContext(ctx string) error {
currentCtx, err := a.config.CurrentContextName()
if err != nil {
log.Fatal().Err(err).Msg("Fetching current context")
return err
}
if currentCtx == ctx {
return nil
}
if currentCtx != ctx {
a.cachedClient = nil
a.reset()
if err := a.config.SwitchContext(ctx); err != nil {
log.Fatal().Err(err).Msg("Switching context")
return err
}
a.clearCache()
a.reset()
_ = a.supportsMxServer()
}
return nil
}
func (a *APIClient) reset() {

View File

@ -48,6 +48,10 @@ func (c *Config) SwitchContext(name string) error {
return err
}
if _, err := c.GetContext(name); err != nil {
return fmt.Errorf("context %s does not exist", name)
}
if currentCtx != name {
c.reset()
c.flags.Context, c.currentContext = &name, name

View File

@ -64,7 +64,7 @@ type Connection interface {
Config() *Config
DialOrDie() kubernetes.Interface
SwitchContextOrDie(ctx string)
SwitchContext(ctx string) error
CachedDiscoveryOrDie() *disk.CachedDiscoveryClient
RestConfigOrDie() *restclient.Config
MXDial() (*versioned.Clientset, error)

View File

@ -267,12 +267,14 @@ func (mock *MockConnection) SupportsResource(_param0 string) bool {
return ret0
}
func (mock *MockConnection) SwitchContextOrDie(_param0 string) {
func (mock *MockConnection) SwitchContext(_param0 string) error {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{_param0}
pegomock.GetGenericMockFrom(mock).Invoke("SwitchContextOrDie", params, []reflect.Type{})
pegomock.GetGenericMockFrom(mock).Invoke("SwitchContext", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})
return nil
}
func (mock *MockConnection) ValidNamespaces() ([]v1.Namespace, error) {

View File

@ -389,6 +389,9 @@ func (s *Styles) Update() {
// AsColor checks color index, if match return color otherwise pink it is.
func AsColor(c string) tcell.Color {
if c == "default" {
return tcell.ColorDefault
}
if color, ok := tcell.ColorNames[c]; ok {
return color
}

View File

@ -44,7 +44,7 @@ func makeConn() *conn {
func (c *conn) Config() *client.Config { return nil }
func (c *conn) DialOrDie() kubernetes.Interface { return nil }
func (c *conn) SwitchContextOrDie(ctx string) {}
func (c *conn) SwitchContext(ctx string) error { return nil }
func (c *conn) CachedDiscoveryOrDie() *disk.CachedDiscoveryClient { return nil }
func (c *conn) RestConfigOrDie() *restclient.Config { return nil }
func (c *conn) MXDial() (*versioned.Clientset, error) { return nil, nil }

View File

@ -58,8 +58,7 @@ func (c *Context) MustCurrentContextName() string {
// Switch to another context.
func (c *Context) Switch(ctx string) error {
c.Factory.Client().SwitchContextOrDie(ctx)
return nil
return c.Factory.Client().SwitchContext(ctx)
}
// KubeUpdate modifies kubeconfig default context.

View File

@ -29,8 +29,6 @@ type Node struct {
// List returns a collection of node resources.
func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
log.Debug().Msgf("NODE-LIST %q:%q", ns, n.gvr)
labels, ok := ctx.Value(internal.KeyLabels).(string)
if !ok {
log.Warn().Msgf("No label selector found in context")

View File

@ -332,12 +332,14 @@ func (mock *MockClusterMeta) SupportsResource(_param0 string) bool {
return ret0
}
func (mock *MockClusterMeta) SwitchContextOrDie(_param0 string) {
func (mock *MockClusterMeta) SwitchContext(_param0 string) error {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockClusterMeta().")
}
params := []pegomock.Param{_param0}
pegomock.GetGenericMockFrom(mock).Invoke("SwitchContextOrDie", params, []reflect.Type{})
pegomock.GetGenericMockFrom(mock).Invoke("SwitchContext", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})
return nil
}
func (mock *MockClusterMeta) UserName() (string, error) {

View File

@ -2,6 +2,7 @@ package model
import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
@ -75,10 +76,15 @@ func (t *Table) RemoveListener(l TableListener) {
// Watch initiates model updates.
func (t *Table) Watch(ctx context.Context) {
t.Refresh(ctx)
t.refresh(ctx)
go t.updater(ctx)
}
// Refresh updates the table content.
func (t *Table) Refresh(ctx context.Context) {
t.refresh(ctx)
}
// Get returns a resource instance if found, else an error.
func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) {
meta, err := t.getMeta(ctx)
@ -134,11 +140,6 @@ func (t *Table) ToYAML(ctx context.Context, path string) (string, error) {
return desc.ToYAML(path)
}
// Refresh update the model now.
func (t *Table) Refresh(ctx context.Context) {
t.refresh(ctx)
}
// GetNamespace returns the model namespace.
func (t *Table) GetNamespace() string {
return t.namespace
@ -185,6 +186,7 @@ func (t *Table) updater(ctx context.Context) {
for {
select {
case <-ctx.Done():
t.fireTableLoadFailed(errors.New("operation canceled"))
return
case <-time.After(rate):
rate = t.refreshRate

View File

@ -3,6 +3,8 @@ package ui
import (
"fmt"
"github.com/gdamore/tcell"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/tview"
)
@ -94,6 +96,7 @@ func (l *Logo) refreshLogo(c string) {
func logo() *tview.TextView {
v := tview.NewTextView()
v.SetBackgroundColor(tcell.ColorDefault)
v.SetWordWrap(false)
v.SetWrap(false)
v.SetTextAlign(tview.AlignLeft)
@ -104,6 +107,7 @@ func logo() *tview.TextView {
func status() *tview.TextView {
v := tview.NewTextView()
v.SetBackgroundColor(tcell.ColorDefault)
v.SetWordWrap(false)
v.SetWrap(false)
v.SetTextAlign(tview.AlignCenter)

View File

@ -35,7 +35,7 @@ func NewTree() *Tree {
// Init initializes the view
func (t *Tree) Init(ctx context.Context) error {
t.bindKeys()
t.BindKeys()
t.SetBorder(true)
t.SetBorderAttributes(tcell.AttrBold)
t.SetBorderPadding(0, 0, 1, 1)
@ -86,7 +86,7 @@ func (t *Tree) ExtraHints() map[string]string {
return nil
}
func (t *Tree) bindKeys() {
func (t *Tree) BindKeys() {
t.Actions().Add(KeyActions{
KeySpace: NewKeyAction("Expand/Collapse", t.noopCmd, true),
KeyX: NewKeyAction("Expand/Collapse All", t.toggleCollapseCmd, true),

View File

@ -118,12 +118,16 @@ func execCmd(r Runner, bin string, bg bool, args ...string) ui.ActionHandler {
ns, _ := client.Namespaced(path)
var (
env = r.EnvFn()()
aa = make([]string, len(args))
err error
)
if r.EnvFn() == nil {
return nil
}
for i, a := range args {
aa[i], err = env.envFor(ns, a)
aa[i], err = r.EnvFn()().envFor(ns, a)
if err != nil {
log.Error().Err(err).Msg("Plugin Args match failed")
return nil

View File

@ -4,7 +4,7 @@ import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/derailed/k9s/internal"
@ -24,7 +24,7 @@ var ExitStatus = ""
const (
splashDelay = 1 * time.Second
clusterRefresh = 5 * time.Second
maxConRetry = 5
maxConRetry = 10
clusterInfoWidth = 50
clusterInfoPad = 15
)
@ -39,9 +39,8 @@ type App struct {
version string
showHeader bool
cancelFn context.CancelFunc
conRetry int
conRetry int32
clusterModel *model.ClusterInfo
mx sync.Mutex
}
// NewApp returns a K9s app instance.
@ -61,9 +60,7 @@ func NewApp(cfg *config.Config) *App {
// ConOK checks the connection is cool, returns false otherwise.
func (a *App) ConOK() bool {
a.mx.Lock()
defer a.mx.Unlock()
return a.conRetry == 0
return atomic.LoadInt32(&a.conRetry) == 0
}
// Init initializes the application.
@ -198,32 +195,31 @@ func (a *App) clusterUpdater(ctx context.Context) {
}
func (a *App) refreshCluster() {
a.mx.Lock()
defer a.mx.Unlock()
c := a.Content.Top()
if ok := a.Conn().CheckConnectivity(); ok {
if a.conRetry > 0 {
if atomic.LoadInt32(&a.conRetry) > 0 {
atomic.StoreInt32(&a.conRetry, 0)
a.Status(ui.FlashInfo, "K8s connectivity OK")
if c != nil {
c.Start()
}
a.Status(ui.FlashInfo, "K8s connectivity OK")
}
a.conRetry = 0
} else {
a.conRetry++
log.Warn().Msgf("Conn check failed (%d/%d)", a.conRetry, maxConRetry)
atomic.AddInt32(&a.conRetry, 1)
if c != nil {
c.Stop()
}
a.Status(ui.FlashWarn, fmt.Sprintf("Dial K8s failed (%d)", a.conRetry))
count := atomic.LoadInt32(&a.conRetry)
log.Warn().Msgf("Conn check failed (%d/%d)", count, maxConRetry)
a.Status(ui.FlashWarn, fmt.Sprintf("Dial K8s failed (%d)", count))
}
if a.conRetry >= maxConRetry {
ExitStatus = fmt.Sprintf("Lost K8s connection (%d). Bailing out!", a.conRetry)
count := atomic.LoadInt32(&a.conRetry)
if count >= maxConRetry {
ExitStatus = fmt.Sprintf("Lost K8s connection (%d). Bailing out!", count)
a.BailOut()
}
if a.conRetry > 0 {
if count > 0 {
return
}

View File

@ -93,7 +93,6 @@ func (b *Browser) SetInstance(path string) {
func (b *Browser) Start() {
b.Stop()
log.Debug().Msgf("BROWSER started!")
b.Table.Start()
ctx := b.defaultContext()
ctx, b.cancelFn = context.WithCancel(ctx)
@ -111,7 +110,6 @@ func (b *Browser) Stop() {
if b.cancelFn == nil {
return
}
log.Debug().Msgf("BROWSER Stopped!")
b.Table.Stop()
b.cancelFn()
b.cancelFn = nil
@ -373,7 +371,6 @@ func (b *Browser) refreshActions() {
if b.app.ConOK() {
b.namespaceActions(aa)
if !b.app.Config.K9s.GetReadOnly() {
if client.Can(b.meta.Verbs, "edit") {
aa[ui.KeyE] = ui.NewKeyAction("Edit", b.editCmd, true)

View File

@ -55,6 +55,7 @@ func useContext(app *App, name string) error {
return errors.New("Expecting a switchable resource")
}
if err := switcher.Switch(name); err != nil {
log.Error().Err(err).Msgf("Context switch failed")
return err
}
if err := app.switchCtx(name, false); err != nil {

View File

@ -16,39 +16,44 @@ import (
"github.com/rs/zerolog/log"
)
func defaultK9sEnv(app *App, sel string, row render.Row) K9sEnv {
ns, n := client.Namespaced(sel)
ctx, err := app.Conn().Config().CurrentContextName()
func generalEnv(a *App) K9sEnv {
ctx, err := a.Conn().Config().CurrentContextName()
if err != nil {
ctx = render.NAValue
}
cluster, err := app.Conn().Config().CurrentClusterName()
cluster, err := a.Conn().Config().CurrentClusterName()
if err != nil {
cluster = render.NAValue
}
user, err := app.Conn().Config().CurrentUserName()
user, err := a.Conn().Config().CurrentUserName()
if err != nil {
user = render.NAValue
}
groups, err := app.Conn().Config().CurrentGroupNames()
groups, err := a.Conn().Config().CurrentGroupNames()
if err != nil {
groups = []string{render.NAValue}
}
var cfg string
kcfg := app.Conn().Config().Flags().KubeConfig
kcfg := a.Conn().Config().Flags().KubeConfig
if kcfg != nil && *kcfg != "" {
cfg = *kcfg
}
env := K9sEnv{
"NAMESPACE": ns,
"NAME": n,
return K9sEnv{
"CONTEXT": ctx,
"CLUSTER": cluster,
"USER": user,
"GROUPS": strings.Join(groups, ","),
"KUBECONFIG": cfg,
}
}
func defaultK9sEnv(a *App, sel string, row render.Row) K9sEnv {
ns, n := client.Namespaced(sel)
env := generalEnv(a)
env["NAMESPACE"], env["NAME"] = ns, n
for i, r := range row.Fields {
env["COL"+strconv.Itoa(i)] = r

View File

@ -8,6 +8,7 @@ import (
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/watch"
@ -108,47 +109,86 @@ func (p *Pod) killCmd(evt *tcell.EventKey) *tcell.EventKey {
}
func (p *Pod) shellCmd(evt *tcell.EventKey) *tcell.EventKey {
sel := p.GetTable().GetSelectedItem()
if sel == "" {
path := p.GetTable().GetSelectedItem()
if path == "" {
return evt
}
row := p.GetTable().GetSelectedRowIndex()
status := ui.TrimCell(p.GetTable().SelectTable, row, p.GetTable().NameColIndex()+2)
if status != render.Running {
p.App().Flash().Errf("%s is not in a running state", sel)
p.App().Flash().Errf("%s is not in a running state", path)
return nil
}
cc, err := fetchContainers(p.App().factory, sel, false)
if err != nil {
p.App().Flash().Errf("Unable to retrieve containers %s", err)
return evt
}
if len(cc) == 1 {
p.shellIn(sel, cc[0])
return nil
}
picker := NewPicker()
picker.populate(cc)
picker.SetSelectedFunc(func(i int, t, d string, r rune) {
p.shellIn(sel, t)
})
if err := p.App().inject(picker); err != nil {
if err := containerShellin(p.App(), p, path, ""); err != nil {
p.App().Flash().Err(err)
}
return evt
}
func (p *Pod) shellIn(path, co string) {
p.Stop()
shellIn(p.App(), path, co)
p.Start()
return nil
}
// ----------------------------------------------------------------------------
// Helpers...
func containerShellin(a *App, comp model.Component, path, co string) error {
if co != "" {
resumeShellIn(a, comp, path, co)
return nil
}
cc, err := fetchContainers(a.factory, path, false)
if err != nil {
return err
}
if len(cc) == 1 {
resumeShellIn(a, comp, path, cc[0])
return nil
}
picker := NewPicker()
picker.populate(cc)
picker.SetSelectedFunc(func(_ int, co, _ string, _ rune) {
resumeShellIn(a, comp, path, co)
})
if err := a.inject(picker); err != nil {
return err
}
return nil
}
func resumeShellIn(a *App, c model.Component, path, co string) {
c.Stop()
defer c.Start()
shellIn(a, path, co)
}
func shellIn(a *App, path, co string) {
args := computeShellArgs(path, co, a.Config.K9s.CurrentContext, a.Conn().Config().Flags().KubeConfig)
c := color.New(color.BgGreen).Add(color.FgBlack).Add(color.Bold)
if !runK(a, shellOpts{clear: true, banner: c.Sprintf(bannerFmt, path, co), args: args}) {
a.Flash().Err(errors.New("Shell exec failed"))
}
}
func computeShellArgs(path, co, context string, kcfg *string) []string {
args := make([]string, 0, 15)
args = append(args, "exec", "-it")
args = append(args, "--context", context)
ns, po := client.Namespaced(path)
args = append(args, "-n", ns)
args = append(args, po)
if kcfg != nil && *kcfg != "" {
args = append(args, "--kubeconfig", *kcfg)
}
if co != "" {
args = append(args, "-c", co)
}
return append(args, "--", "sh", "-c", shellCheck)
}
func fetchContainers(f *watch.Factory, path string, includeInit bool) ([]string, error) {
o, err := f.Get("v1/pods", path, true, labels.Everything())
if err != nil {
@ -172,30 +212,3 @@ func fetchContainers(f *watch.Factory, path string, includeInit bool) ([]string,
}
return nn, nil
}
func shellIn(a *App, path, co string) {
args := computeShellArgs(path, co, a.Config.K9s.CurrentContext, a.Conn().Config().Flags().KubeConfig)
log.Debug().Msgf("Shell args %v", args)
c := color.New(color.BgGreen).Add(color.FgBlack).Add(color.Bold)
if !runK(a, shellOpts{clear: true, banner: c.Sprintf(bannerFmt, path, co), args: args}) {
a.Flash().Err(errors.New("Shell exec failed"))
}
}
func computeShellArgs(path, co, context string, kcfg *string) []string {
args := make([]string, 0, 15)
args = append(args, "exec", "-it")
args = append(args, "--context", context)
ns, po := client.Namespaced(path)
args = append(args, "-n", ns)
args = append(args, po)
if kcfg != nil && *kcfg != "" {
args = append(args, "--kubeconfig", *kcfg)
}
if co != "" {
args = append(args, "-c", co)
}
return append(args, "--", "sh", "-c", shellCheck)
}

View File

@ -50,6 +50,8 @@ func NewXray(gvr client.GVR) ResourceViewer {
// Init initializes the view
func (x *Xray) Init(ctx context.Context) error {
x.envFn = x.k9sEnv
if err := x.Tree.Init(ctx); err != nil {
return err
}
@ -77,12 +79,12 @@ func (x *Xray) Init(ctx context.Context) error {
x.model.AddListener(x)
x.SetChangedFunc(func(n *tview.TreeNode) {
ref, ok := n.GetReference().(xray.NodeSpec)
spec, ok := n.GetReference().(xray.NodeSpec)
if !ok {
log.Error().Msgf("No ref found on node %s", n.GetText())
return
}
x.SetSelectedItem(ref.Path)
x.SetSelectedItem(spec.AsPath())
x.refreshActions()
})
x.refreshActions()
@ -131,16 +133,18 @@ func (x *Xray) refreshActions() {
x.Actions().Clear()
x.bindKeys()
x.Tree.BindKeys()
ref := x.selectedSpec()
if ref == nil {
spec := x.selectedSpec()
if spec == nil {
return
}
gvr := spec.GVR()
var err error
x.meta, err = dao.MetaAccess.MetaFor(client.NewGVR(ref.GVR))
x.meta, err = dao.MetaAccess.MetaFor(client.NewGVR(gvr))
if err != nil {
log.Warn().Msgf("NO meta for %q -- %s", ref.GVR, err)
log.Warn().Msgf("NO meta for %q -- %s", gvr, err)
return
}
@ -155,22 +159,27 @@ func (x *Xray) refreshActions() {
aa[ui.KeyD] = ui.NewKeyAction("Describe", x.describeCmd, true)
}
if ref.GVR == "containers" {
switch gvr {
case "containers":
x.Actions().Delete(tcell.KeyEnter)
aa[ui.KeyS] = ui.NewKeyAction("Shell", x.shellCmd, true)
aa[ui.KeyL] = ui.NewKeyAction("Logs", x.logsCmd(false), true)
aa[ui.KeyShiftL] = ui.NewKeyAction("Logs Previous", x.logsCmd(true), true)
case "v1/pods":
aa[ui.KeyS] = ui.NewKeyAction("Shell", x.shellCmd, true)
aa[ui.KeyL] = ui.NewKeyAction("Logs", x.logsCmd(false), true)
aa[ui.KeyShiftL] = ui.NewKeyAction("Logs Previous", x.logsCmd(true), true)
}
x.Actions().Add(aa)
}
// GetSelectedPath returns the current selection as string.
func (x *Xray) GetSelectedPath() string {
ref := x.selectedSpec()
if ref == nil {
spec := x.selectedSpec()
if spec == nil {
return ""
}
return ref.Path
return spec.Path()
}
func (x *Xray) selectedSpec() *xray.NodeSpec {
@ -193,6 +202,34 @@ func (x *Xray) EnvFn() EnvFunc {
return x.envFn
}
func (x *Xray) k9sEnv() K9sEnv {
env := generalEnv(x.app)
spec := x.selectedSpec()
if spec == nil {
return env
}
env["FILTER"] = x.CmdBuff().String()
if env["FILTER"] == "" {
ns, n := client.Namespaced(spec.Path())
env["NAMESPACE"], env["FILTER"] = ns, n
}
switch spec.GVR() {
case "containers":
_, co := client.Namespaced(spec.Path())
env["CONTAINER"] = co
ns, n := client.Namespaced(*spec.ParentPath())
env["NAMESPACE"], env["POD"], env["NAME"] = ns, n, co
default:
ns, n := client.Namespaced(spec.Path())
env["NAMESPACE"], env["NAME"] = ns, n
}
return env
}
// Aliases returns all available aliases.
func (x *Xray) Aliases() []string {
return append(x.meta.ShortNames, x.meta.SingularName, x.meta.Name)
@ -200,100 +237,98 @@ func (x *Xray) Aliases() []string {
func (x *Xray) logsCmd(prev bool) func(evt *tcell.EventKey) *tcell.EventKey {
return func(evt *tcell.EventKey) *tcell.EventKey {
ref := x.selectedSpec()
if ref == nil {
spec := x.selectedSpec()
if spec == nil {
return nil
}
if ref.Parent != nil {
x.showLogs(ref.Parent, ref, prev)
} else {
log.Error().Msgf("No parent found for container %q", ref.Path)
}
x.showLogs(spec, prev)
return nil
}
}
func (x *Xray) showLogs(pod, co *xray.NodeSpec, prev bool) {
func (x *Xray) showLogs(spec *xray.NodeSpec, prev bool) {
// Need to load and wait for pods
ns, _ := client.Namespaced(pod.Path)
path, co := spec.Path(), ""
if spec.GVR() == "containers" {
_, coName := client.Namespaced(spec.Path())
path, co = *spec.ParentPath(), coName
}
ns, _ := client.Namespaced(path)
_, err := x.app.factory.CanForResource(ns, "v1/pods", client.MonitorAccess)
if err != nil {
x.app.Flash().Err(err)
return
}
if err := x.app.inject(NewLog(client.NewGVR(co.GVR), pod.Path, co.Path, prev)); err != nil {
if err := x.app.inject(NewLog(client.NewGVR("v1/pods"), path, co, prev)); err != nil {
x.app.Flash().Err(err)
}
}
func (x *Xray) shellCmd(evt *tcell.EventKey) *tcell.EventKey {
ref := x.selectedSpec()
if ref == nil {
spec := x.selectedSpec()
if spec == nil {
return nil
}
if ref.Status != "" {
x.app.Flash().Errf("%s is not in a running state", ref.Path)
if spec.Status() != "ok" {
x.app.Flash().Errf("%s is not in a running state", spec.Path())
return nil
}
if ref.Parent != nil {
_, co := client.Namespaced(ref.Path)
x.shellIn(ref.Parent.Path, co)
} else {
log.Error().Msgf("No parent found on container node %q", ref.Path)
path, co := spec.Path(), ""
if spec.GVR() == "containers" {
_, co = client.Namespaced(spec.Path())
path = *spec.ParentPath()
}
if err := containerShellin(x.app, x, path, co); err != nil {
x.app.Flash().Err(err)
}
return nil
}
func (x *Xray) shellIn(path, co string) {
x.Stop()
shellIn(x.app, path, co)
x.Start()
}
func (x *Xray) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
ref := x.selectedSpec()
if ref == nil {
spec := x.selectedSpec()
if spec == nil {
return evt
}
ctx := x.defaultContext()
raw, err := x.model.ToYAML(ctx, ref.GVR, ref.Path)
raw, err := x.model.ToYAML(ctx, spec.GVR(), spec.Path())
if err != nil {
x.App().Flash().Errf("unable to get resource %q -- %s", ref.GVR, err)
x.App().Flash().Errf("unable to get resource %q -- %s", spec.GVR(), err)
return nil
}
details := NewDetails(x.app, "YAML", ref.Path, true).Update(raw)
details := NewDetails(x.app, "YAML", spec.Path(), true).Update(raw)
if err := x.app.inject(details); err != nil {
x.app.Flash().Err(err)
}
return nil
}
func (x *Xray) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
ref := x.selectedSpec()
if ref == nil {
spec := x.selectedSpec()
if spec == nil {
return evt
}
x.Stop()
defer x.Start()
{
gvr := client.NewGVR(ref.GVR)
gvr := client.NewGVR(spec.GVR())
meta, err := dao.MetaAccess.MetaFor(gvr)
if err != nil {
log.Warn().Msgf("NO meta for %q -- %s", ref.GVR, err)
log.Warn().Msgf("NO meta for %q -- %s", spec.GVR(), err)
return nil
}
x.resourceDelete(gvr, ref, fmt.Sprintf("Delete %s %s?", meta.SingularName, ref.Path))
x.resourceDelete(gvr, spec, fmt.Sprintf("Delete %s %s?", meta.SingularName, spec.Path()))
}
return nil
@ -301,12 +336,12 @@ func (x *Xray) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
}
func (x *Xray) describeCmd(evt *tcell.EventKey) *tcell.EventKey {
ref := x.selectedSpec()
if ref == nil {
spec := x.selectedSpec()
if spec == nil {
return evt
}
x.describe(ref.GVR, ref.Path)
x.describe(spec.GVR(), spec.Path())
return nil
}
@ -328,18 +363,18 @@ func (x *Xray) describe(gvr, path string) {
}
func (x *Xray) editCmd(evt *tcell.EventKey) *tcell.EventKey {
ref := x.selectedSpec()
if ref == nil {
spec := x.selectedSpec()
if spec == nil {
return evt
}
x.Stop()
defer x.Start()
{
ns, n := client.Namespaced(ref.Path)
ns, n := client.Namespaced(spec.Path())
args := make([]string, 0, 10)
args = append(args, "edit")
args = append(args, client.NewGVR(ref.GVR).R())
args = append(args, client.NewGVR(spec.GVR()).R())
args = append(args, "-n", ns)
args = append(args, "--context", x.app.Config.K9s.CurrentContext)
if cfg := x.app.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" {
@ -408,14 +443,15 @@ func (x *Xray) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
ref := x.selectedSpec()
if ref == nil {
spec := x.selectedSpec()
if spec == nil {
return nil
}
if len(strings.Split(ref.Path, "/")) == 1 {
log.Debug().Msgf("SELECTED REF %#v", spec)
if len(strings.Split(spec.Path(), "/")) == 1 {
return nil
}
if err := x.app.viewResource(client.NewGVR(ref.GVR).R(), ref.Path, false); err != nil {
if err := x.app.viewResource(client.NewGVR(spec.GVR()).R(), spec.Path(), false); err != nil {
x.app.Flash().Err(err)
}
@ -464,13 +500,13 @@ func (x *Xray) update(node *xray.TreeNode) {
x.hydrate(root, c)
}
if x.GetSelectedItem() == "" {
x.SetSelectedItem(node.ID)
x.SetSelectedItem(node.Spec().Path())
}
x.app.QueueUpdateDraw(func() {
x.SetRoot(root)
root.Walk(func(node, parent *tview.TreeNode) bool {
ref, ok := node.GetReference().(xray.NodeSpec)
spec, ok := node.GetReference().(xray.NodeSpec)
if !ok {
log.Error().Msgf("Expecting a NodeSpec but got %T", node.GetReference())
return false
@ -482,7 +518,8 @@ func (x *Xray) update(node *xray.TreeNode) {
node.SetExpanded(true)
}
if ref.Path == x.GetSelectedItem() {
if spec.AsPath() == x.GetSelectedItem() {
log.Debug().Msgf("SEL %q--%q", spec.Path(), x.GetSelectedItem())
node.SetExpanded(true).SetSelectable(true)
x.SetCurrentNode(node)
}
@ -611,9 +648,9 @@ func (x *Xray) styleTitle() string {
return title + ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), x.app.Styles.Frame())
}
func (x *Xray) resourceDelete(gvr client.GVR, ref *xray.NodeSpec, msg string) {
func (x *Xray) resourceDelete(gvr client.GVR, spec *xray.NodeSpec, msg string) {
dialog.ShowDelete(x.app.Content.Pages, msg, func(cascade, force bool) {
x.app.Flash().Infof("Delete resource %s %s", ref.GVR, ref.Path)
x.app.Flash().Infof("Delete resource %s %s", spec.GVR(), spec.Path())
accessor, err := dao.AccessorFor(x.app.factory, gvr)
if err != nil {
log.Error().Err(err).Msgf("No accessor")
@ -625,11 +662,11 @@ func (x *Xray) resourceDelete(gvr client.GVR, ref *xray.NodeSpec, msg string) {
x.app.Flash().Errf("Invalid nuker %T", accessor)
return
}
if err := nuker.Delete(ref.Path, true, true); err != nil {
if err := nuker.Delete(spec.Path(), true, true); err != nil {
x.app.Flash().Errf("Delete failed with `%s", err)
} else {
x.app.Flash().Infof("%s `%s deleted successfully", x.GVR(), ref.Path)
x.app.factory.DeleteForwarder(ref.Path)
x.app.Flash().Infof("%s `%s deleted successfully", x.GVR(), spec.Path())
x.app.factory.DeleteForwarder(spec.Path())
}
x.Refresh()
}, func() {})
@ -661,15 +698,7 @@ func makeTreeNode(node *xray.TreeNode, expanded bool, styles *config.Styles) *tv
n := tview.NewTreeNode("No data...")
if node != nil {
n.SetText(node.Title(styles.Xray()))
spec := xray.NodeSpec{}
if p := node.Parent; p != nil {
spec.GVR, spec.Path = p.GVR, p.ID
}
n.SetReference(xray.NodeSpec{
GVR: node.GVR,
Path: node.ID,
Parent: &spec,
})
n.SetReference(node.Spec())
}
n.SetSelectable(true)
n.SetExpanded(expanded)

View File

@ -35,9 +35,9 @@ func (c *Container) Render(ctx context.Context, ns string, o interface{}) error
}
pns, _ := client.Namespaced(parent.ID)
c.envRefs(f, root, pns, co.Container)
if !root.IsLeaf() {
// if !root.IsLeaf() {
parent.Add(root)
}
// }
return nil
}

View File

@ -235,7 +235,7 @@ func makeDoubleCMKeysContainer(n string, optional bool) *v1.Container {
}
func load(t *testing.T, n string) *unstructured.Unstructured {
raw, err := ioutil.ReadFile(fmt.Sprintf("test_assets/%s.json", n))
raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n))
assert.Nil(t, err)
var o unstructured.Unstructured

View File

@ -40,7 +40,6 @@ func (p *Pod) Render(ctx context.Context, ns string, o interface{}) error {
if !ok {
return fmt.Errorf("Expecting a TreeNode but got %T", ctx.Value(KeyParent))
}
parent.Add(node)
if err := p.containerRefs(ctx, node, po.Namespace, po.Spec); err != nil {
return err
@ -50,6 +49,14 @@ func (p *Pod) Render(ctx context.Context, ns string, o interface{}) error {
return err
}
gvr, nsID := "v1/namespaces", client.FQN(client.ClusterScope, po.Namespace)
nsn := parent.Find(gvr, nsID)
if nsn == nil {
nsn = NewTreeNode(gvr, nsID)
parent.Add(nsn)
}
nsn.Add(node)
return p.validate(node, po)
}

View File

@ -13,25 +13,25 @@ import (
func TestPodRender(t *testing.T) {
uu := map[string]struct {
file string
level1, level2 int
count, children int
status string
}{
"plain": {
file: "po",
level1: 1,
level2: 3,
children: 1,
count: 7,
status: xray.OkStatus,
},
"withInit": {
file: "init",
level1: 1,
level2: 2,
children: 1,
count: 7,
status: xray.OkStatus,
},
"cilium": {
file: "cilium",
level1: 1,
level2: 3,
children: 1,
count: 8,
status: xray.OkStatus,
},
}
@ -46,8 +46,8 @@ func TestPodRender(t *testing.T) {
ctx = context.WithValue(ctx, internal.KeyFactory, makeFactory())
assert.Nil(t, re.Render(ctx, "", &render.PodWithMetrics{Raw: o}))
assert.Equal(t, u.level1, root.CountChildren())
assert.Equal(t, u.level2, root.Children[0].CountChildren())
assert.Equal(t, u.children, root.CountChildren())
assert.Equal(t, u.count, root.Count(""))
})
}
}

View File

@ -52,8 +52,51 @@ type TreeRef string
// NodeSpec represents a node resource specification.
type NodeSpec struct {
GVR, Path, Status string
Parent *NodeSpec
GVRs, Paths, Statuses []string
}
func (s NodeSpec) ParentGVR() *string {
if len(s.GVRs) > 1 {
return &s.GVRs[1]
}
return nil
}
func (s NodeSpec) ParentPath() *string {
if len(s.Paths) > 1 {
return &s.Paths[1]
}
return nil
}
// GVR returns the current GVR.
func (s NodeSpec) GVR() string {
return s.GVRs[0]
}
// Path returns the current path.
func (s NodeSpec) Path() string {
return s.Paths[0]
}
// Status returns the current status.
func (s NodeSpec) Status() string {
return s.Statuses[0]
}
// AsPath returns path hierarchy as string.
func (s NodeSpec) AsPath() string {
return strings.Join(s.Paths, PathSeparator)
}
// AsGVR returns a gvr hierarchy as string.
func (s NodeSpec) AsGVR() string {
return strings.Join(s.GVRs, PathSeparator)
}
// AsStatus returns a status hierarchy as string.
func (s NodeSpec) AsStatus() string {
return strings.Join(s.Statuses, PathSeparator)
}
// ----------------------------------------------------------------------------
@ -145,19 +188,17 @@ func (t *TreeNode) Sort() {
// Spec returns this node specification.
func (t *TreeNode) Spec() NodeSpec {
parent := t
var gvr, path, status []string
for parent != nil {
gvr = append(gvr, parent.GVR)
path = append(path, parent.ID)
status = append(status, parent.Extras[StatusKey])
parent = parent.Parent
var GVRs, Paths, Statuses []string
for parent := t; parent != nil; parent = parent.Parent {
GVRs = append(GVRs, parent.GVR)
Paths = append(Paths, parent.ID)
Statuses = append(Statuses, parent.Extras[StatusKey])
}
return NodeSpec{
GVR: strings.Join(gvr, PathSeparator),
Path: strings.Join(path, PathSeparator),
Status: strings.Join(status, PathSeparator),
GVRs: GVRs,
Paths: Paths,
Statuses: Statuses,
}
}
@ -180,21 +221,18 @@ func (t *TreeNode) Blank() bool {
}
// Hydrate hydrates a full tree bases on a collection of specifications.
func Hydrate(refs []NodeSpec) *TreeNode {
func Hydrate(specs []NodeSpec) *TreeNode {
root := NewTreeNode("", "")
nav := root
for _, ref := range refs {
gvrs := strings.Split(ref.GVR, PathSeparator)
paths := strings.Split(ref.Path, PathSeparator)
statuses := strings.Split(ref.Status, PathSeparator)
for i := len(paths) - 1; i >= 0; i-- {
for _, spec := range specs {
for i := len(spec.Paths) - 1; i >= 0; i-- {
if nav.Blank() {
nav.GVR, nav.ID, nav.Extras[StatusKey] = gvrs[i], paths[i], statuses[i]
nav.GVR, nav.ID, nav.Extras[StatusKey] = spec.GVRs[i], spec.Paths[i], spec.Statuses[i]
continue
}
c := NewTreeNode(gvrs[i], paths[i])
c.Extras[StatusKey] = statuses[i]
if n := nav.Find(gvrs[i], paths[i]); n == nil {
c := NewTreeNode(spec.GVRs[i], spec.Paths[i])
c.Extras[StatusKey] = spec.Statuses[i]
if n := nav.Find(spec.GVRs[i], spec.Paths[i]); n == nil {
nav.Add(c)
nav = c
} else {
@ -260,7 +298,7 @@ func (t *TreeNode) Filter(q string, filter func(q, path string) bool) *TreeNode
specs := t.Flatten()
matches := make([]NodeSpec, 0, len(specs))
for _, s := range specs {
if filter(q, s.Path+s.Status) {
if filter(q, s.AsPath()+s.AsStatus()) {
matches = append(matches, s)
}
}
@ -461,7 +499,7 @@ func toEmoji(gvr string) string {
// EmojiInfo returns emoji help.
func EmojiInfo() map[string]string {
gvrs := []string{
GVRs := []string{
"containers",
"v1/namespaces",
"v1/pods",
@ -476,8 +514,8 @@ func EmojiInfo() map[string]string {
"apps/v1/daemonsets",
}
m := make(map[string]string, len(gvrs))
for _, g := range gvrs {
m := make(map[string]string, len(GVRs))
for _, g := range GVRs {
m[client.NewGVR(g).R()] = toEmoji(g)
}

View File

@ -86,8 +86,8 @@ func TestTreeNodeFilter(t *testing.T) {
}
func TestTreeNodeHydrate(t *testing.T) {
threeOK := strings.Join([]string{"ok", "ok", "ok"}, xray.PathSeparator)
fiveOK := strings.Join([]string{"ok", "ok", "ok", "ok", "ok"}, xray.PathSeparator)
threeOK := []string{"ok", "ok", "ok"}
fiveOK := append(threeOK, "ok", "ok")
uu := map[string]struct {
spec []xray.NodeSpec
@ -96,14 +96,14 @@ func TestTreeNodeHydrate(t *testing.T) {
"flat_simple": {
spec: []xray.NodeSpec{
{
GVR: "containers::v1/pods",
Path: "c1::default/p1",
Status: threeOK,
GVRs: []string{"containers", "v1/pods"},
Paths: []string{"c1", "default/p1"},
Statuses: threeOK,
},
{
GVR: "containers::v1/pods",
Path: "c2::default/p1",
Status: threeOK,
GVRs: []string{"containers", "v1/pods"},
Paths: []string{"c2", "default/p1"},
Statuses: threeOK,
},
},
e: root1(),
@ -111,14 +111,14 @@ func TestTreeNodeHydrate(t *testing.T) {
"flat_complex": {
spec: []xray.NodeSpec{
{
GVR: "v1/secrets::containers::v1/pods",
Path: "s1::c1::default/p1",
Status: threeOK,
GVRs: []string{"v1/secrets", "containers", "v1/pods"},
Paths: []string{"s1", "c1", "default/p1"},
Statuses: threeOK,
},
{
GVR: "v1/secrets::containers::v1/pods",
Path: "s2::c2::default/p1",
Status: threeOK,
GVRs: []string{"v1/secrets", "containers", "v1/pods"},
Paths: []string{"s2", "c2", "default/p1"},
Statuses: threeOK,
},
},
e: root2(),
@ -126,49 +126,49 @@ func TestTreeNodeHydrate(t *testing.T) {
"complex1": {
spec: []xray.NodeSpec{
{
GVR: "v1/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments",
Path: "default/default-token-rr22g::default/nginx-6b866d578b-c6tcn::default/nginx::-/default::deployments",
Status: fiveOK,
GVRs: []string{"v1/secrets", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"},
Paths: []string{"default/default-token-rr22g", "default/nginx-6b866d578b-c6tcn", "default/nginx", "-/default", "deployments"},
Statuses: fiveOK,
},
{
GVR: "v1/configmaps::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments",
Path: "kube-system/coredns::kube-system/coredns-6955765f44-89q2p::kube-system/coredns::-/kube-system::deployments",
Status: fiveOK,
GVRs: []string{"v1/configmaps", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"},
Paths: []string{"kube-system/coredns", "kube-system/coredns-6955765f44-89q2p", "kube-system/coredns", "-/kube-system", "deployments"},
Statuses: fiveOK,
},
{
GVR: "v1/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments",
Path: "kube-system/coredns-token-5cq9j::kube-system/coredns-6955765f44-89q2p::kube-system/coredns::-/kube-system::deployments",
Status: fiveOK,
GVRs: []string{"v1/secrets", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"},
Paths: []string{"kube-system/coredns-token-5cq9j", "kube-system/coredns-6955765f44-89q2p", "kube-system/coredns", "-/kube-system", "deployments"},
Statuses: fiveOK,
},
{
GVR: "v1/configmaps::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments",
Path: "kube-system/coredns::kube-system/coredns-6955765f44-r9j9t::kube-system/coredns::-/kube-system::deployments",
Status: fiveOK,
GVRs: []string{"v1/configmaps", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"},
Paths: []string{"kube-system/coredns", "kube-system/coredns-6955765f44-r9j9t", "kube-system/coredns", "-/kube-system", "deployments"},
Statuses: fiveOK,
},
{
GVR: "v1/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments",
Path: "kube-system/coredns-token-5cq9j::kube-system/coredns-6955765f44-r9j9t::kube-system/coredns::-/kube-system::deployments",
Status: fiveOK,
GVRs: []string{"v1/secrets", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"},
Paths: []string{"kube-system/coredns-token-5cq9j", "kube-system/coredns-6955765f44-r9j9t", "kube-system/coredns", "-/kube-system", "deployments"},
Statuses: fiveOK,
},
{
GVR: "v1/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments",
Path: "kube-system/default-token-thzt8::kube-system/metrics-server-6754dbc9df-88bk4::kube-system/metrics-server::-/kube-system::deployments",
Status: fiveOK,
GVRs: []string{"v1/secrets", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"},
Paths: []string{"kube-system/default-token-thzt8", "kube-system/metrics-server-6754dbc9df-88bk4", "kube-system/metrics-server", "-/kube-system", "deployments"},
Statuses: fiveOK,
},
{
GVR: "v1/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments",
Path: "kube-system/nginx-ingress-token-kff5q::kube-system/nginx-ingress-controller-6fc5bcc8c9-cwp55::kube-system/nginx-ingress-controller::-/kube-system::deployments",
Status: fiveOK,
GVRs: []string{"v1/secrets", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"},
Paths: []string{"kube-system/nginx-ingress-token-kff5q", "kube-system/nginx-ingress-controller-6fc5bcc8c9-cwp55", "kube-system/nginx-ingress-controller", "-/kube-system", "deployments"},
Statuses: fiveOK,
},
{
GVR: "v1/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments",
Path: "kubernetes-dashboard/kubernetes-dashboard-token-d6rt4::kubernetes-dashboard/dashboard-metrics-scraper-7b64584c5c-c7b56::kubernetes-dashboard/dashboard-metrics-scraper::-/kubernetes-dashboard::deployments",
Status: fiveOK,
GVRs: []string{"v1/secrets", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"},
Paths: []string{"kubernetes-dashboard/kubernetes-dashboard-token-d6rt4", "kubernetes-dashboard/dashboard-metrics-scraper-7b64584c5c-c7b56", "kubernetes-dashboard/dashboard-metrics-scraper", "-/kubernetes-dashboard", "deployments"},
Statuses: fiveOK,
},
{
GVR: "v1/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments",
Path: "kubernetes-dashboard/kubernetes-dashboard-token-d6rt4::kubernetes-dashboard/kubernetes-dashboard-79d9cd965-b4c7d::kubernetes-dashboard/kubernetes-dashboard::-/kubernetes-dashboard::deployments",
Status: fiveOK,
GVRs: []string{"v1/secrets", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"},
Paths: []string{"kubernetes-dashboard/kubernetes-dashboard-token-d6rt4", "kubernetes-dashboard/kubernetes-dashboard-79d9cd965-b4c7d", "kubernetes-dashboard/kubernetes-dashboard", "-/kubernetes-dashboard", "deployments"},
Statuses: fiveOK,
},
},
e: root3(),
@ -193,14 +193,14 @@ func TestTreeNodeFlatten(t *testing.T) {
root: root1(),
e: []xray.NodeSpec{
{
GVR: "containers::v1/pods",
Path: "c1::default/p1",
Status: strings.Join([]string{"ok", "ok"}, xray.PathSeparator),
GVRs: []string{"containers", "v1/pods"},
Paths: []string{"c1", "default/p1"},
Statuses: []string{"ok", "ok"},
},
{
GVR: "containers::v1/pods",
Path: "c2::default/p1",
Status: strings.Join([]string{"ok", "ok"}, xray.PathSeparator),
GVRs: []string{"containers", "v1/pods"},
Paths: []string{"c2", "default/p1"},
Statuses: []string{"ok", "ok"},
},
},
},
@ -208,14 +208,14 @@ func TestTreeNodeFlatten(t *testing.T) {
root: root2(),
e: []xray.NodeSpec{
{
GVR: "v1/secrets::containers::v1/pods",
Path: "s1::c1::default/p1",
Status: strings.Join([]string{"ok", "ok", "ok"}, xray.PathSeparator),
GVRs: []string{"v1/secrets", "containers", "v1/pods"},
Paths: []string{"s1", "c1", "default/p1"},
Statuses: []string{"ok", "ok", "ok"},
},
{
GVR: "v1/secrets::containers::v1/pods",
Path: "s2::c2::default/p1",
Status: strings.Join([]string{"ok", "ok", "ok"}, xray.PathSeparator),
GVRs: []string{"v1/secrets", "containers", "v1/pods"},
Paths: []string{"s2", "c2", "default/p1"},
Statuses: []string{"ok", "ok", "ok"},
},
},
},
@ -323,18 +323,18 @@ func diff1() *xray.TreeNode {
}
func root2() *xray.TreeNode {
n := xray.NewTreeNode("v1/pods", "default/p1")
c1 := xray.NewTreeNode("containers", "c1")
c2 := xray.NewTreeNode("containers", "c2")
n.Add(c1)
n.Add(c2)
s1 := xray.NewTreeNode("v1/secrets", "s1")
c1.Add(s1)
c2 := xray.NewTreeNode("containers", "c2")
s2 := xray.NewTreeNode("v1/secrets", "s2")
c2.Add(s2)
n := xray.NewTreeNode("v1/pods", "default/p1")
n.Add(c1)
n.Add(c2)
return n
}