Created
August 31, 2025 18:40
-
-
Save sarvsav/ce7bd63d2beb2a71e32f4c480b917767 to your computer and use it in GitHub Desktop.
Script to generate the vault secret store path
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // For generating vault secret store path | |
| // It is a part of medium blog | |
| package main | |
| import ( | |
| "fmt" | |
| "os" | |
| "regexp" | |
| "strings" | |
| "github.com/charmbracelet/bubbles/textinput" | |
| tea "github.com/charmbracelet/bubbletea" | |
| "github.com/charmbracelet/lipgloss" | |
| ) | |
| type field struct { | |
| Key string | |
| Prompt string | |
| Help string | |
| Placeholder string | |
| Required bool | |
| Value string | |
| Validate func(string) (string, bool) | |
| } | |
| var ( | |
| titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("213")) | |
| labelStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("81")) | |
| errStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1")) | |
| helpStyle = lipgloss.NewStyle().Faint(true) | |
| confirmStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Bold(true) | |
| pathStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("13")).Bold(true) | |
| dimStyle = lipgloss.NewStyle().Faint(true) | |
| focusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("229")).Background(lipgloss.Color("57")).Bold(true).Padding(0, 1) | |
| ) | |
| func nonEmpty(v string) (string, bool) { | |
| v = strings.TrimSpace(v) | |
| return v, v != "" | |
| } | |
| var allowedRe = regexp.MustCompile(`^[a-z0-9._:-]+$`) | |
| func axisValidate(allowEmpty bool) func(string) (string, bool) { | |
| return func(s string) (string, bool) { | |
| s = strings.TrimSpace(strings.ToLower(s)) | |
| if s == "" { | |
| return s, !(!allowEmpty) // true if allowEmpty, false otherwise | |
| } | |
| return s, allowedRe.MatchString(s) | |
| } | |
| } | |
| type mode int | |
| const ( | |
| modeWizard mode = iota | |
| modeReview | |
| modeDone | |
| ) | |
| type model struct { | |
| fields []field | |
| index int | |
| ti textinput.Model | |
| errMsg string | |
| m mode | |
| lastPath string | |
| editIndex int // which field to edit in review (if any) | |
| } | |
| func newModel() model { | |
| // Define the decision tree axes (the wizard steps) | |
| fields := []field{ | |
| { | |
| Key: "provider", | |
| Prompt: "1) Provider – What system does this unlock?", | |
| Help: "Examples: aws, azure, gcp, bitbucket, onprem", | |
| Placeholder: "aws", | |
| Required: true, | |
| Validate: axisValidate(false), | |
| }, | |
| { | |
| Key: "account", | |
| Prompt: "2) Account / Subscription / Project / Org?", | |
| Help: "Examples: 111111111111, sub123, org1", | |
| Placeholder: "111111111111", | |
| Required: true, | |
| Validate: axisValidate(false), | |
| }, | |
| { | |
| Key: "region", | |
| Prompt: "3) Region (or 'global' if not region-specific)?", | |
| Help: "Examples: us-east-1, westus, eu-central-1, global", | |
| Placeholder: "us-east-1", | |
| Required: true, | |
| Validate: axisValidate(false), | |
| }, | |
| { | |
| Key: "environment", | |
| Prompt: "4) Environment?", | |
| Help: "Examples: prod, staging, dev, test, sandbox", | |
| Placeholder: "prod", | |
| Required: true, | |
| Validate: axisValidate(false), | |
| }, | |
| { | |
| Key: "service", | |
| Prompt: "5) Service / Resource Type?", | |
| Help: "Examples: rds, vm, storage-account, workspace", | |
| Placeholder: "rds", | |
| Required: true, | |
| Validate: axisValidate(false), | |
| }, | |
| { | |
| Key: "resource", | |
| Prompt: "6) Resource instance name?", | |
| Help: "Examples: orders-db, rg1-myapp, workspace1", | |
| Placeholder: "orders-db", | |
| Required: true, | |
| Validate: axisValidate(false), | |
| }, | |
| { | |
| Key: "secret-type", | |
| Prompt: "7) Secret type?", | |
| Help: "Examples: user, ssh, password, api-key, key, cert, connection-string", | |
| Placeholder: "user", | |
| Required: true, | |
| Validate: axisValidate(false), | |
| }, | |
| { | |
| Key: "variant", | |
| Prompt: "8) Variant (if multiple of same type)?", | |
| Help: "Examples: admin, readonly, key1, key2, deploy", | |
| Placeholder: "admin", | |
| Required: true, | |
| Validate: axisValidate(false), | |
| }, | |
| } | |
| ti := textinput.New() | |
| ti.Prompt = "> " | |
| ti.Placeholder = fields[0].Placeholder | |
| ti.Focus() | |
| ti.CharLimit = 128 | |
| return model{ | |
| fields: fields, | |
| index: 0, | |
| ti: ti, | |
| m: modeWizard, | |
| } | |
| } | |
| func (m model) Init() tea.Cmd { return textinput.Blink } | |
| // Build the final path | |
| func (m *model) buildPath() string { | |
| parts := make([]string, 0, len(m.fields)) | |
| for _, f := range m.fields { | |
| v := strings.TrimSpace(strings.ToLower(f.Value)) | |
| parts = append(parts, v) | |
| } | |
| return "/" + strings.Join(parts, "/") | |
| } | |
| func (m model) View() string { | |
| switch m.m { | |
| case modeWizard: | |
| return m.viewWizard() | |
| case modeReview: | |
| return m.viewReview() | |
| case modeDone: | |
| return m.viewDone() | |
| default: | |
| return "" | |
| } | |
| } | |
| func (m model) viewWizard() string { | |
| f := m.fields[m.index] | |
| header := titleStyle.Render("Vault Secret Path Wizard") | |
| step := fmt.Sprintf("Step %d of %d", m.index+1, len(m.fields)) | |
| body := strings.Builder{} | |
| body.WriteString(header + "\n") | |
| body.WriteString(dimStyle.Render(step) + "\n\n") | |
| body.WriteString(labelStyle.Render(f.Prompt) + "\n") | |
| body.WriteString(helpStyle.Render(f.Help) + "\n\n") | |
| body.WriteString(m.ti.View() + "\n") | |
| if m.errMsg != "" { | |
| body.WriteString(errStyle.Render("Error: " + m.errMsg)) | |
| body.WriteString("\n") | |
| } | |
| body.WriteString("\n") | |
| body.WriteString(dimStyle.Render("Enter to continue • Ctrl+C to quit • Tab to accept placeholder • Ctrl+H to go back")) | |
| return body.String() | |
| } | |
| func (m model) viewReview() string { | |
| header := titleStyle.Render("Review & Confirm") | |
| body := strings.Builder{} | |
| body.WriteString(header + "\n\n") | |
| for i, f := range m.fields { | |
| line := fmt.Sprintf("%s: %s", f.Key, labelStyle.Render(f.Value)) | |
| if i == m.editIndex { | |
| line = focusStyle.Render(line + " (editing)") | |
| } | |
| body.WriteString(line + "\n") | |
| } | |
| body.WriteString("\n") | |
| path := m.buildPath() | |
| body.WriteString("Final path:\n") | |
| body.WriteString(pathStyle.Render(path) + "\n\n") | |
| body.WriteString(dimStyle.Render("Press Enter to confirm and print the path.\n")) | |
| body.WriteString(dimStyle.Render("Or type a field name to edit (e.g., 'region') and press Enter.\n")) | |
| body.WriteString(dimStyle.Render("Ctrl+H to go back to the last step • Ctrl+C to quit")) | |
| return body.String() | |
| } | |
| func (m model) viewDone() string { | |
| header := confirmStyle.Render("✓ Secret path generated") | |
| body := strings.Builder{} | |
| body.WriteString(header + "\n\n") | |
| body.WriteString(pathStyle.Render(m.lastPath) + "\n\n") | |
| body.WriteString(dimStyle.Render("Copy the path above.\n")) | |
| return body.String() | |
| } | |
| func (m *model) gotoFieldByKey(key string) bool { | |
| key = strings.TrimSpace(strings.ToLower(key)) | |
| for i, f := range m.fields { | |
| if f.Key == key { | |
| m.index = i | |
| m.ti.SetValue(f.Value) | |
| m.ti.Placeholder = f.Placeholder | |
| m.m = modeWizard | |
| m.errMsg = "" | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |
| switch msg := msg.(type) { | |
| case tea.KeyMsg: | |
| switch m.m { | |
| case modeWizard: | |
| switch msg.Type { | |
| case tea.KeyCtrlC, tea.KeyEsc: | |
| return m, tea.Quit | |
| case tea.KeyCtrlH: // back | |
| if m.index > 0 { | |
| m.index-- | |
| prev := m.fields[m.index] | |
| m.ti.SetValue(prev.Value) | |
| m.ti.Placeholder = prev.Placeholder | |
| m.errMsg = "" | |
| return m, nil | |
| } | |
| return m, nil | |
| case tea.KeyTab: // accept placeholder quickly | |
| m.ti.SetValue(m.fields[m.index].Placeholder) | |
| return m, nil | |
| case tea.KeyEnter: | |
| val := m.ti.Value() | |
| normalized, ok := m.fields[m.index].Validate(val) | |
| if !ok { | |
| m.errMsg = "Invalid value. Use lowercase a-z, 0-9, dot, underscore, hyphen, or colon." | |
| return m, nil | |
| } | |
| if m.fields[m.index].Required && normalized == "" { | |
| m.errMsg = "This field is required." | |
| return m, nil | |
| } | |
| // Save and advance | |
| m.fields[m.index].Value = normalized | |
| m.errMsg = "" | |
| if m.index < len(m.fields)-1 { | |
| m.index++ | |
| next := m.fields[m.index] | |
| m.ti.SetValue(next.Value) | |
| m.ti.Placeholder = next.Placeholder | |
| return m, nil | |
| } | |
| // Go to review | |
| m.m = modeReview | |
| m.ti.Blur() | |
| return m, nil | |
| default: | |
| // pass to textinput | |
| } | |
| var cmd tea.Cmd | |
| m.ti, cmd = m.ti.Update(msg) | |
| return m, cmd | |
| case modeReview: | |
| switch msg.Type { | |
| case tea.KeyCtrlC, tea.KeyEsc: | |
| return m, tea.Quit | |
| case tea.KeyCtrlH: | |
| // Jump back to last field for quick tweak | |
| m.m = modeWizard | |
| m.index = len(m.fields) - 1 | |
| m.ti.SetValue(m.fields[m.index].Value) | |
| m.ti.Placeholder = m.fields[m.index].Placeholder | |
| m.ti.Focus() | |
| return m, nil | |
| case tea.KeyEnter: | |
| // If the user typed something into the (invisible) textinput, treat it as a field key to edit | |
| input := strings.TrimSpace(strings.ToLower(m.ti.Value())) | |
| if input == "" { | |
| // Confirm | |
| m.lastPath = m.buildPath() | |
| m.m = modeDone | |
| return m, nil | |
| } | |
| if m.gotoFieldByKey(input) { | |
| m.ti.Focus() | |
| return m, nil | |
| } | |
| // Not a valid key; ignore and clear | |
| m.ti.SetValue("") | |
| return m, nil | |
| default: | |
| // We keep a hidden textinput for users to type a field to edit | |
| var cmd tea.Cmd | |
| m.ti, cmd = m.ti.Update(msg) | |
| return m, cmd | |
| } | |
| case modeDone: | |
| switch msg.Type { | |
| case tea.KeyCtrlC, tea.KeyEsc, tea.KeyEnter: | |
| // Print the final path to stdout on exit for easy scripting | |
| fmt.Println(m.lastPath) | |
| return m, tea.Quit | |
| default: | |
| return m, nil | |
| } | |
| } | |
| case tea.WindowSizeMsg: | |
| // No special layout logic needed here | |
| return m, nil | |
| } | |
| // Default: forward to textinput if in wizard/review | |
| var cmd tea.Cmd | |
| if m.m == modeWizard || m.m == modeReview { | |
| m.ti, cmd = m.ti.Update(msg) | |
| } | |
| return m, cmd | |
| } | |
| func main() { | |
| p := tea.NewProgram(newModel()) | |
| if _, err := p.Run(); err != nil { | |
| fmt.Println("error:", err) | |
| os.Exit(1) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment