Skip to content

Commit 8d225f0

Browse files
dilyevskyclaude
andcommitted
[cli] default k8s install cluster-name to kube context with interactive picker
When --cluster-name is not provided, defaults to the Kubernetes context name. In interactive mode, shows a bubbletea menu to confirm the context name, generate a random name, or enter a custom one. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ca4745a commit 8d225f0

File tree

3 files changed

+181
-4
lines changed

3 files changed

+181
-4
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ require (
144144
github.com/apache/thrift v0.20.0 // indirect
145145
github.com/apparentlymart/go-cidr v1.1.0 // indirect
146146
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da // indirect
147+
github.com/atotto/clipboard v0.1.4 // indirect
147148
github.com/aws/aws-sdk-go v1.55.5 // indirect
148149
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
149150
github.com/benbjohnson/clock v1.3.5 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
137137
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
138138
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
139139
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
140+
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
141+
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
140142
github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk=
141143
github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA=
142144
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=

pkg/cmd/k8s.go

Lines changed: 178 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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

491651
type 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

Comments
 (0)