@@ -6,12 +6,14 @@ import (
66 "encoding/json"
77 "fmt"
88 "io"
9+ "math/rand/v2"
910 "net/http"
1011 "net/url"
1112 "os"
1213 "strings"
1314
1415 "github.com/charmbracelet/bubbles/spinner"
16+ "github.com/charmbracelet/bubbles/textinput"
1517 tea "github.com/charmbracelet/bubbletea"
1618 "github.com/charmbracelet/lipgloss"
1719 "github.com/spf13/cobra"
@@ -486,6 +488,164 @@ func runConfirmation(kubeContext string) (bool, error) {
486488 return result .(confirmModel ).confirmed , nil
487489}
488490
491+ // --- Cluster name selection (bubbletea) ---
492+
493+ var (
494+ nameAdjs = []string {
495+ "autumn" , "bold" , "calm" , "deft" , "eager" ,
496+ "fair" , "glad" , "hale" , "keen" , "live" ,
497+ "neat" , "open" , "pure" , "rare" , "safe" ,
498+ "true" , "warm" , "wise" , "bright" , "swift" ,
499+ }
500+ nameNouns = []string {
501+ "arch" , "bay" , "cape" , "dale" , "edge" ,
502+ "ford" , "glen" , "hill" , "isle" , "knoll" ,
503+ "lake" , "mesa" , "node" , "oak" , "peak" ,
504+ "reef" , "sky" , "vale" , "wave" , "zone" ,
505+ }
506+ )
507+
508+ func randomClusterName () string {
509+ return nameAdjs [rand .IntN (len (nameAdjs ))] + "-" + nameNouns [rand .IntN (len (nameNouns ))]
510+ }
511+
512+ type clusterNameModel struct {
513+ contextName string
514+ randomName string
515+ cursor int // 0=context, 1=random, 2=custom
516+ textInput textinput.Model
517+ inputMode bool
518+ result string
519+ done bool
520+ aborted bool
521+ }
522+
523+ func newClusterNameModel (contextName string ) clusterNameModel {
524+ ti := textinput .New ()
525+ ti .Placeholder = "my-cluster"
526+ ti .CharLimit = 63
527+ return clusterNameModel {
528+ contextName : contextName ,
529+ randomName : randomClusterName (),
530+ textInput : ti ,
531+ }
532+ }
533+
534+ func (m clusterNameModel ) Init () tea.Cmd { return nil }
535+
536+ func (m clusterNameModel ) Update (msg tea.Msg ) (tea.Model , tea.Cmd ) {
537+ if m .inputMode {
538+ switch msg := msg .(type ) {
539+ case tea.KeyMsg :
540+ switch msg .String () {
541+ case "enter" :
542+ v := strings .TrimSpace (m .textInput .Value ())
543+ if v != "" {
544+ m .result = v
545+ m .done = true
546+ return m , tea .Quit
547+ }
548+ return m , nil
549+ case "esc" :
550+ m .inputMode = false
551+ m .textInput .Blur ()
552+ return m , nil
553+ case "ctrl+c" :
554+ m .aborted = true
555+ m .done = true
556+ return m , tea .Quit
557+ }
558+ }
559+ var cmd tea.Cmd
560+ m .textInput , cmd = m .textInput .Update (msg )
561+ return m , cmd
562+ }
563+
564+ if msg , ok := msg .(tea.KeyMsg ); ok {
565+ switch msg .String () {
566+ case "up" , "k" :
567+ if m .cursor > 0 {
568+ m .cursor --
569+ }
570+ case "down" , "j" :
571+ if m .cursor < 2 {
572+ m .cursor ++
573+ }
574+ case "r" :
575+ m .randomName = randomClusterName ()
576+ case "enter" :
577+ switch m .cursor {
578+ case 0 :
579+ m .result = m .contextName
580+ m .done = true
581+ return m , tea .Quit
582+ case 1 :
583+ m .result = m .randomName
584+ m .done = true
585+ return m , tea .Quit
586+ case 2 :
587+ m .inputMode = true
588+ m .textInput .Focus ()
589+ return m , m .textInput .Cursor .BlinkCmd ()
590+ }
591+ case "ctrl+c" , "q" , "esc" :
592+ m .aborted = true
593+ m .done = true
594+ return m , tea .Quit
595+ }
596+ }
597+ return m , nil
598+ }
599+
600+ func (m clusterNameModel ) View () string {
601+ if m .done && ! m .aborted {
602+ return fmt .Sprintf ("Cluster name: %s\n " , styleCreate .Render (m .result ))
603+ }
604+
605+ var b strings.Builder
606+ b .WriteString ("Select cluster name:\n \n " )
607+
608+ choices := []string {
609+ fmt .Sprintf ("%s (kube context)" , m .contextName ),
610+ fmt .Sprintf ("%s (random — r to regenerate)" , m .randomName ),
611+ "Enter custom name" ,
612+ }
613+
614+ for i , choice := range choices {
615+ cursor := " "
616+ if m .cursor == i {
617+ cursor = styleCreate .Render ("▸ " )
618+ }
619+ b .WriteString (fmt .Sprintf (" %s%s\n " , cursor , choice ))
620+ }
621+
622+ if m .inputMode {
623+ b .WriteString (fmt .Sprintf ("\n Name: %s\n " , m .textInput .View ()))
624+ }
625+
626+ b .WriteString ("\n ↑/↓ select • enter confirm" )
627+ if ! m .inputMode {
628+ b .WriteString (" • r randomize" )
629+ } else {
630+ b .WriteString (" • esc back" )
631+ }
632+ b .WriteString ("\n " )
633+ return b .String ()
634+ }
635+
636+ func runClusterNameSelection (contextName string ) (string , error ) {
637+ p := tea .NewProgram (newClusterNameModel (contextName ))
638+ result , err := p .Run ()
639+ if err != nil {
640+ return "" , err
641+ }
642+ m := result .(clusterNameModel )
643+ if m .aborted {
644+ return "" , fmt .Errorf ("aborted" )
645+ }
646+ return m .result , nil
647+ }
648+
489649// --- Apply model (bubbletea with spinner per resource) ---
490650
491651type applyResultMsg applyResult
@@ -875,9 +1035,11 @@ will automatically connect to the Apoxy API and begin managing your in-cluster A
8751035 return err
8761036 }
8771037
878- // If --cluster-name wasn't explicitly provided, recover it from the
879- // existing namespace's apoxy.dev/cluster-name annotation so the API
880- // returns YAML consistent with the current install.
1038+ // Resolve cluster name:
1039+ // 1. Explicit --cluster-name flag takes priority.
1040+ // 2. Existing namespace annotation (re-install).
1041+ // 3. Interactive: prompt with kube context name as default.
1042+ // 4. Non-interactive: use kube context name.
8811043 if clusterName == "" {
8821044 clientset , err := kubernetes .NewForConfig (kc )
8831045 if err == nil {
@@ -889,6 +1051,18 @@ will automatically connect to the Apoxy API and begin managing your in-cluster A
8891051 }
8901052 }
8911053 }
1054+ if clusterName == "" {
1055+ isTTY := term .IsTerminal (int (os .Stdout .Fd ())) && term .IsTerminal (int (os .Stdin .Fd ()))
1056+ if isTTY && ! yes {
1057+ selected , err := runClusterNameSelection (kubeContext )
1058+ if err != nil {
1059+ return err
1060+ }
1061+ clusterName = selected
1062+ } else {
1063+ clusterName = kubeContext
1064+ }
1065+ }
8921066
8931067 yamlz , err := getYAML (clusterName , mirror , image )
8941068 if err != nil {
@@ -915,7 +1089,7 @@ func init() {
9151089 installK8sCmd .Flags ().String ("namespace" , "apoxy" , "The namespace to install the controller into" )
9161090 installK8sCmd .Flags ().Bool ("dry-run" , false , "If true, only print the YAML that would be applied" )
9171091 installK8sCmd .Flags ().Bool ("force" , false , "If true, forces value overwrites (See: https://v1-28.docs.kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts)" )
918- installK8sCmd .Flags ().String ("cluster-name" , "" , "Cluster name identifier for multi-cluster deployments " )
1092+ installK8sCmd .Flags ().String ("cluster-name" , "" , "Cluster name identifier (defaults to kube context name) " )
9191093 installK8sCmd .Flags ().String ("mirror" , "" , "Mirror mode (gateway, ingress, all)" )
9201094 installK8sCmd .Flags ().String ("image" , "" , "Controller image override to pass to the onboarding manifest generator" )
9211095 installK8sCmd .Flags ().BoolP ("yes" , "y" , false , "Skip confirmation and apply changes immediately" )
0 commit comments