Add AWS auth check before secrets workflow, with interactive SSO login prompt on expired session

This commit is contained in:
Stefan Koelle
2026-08-09 12:40:02 +02:00
parent c229f4d5d8
commit 79c861682b
+71 -4
View File
@@ -13,6 +13,7 @@ import (
"github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/skoelle/kctl-tui/internal/config"
"github.com/skoelle/kctl-tui/internal/kctl" "github.com/skoelle/kctl-tui/internal/kctl"
"github.com/skoelle/kctl-tui/internal/kubeexec" "github.com/skoelle/kctl-tui/internal/kubeexec"
) )
@@ -24,6 +25,7 @@ const (
stepMenu panelStep = iota stepMenu panelStep = iota
stepRedeployList stepRedeployList
stepRedeployConfirm stepRedeployConfirm
stepAWSAuthPrompt
stepSecretRegion stepSecretRegion
stepSecretList stepSecretList
stepK8sSecretName stepK8sSecretName
@@ -36,6 +38,7 @@ const (
type panelModel struct { type panelModel struct {
ctx, ns, team string ctx, ns, team string
cfg config.Config
step panelStep step panelStep
list list.Model list list.Model
@@ -75,7 +78,10 @@ func newPanelModel(ctx, ns, team string) *panelModel {
ti := textinput.New() ti := textinput.New()
ti.Focus() ti.Focus()
return &panelModel{ctx: ctx, ns: ns, team: team, step: stepMenu, list: l, input: ti} cfgPath, _ := config.DefaultPath()
cfg, _ := config.Load(cfgPath)
return &panelModel{ctx: ctx, ns: ns, team: team, cfg: cfg, step: stepMenu, list: l, input: ti}
} }
func menuItems() []list.Item { func menuItems() []list.Item {
@@ -88,12 +94,17 @@ func menuItems() []list.Item {
func (m *panelModel) Init() tea.Cmd { return nil } func (m *panelModel) Init() tea.Cmd { return nil }
type awsLoginDoneMsg struct{ err error }
func (m *panelModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *panelModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
m.list.SetSize(msg.Width, msg.Height-2) m.list.SetSize(msg.Width, msg.Height-2)
return m, nil return m, nil
case awsLoginDoneMsg:
return m.afterAWSLogin(msg.err)
case tea.KeyMsg: case tea.KeyMsg:
switch msg.String() { switch msg.String() {
case "ctrl+c": case "ctrl+c":
@@ -139,6 +150,8 @@ func (m *panelModel) handleEnter() (tea.Model, tea.Cmd) {
return m.fromRedeployList() return m.fromRedeployList()
case stepRedeployConfirm: case stepRedeployConfirm:
return m.fromRedeployConfirm() return m.fromRedeployConfirm()
case stepAWSAuthPrompt:
return m.fromAWSAuthPrompt()
case stepSecretRegion: case stepSecretRegion:
m.awsRegion = m.input.Value() m.awsRegion = m.input.Value()
return m.fetchSecretList() return m.fetchSecretList()
@@ -194,15 +207,63 @@ func (m *panelModel) fromMenu() (tea.Model, tea.Cmd) {
m.list.Title = "Select deployment to restart (esc = back)" m.list.Title = "Select deployment to restart (esc = back)"
m.step = stepRedeployList m.step = stepRedeployList
case "secrets": case "secrets":
m.step = stepSecretRegion return m.checkAWSAuthAndProceed()
m.input.SetValue("eu-central-1")
m.input.Placeholder = "AWS region"
case "quit": case "quit":
return m.handleEsc() return m.handleEsc()
} }
return m, nil return m, nil
} }
// checkAWSAuthAndProceed verifies the current AWS credentials/SSO session
// before entering the secrets workflow. If the check fails (e.g. an
// expired SSO session), it offers to run the configured login command
// interactively instead of letting the user hit a confusing failure
// several steps later.
func (m *panelModel) checkAWSAuthAndProceed() (tea.Model, tea.Cmd) {
if err := kubeexec.CheckAWSAuth(); err != nil {
m.err = err
m.list.SetItems([]list.Item{
simpleItem{label: "Run AWS login now (" + m.cfg.LoginCommand() + ")", value: "login"},
simpleItem{label: "Cancel", value: "cancel"},
})
m.list.Title = "AWS session invalid or expired"
m.step = stepAWSAuthPrompt
return m, nil
}
m.step = stepSecretRegion
m.input.SetValue("eu-central-1")
m.input.Placeholder = "AWS region"
return m, nil
}
func (m *panelModel) fromAWSAuthPrompt() (tea.Model, tea.Cmd) {
item, ok := m.list.SelectedItem().(simpleItem)
if !ok || item.value != "login" {
m.resetToMenu()
return m, nil
}
cmd := kubeexec.RunAWSLogin(m.cfg.LoginCommand())
return m, tea.ExecProcess(cmd, func(err error) tea.Msg {
return awsLoginDoneMsg{err: err}
})
}
// afterAWSLogin re-checks AWS auth once the interactive login command has
// finished (successfully or not) and either proceeds into the secrets
// workflow or shows the remaining error.
func (m *panelModel) afterAWSLogin(execErr error) (tea.Model, tea.Cmd) {
if execErr != nil {
return m.showError(fmt.Errorf("login command failed to run: %w", execErr))
}
if err := kubeexec.CheckAWSAuth(); err != nil {
return m.showError(fmt.Errorf("still not authenticated with AWS after running '%s': %w", m.cfg.LoginCommand(), err))
}
m.step = stepSecretRegion
m.input.SetValue("eu-central-1")
m.input.Placeholder = "AWS region"
return m, nil
}
func (m *panelModel) fromRedeployList() (tea.Model, tea.Cmd) { func (m *panelModel) fromRedeployList() (tea.Model, tea.Cmd) {
item, ok := m.list.SelectedItem().(simpleItem) item, ok := m.list.SelectedItem().(simpleItem)
if !ok { if !ok {
@@ -363,6 +424,12 @@ func (m *panelModel) View() string {
switch m.step { switch m.step {
case stepMenu, stepRedeployList, stepRedeployConfirm, stepSecretList: case stepMenu, stepRedeployList, stepRedeployConfirm, stepSecretList:
return m.list.View() return m.list.View()
case stepAWSAuthPrompt:
errText := ""
if m.err != nil {
errText = m.err.Error() + "\n\n"
}
return errText + m.list.View()
case stepForceSyncConfirm: case stepForceSyncConfirm:
return m.message + "\n\n" + m.list.View() return m.message + "\n\n" + m.list.View()
case stepDiffResult, stepDone: case stepDiffResult, stepDone: