diff --git a/go.mod b/go.mod index 5af8ab15..b8356134 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 16167445..824aab0a 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/client/client.go b/internal/client/client.go index 7c287284..11a1132e 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -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") - } - _ = a.supportsMxServer() + if err := a.config.SwitchContext(ctx); err != nil { + return err } + a.clearCache() + a.reset() + _ = a.supportsMxServer() + + return nil } func (a *APIClient) reset() { diff --git a/internal/client/config.go b/internal/client/config.go index bcfec129..58f9d74d 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -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 diff --git a/internal/client/types.go b/internal/client/types.go index 91e1f247..48fe4146 100644 --- a/internal/client/types.go +++ b/internal/client/types.go @@ -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) diff --git a/internal/config/mock_connection_test.go b/internal/config/mock_connection_test.go index 57a8104b..d2fd8c48 100644 --- a/internal/config/mock_connection_test.go +++ b/internal/config/mock_connection_test.go @@ -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) { diff --git a/internal/config/styles.go b/internal/config/styles.go index f72c8233..ef7596bd 100644 --- a/internal/config/styles.go +++ b/internal/config/styles.go @@ -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 } diff --git a/internal/dao/container_test.go b/internal/dao/container_test.go index 2b61a9d1..a486017f 100644 --- a/internal/dao/container_test.go +++ b/internal/dao/container_test.go @@ -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 } diff --git a/internal/dao/context.go b/internal/dao/context.go index 1741dbd1..da13e7b2 100644 --- a/internal/dao/context.go +++ b/internal/dao/context.go @@ -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. diff --git a/internal/dao/node.go b/internal/dao/node.go index c821fdc1..ec58eb6d 100644 --- a/internal/dao/node.go +++ b/internal/dao/node.go @@ -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") diff --git a/internal/model/mock_clustermeta_test.go b/internal/model/mock_clustermeta_test.go index cf5426c2..a47a8434 100644 --- a/internal/model/mock_clustermeta_test.go +++ b/internal/model/mock_clustermeta_test.go @@ -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) { diff --git a/internal/model/table.go b/internal/model/table.go index 327d5586..2fbb6376 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -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 diff --git a/internal/ui/logo.go b/internal/ui/logo.go index 7789833e..0935de22 100644 --- a/internal/ui/logo.go +++ b/internal/ui/logo.go @@ -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) diff --git a/internal/ui/tree.go b/internal/ui/tree.go index 61363d14..8179418c 100644 --- a/internal/ui/tree.go +++ b/internal/ui/tree.go @@ -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), diff --git a/internal/view/actions.go b/internal/view/actions.go index 38ccec30..f0ec5529 100644 --- a/internal/view/actions.go +++ b/internal/view/actions.go @@ -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 diff --git a/internal/view/app.go b/internal/view/app.go index 43efb7f8..aded6e6f 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -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 } diff --git a/internal/view/browser.go b/internal/view/browser.go index 4d265775..8dc6d351 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -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) diff --git a/internal/view/context.go b/internal/view/context.go index 54ffb613..fce01e41 100644 --- a/internal/view/context.go +++ b/internal/view/context.go @@ -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 { diff --git a/internal/view/exec.go b/internal/view/exec.go index f85d010c..ff905ca7 100644 --- a/internal/view/exec.go +++ b/internal/view/exec.go @@ -76,7 +76,7 @@ func execute(opts shellOpts) error { cancel() }() - log.Debug().Msgf("Running command > %s %s", opts.binary, strings.Join(opts.args, " ")) + log.Debug().Msgf("Running command> %s %s", opts.binary, strings.Join(opts.args, " ")) cmd := exec.Command(opts.binary, opts.args...) diff --git a/internal/view/helpers.go b/internal/view/helpers.go index 58d05ffe..a48e5057 100644 --- a/internal/view/helpers.go +++ b/internal/view/helpers.go @@ -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 diff --git a/internal/view/pod.go b/internal/view/pod.go index 436fa883..11f50ec0 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -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) -} diff --git a/internal/view/xray.go b/internal/view/xray.go index c4847839..8d0841e9 100644 --- a/internal/view/xray.go +++ b/internal/view/xray.go @@ -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) diff --git a/internal/xray/container.go b/internal/xray/container.go index ae1a8eb5..c4ba5ead 100644 --- a/internal/xray/container.go +++ b/internal/xray/container.go @@ -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() { - parent.Add(root) - } + // if !root.IsLeaf() { + parent.Add(root) + // } return nil } diff --git a/internal/xray/container_test.go b/internal/xray/container_test.go index 31aca2a3..bf18164c 100644 --- a/internal/xray/container_test.go +++ b/internal/xray/container_test.go @@ -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 diff --git a/internal/xray/pod.go b/internal/xray/pod.go index 9a9e0cf6..434253a0 100644 --- a/internal/xray/pod.go +++ b/internal/xray/pod.go @@ -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) } diff --git a/internal/xray/pod_test.go b/internal/xray/pod_test.go index ffa1fbc8..7d144304 100644 --- a/internal/xray/pod_test.go +++ b/internal/xray/pod_test.go @@ -12,27 +12,27 @@ import ( func TestPodRender(t *testing.T) { uu := map[string]struct { - file string - level1, level2 int - status string + file string + count, children int + status string }{ "plain": { - file: "po", - level1: 1, - level2: 3, - status: xray.OkStatus, + file: "po", + children: 1, + count: 7, + status: xray.OkStatus, }, "withInit": { - file: "init", - level1: 1, - level2: 2, - status: xray.OkStatus, + file: "init", + children: 1, + count: 7, + status: xray.OkStatus, }, "cilium": { - file: "cilium", - level1: 1, - level2: 3, - status: xray.OkStatus, + file: "cilium", + 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("")) }) } } diff --git a/internal/xray/test_assets/cilium.json b/internal/xray/testdata/cilium.json similarity index 100% rename from internal/xray/test_assets/cilium.json rename to internal/xray/testdata/cilium.json diff --git a/internal/xray/test_assets/dp.json b/internal/xray/testdata/dp.json similarity index 100% rename from internal/xray/test_assets/dp.json rename to internal/xray/testdata/dp.json diff --git a/internal/xray/test_assets/ds.json b/internal/xray/testdata/ds.json similarity index 100% rename from internal/xray/test_assets/ds.json rename to internal/xray/testdata/ds.json diff --git a/internal/xray/test_assets/init.json b/internal/xray/testdata/init.json similarity index 100% rename from internal/xray/test_assets/init.json rename to internal/xray/testdata/init.json diff --git a/internal/xray/test_assets/ns.json b/internal/xray/testdata/ns.json similarity index 100% rename from internal/xray/test_assets/ns.json rename to internal/xray/testdata/ns.json diff --git a/internal/xray/test_assets/po.json b/internal/xray/testdata/po.json similarity index 100% rename from internal/xray/test_assets/po.json rename to internal/xray/testdata/po.json diff --git a/internal/xray/test_assets/rs.json b/internal/xray/testdata/rs.json similarity index 100% rename from internal/xray/test_assets/rs.json rename to internal/xray/testdata/rs.json diff --git a/internal/xray/test_assets/sa.json b/internal/xray/testdata/sa.json similarity index 100% rename from internal/xray/test_assets/sa.json rename to internal/xray/testdata/sa.json diff --git a/internal/xray/test_assets/sts.json b/internal/xray/testdata/sts.json similarity index 100% rename from internal/xray/test_assets/sts.json rename to internal/xray/testdata/sts.json diff --git a/internal/xray/test_assets/svc.json b/internal/xray/testdata/svc.json similarity index 100% rename from internal/xray/test_assets/svc.json rename to internal/xray/testdata/svc.json diff --git a/internal/xray/tree_node.go b/internal/xray/tree_node.go index bab59321..8825e408 100644 --- a/internal/xray/tree_node.go +++ b/internal/xray/tree_node.go @@ -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) } diff --git a/internal/xray/tree_node_test.go b/internal/xray/tree_node_test.go index 652bc7fd..fbce2e10 100644 --- a/internal/xray/tree_node_test.go +++ b/internal/xray/tree_node_test.go @@ -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 }