Add internal/kubeexec wrapper and cmd/kctl-tui entrypoint (main, full mode)

This commit is contained in:
Stefan Koelle
2026-08-09 11:22:15 +02:00
parent dc8bcc555f
commit 96ec9d2e1a
4 changed files with 199 additions and 0 deletions
+15
View File
@@ -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)
}
+12
View File
@@ -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 }
+25
View File
@@ -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)
}
}
+147
View File
@@ -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")
}