From 878aedf2932f6ae5ff0c8c2013039df606fc2ccc Mon Sep 17 00:00:00 2001 From: Dan Pantry Date: Wed, 12 Feb 2025 19:20:09 -0800 Subject: [PATCH 1/8] Add huh for interactive prompts --- command/get.go | 6 ++++++ go.mod | 24 +++++++++++++++++++++++- go.sum | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/command/get.go b/command/get.go index bb661c60..ad7b9c16 100644 --- a/command/get.go +++ b/command/get.go @@ -11,6 +11,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/charmbracelet/huh" "github.com/riotgames/key-conjurer/pkg/oauth2cli" "github.com/spf13/cobra" ) @@ -107,6 +108,11 @@ func (g GetCommand) printUsage() error { return g.UsageFunc() } +func (g GetCommand) rolesInteractivePrompt(roles []string) (string, error) { + huh.NewForm() + return "", nil +} + func (g GetCommand) Execute(ctx context.Context, config *Config) error { var accountID string if g.AccountIDOrName != "" { diff --git a/go.mod b/go.mod index c32bd720..5abf5f3b 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.28.3 github.com/aws/aws-sdk-go-v2/service/sts v1.32.4 github.com/aws/smithy-go v1.22.0 + github.com/charmbracelet/huh v0.6.0 github.com/coreos/go-oidc v2.2.1+incompatible github.com/go-ini/ini v1.61.0 github.com/hashicorp/vault/api v1.15.0 @@ -24,6 +25,7 @@ require ( require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect + github.com/atotto/clipboard v0.1.4 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.44 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 // indirect @@ -33,9 +35,19 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.24.5 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/catppuccin/go v0.2.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/charmbracelet/bubbles v0.20.0 // indirect + github.com/charmbracelet/bubbletea v1.1.0 // indirect + github.com/charmbracelet/lipgloss v0.13.0 // indirect + github.com/charmbracelet/x/ansi v0.2.3 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect + github.com/charmbracelet/x/term v0.2.0 // indirect github.com/danieljoos/wincred v1.2.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/go-jose/go-jose/v4 v4.0.1 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -51,17 +63,27 @@ require ( github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/kelseyhightower/envconfig v1.4.0 // indirect github.com/kr/text v0.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // 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/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/mapstructure v1.5.0 // 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.3-0.20240618155329-98d742f6907a // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pquerna/cachecontrol v0.2.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a // indirect golang.org/x/crypto v0.23.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.15.0 // indirect + golang.org/x/text v0.18.0 // indirect golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/ini.v1 v1.42.0 // indirect diff --git a/go.sum b/go.sum index cd105366..e97c0ac1 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWt github.com/RobotsAndPencils/go-saml v0.0.0-20170520135329-fb13cb52a46b h1:EgJ6N2S0h1WfFIjU5/VVHWbMSVYXAluop97Qxpr/lfQ= github.com/RobotsAndPencils/go-saml v0.0.0-20170520135329-fb13cb52a46b/go.mod h1:3SAoF0F5EbcOuBD5WT9nYkbIJieBS84cUQXADbXeBsU= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-lambda-go v1.47.0 h1:0H8s0vumYx/YKs4sE7YM0ktwL2eWse+kfopsRI1sXVI= github.com/aws/aws-lambda-go v1.47.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= github.com/aws/aws-sdk-go-v2 v1.32.4 h1:S13INUiTxgrPueTmrm5DZ+MiAo99zYzHEFh1UNkOxNE= @@ -31,10 +33,28 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.32.4 h1:yDxvkz3/uOKfxnv8YhzOi9m+2OGI github.com/aws/aws-sdk-go-v2/service/sts v1.32.4/go.mod h1:9XEUty5v5UAsMiFOBJrNibZgwCeOma73jgGwwhgffa8= github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= +github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +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.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c= +github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= +github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8= +github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +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.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -44,6 +64,10 @@ github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7 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/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/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.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= @@ -101,21 +125,35 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 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.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/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +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.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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/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.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= @@ -131,6 +169,9 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/pquerna/cachecontrol v0.2.0 h1:vBXSNuE5MYP9IJ5kjsdo8uq+w41jSPgvba2DEnkRx9k= github.com/pquerna/cachecontrol v0.2.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= +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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= @@ -162,15 +203,21 @@ golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +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-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/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.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y= golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= From 95a9312d09fbcdc54949f639703d3c45a4e9b543 Mon Sep 17 00:00:00 2001 From: Dan Pantry Date: Wed, 12 Feb 2025 19:27:28 -0800 Subject: [PATCH 2/8] Add new interactive flag for picking role if none specified --- command/get.go | 70 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/command/get.go b/command/get.go index ad7b9c16..c6c338e5 100644 --- a/command/get.go +++ b/command/get.go @@ -23,6 +23,10 @@ var ( FlagTimeToLive = "ttl" FlagBypassCache = "bypass-cache" FlagLogin = "login" + FlagInteractive = "interactive" + + ErrNoRoles = errors.New("no roles") + ErrNoRole = errors.New("no role") ) var ( @@ -36,18 +40,20 @@ var ( ) func init() { - getCmd.Flags().String(FlagRegion, "us-west-2", "The AWS region to use") - getCmd.Flags().Uint(FlagTimeToLive, 1, "The key timeout in hours from 1 to 8.") - getCmd.Flags().UintP(FlagTimeRemaining, "t", DefaultTimeRemaining, "Request new keys if there are no keys in the environment or the current keys expire within minutes. Defaults to 60.") - getCmd.Flags().StringP(FlagRoleName, "r", "", "The name of the role to assume.") - getCmd.Flags().String(FlagRoleSessionName, "KeyConjurer-AssumeRole", "the name of the role session name that will show up in CloudTrail logs") - getCmd.Flags().StringP(FlagOutputType, "o", outputTypeEnvironmentVariable, "Format to save new credentials in. Supported outputs: env, awscli, json") - getCmd.Flags().String(FlagShellType, shellTypeInfer, "If output type is env, determines which format to output credentials in - by default, the format is inferred based on the execution environment. WSL users may wish to overwrite this to `bash`") - getCmd.Flags().Bool(FlagBypassCache, false, "Do not check the cache for accounts and send the application ID as-is to Okta. This is useful if you have an ID you know is an Okta application ID and it is not stored in your local account cache.") - getCmd.Flags().Bool(FlagLogin, false, "Login to Okta before running the command") - getCmd.Flags().String(FlagAWSCLIPath, "~/.aws/", "Path for directory used by the aws CLI") - getCmd.Flags().BoolP(FlagURLOnly, "u", false, "Print only the URL to visit rather than a user-friendly message") - getCmd.Flags().BoolP(FlagNoBrowser, "b", false, "Do not open a browser window, printing the URL instead") + flags := getCmd.Flags() + flags.String(FlagRegion, "us-west-2", "The AWS region to use") + flags.Uint(FlagTimeToLive, 1, "The key timeout in hours from 1 to 8.") + flags.UintP(FlagTimeRemaining, "t", DefaultTimeRemaining, "Request new keys if there are no keys in the environment or the current keys expire within minutes. Defaults to 60.") + flags.StringP(FlagRoleName, "r", "", "The name of the role to assume.") + flags.String(FlagRoleSessionName, "KeyConjurer-AssumeRole", "the name of the role session name that will show up in CloudTrail logs") + flags.StringP(FlagOutputType, "o", outputTypeEnvironmentVariable, "Format to save new credentials in. Supported outputs: env, awscli, json") + flags.String(FlagShellType, shellTypeInfer, "If output type is env, determines which format to output credentials in - by default, the format is inferred based on the execution environment. WSL users may wish to overwrite this to `bash`") + flags.Bool(FlagBypassCache, false, "Do not check the cache for accounts and send the application ID as-is to Okta. This is useful if you have an ID you know is an Okta application ID and it is not stored in your local account cache.") + flags.Bool(FlagLogin, false, "Login to Okta before running the command") + flags.String(FlagAWSCLIPath, "~/.aws/", "Path for directory used by the aws CLI") + flags.BoolP(FlagURLOnly, "u", false, "Print only the URL to visit rather than a user-friendly message") + flags.BoolP(FlagNoBrowser, "b", false, "Do not open a browser window, printing the URL instead") + flags.Bool(FlagInteractive, false, "Use interactive prompts to supply information not otherwise supplied with flags") } func resolveApplicationInfo(cfg *Config, bypassCache bool, nameOrID string) (*Account, bool) { @@ -62,7 +68,7 @@ type GetCommand struct { TimeToLive uint TimeRemaining uint OutputType, ShellType, RoleName, AWSCLIPath, OIDCDomain, ClientID, Region string - Login, URLOnly, NoBrowser, BypassCache, MachineOutput bool + Login, URLOnly, NoBrowser, BypassCache, MachineOutput, Interactive bool UsageFunc func() error PrintErrln func(...any) @@ -85,6 +91,7 @@ func (g *GetCommand) Parse(cmd *cobra.Command, args []string) error { g.Region, _ = flags.GetString(FlagRegion) g.UsageFunc = cmd.Usage g.PrintErrln = cmd.PrintErrln + g.Interactive, _ = flags.GetBool(FlagInteractive) g.MachineOutput = ShouldUseMachineOutput(flags) || g.URLOnly if len(args) == 0 { return fmt.Errorf("account name or alias is required") @@ -108,7 +115,8 @@ func (g GetCommand) printUsage() error { return g.UsageFunc() } -func (g GetCommand) rolesInteractivePrompt(roles []string) (string, error) { +func (g GetCommand) rolesInteractivePrompt(roles []string, mostRecent string) (string, error) { + panic("nyi") huh.NewForm() return "", nil } @@ -129,14 +137,6 @@ func (g GetCommand) Execute(ctx context.Context, config *Config) error { return UnknownAccountError(g.AccountIDOrName, FlagBypassCache) } - if g.RoleName == "" { - if account.MostRecentRole == "" { - g.PrintErrln("You must specify the --role flag with this command") - return nil - } - g.RoleName = account.MostRecentRole - } - if config.TimeRemaining != 0 && g.TimeRemaining == DefaultTimeRemaining { g.TimeRemaining = config.TimeRemaining } @@ -158,6 +158,16 @@ func (g GetCommand) Execute(ctx context.Context, config *Config) error { newCredentials, err = g.fetchNewCredentials(ctx, *account, config) } + if errors.Is(err, ErrNoRoles) { + g.PrintErrln("You don't have access to any roles on this account.") + return nil + } + + if errors.Is(err, ErrNoRole) { + g.PrintErrln("You must specify a role with --role or using the interactive prompt.") + return nil + } + if err != nil { return err } @@ -179,6 +189,22 @@ func (g GetCommand) fetchNewCredentials(ctx context.Context, account Account, cf return nil, err } + roles := listRoles(samlResponse) + if len(roles) == 0 { + return nil, ErrNoRoles + } + + if g.RoleName == "" { + if account.MostRecentRole == "" || g.Interactive { + g.RoleName, err = g.rolesInteractivePrompt(listRoles(samlResponse), account.MostRecentRole) + if err != nil { + return nil, ErrNoRole + } + } else { + g.RoleName = account.MostRecentRole + } + } + pair, ok := findRoleInSAML(g.RoleName, samlResponse) if !ok { return nil, UnknownRoleError(g.RoleName, g.AccountIDOrName) From 35847f571b3124a1d963f1a8ee961813d572cef8 Mon Sep 17 00:00:00 2001 From: Dan Pantry Date: Wed, 12 Feb 2025 19:36:38 -0800 Subject: [PATCH 3/8] Implement interactive prompt --- command/get.go | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/command/get.go b/command/get.go index c6c338e5..084ee6be 100644 --- a/command/get.go +++ b/command/get.go @@ -115,12 +115,6 @@ func (g GetCommand) printUsage() error { return g.UsageFunc() } -func (g GetCommand) rolesInteractivePrompt(roles []string, mostRecent string) (string, error) { - panic("nyi") - huh.NewForm() - return "", nil -} - func (g GetCommand) Execute(ctx context.Context, config *Config) error { var accountID string if g.AccountIDOrName != "" { @@ -143,7 +137,7 @@ func (g GetCommand) Execute(ctx context.Context, config *Config) error { credentials := LoadAWSCredentialsFromEnvironment() if !credentials.ValidUntil(account, time.Duration(g.TimeRemaining)*time.Minute) { - newCredentials, err := g.fetchNewCredentials(ctx, *account, config) + newCredentials, err := g.fetchNewCredentials(ctx, account, config) if errors.Is(err, ErrTokensExpiredOrAbsent) && g.Login { loginCommand := LoginCommand{ OIDCDomain: g.OIDCDomain, @@ -155,7 +149,7 @@ func (g GetCommand) Execute(ctx context.Context, config *Config) error { if err != nil { return err } - newCredentials, err = g.fetchNewCredentials(ctx, *account, config) + newCredentials, err = g.fetchNewCredentials(ctx, account, config) } if errors.Is(err, ErrNoRoles) { @@ -175,15 +169,14 @@ func (g GetCommand) Execute(ctx context.Context, config *Config) error { credentials = *newCredentials } - if account != nil { - account.MostRecentRole = g.RoleName - } - config.LastUsedAccount = &accountID return echoCredentials(accountID, accountID, credentials, g.OutputType, g.ShellType, g.AWSCLIPath) } -func (g GetCommand) fetchNewCredentials(ctx context.Context, account Account, cfg *Config) (*CloudCredentials, error) { +// fetchNewCredentials fetches new credentials for the given account. +// +// 'account' will have its MostRecentRole field updated to the role used if this call is successful. +func (g GetCommand) fetchNewCredentials(ctx context.Context, account *Account, cfg *Config) (*CloudCredentials, error) { samlResponse, assertionStr, err := oauth2cli.DiscoverConfigAndExchangeTokenForAssertion(ctx, &keychainTokenSource{}, g.OIDCDomain, g.ClientID, account.ID) if err != nil { return nil, err @@ -196,7 +189,7 @@ func (g GetCommand) fetchNewCredentials(ctx context.Context, account Account, cf if g.RoleName == "" { if account.MostRecentRole == "" || g.Interactive { - g.RoleName, err = g.rolesInteractivePrompt(listRoles(samlResponse), account.MostRecentRole) + g.RoleName, err = rolesInteractivePrompt(listRoles(samlResponse), account.MostRecentRole) if err != nil { return nil, ErrNoRole } @@ -209,6 +202,7 @@ func (g GetCommand) fetchNewCredentials(ctx context.Context, account Account, cf if !ok { return nil, UnknownRoleError(g.RoleName, g.AccountIDOrName) } + account.MostRecentRole = g.RoleName if g.TimeToLive == 1 && cfg.TTL != 0 { g.TimeToLive = cfg.TTL @@ -283,3 +277,17 @@ func echoCredentials(id, name string, credentials CloudCredentials, outputType, return fmt.Errorf("%s is an invalid output type", outputType) } } + +func rolesInteractivePrompt(roles []string, mostRecent string) (string, error) { + opts := huh.NewOptions(roles...) + ctrl := huh.NewSelect[string](). + Options(opts...). + Value(&mostRecent). + Description("Choose a role using your arrow keys and press the return key to confirm.") + + err := huh.Run(ctrl) + if err != nil { + return "", err + } + return ctrl.GetValue().(string), nil +} From e147da6a70a09ef1c57d90205df6889a6efebd01 Mon Sep 17 00:00:00 2001 From: Dan Pantry Date: Wed, 12 Feb 2025 19:58:46 -0800 Subject: [PATCH 4/8] Add interactive filter for account selection --- command/config.go | 15 ++++++++++++++ command/get.go | 52 +++++++++++++++++++++++++++++++++++++++++------ go.sum | 4 ++-- 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/command/config.go b/command/config.go index e60dc975..6b0c6e4b 100644 --- a/command/config.go +++ b/command/config.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "iter" "os" "path/filepath" "sort" @@ -62,6 +63,16 @@ func generateDefaultAlias(name string) string { return strings.ToLower(strings.ReplaceAll(name, " ", "-")) } +func (a *accountSet) Seq() iter.Seq[Account] { + return iter.Seq[Account](func(yield func(account Account) bool) { + for _, v := range a.accounts { + if !yield(*v) { + return + } + } + }) +} + func (a *accountSet) ForEach(f func(id string, account Account, alias string)) { // Golang does not maintain the order of maps, so we create a slice which is sorted instead. var accounts []*Account @@ -218,6 +229,10 @@ func (c *Config) Decode(reader io.Reader) error { return nil } +func (c Config) EnumerateAccounts() iter.Seq[Account] { + return c.Accounts.Seq() +} + func (c *Config) AddAccount(id string, account Account) { if c.Accounts == nil { c.Accounts = &accountSet{accounts: make(map[string]*Account)} diff --git a/command/get.go b/command/get.go index 084ee6be..3c22f744 100644 --- a/command/get.go +++ b/command/get.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "iter" "os" "slices" "time" @@ -25,8 +26,9 @@ var ( FlagLogin = "login" FlagInteractive = "interactive" - ErrNoRoles = errors.New("no roles") - ErrNoRole = errors.New("no role") + ErrNoRoles = errors.New("no roles") + ErrNoRole = errors.New("no role") + ErrNoAccountArg = errors.New("account name or alias is required") ) var ( @@ -93,10 +95,14 @@ func (g *GetCommand) Parse(cmd *cobra.Command, args []string) error { g.PrintErrln = cmd.PrintErrln g.Interactive, _ = flags.GetBool(FlagInteractive) g.MachineOutput = ShouldUseMachineOutput(flags) || g.URLOnly - if len(args) == 0 { - return fmt.Errorf("account name or alias is required") + if len(args) > 0 { + g.AccountIDOrName = args[0] + } else if g.Interactive { + // We can resolve this at execution time with an interactive prompt. + g.AccountIDOrName = "" + } else { + return ErrNoAccountArg } - g.AccountIDOrName = args[0] return nil } @@ -119,6 +125,12 @@ func (g GetCommand) Execute(ctx context.Context, config *Config) error { var accountID string if g.AccountIDOrName != "" { accountID = g.AccountIDOrName + } else if g.Interactive { + acc, err := accountsInteractivePrompt(config.EnumerateAccounts(), nil) + if err != nil { + return err + } + accountID = acc.ID } else if config.LastUsedAccount != nil { // No account specified. Can we use the most recent one? accountID = *config.LastUsedAccount @@ -278,12 +290,40 @@ func echoCredentials(id, name string, credentials CloudCredentials, outputType, } } +func accountsInteractivePrompt(accounts iter.Seq[Account], selected *Account) (Account, error) { + var opts []huh.Option[Account] + for account := range accounts { + opts = append(opts, huh.Option[Account]{ + Key: account.Alias, + Value: account, + }) + } + + ctrl := huh.NewSelect[Account](). + Options(opts...). + Filtering(true). + Title("account"). + Description("Choose an account using your arrow keys or by typing the account name and pressing return to confirm your selection.") + + if selected != nil { + ctrl = ctrl.Value(selected) + } + + err := huh.Run(ctrl) + if err != nil { + return Account{}, err + } + return ctrl.GetValue().(Account), nil +} + func rolesInteractivePrompt(roles []string, mostRecent string) (string, error) { opts := huh.NewOptions(roles...) ctrl := huh.NewSelect[string](). Options(opts...). + Filtering(true). + Title("role"). Value(&mostRecent). - Description("Choose a role using your arrow keys and press the return key to confirm.") + Description("Choose a role using your arrow keys or by typing the role name and press the return key to confirm.") err := huh.Run(ctrl) if err != nil { diff --git a/go.sum b/go.sum index e97c0ac1..57feba49 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/RobotsAndPencils/go-saml v0.0.0-20170520135329-fb13cb52a46b h1:EgJ6N2S0h1WfFIjU5/VVHWbMSVYXAluop97Qxpr/lfQ= github.com/RobotsAndPencils/go-saml v0.0.0-20170520135329-fb13cb52a46b/go.mod h1:3SAoF0F5EbcOuBD5WT9nYkbIJieBS84cUQXADbXeBsU= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= @@ -214,8 +216,6 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y= From 07e1b480a9c662ebf26abc91c3ec24a6663b3e7d Mon Sep 17 00:00:00 2001 From: Dan Pantry Date: Wed, 12 Feb 2025 20:11:51 -0800 Subject: [PATCH 5/8] Make interactive the default --- command/get.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/command/get.go b/command/get.go index 3c22f744..0b6d0c81 100644 --- a/command/get.go +++ b/command/get.go @@ -24,7 +24,7 @@ var ( FlagTimeToLive = "ttl" FlagBypassCache = "bypass-cache" FlagLogin = "login" - FlagInteractive = "interactive" + FlagNoInteractive = "no-interactive" ErrNoRoles = errors.New("no roles") ErrNoRole = errors.New("no role") @@ -55,7 +55,7 @@ func init() { flags.String(FlagAWSCLIPath, "~/.aws/", "Path for directory used by the aws CLI") flags.BoolP(FlagURLOnly, "u", false, "Print only the URL to visit rather than a user-friendly message") flags.BoolP(FlagNoBrowser, "b", false, "Do not open a browser window, printing the URL instead") - flags.Bool(FlagInteractive, false, "Use interactive prompts to supply information not otherwise supplied with flags") + flags.Bool(FlagNoInteractive, false, "Disable interactive prompts") } func resolveApplicationInfo(cfg *Config, bypassCache bool, nameOrID string) (*Account, bool) { @@ -70,7 +70,7 @@ type GetCommand struct { TimeToLive uint TimeRemaining uint OutputType, ShellType, RoleName, AWSCLIPath, OIDCDomain, ClientID, Region string - Login, URLOnly, NoBrowser, BypassCache, MachineOutput, Interactive bool + Login, URLOnly, NoBrowser, BypassCache, MachineOutput, NoInteractive bool UsageFunc func() error PrintErrln func(...any) @@ -93,15 +93,16 @@ func (g *GetCommand) Parse(cmd *cobra.Command, args []string) error { g.Region, _ = flags.GetString(FlagRegion) g.UsageFunc = cmd.Usage g.PrintErrln = cmd.PrintErrln - g.Interactive, _ = flags.GetBool(FlagInteractive) + g.NoInteractive, _ = flags.GetBool(FlagNoInteractive) g.MachineOutput = ShouldUseMachineOutput(flags) || g.URLOnly + if len(args) > 0 { g.AccountIDOrName = args[0] - } else if g.Interactive { + } else if g.NoInteractive { + return ErrNoAccountArg + } else { // We can resolve this at execution time with an interactive prompt. g.AccountIDOrName = "" - } else { - return ErrNoAccountArg } return nil } @@ -125,7 +126,7 @@ func (g GetCommand) Execute(ctx context.Context, config *Config) error { var accountID string if g.AccountIDOrName != "" { accountID = g.AccountIDOrName - } else if g.Interactive { + } else if !g.NoInteractive { acc, err := accountsInteractivePrompt(config.EnumerateAccounts(), nil) if err != nil { return err @@ -200,7 +201,7 @@ func (g GetCommand) fetchNewCredentials(ctx context.Context, account *Account, c } if g.RoleName == "" { - if account.MostRecentRole == "" || g.Interactive { + if account.MostRecentRole == "" && !g.NoInteractive { g.RoleName, err = rolesInteractivePrompt(listRoles(samlResponse), account.MostRecentRole) if err != nil { return nil, ErrNoRole From d327daf63420c62b4b5e8b6467059cc5edaea099 Mon Sep 17 00:00:00 2001 From: Dan Pantry Date: Wed, 12 Feb 2025 20:13:08 -0800 Subject: [PATCH 6/8] Sort accounts by alias name --- command/get.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/command/get.go b/command/get.go index 0b6d0c81..d407cf1b 100644 --- a/command/get.go +++ b/command/get.go @@ -8,6 +8,7 @@ import ( "iter" "os" "slices" + "strings" "time" "github.com/aws/aws-sdk-go-v2/aws" @@ -300,6 +301,10 @@ func accountsInteractivePrompt(accounts iter.Seq[Account], selected *Account) (A }) } + slices.SortStableFunc(opts, func(a huh.Option[Account], b huh.Option[Account]) int { + return strings.Compare(a.Key, b.Key) + }) + ctrl := huh.NewSelect[Account](). Options(opts...). Filtering(true). From c417329a35c060f750b57cfb56396985a465cb8b Mon Sep 17 00:00:00 2001 From: Dan Pantry Date: Wed, 12 Feb 2025 20:16:55 -0800 Subject: [PATCH 7/8] Use iter.Seq instead of ForEach --- command/config.go | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/command/config.go b/command/config.go index 6b0c6e4b..a67387fe 100644 --- a/command/config.go +++ b/command/config.go @@ -7,9 +7,10 @@ import ( "fmt" "io" "iter" + "maps" "os" "path/filepath" - "sort" + "slices" "strings" ) @@ -73,22 +74,6 @@ func (a *accountSet) Seq() iter.Seq[Account] { }) } -func (a *accountSet) ForEach(f func(id string, account Account, alias string)) { - // Golang does not maintain the order of maps, so we create a slice which is sorted instead. - var accounts []*Account - for _, acc := range a.accounts { - accounts = append(accounts, acc) - } - - sort.SliceStable(accounts, func(i, j int) bool { - return accounts[i].Name < accounts[j].Name - }) - - for _, acc := range accounts { - f(acc.ID, *acc, acc.Alias) - } -} - // Add adds an account to the set. func (a *accountSet) Add(id string, account Account) { // TODO: This is bad @@ -180,6 +165,18 @@ func (a *accountSet) ReplaceWith(other []Account) { } } +func (a accountSet) Sorted() iter.Seq[Account] { + keys := slices.Sorted(maps.Keys(a.accounts)) + return iter.Seq[Account](func(yield func(Account) bool) { + for _, k := range keys { + v := a.accounts[k] + if !yield(*v) { + return + } + } + }) +} + func (a accountSet) WriteTable(w io.Writer, withHeaders bool) { tbl := csv.NewWriter(w) tbl.Comma = '\t' @@ -188,9 +185,9 @@ func (a accountSet) WriteTable(w io.Writer, withHeaders bool) { tbl.Write([]string{"id", "name", "alias"}) } - a.ForEach(func(id string, acc Account, alias string) { - tbl.Write([]string{id, acc.Name, alias}) - }) + for account := range a.Sorted() { + tbl.Write([]string{account.ID, account.Name, account.Alias}) + } tbl.Flush() } From e8666510b2bb763c2eff2b2748615259dcc90ae3 Mon Sep 17 00:00:00 2001 From: Dan Pantry Date: Wed, 12 Feb 2025 20:27:53 -0800 Subject: [PATCH 8/8] Improve role selection logic Only uses the most recent role if the user has specified --no-interactive, otherwise prompts the user to tell us what role they want, even if one exists previously. --- command/get.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/command/get.go b/command/get.go index d407cf1b..acf7fdf1 100644 --- a/command/get.go +++ b/command/get.go @@ -202,13 +202,17 @@ func (g GetCommand) fetchNewCredentials(ctx context.Context, account *Account, c } if g.RoleName == "" { - if account.MostRecentRole == "" && !g.NoInteractive { + if g.NoInteractive { + if account.MostRecentRole == "" { + return nil, ErrNoRole + } else { + g.RoleName = account.MostRecentRole + } + } else { g.RoleName, err = rolesInteractivePrompt(listRoles(samlResponse), account.MostRecentRole) if err != nil { return nil, ErrNoRole } - } else { - g.RoleName = account.MostRecentRole } } @@ -307,7 +311,6 @@ func accountsInteractivePrompt(accounts iter.Seq[Account], selected *Account) (A ctrl := huh.NewSelect[Account](). Options(opts...). - Filtering(true). Title("account"). Description("Choose an account using your arrow keys or by typing the account name and pressing return to confirm your selection.") @@ -326,10 +329,8 @@ func rolesInteractivePrompt(roles []string, mostRecent string) (string, error) { opts := huh.NewOptions(roles...) ctrl := huh.NewSelect[string](). Options(opts...). - Filtering(true). - Title("role"). Value(&mostRecent). - Description("Choose a role using your arrow keys or by typing the role name and press the return key to confirm.") + Description("Choose a role using your arrow keys and press the return key to confirm.") err := huh.Run(ctrl) if err != nil {