diff --git a/cmd/kctl-tui/helpers.go b/cmd/kctl-tui/helpers.go new file mode 100644 index 0000000..189533a --- /dev/null +++ b/cmd/kctl-tui/helpers.go @@ -0,0 +1,15 @@ +package main + +import "github.com/skoelle/kctl-tui/internal/kctl" + +func distinctLabelValues(namespaces map[string]map[string]string, labelKey string) []string { + return kctl.DistinctLabelValues(namespaces, labelKey) +} + +func namespacesForLabelValue(namespaces map[string]map[string]string, labelKey, value string) []string { + return kctl.NamespacesForLabelValue(namespaces, labelKey, value) +} + +func findNextContext(current string, pairs []kctl.ContextPair) (string, bool) { + return kctl.FindNextContext(current, pairs) +} diff --git a/cmd/kctl-tui/list_item.go b/cmd/kctl-tui/list_item.go new file mode 100644 index 0000000..2f9682c --- /dev/null +++ b/cmd/kctl-tui/list_item.go @@ -0,0 +1,12 @@ +package main + +// simpleItem is a minimal implementation of list.Item used for all +// selection screens (contexts, teams, namespaces, menu actions). +type simpleItem struct { + label string // what is shown to the user + value string // the underlying value (context name, team value, ...) +} + +func (i simpleItem) Title() string { return i.label } +func (i simpleItem) Description() string { return "" } +func (i simpleItem) FilterValue() string { return i.label } diff --git a/cmd/kctl-tui/main.go b/cmd/kctl-tui/main.go new file mode 100644 index 0000000..56b5bb3 --- /dev/null +++ b/cmd/kctl-tui/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "os" + + tea "github.com/charmbracelet/bubbletea" +) + +func main() { + if len(os.Args) > 1 && os.Args[1] == "panel" { + if err := runPanel(os.Args[2:]); err != nil { + fmt.Fprintln(os.Stderr, "kctl-tui panel error:", err) + os.Exit(1) + } + return + } + + m := newFullModel() + p := tea.NewProgram(m, tea.WithAltScreen()) + if _, err := p.Run(); err != nil { + fmt.Fprintln(os.Stderr, "kctl-tui error:", err) + os.Exit(1) + } +} diff --git a/internal/kubeexec/kubeexec.go b/internal/kubeexec/kubeexec.go new file mode 100644 index 0000000..bb2f995 --- /dev/null +++ b/internal/kubeexec/kubeexec.go @@ -0,0 +1,147 @@ +// Package kubeexec wraps kubectl/aws-cli invocations used by kctl-tui. +// All functions here have side effects (they run external processes) and +// are therefore not covered by unit tests; the pure logic they depend on +// lives in the kctl package instead. +package kubeexec + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "os/exec" + "strings" +) + +func runOutput(name string, args ...string) (string, error) { + cmd := exec.Command(name, args...) + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("%s %s failed: %w\n%s", name, strings.Join(args, " "), err, string(out)) + } + return strings.TrimSpace(string(out)), nil +} + +// GetContexts returns all configured kubectl context names. +func GetContexts() ([]string, error) { + out, err := runOutput("kubectl", "config", "get-contexts", "-o", "name") + if err != nil { + return nil, err + } + if out == "" { + return []string{}, nil + } + return strings.Split(out, "\n"), nil +} + +// GetCurrentContext returns the currently active kubectl context, or an +// empty string if none is set. +func GetCurrentContext() string { + out, _ := runOutput("kubectl", "config", "current-context") + return out +} + +// GetCurrentNamespace returns the namespace bound to the current context, +// defaulting to "default" if unset. +func GetCurrentNamespace() string { + out, _ := runOutput("kubectl", "config", "view", "--minify", "-o", "jsonpath={..namespace}") + if out == "" { + return "default" + } + return out +} + +// UseContext switches the active kubectl context. +func UseContext(ctx string) error { + _, err := runOutput("kubectl", "config", "use-context", ctx) + return err +} + +// SetNamespace binds a namespace to the current kubectl context. +func SetNamespace(ns string) error { + _, err := runOutput("kubectl", "config", "set-context", "--current", "--namespace="+ns) + return err +} + +type nsItem struct { + Metadata struct { + Name string `json:"name"` + Labels map[string]string `json:"labels"` + } `json:"metadata"` +} + +type nsList struct { + Items []nsItem `json:"items"` +} + +// GetNamespacesWithLabels returns a map of namespace name -> labels for all +// namespaces visible in the current context. +func GetNamespacesWithLabels() (map[string]map[string]string, error) { + out, err := runOutput("kubectl", "get", "ns", "-o", "json") + if err != nil { + return nil, err + } + var list nsList + if err := json.Unmarshal([]byte(out), &list); err != nil { + return nil, err + } + result := make(map[string]map[string]string, len(list.Items)) + for _, item := range list.Items { + result[item.Metadata.Name] = item.Metadata.Labels + } + return result, nil +} + +// GetDeployments lists deployment names in the given namespace. +func GetDeployments(namespace string) ([]string, error) { + out, err := runOutput("kubectl", "-n", namespace, "get", "deploy", + "-o", `jsonpath={range .items[*]}{.metadata.name}{"\n"}{end}`) + if err != nil { + return nil, err + } + if out == "" { + return []string{}, nil + } + return strings.Split(out, "\n"), nil +} + +// RolloutRestart triggers a rolling restart of a deployment. +func RolloutRestart(namespace, deployment string) (string, error) { + return runOutput("kubectl", "-n", namespace, "rollout", "restart", "deploy/"+deployment) +} + +// RolloutStatus waits for and returns the rollout status of a deployment. +func RolloutStatus(namespace, deployment string) (string, error) { + return runOutput("kubectl", "-n", namespace, "rollout", "status", "deploy/"+deployment) +} + +// GetSecretValueBase64 returns the raw (still base64-encoded) value of a +// single field in a Kubernetes secret. +func GetSecretValueBase64(namespace, secretName, field string) (string, error) { + path := fmt.Sprintf("jsonpath={.data.%s}", field) + return runOutput("kubectl", "-n", namespace, "get", "secret", secretName, "-o", path) +} + +// DecodeBase64 decodes a base64-encoded Kubernetes secret value. +func DecodeBase64(value string) (string, error) { + decoded, err := base64.StdEncoding.DecodeString(value) + if err != nil { + return "", err + } + return string(decoded), nil +} + +// AnnotateForceSync sets the force-sync annotation on an ExternalSecret +// object to trigger an immediate re-sync from the upstream secret store. +func AnnotateForceSync(namespace, externalSecretName string, unixTimestamp int64) (string, error) { + annotation := fmt.Sprintf("force-sync=%d", unixTimestamp) + return runOutput("kubectl", "-n", namespace, "annotate", "externalsecret", + externalSecretName, annotation, "--overwrite") +} + +// GetAWSSecretString fetches the SecretString of an AWS Secrets Manager +// secret via the aws-cli. +func GetAWSSecretString(secretID, region string) (string, error) { + return runOutput("aws", "secretsmanager", "get-secret-value", + "--secret-id", secretID, "--region", region, + "--query", "SecretString", "--output", "text") +}