diff --git a/go.mod b/go.mod index 01cf4430..2db5408d 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/Masterminds/semver v1.5.0 github.com/argoproj/argo-rollouts v1.8.3 github.com/chainguard-dev/git-urls v1.0.2 + github.com/charmbracelet/huh v0.7.0 github.com/docker/go-units v0.5.0 github.com/go-git/go-git/v5 v5.14.0 github.com/go-openapi/runtime v0.28.0 @@ -40,11 +41,24 @@ require ( dario.cat/mergo v1.0.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.1.5 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bwmarrin/snowflake v0.3.0 // indirect + github.com/catppuccin/go v0.3.0 // indirect + github.com/charmbracelet/bubbles v0.21.0 // indirect + github.com/charmbracelet/bubbletea v1.3.4 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/danieljoos/wincred v1.2.2 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect @@ -55,6 +69,12 @@ require ( github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect github.com/mwitkow/grpc-proxy v0.0.0-20230212185441-f345521cb9c9 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect @@ -64,6 +84,7 @@ require ( github.com/skeema/knownhosts v1.3.1 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect golang.org/x/sync v0.16.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect @@ -101,7 +122,7 @@ require ( github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/spdystream v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -111,7 +132,7 @@ require ( github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pelletier/go-toml v1.9.4 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect diff --git a/go.sum b/go.sum index e8f73dfa..757dc316 100644 --- a/go.sum +++ b/go.sum @@ -42,6 +42,8 @@ dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= @@ -57,6 +59,12 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +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 v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= @@ -64,11 +72,41 @@ github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= +github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= +github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ= github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o= +github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= +github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc= +github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= +github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= +github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -79,6 +117,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= @@ -89,6 +129,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= @@ -101,6 +143,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/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/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= @@ -281,6 +325,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= @@ -292,8 +338,12 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky 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-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +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/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= @@ -303,6 +353,12 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +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.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 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/grpc-proxy v0.0.0-20230212185441-f345521cb9c9 h1:62uLwA3l2JMH84liO4ZhnjTH5PjFyCYxbHLgXPaJMtI= @@ -346,16 +402,15 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -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/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/signadot/go-sdk v0.3.8-0.20250730210632-13a6fc0827df h1:3Ii88OxvhZFzNru0tAziGti2XBtwrQqChzcCKKigbjU= -github.com/signadot/go-sdk v0.3.8-0.20250730210632-13a6fc0827df/go.mod h1:oX12C9I/8QaXcl9XLIrc6bUOYVCOGrYPExe+Sln/3IY= github.com/signadot/go-sdk v0.3.8-0.20250828202325-847d6b735fc1 h1:nq8WXCqtlN8J615VVwdotSM0493W3EEr1VHOucDf5Rc= github.com/signadot/go-sdk v0.3.8-0.20250828202325-847d6b735fc1/go.mod h1:qgQQHdnLzfDIWJwTvVCfBr5hfPC6pgl/N8yiAMozQLc= github.com/signadot/libconnect v0.1.1-0.20250909135527-85dee197b1f5 h1:Djj9eiOrieTi3HyTGotyaGr+4lwzzohzPwNu9lE05WY= @@ -400,6 +455,8 @@ github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xeonx/timeago v1.0.0-rc5 h1:pwcQGpaH3eLfPtXeyPA4DmHWjoQt0Ea7/++FwpxqLxg= github.com/xeonx/timeago v1.0.0-rc5/go.mod h1:qDLrYEFynLO7y5Ho7w3GwgtYgpy5UfhcXIIQvMKVDkA= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -450,6 +507,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -575,6 +634,7 @@ golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/command/sandbox/command.go b/internal/command/sandbox/command.go index d4265a18..e19779c9 100644 --- a/internal/command/sandbox/command.go +++ b/internal/command/sandbox/command.go @@ -20,6 +20,7 @@ func New(api *config.API) *cobra.Command { newList(cfg), newApply(cfg), newDelete(cfg), + newCreate(cfg), newGetEnv(cfg), newGetFiles(cfg), ) diff --git a/internal/command/sandbox/create.go b/internal/command/sandbox/create.go new file mode 100644 index 00000000..0e54151b --- /dev/null +++ b/internal/command/sandbox/create.go @@ -0,0 +1,163 @@ +package sandbox + +import ( + "context" + "fmt" + "io" + "strings" + "time" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/print" + sandbox_ui "github.com/signadot/cli/internal/ui/sandbox" + "github.com/signadot/go-sdk/client/sandboxes" + "github.com/signadot/go-sdk/models" + "github.com/spf13/cobra" +) + +func newCreate(sandbox *config.Sandbox) *cobra.Command { + cfg := &config.SandboxCreate{Sandbox: sandbox} + + cmd := &cobra.Command{ + Use: "create --kubernetes-workload=KIND/NAMESPACE/NAME [--ttl=DURATION]", + Short: "Create a sandbox from a Kubernetes workload", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return create(cmd.Context(), cfg, cmd.OutOrStdout(), cmd.ErrOrStderr()) + }, + } + cfg.AddFlags(cmd) + return cmd +} + +func create(ctx context.Context, cfg *config.SandboxCreate, out, log io.Writer) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + + sandbox_ui.Run(ctx, cfg) + + // Parse the kubernetes workload + kind, namespace, name, err := parseKubernetesWorkload(cfg.KubernetesWorkload) + if err != nil { + return fmt.Errorf("invalid kubernetes workload format: %v", err) + } + + // Generate sandbox name + sandboxName := generateSandboxName(name) + + // Create sandbox spec + sandboxSpec := &models.Sandbox{ + Name: sandboxName, + Spec: &models.SandboxSpec{ + Cluster: &cfg.Cluster, + Forks: []*models.SandboxFork{ + { + + ForkOf: &models.SandboxForkOf{ + Kind: &kind, + Name: &name, + Namespace: &namespace, + }, + }, + }, + }, + } + + // Add TTL if specified + if cfg.TTL != "" { + sandboxSpec.Spec.TTL = &models.SandboxTTL{ + Duration: cfg.TTL, + } + } + + // Send the request to the API + params := sandboxes.NewApplySandboxParams(). + WithOrgName(cfg.Org).WithSandboxName(sandboxName).WithData(sandboxSpec) + result, err := cfg.Client.Sandboxes.ApplySandbox(params, nil) + if err != nil { + return err + } + resp := result.Payload + + fmt.Fprintf(log, "Created sandbox %q (routing key: %s) in cluster %q.\n\n", + sandboxName, resp.RoutingKey, cfg.Cluster) + + if cfg.Wait { + // Wait for the sandbox to be ready + resp, err = waitForReadyCreate(cfg, log, resp) + if err != nil { + writeOutputCreate(cfg, out, resp) + fmt.Fprintf(log, "\nThe sandbox was created, but it may not be ready yet. To check status, run:\n\n") + fmt.Fprintf(log, " signadot sandbox get %v\n\n", sandboxName) + return err + } + writeOutputCreate(cfg, out, resp) + fmt.Fprintf(log, "\nThe sandbox %q was created and is ready.\n", resp.Name) + return nil + } + return writeOutputCreate(cfg, out, resp) +} + +func parseKubernetesWorkload(workload string) (kind, namespace, name string, err error) { + parts := strings.Split(workload, "/") + if len(parts) != 3 { + return "", "", "", fmt.Errorf("expected format KIND/NAMESPACE/NAME, got %q", workload) + } + return parts[0], parts[1], parts[2], nil +} + +func generateSandboxName(workloadName string) string { + // Generate a unique sandbox name based on workload name and timestamp + timestamp := time.Now().Format("20060102-150405") + return fmt.Sprintf("%s-%s", workloadName, timestamp) +} + +func writeOutputCreate(cfg *config.SandboxCreate, out io.Writer, resp *models.Sandbox) error { + switch cfg.OutputFormat { + case config.OutputFormatDefault: + // Print info on how to access the sandbox + sbURL := cfg.SandboxDashboardURL(resp.Name) + fmt.Fprintf(out, "\nDashboard page: %v\n\n", sbURL) + + if len(resp.Endpoints) > 0 { + if err := printEndpointTable(out, resp.Endpoints); err != nil { + return err + } + } + return nil + case config.OutputFormatJSON: + return print.RawJSON(out, resp) + case config.OutputFormatYAML: + return print.RawYAML(out, resp) + default: + return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat) + } +} + +func waitForReadyCreate(cfg *config.SandboxCreate, out io.Writer, sb *models.Sandbox) (*models.Sandbox, error) { + fmt.Fprintf(out, "Waiting (up to --wait-timeout=%v) for sandbox to be ready...\n", cfg.WaitTimeout) + + params := sandboxes.NewGetSandboxParams().WithOrgName(cfg.Org).WithSandboxName(sb.Name) + + // Simple polling implementation (can be enhanced with spinner like in apply.go) + timeout := time.After(cfg.WaitTimeout) + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-timeout: + return sb, fmt.Errorf("timeout waiting for sandbox to be ready") + case <-ticker.C: + result, err := cfg.Client.Sandboxes.GetSandbox(params, nil) + if err != nil { + continue // Keep retrying + } + sb = result.Payload + if sb.Status.Ready { + return sb, nil + } + } + } +} diff --git a/internal/config/root.go b/internal/config/root.go index 06a016ed..caf4e245 100644 --- a/internal/config/root.go +++ b/internal/config/root.go @@ -15,15 +15,17 @@ type Root struct { DashboardURL *url.URL // Flags - Debug bool - ConfigFile string - OutputFormat OutputFormat + Debug bool + ConfigFile string + OutputFormat OutputFormat + NoInteractive bool } func (c *Root) AddFlags(cmd *cobra.Command) { cmd.PersistentFlags().BoolVar(&c.Debug, "debug", false, "enable debug output") cmd.PersistentFlags().StringVar(&c.ConfigFile, "config", "", "config file (default is $HOME/.signadot/config.yaml)") cmd.PersistentFlags().VarP(&c.OutputFormat, "output", "o", "output format (json|yaml)") + cmd.PersistentFlags().BoolVar(&c.NoInteractive, "no-interactive", false, "disable interactive mode") } func (c *Root) Init() { diff --git a/internal/config/sandbox.go b/internal/config/sandbox.go index 5b5dcdd3..995336b1 100644 --- a/internal/config/sandbox.go +++ b/internal/config/sandbox.go @@ -1,86 +1,131 @@ package config import ( - "time" + "time" - "github.com/spf13/cobra" + "github.com/spf13/cobra" ) type Sandbox struct { - *API + *API } type SandboxApply struct { - *Sandbox + *Sandbox - // Flags - Filename string - Wait bool - WaitTimeout time.Duration - TemplateVals TemplateVals + // Flags + Filename string + Wait bool + WaitTimeout time.Duration + TemplateVals TemplateVals } func (c *SandboxApply) AddFlags(cmd *cobra.Command) { - cmd.Flags().StringVarP(&c.Filename, "filename", "f", "", "YAML or JSON file containing the sandbox creation request") - cmd.Flags().BoolVar(&c.Wait, "wait", true, "wait for the sandbox status to be Ready before returning") - cmd.Flags().DurationVar(&c.WaitTimeout, "wait-timeout", 3*time.Minute, "timeout when waiting for the sandbox to be Ready") - cmd.MarkFlagRequired("filename") - cmd.Flags().Var(&c.TemplateVals, "set", "--set var=val") + cmd.Flags().StringVarP(&c.Filename, "filename", "f", "", "YAML or JSON file containing the sandbox creation request") + cmd.Flags().BoolVar(&c.Wait, "wait", true, "wait for the sandbox status to be Ready before returning") + cmd.Flags().DurationVar(&c.WaitTimeout, "wait-timeout", 3*time.Minute, "timeout when waiting for the sandbox to be Ready") + cmd.MarkFlagRequired("filename") + cmd.Flags().Var(&c.TemplateVals, "set", "--set var=val") } type SandboxDelete struct { - *Sandbox - - // Flags - Filename string - Wait bool - WaitTimeout time.Duration - TemplateVals TemplateVals - Force bool + *Sandbox + + // Flags + Filename string + Wait bool + WaitTimeout time.Duration + TemplateVals TemplateVals + Force bool } func (c *SandboxDelete) AddFlags(cmd *cobra.Command) { - cmd.Flags().StringVarP(&c.Filename, "filename", "f", "", "optional YAML or JSON file containing the original sandbox creation request") - cmd.Flags().BoolVar(&c.Wait, "wait", true, "wait for the sandbox to finish terminating before returning") - cmd.Flags().DurationVar(&c.WaitTimeout, "wait-timeout", 5*time.Minute, "timeout when waiting for the sandbox to finish terminating") - cmd.Flags().BoolVar(&c.Force, "force", false, "force delete the sandbox, removing resources without deprovisioning them") - cmd.Flags().Var(&c.TemplateVals, "set", "--set var=val") + cmd.Flags().StringVarP(&c.Filename, "filename", "f", "", "optional YAML or JSON file containing the original sandbox creation request") + cmd.Flags().BoolVar(&c.Wait, "wait", true, "wait for the sandbox to finish terminating before returning") + cmd.Flags().DurationVar(&c.WaitTimeout, "wait-timeout", 5*time.Minute, "timeout when waiting for the sandbox to finish terminating") + cmd.Flags().BoolVar(&c.Force, "force", false, "force delete the sandbox, removing resources without deprovisioning them") + cmd.Flags().Var(&c.TemplateVals, "set", "--set var=val") } type SandboxGet struct { - *Sandbox + *Sandbox } type SandboxList struct { - *Sandbox + *Sandbox } type SandboxGetFiles struct { - *Sandbox - Local string - Container string - OutputDir string - NoClobber bool + *Sandbox + Local string + Container string + OutputDir string + NoClobber bool } func (c *SandboxGetFiles) AddFlags(cmd *cobra.Command) { - cmd.Flags().StringVarP(&c.Local, "local", "l", "", "local workload name (default to first)") - cmd.Flags().StringVarP(&c.Container, "container", "c", "", "container name (defaults to first)") - cmd.Flags().BoolVar(&c.NoClobber, "no-clobber", false, "do not overwrite files") - cmd.Flags().StringVarP(&c.OutputDir, "output-dir", "d", "", "output directory") + cmd.Flags().StringVarP(&c.Local, "local", "l", "", "local workload name (default to first)") + cmd.Flags().StringVarP(&c.Container, "container", "c", "", "container name (defaults to first)") + cmd.Flags().BoolVar(&c.NoClobber, "no-clobber", false, "do not overwrite files") + cmd.Flags().StringVarP(&c.OutputDir, "output-dir", "d", "", "output directory") } type SandboxCleanFiles struct { - *Sandbox + *Sandbox } type SandboxGetEnv struct { - *Sandbox - Local string - Container string + *Sandbox + Local string + Container string } func (c *SandboxGetEnv) AddFlags(cmd *cobra.Command) { - cmd.Flags().StringVarP(&c.Local, "local", "l", "", "local workload name (default to first)") - cmd.Flags().StringVarP(&c.Container, "container", "c", "", "container") + cmd.Flags().StringVarP(&c.Local, "local", "l", "", "local workload name (default to first)") + cmd.Flags().StringVarP(&c.Container, "container", "c", "", "container") +} + +type SandboxCreate struct { + *Sandbox + + // Flags + Cluster string + KubernetesWorkload string + TTL string + Wait bool + WaitTimeout time.Duration +} + +func (c *SandboxCreate) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&c.Cluster, "cluster", "", "specify cluster connection config") + cmd.Flags().StringVar(&c.KubernetesWorkload, "kubernetes-workload", "", "Kubernetes workload in format kind/namespace/name (e.g., deployment/default/myapp)") + cmd.Flags().StringVar(&c.TTL, "ttl", "", "Time to live for the sandbox (e.g., 20h, 30m, 1d)") + cmd.Flags().BoolVar(&c.Wait, "wait", true, "wait for the sandbox status to be Ready before returning") + cmd.Flags().DurationVar(&c.WaitTimeout, "wait-timeout", 3*time.Minute, "timeout when waiting for the sandbox to be Ready") +} + +type SandboxSetImage struct { + *Sandbox + + // Flags + Workload string + Image string +} + +func (c *SandboxSetImage) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&c.Workload, "workload", "", "workload name to set image for") + cmd.MarkFlagRequired("workload") +} + +type SandboxSetEnv struct { + *Sandbox + + // Flags + Workload string + EnvVars []string +} + +func (c *SandboxSetEnv) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&c.Workload, "workload", "", "workload name to set environment variables for") + cmd.MarkFlagRequired("workload") } diff --git a/internal/ui/sandbox/create.go b/internal/ui/sandbox/create.go new file mode 100644 index 00000000..3e4cf169 --- /dev/null +++ b/internal/ui/sandbox/create.go @@ -0,0 +1,164 @@ +package sandbox_ui + +import ( + "context" + "fmt" + "time" + + "github.com/signadot/cli/internal/config" + + "github.com/charmbracelet/huh" +) + +type SandboxCreateForm struct { + formData *config.SandboxCreate +} + +func Run(ctx context.Context, cfg *config.SandboxCreate) error { + + s := SandboxCreateForm{ + formData: cfg, + } + + if cfg.NoInteractive { + return nil + } + + if cfg.Cluster == "" { + err := s.getClusterInput().Run() + if err != nil { + return err + } + } + + if cfg.KubernetesWorkload == "" { + err := s.getKubernetesWorkloadInput() + if err != nil { + return err + } + } + + if cfg.TTL == "" { + err := s.getTTLInput() + if err != nil { + return err + } + } + + return nil +} + +func getClusterOptions() []huh.Option[string] { + return []huh.Option[string]{ + {Value: "signadot-staging", Key: "signadot-staging"}, + {Value: "signadot-production", Key: "signadot-production"}, + {Value: "demo", Key: "demo"}, + } +} + +func (s *SandboxCreateForm) getClusterInput() *huh.Select[string] { + return huh. + NewSelect[string](). + Title("Cluster"). + Description("The cluster to create the sandbox in"). + Value(&s.formData.Cluster). + Options(getClusterOptions()...) + +} + +func (s *SandboxCreateForm) getKubernetesWorkloadInput() error { + + type workload struct { + Kind string + Namespace string + Name string + } + + w := workload{} + + kindRollout := huh.NewOption("Rollout", "Rollout") + kindDeployment := huh.NewOption("Deployment", "Deployment") + + kind := huh.NewSelect[string](). + Title("Kind"). + Description("The kind of the Kubernetes workload"). + Value(&w.Kind). + Options(kindRollout, kindDeployment) + + if err := kind.Run(); err != nil { + return err + } + + n1 := huh.NewOption("default", "default") + n2 := huh.NewOption("hotrod", "hotrod") + n3 := huh.NewOption("hotrod-devmesh", "hotrod-devmesh") + + namespace := huh.NewSelect[string](). + Title("Namespace"). + Description("The namespace of the Kubernetes workload"). + Value(&w.Namespace). + Options(n1, n2, n3) + + if err := namespace.Run(); err != nil { + return err + } + + name := huh.NewInput(). + Title("Name"). + Description("The name of the Kubernetes workload"). + Value(&w.Name). + SuggestionsFunc(func() []string { + return []string{w.Kind, w.Namespace} + }, nil) + + if err := name.Run(); err != nil { + return err + } + + s.formData.KubernetesWorkload = fmt.Sprintf("%s/%s/%s", w.Kind, w.Namespace, w.Name) + + return nil +} + +func (s *SandboxCreateForm) getTTLInput() error { + // First ask if user wants to add TTL + wantsTTL := false + confirm := huh.NewConfirm(). + Title("Add TTL?"). + Description("Do you want to set a time-to-live for this sandbox?"). + Value(&wantsTTL) + + if err := confirm.Run(); err != nil { + return err + } + + if !wantsTTL { + // User doesn't want TTL, leave it empty + return nil + } + + // User wants TTL, ask for the duration + ttlInput := huh.NewInput(). + Title("TTL Duration"). + Description("Enter the TTL duration (e.g., 1h, 30m, 2d, 1w)"). + Value(&s.formData.TTL). + Validate(func(str string) error { + if str == "" { + return fmt.Errorf("TTL duration cannot be empty") + } + // Validate that it's a valid Go duration + _, err := time.ParseDuration(str) + if err != nil { + // Try parsing with day and week units (not supported by time.ParseDuration) + // but commonly used in TTL contexts + if str == "1d" || str == "2d" || str == "3d" || str == "7d" || + str == "1w" || str == "2w" || str == "3w" || str == "4w" { + return nil + } + return fmt.Errorf("invalid duration format: %v. Use formats like 1h, 30m, 2d, 1w", err) + } + return nil + }) + + return ttlInput.Run() +}