diff --git a/go.mod b/go.mod index 39fcf928..9b250fbf 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.23 toolchain go1.23.0 require ( - github.com/AlecAivazis/survey/v2 v2.3.6 github.com/aws/aws-sdk-go-v2 v1.30.3 github.com/aws/aws-sdk-go-v2/config v1.27.27 github.com/aws/aws-sdk-go-v2/service/backup v1.36.3 @@ -14,6 +13,8 @@ require ( github.com/aws/aws-sdk-go-v2/service/iam v1.34.3 github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2 github.com/aws/smithy-go v1.20.3 + github.com/charmbracelet/bubbletea v0.27.0 + github.com/fatih/color v1.17.0 github.com/olekukonko/tablewriter v0.0.5 github.com/rs/zerolog v1.33.0 github.com/urfave/cli/v2 v2.27.4 @@ -37,16 +38,23 @@ require ( github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect + github.com/charmbracelet/x/ansi v0.1.4 // indirect + github.com/charmbracelet/x/input v0.1.0 // indirect + github.com/charmbracelet/x/term v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-runewidth v0.0.9 // indirect - github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // 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.15 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect - golang.org/x/sys v0.12.0 // indirect - golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect + golang.org/x/sys v0.24.0 // indirect golang.org/x/text v0.3.8 // indirect ) diff --git a/go.sum b/go.sum index fe9f5adc..2c690249 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,3 @@ -github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw= -github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg= @@ -46,74 +42,86 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudr github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ= github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/charmbracelet/bubbletea v0.27.0 h1:Mznj+vvYuYagD9Pn2mY7fuelGvP0HAXtZYGgRBCbHvU= +github.com/charmbracelet/bubbletea v0.27.0/go.mod h1:5MdP9XH6MbQkgGhnlxUqCNmBXf9I74KRQ8HIidRxV1Y= +github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM= +github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ= +github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28= +github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= +github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= +github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4= +github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= -github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/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/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 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.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +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.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +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/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= +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/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w= -golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/app/app.go b/internal/app/app.go index f4e4d5c6..d7f8cff6 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -90,16 +90,18 @@ func (a *App) getAction() func(c *cli.Context) error { deduplicatedStackNames := a.deduplicateStackNames() - sortedStackNames, err := a.getSortedStackNames(c.Context, cloudformationStackOperator, deduplicatedStackNames) + sortedStackNames, continuation, err := a.getSortedStackNames(c.Context, cloudformationStackOperator, deduplicatedStackNames) if err != nil { return err } - // The case for interruption(Ctrl + C) - if len(sortedStackNames) == 0 { + if !continuation { return nil } - targetStacks := a.attachTargetResourceTypes(sortedStackNames, deduplicatedStackNames) + targetStacks, err := a.attachTargetResourceTypes(sortedStackNames, deduplicatedStackNames) + if err != nil { + return err + } // Explanation of deletion order in the case of multiple stacks if len(targetStacks) > 1 { io.Logger.Info().Msg("The stacks are removed in order of the latest creation time, taking into account dependencies.") @@ -142,41 +144,49 @@ func (a *App) deduplicateStackNames() []string { return deduplicatedStackNames } -func (a *App) getSortedStackNames(ctx context.Context, cloudformationStackOperator *operation.CloudFormationStackOperator, specifiedStackNames []string) ([]string, error) { - sortedStackNames := []string{} - +func (a *App) getSortedStackNames(ctx context.Context, cloudformationStackOperator *operation.CloudFormationStackOperator, specifiedStackNames []string) ([]string, bool, error) { if len(specifiedStackNames) != 0 { stackNames, err := cloudformationStackOperator.GetSortedStackNames(ctx, specifiedStackNames) if err != nil { - return sortedStackNames, err + return nil, false, err } - sortedStackNames = append(sortedStackNames, stackNames...) - } else if a.InteractiveMode { + return stackNames, true, nil + } + + if a.InteractiveMode { keyword := a.inputKeywordForFilter() stacks, err := cloudformationStackOperator.ListStacksFilteredByKeyword(ctx, aws.String(keyword)) if err != nil { - return sortedStackNames, err + return nil, false, err } // The `ListStacksFilteredByKeyword` with SDK's `DescribeStacks` returns the stacks in descending order of CreationTime. // Therefore, by deleting stacks in the same order, we can delete from a new stack that is not depended on by any stack. - stackNames := a.selectStackNames(stacks) - sortedStackNames = append(sortedStackNames, stackNames...) + stackNames, continuation, err := a.selectStackNames(stacks) + if err != nil { + return nil, false, err + } + + return stackNames, continuation, nil } - return sortedStackNames, nil + // never reach here + return nil, false, nil } -func (a *App) attachTargetResourceTypes(sortedStackNames []string, specifiedStackNames []string) []targetStack { +func (a *App) attachTargetResourceTypes(sortedStackNames []string, specifiedStackNames []string) ([]targetStack, error) { targetStacks := []targetStack{} // If stackNames are specified with an interactive mode option, select ResourceTypes in the order specified (not sorted order). if a.InteractiveMode && len(specifiedStackNames) != 0 { var selectedResourceTypes []targetStack for _, stackName := range specifiedStackNames { - targetResourceTypes, continuation := a.selectResourceTypes(stackName) + targetResourceTypes, continuation, err := a.selectResourceTypes(stackName) + if err != nil { + return nil, err + } if !continuation { - return nil + return nil, nil } selectedResourceTypes = append(selectedResourceTypes, targetStack{ stackName: stackName, @@ -193,9 +203,12 @@ func (a *App) attachTargetResourceTypes(sortedStackNames []string, specifiedStac } if a.InteractiveMode && len(specifiedStackNames) == 0 { for _, stackName := range sortedStackNames { - targetResourceTypes, continuation := a.selectResourceTypes(stackName) + targetResourceTypes, continuation, err := a.selectResourceTypes(stackName) + if err != nil { + return nil, err + } if !continuation { - return nil + return nil, nil } targetStacks = append(targetStacks, targetStack{ stackName: stackName, @@ -213,7 +226,7 @@ func (a *App) attachTargetResourceTypes(sortedStackNames []string, specifiedStac } } - return targetStacks + return targetStacks, nil } func (a *App) inputKeywordForFilter() string { @@ -221,65 +234,33 @@ func (a *App) inputKeywordForFilter() string { return io.InputKeywordForFilter(label) } -func (a *App) selectResourceTypes(stackName string) ([]string, bool) { +func (a *App) selectResourceTypes(stackName string) ([]string, bool, error) { var checkboxes []string - label := stackName + - "\n" + - "Select ResourceTypes you wish to delete even if DELETE_FAILED.\n" + - "However, if a resource can be deleted without becoming DELETE_FAILED by the normal CloudFormation stack deletion feature, the resource will be deleted even if you do not select that resource type. " + - "\n" + label := []string{ + stackName, + "Select ResourceTypes you wish to delete even if DELETE_FAILED.", + "However, if a resource can be deleted without becoming DELETE_FAILED by the normal CloudFormation stack deletion feature, the resource will be deleted even if you do not select that resource type.", + } opts := resourcetype.GetResourceTypes() - for { - checkboxes = io.GetCheckboxes(label, opts) - - if len(checkboxes) == 0 { - ok := io.GetYesNo("No selection?") - if ok { - return checkboxes, true - } - - // The case for interruption(Ctrl + C) - ok = io.GetYesNo("Do you want to finish?") - if ok { - io.Logger.Info().Msg("Finished...") - return checkboxes, false - } - continue - } - - ok := io.GetYesNo("OK?") - if ok { - return checkboxes, true - } + checkboxes, continuation, err := io.GetCheckboxes(label, opts, true) + if err != nil { + return nil, false, err } + return checkboxes, continuation, nil } -func (a *App) selectStackNames(stackNames []string) []string { - var selectedStackNames []string - - label := "Select StackNames." + "\n" + - "Nested child stacks, XXX_IN_PROGRESS(e.g. ROLLBACK_IN_PROGRESS) status stacks and EnableTerminationProtection stacks are not displayed." + - "\n" - - for { - selectedStackNames = io.GetCheckboxes(label, stackNames) - - if len(selectedStackNames) == 0 { - // The case for interruption(Ctrl + C) - ok := io.GetYesNo("Do you want to finish?") - if ok { - io.Logger.Info().Msg("Finished...") - return selectedStackNames - } - continue - } +func (a *App) selectStackNames(stackNames []string) ([]string, bool, error) { + label := []string{ + "Select StackNames.", + "Nested child stacks, XXX_IN_PROGRESS(e.g. ROLLBACK_IN_PROGRESS) status stacks and EnableTerminationProtection stacks are not displayed.", + } - ok := io.GetYesNo("OK?") - if ok { - return selectedStackNames - } + selectedStackNames, continuation, err := io.GetCheckboxes(label, stackNames, false) + if err != nil { + return nil, false, err } + return selectedStackNames, continuation, nil } diff --git a/internal/io/input.go b/internal/io/input.go index 5179dbfb..7b8b99ec 100644 --- a/internal/io/input.go +++ b/internal/io/input.go @@ -6,23 +6,52 @@ import ( "os" "strings" - "github.com/AlecAivazis/survey/v2" + tea "github.com/charmbracelet/bubbletea" + "github.com/fatih/color" ) -const SelectionPageSize = 20 +func GetCheckboxes(headers []string, opts []string, noSelectionMode bool) ([]string, bool, error) { + for { + ui := NewUI(opts, headers) + p := tea.NewProgram(ui) + if _, err := p.Run(); err != nil { + return nil, false, err + } -func GetCheckboxes(label string, opts []string) []string { - res := []string{} + checkboxes := []string{} + for c := range ui.Choices { + if _, ok := ui.Selected[c]; ok { + checkboxes = append(checkboxes, ui.Choices[c]) + } + } - prompt := &survey.MultiSelect{ - Message: label, - Options: opts, - PageSize: SelectionPageSize, - } - //nolint:errcheck - survey.AskOne(prompt, &res, survey.WithKeepFilter(true)) + switch { + case ui.IsCanceled: + Logger.Warn().Msg("Canceled!") + case len(checkboxes) == 0 && noSelectionMode: + ok := GetYesNo("No selection?") + if ok { + return checkboxes, true, nil + } + case len(checkboxes) == 0: + Logger.Warn().Msg("Not selected!") + } + if len(checkboxes) == 0 || ui.IsCanceled { + ok := GetYesNo("Do you want to finish?") + if ok { + Logger.Info().Msg("Finished...") + return checkboxes, false, nil + } + continue + } + + fmt.Fprintf(os.Stderr, " %s\n", color.CyanString(strings.Join(checkboxes, ", "))) - return res + ok := GetYesNo("OK?") + if ok { + return checkboxes, true, nil + } + } } func InputKeywordForFilter(label string) string { diff --git a/internal/io/ui.go b/internal/io/ui.go new file mode 100644 index 00000000..b7810ce7 --- /dev/null +++ b/internal/io/ui.go @@ -0,0 +1,313 @@ +package io + +import ( + "fmt" + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/fatih/color" +) + +const SelectionPageSize = 20 + +type UI struct { + Choices []string + Headers []string + Cursor int + Selected map[int]struct{} + Filtered *Filtered + Keyword string + IsEntered bool + IsCanceled bool +} + +type Filtered struct { + Choices map[int]struct{} + Prev *Filtered + Cursor int +} + +var _ tea.Model = (*UI)(nil) + +func NewUI(choices []string, headers []string) *UI { + return &UI{ + Choices: choices, + Headers: headers, + Selected: make(map[int]struct{}), + } +} + +func (u *UI) Init() tea.Cmd { + filtered := make(map[int]struct{}) + for i := range u.Choices { + filtered[i] = struct{}{} + } + u.Filtered = &Filtered{Choices: filtered} + + return nil +} + +func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + //nolint:gocritic + switch msg := msg.(type) { + + case tea.KeyMsg: + + switch msg.Type { + + // Quit the selection + case tea.KeyEnter: + u.IsEntered = true + return u, tea.Quit + + // Quit the selection + case tea.KeyCtrlC: + u.IsCanceled = true + return u, tea.Quit + + case tea.KeyUp, tea.KeyShiftTab: + if len(u.Filtered.Choices) < 2 { + return u, nil + } + for range u.Choices { + if u.Cursor == 0 { + u.Cursor = len(u.Choices) - 1 + } else if u.Cursor > 0 { + u.Cursor-- + } + + if _, ok := u.Filtered.Choices[u.Cursor]; !ok { + continue + } + if u.Filtered.Cursor == 0 { + u.Filtered.Cursor = len(u.Filtered.Choices) - 1 + } else if u.Filtered.Cursor > 0 { + u.Filtered.Cursor-- + } + + f := u.Filtered + for f.Prev != nil { + f.Prev.Cursor = u.Filtered.Cursor + f = f.Prev + } + break + } + + case tea.KeyDown, tea.KeyTab: + if len(u.Filtered.Choices) < 2 { + return u, nil + } + for range u.Choices { + if u.Cursor < len(u.Choices)-1 { + u.Cursor++ + } else if u.Cursor == len(u.Choices)-1 { + u.Cursor = 0 + } + + if _, ok := u.Filtered.Choices[u.Cursor]; !ok { + continue + } + if u.Filtered.Cursor < len(u.Filtered.Choices)-1 { + u.Filtered.Cursor++ + } else if u.Filtered.Cursor == len(u.Filtered.Choices)-1 { + u.Filtered.Cursor = 0 + } + + f := u.Filtered + for f.Prev != nil { + f.Prev.Cursor = u.Filtered.Cursor + f = f.Prev + } + break + } + + // select or deselect an item + case tea.KeySpace: + if _, ok := u.Filtered.Choices[u.Cursor]; !ok { + return u, nil + } + _, ok := u.Selected[u.Cursor] + if ok { + delete(u.Selected, u.Cursor) + } else { + u.Selected[u.Cursor] = struct{}{} + } + + // select all items in filtered list + case tea.KeyRight: + for i := range u.Choices { + if _, ok := u.Filtered.Choices[i]; !ok { + continue + } + _, ok := u.Selected[i] + if !ok { + u.Selected[i] = struct{}{} + } + } + + // clear all selected items in filtered list + case tea.KeyLeft: + for i := range u.Choices { + if _, ok := u.Filtered.Choices[i]; !ok { + continue + } + _, ok := u.Selected[i] + if ok { + delete(u.Selected, i) + } + } + + // clear one character from the keyword + case tea.KeyBackspace: + u.backspace() + + // clear the keyword + case tea.KeyCtrlW: + for u.Keyword != "" { + u.backspace() + } + + // add a character to the keyword + case tea.KeyRunes: + str := msg.String() + if !msg.Paste { + for _, r := range str { + u.addCharacter(string(r)) + } + } else { + if strings.Contains(str, string('\n')) || strings.Contains(str, string('\r')) { + u.IsEntered = true + return u, tea.Quit + } + + runes := []rune(str) + for i, r := range runes { + // characters by paste key are enclosed by '[' and ']' + if i == 0 || i == len(runes)-1 { + continue + } + if r != ' ' && r != '\t' { + u.addCharacter(string(r)) + } + } + } + + } + } + + return u, nil +} + +func (u *UI) backspace() { + if len(u.Keyword) == 0 { + return + } + + keywordRunes := []rune(u.Keyword) + keywordRunes = keywordRunes[:len(keywordRunes)-1] + u.Keyword = string(keywordRunes) + u.Filtered = u.Filtered.Prev + cnt := 0 + for i := range u.Choices { + if _, ok := u.Filtered.Choices[i]; !ok { + continue + } + if cnt == u.Filtered.Cursor { + u.Cursor = i + break + } + cnt++ + } +} + +func (u *UI) addCharacter(c string) { + u.Keyword += c + u.Filtered = &Filtered{ + Choices: make(map[int]struct{}), + Prev: u.Filtered, + } + + tmpCursor := u.Cursor + for i, choice := range u.Choices { + lk := strings.ToLower(u.Keyword) + lc := strings.ToLower(choice) + contains := strings.Contains(lc, lk) + + fLen := len(u.Filtered.Choices) + if contains && fLen != 0 && fLen <= u.Filtered.Prev.Cursor { + u.Filtered.Cursor++ + u.Cursor = i + } + + switch { + case contains: + u.Filtered.Choices[i] = struct{}{} + tmpCursor = i + case u.Cursor == i && u.Cursor < len(u.Choices)-1: + u.Cursor++ + case u.Cursor == i: + u.Cursor = tmpCursor + } + } + + if len(u.Filtered.Choices) == 0 { + return + } + f := u.Filtered + for f.Prev != nil { + f.Prev.Cursor = u.Filtered.Cursor + f = f.Prev + } +} + +func (u *UI) View() string { + bold := color.New(color.Bold) + + s := color.CyanString("? ") + + for _, header := range u.Headers { + s += bold.Sprintln(header) + } + + if u.IsEntered && len(u.Selected) != 0 { + return s + } + + s += bold.Sprintln(u.Keyword) + + s += color.CyanString(" [Use arrows to move, space to select, to all, to none, type to filter]") + s += "\n" + + var contents []string + for i, choice := range u.Choices { + if _, ok := u.Filtered.Choices[i]; !ok { + continue + } + + cursor := " " // no cursor + if u.Cursor == i { + cursor = color.CyanString(bold.Sprint(">")) // cursor! + } + + checked := bold.Sprint("[ ]") // not selected + if _, ok := u.Selected[i]; ok { + checked = color.GreenString("[x]") // selected! + } + + contents = append(contents, fmt.Sprintf("%s %s %s\n", cursor, checked, choice)) + } + + if len(contents) > SelectionPageSize { + switch { + case u.Filtered.Cursor < SelectionPageSize/2: + contents = contents[:SelectionPageSize] + case u.Filtered.Cursor > len(contents)-SelectionPageSize/2: + contents = contents[len(contents)-SelectionPageSize:] + default: + contents = contents[u.Filtered.Cursor-SelectionPageSize/2 : u.Filtered.Cursor+SelectionPageSize/2] + } + } + + s += strings.Join(contents, "") + return s +}