From 98710ba2d77f7c5565897f332b3eeca0356567cd Mon Sep 17 00:00:00 2001 From: Mario Constanti Date: Tue, 10 Dec 2024 15:45:08 +0100 Subject: [PATCH] feat: add interactive mode for run subcmd --- README.md | 15 ++++ go.mod | 18 ++++- go.sum | 40 ++++++++-- pkg/command/interactive.go | 69 ++++++++++++++++++ pkg/command/{debug.go => run.go} | 24 +++--- pkg/command/{debug_test.go => run_test.go} | 0 pkg/command/validate.go | 6 +- pkg/profile/type.go | 8 ++ pkg/profile/util.go | 5 ++ pkg/profile/validate.go | 68 ++++++++++++++--- pkg/table/table.go | 85 ++++++++++++++++++++++ 11 files changed, 306 insertions(+), 32 deletions(-) create mode 100644 pkg/command/interactive.go rename pkg/command/{debug.go => run.go} (92%) rename pkg/command/{debug_test.go => run_test.go} (100%) create mode 100644 pkg/table/table.go diff --git a/README.md b/README.md index a9d103a..b630bdc 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,21 @@ This only works, if the `dpm` is run as a `kubectl` plugin. As standalone binary, the `kubectlPath` value must be defined. +### style + +`dpm` has an interactive mode where the user can select the profile to use. +To overwrite the default style, the `style` field can be used. + +```yaml +style: + headerForeground: + headerBackground: + selectedForeground: + selectedBackground: +``` + +The `COLOR` value must be a valid color value either from the [ANSI color list](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors) or the hex value of the color. + ## flags The `dpm` has the following flags: diff --git a/go.mod b/go.mod index 058eef2..91270c3 100644 --- a/go.mod +++ b/go.mod @@ -18,18 +18,25 @@ require ( ) require ( - github.com/fatih/color v1.18.0 + github.com/charmbracelet/bubbles v0.20.0 + github.com/charmbracelet/bubbletea v1.2.4 + github.com/charmbracelet/lipgloss v1.0.0 + github.com/fatih/color v1.9.0 github.com/rodaine/table v1.3.0 ) require ( github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/charmbracelet/x/ansi v0.4.5 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect github.com/fatih/camelcase v1.0.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect @@ -53,9 +60,12 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.6 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -65,10 +75,14 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xlab/treeprint v1.2.0 // indirect diff --git a/go.sum b/go.sum index 83dc7a9..8c3bb4d 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,10 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72H github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -37,6 +41,18 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE= +github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= +github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= +github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= +github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= +github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= +github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -60,14 +76,15 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -226,21 +243,23 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhn github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -277,6 +296,12 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -323,8 +348,9 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rodaine/table v1.3.0 h1:4/3S3SVkHnVZX91EHFvAMV7K42AnJ0XuymRR2C5HlGE= github.com/rodaine/table v1.3.0/go.mod h1:47zRsHar4zw0jgxGxL9YtFfs7EGN6B/TaS+/Dmk4WxU= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= @@ -458,7 +484,7 @@ golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/pkg/command/interactive.go b/pkg/command/interactive.go new file mode 100644 index 0000000..9848652 --- /dev/null +++ b/pkg/command/interactive.go @@ -0,0 +1,69 @@ +package command + +import ( + bubbletable "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + + "github.com/bavarianbidi/kubectl-dpm/pkg/profile" + "github.com/bavarianbidi/kubectl-dpm/pkg/table" +) + +type model struct { + table bubbletable.Model +} + +// initTeaModel initializes the model for the interactive mode +func initTeaModel() model { + // generate a list of profiles which work in interactive mode + interactiveProfiles := profile.InteractiveProfiles() + + // generate the table with image, namespace and matchLabels columns + t := table.GenerateTable(interactiveProfiles, true) + + // read the style config from profile and apply it + profile.CompleteStyle() + + s := bubbletable.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + Background(lipgloss.Color(profile.Config.Style.HeaderBackgroundColor)). + Foreground(lipgloss.Color(profile.Config.Style.HeaderForegroundColor)). + BorderBottom(true). + Bold(true) + s.Selected = s.Selected. + Foreground(lipgloss.Color(profile.Config.Style.SelectedForegroundColor)). + Background(lipgloss.Color(profile.Config.Style.SelectedBackgroundColor)). + Bold(false) + t.SetStyles(s) + + return model{ + table: t, + } +} + +func (m model) Init() tea.Cmd { + return nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + if msg, ok := msg.(tea.KeyMsg); ok { + switch msg.Type { + case tea.KeyCtrlC, tea.KeyCtrlD: + return m, tea.Quit + case tea.KeyEnter: + // set the selected profile name + flagProfileName = m.table.SelectedRow()[0] + + return m, tea.Quit + } + } + m.table, cmd = m.table.Update(msg) + return m, cmd +} + +func (m model) View() string { + return m.table.View() + "\n " + m.table.HelpView() + "\n" +} diff --git a/pkg/command/debug.go b/pkg/command/run.go similarity index 92% rename from pkg/command/debug.go rename to pkg/command/run.go index a058276..5657318 100644 --- a/pkg/command/debug.go +++ b/pkg/command/run.go @@ -7,8 +7,8 @@ import ( "fmt" "os" "os/exec" - "slices" + tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -34,16 +34,22 @@ func NewCmdDebugProfile(_ genericiooptions.IOStreams) *cobra.Command { Long: "create an ephemeral debug container in a pod by using the kubectl debug implementation and a custom profile", RunE: func(c *cobra.Command, args []string) error { - // if no args are given, print help - if c.Flags().NFlag() == 0 { - c.Help() - os.Exit(0) - } - if err := config.GenerateConfig(); err != nil { return err } + // if no args are given, start interactive mode to select a profile + if c.Flags().NFlag() == 0 { + p := tea.NewProgram(initTeaModel()) + if _, err := p.Run(); err != nil { + return fmt.Errorf("error running program: %v", err) + } + + if flagProfileName == "" { + return fmt.Errorf("no profile selected - exiting") + } + } + if err := run(args); err != nil { return err } @@ -90,9 +96,7 @@ func run(args []string) error { } // get the index of the profile where the profile name matches - idx := slices.IndexFunc(profile.Config.Profiles, - func(c profile.Profile) bool { return c.ProfileName == flagProfileName }, - ) + idx := profile.GetProfileIdx(flagProfileName) debugProfile = profile.Config.Profiles[idx] diff --git a/pkg/command/debug_test.go b/pkg/command/run_test.go similarity index 100% rename from pkg/command/debug_test.go rename to pkg/command/run_test.go diff --git a/pkg/command/validate.go b/pkg/command/validate.go index 03d3896..247457d 100644 --- a/pkg/command/validate.go +++ b/pkg/command/validate.go @@ -3,8 +3,6 @@ package command import ( - "fmt" - "github.com/spf13/cobra" "github.com/bavarianbidi/kubectl-dpm/pkg/config" @@ -25,7 +23,9 @@ func ValidateDebugProfileFile() *cobra.Command { return err } - fmt.Printf("all profiles are valid\n") + if err := generateListOutput(); err != nil { + return err + } return nil }, diff --git a/pkg/profile/type.go b/pkg/profile/type.go index b88c9b5..f88b743 100644 --- a/pkg/profile/type.go +++ b/pkg/profile/type.go @@ -19,9 +19,17 @@ type Profile struct { builtInProfile bool } +type Style struct { + HeaderForegroundColor string `koanf:"headerForegroundColor" yaml:"headerForegroundColor"` + HeaderBackgroundColor string `koanf:"headerBackgroundColor" yaml:"headerBackgroundColor"` + SelectedForegroundColor string `koanf:"selectedForegroundColor" yaml:"selectedForegroundColor"` + SelectedBackgroundColor string `koanf:"selectedBackgroundColor" yaml:"selectedBackgroundColor"` +} + type CustomDebugProfile struct { Profiles []Profile `koanf:"profiles" yaml:"profiles"` KubectlPath string `koanf:"kubectlPath" yaml:"kubectlPath"` + Style Style `koanf:"style" yaml:"style"` } // global Profile configuration diff --git a/pkg/profile/util.go b/pkg/profile/util.go index de57b38..6c0ab86 100644 --- a/pkg/profile/util.go +++ b/pkg/profile/util.go @@ -14,3 +14,8 @@ func SortProfiles() { return cmp.Compare(strings.ToLower(a.ProfileName), strings.ToLower(b.ProfileName)) }) } + +func GetProfileIdx(profileName string) int { + // get the index of the profile where the profile name matches + return slices.IndexFunc(Config.Profiles, func(c Profile) bool { return c.ProfileName == profileName }) +} diff --git a/pkg/profile/validate.go b/pkg/profile/validate.go index dfdef40..a256f25 100644 --- a/pkg/profile/validate.go +++ b/pkg/profile/validate.go @@ -67,12 +67,14 @@ func ValidateAllProfiles() error { // update Config.Profiles with compacted profiles Config.Profiles = compactProfiles - for _, p := range Config.Profiles { + for idx, p := range Config.Profiles { switch { case p.ProfileName == "": - return fmt.Errorf("profile %s is missing a custom profile name", p.Profile) + Config.Profiles = slices.Delete(Config.Profiles, idx, idx) + // return fmt.Errorf("profile %s is missing a custom profile name", p.Profile) case p.Profile == "": - return fmt.Errorf("profile name %s is either missing a profile file or the name of a built-in profile", p.ProfileName) + Config.Profiles = slices.Delete(Config.Profiles, idx, idx) + // return fmt.Errorf("profile name %s is either missing a profile file or the name of a built-in profile", p.ProfileName) } if err := ValidateProfile(p.ProfileName); err != nil { @@ -84,9 +86,7 @@ func ValidateAllProfiles() error { // ValidateProfile validates a single profile func ValidateProfile(profileName string) error { - idx := slices.IndexFunc(Config.Profiles, - func(c Profile) bool { return c.ProfileName == profileName }, - ) + idx := GetProfileIdx(profileName) switch Config.Profiles[idx].Profile { case kubectldebug.ProfileLegacy: @@ -109,13 +109,47 @@ func ValidateProfile(profileName string) error { return nil default: if err := validatePodSpec(Config.Profiles[idx].Profile); err != nil { - return err + log.Printf("profile %s is invalid: %s\n", Config.Profiles[idx].Profile, err.Error()) } } return nil } +// InteractiveProfiles returns all profiles that can be used from +// the interactive mode, dpm run (without args) +// these profiles do have all required fields set (namespace, labelSelector, image) +func InteractiveProfiles() []Profile { + // sort profiles by name + SortProfiles() + + ValidateAllProfiles() + + var interactiveProfiles []Profile + + for _, p := range Config.Profiles { + if !namespaceIsMissing(p.ProfileName) && + !labelSelectorIsMissing(p.ProfileName) && + !imageIsMissing(p.ProfileName) { + interactiveProfiles = append(interactiveProfiles, p) + } + } + + return interactiveProfiles +} + +func namespaceIsMissing(profileName string) bool { + return Config.Profiles[GetProfileIdx(profileName)].Namespace == "" +} + +func labelSelectorIsMissing(profileName string) bool { + return Config.Profiles[GetProfileIdx(profileName)].MatchLabels == nil +} + +func imageIsMissing(profileName string) bool { + return Config.Profiles[GetProfileIdx(profileName)].Image == "" +} + func validatePodSpec(podSpec string) error { podSpecByte, err := os.ReadFile(os.ExpandEnv(podSpec)) if err != nil { @@ -134,9 +168,7 @@ func validatePodSpec(podSpec string) error { // CompleteProfile completes a profile with default values func CompleteProfile(profileName string) error { // get the index of the profile where the profile name matches - idx := slices.IndexFunc(Config.Profiles, - func(c Profile) bool { return c.ProfileName == profileName }, - ) + idx := GetProfileIdx(profileName) if Config.Profiles[idx].ImagePullPolicy == "" { Config.Profiles[idx].ImagePullPolicy = corev1.PullIfNotPresent @@ -144,3 +176,19 @@ func CompleteProfile(profileName string) error { return nil } + +// CompleteStyle completes the style with default values +func CompleteStyle() { + if Config.Style.HeaderForegroundColor == "" { + Config.Style.HeaderForegroundColor = "#ffffaf" + } + if Config.Style.HeaderBackgroundColor == "" { + Config.Style.HeaderBackgroundColor = "#5f00ff" + } + if Config.Style.SelectedForegroundColor == "" { + Config.Style.SelectedForegroundColor = "#ffffaf" + } + if Config.Style.SelectedBackgroundColor == "" { + Config.Style.SelectedBackgroundColor = "#5f00ff" + } +} diff --git a/pkg/table/table.go b/pkg/table/table.go new file mode 100644 index 0000000..d8cb057 --- /dev/null +++ b/pkg/table/table.go @@ -0,0 +1,85 @@ +package table + +import ( + "encoding/json" + "log" + + "github.com/charmbracelet/bubbles/table" + + "github.com/bavarianbidi/kubectl-dpm/pkg/profile" +) + +func GenerateTable(profiles []profile.Profile, wide bool) table.Model { + // generate the empty table + rows := []table.Row{} + + // find the longest name and profile + longestName := 0 + longestProfile := 0 + longestImage := 0 + longestNamespace := 0 + longestMatchLabels := 0 + + // get all profiles and get string length for column width + for _, p := range profiles { + if len(p.ProfileName) > longestName { + longestName = len(p.ProfileName) + } + + if len(p.Profile) > longestProfile { + longestProfile = len(p.Profile) + } + + if len(p.Image) > longestImage { + longestImage = len(p.Image) + } + + if len(p.Namespace) > longestNamespace { + longestNamespace = len(p.Namespace) + } + + jsonString, err := json.Marshal(p.MatchLabels) + if err != nil { + log.Printf("error marshalling matchLabels: %v", err) + continue + } + if len(string(jsonString)) > longestMatchLabels { + longestMatchLabels = len(string(jsonString)) + } + + if wide { + rows = append(rows, table.Row{ + p.ProfileName, + p.Profile, + p.Image, + p.Namespace, + string(jsonString), + }) + } else { + rows = append(rows, table.Row{ + p.ProfileName, + p.Profile, + }) + } + } + // define the columns + columns := []table.Column{ + {Title: "Name", Width: longestName}, + {Title: "Profile", Width: longestProfile}, + } + + // add more columns if -w/--wide flag is set + if wide { + columns = append(columns, table.Column{Title: "Image", Width: longestImage}) + columns = append(columns, table.Column{Title: "Namespace", Width: longestNamespace}) + columns = append(columns, table.Column{Title: "MatchLabels", Width: longestMatchLabels}) + } + + t := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + ) + + return t +}