mirror of
https://github.com/skoelle/kctl-tui.git
synced 2026-09-18 04:20:25 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38b28f64fb | ||
|
|
6a749811c7 | ||
|
|
313dffcbbf | ||
|
|
8546ac9bf2 | ||
|
|
4c0650c43b | ||
|
|
e36ac3568f | ||
|
|
697017e496 |
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2026 Stefan Koelle
|
Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -203,8 +203,10 @@ ln -s /mnt/c/Users/<your-windows-username>/.kube/config ~/.kube/config
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
kctl-tui # start the TUI (full navigation mode)
|
kctl-tui # start the TUI (full navigation mode)
|
||||||
|
kctl-tui --help # show all commands and flags
|
||||||
kctl-tui --version # print version
|
kctl-tui --version # print version
|
||||||
kctl-tui --verbose # enable debug logging to stderr
|
kctl-tui --verbose # enable debug logging to stderr
|
||||||
|
kctl-tui doctor # check if all tools, config and connections are OK
|
||||||
kctl-tui config check # validate ~/.kctl-tui/config.yaml
|
kctl-tui config check # validate ~/.kctl-tui/config.yaml
|
||||||
kctl-tui panel --context=... --ns=... --team=... # internal (called by tmux)
|
kctl-tui panel --context=... --ns=... --team=... # internal (called by tmux)
|
||||||
```
|
```
|
||||||
@@ -225,4 +227,4 @@ and untested, since it has no meaningful behavior without a live cluster.
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[MIT](LICENSE)
|
Licensed under the [MIT License](LICENSE) - Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ not cover: **redeploy** and **secrets diff**.
|
|||||||
Example startup command (generic placeholders):
|
Example startup command (generic placeholders):
|
||||||
|
|
||||||
```
|
```
|
||||||
|
# Kill stale session first (separate command — tmux aborts on kill-session error).
|
||||||
|
tmux kill-session -t kctl
|
||||||
tmux new-session -d -s kctl \
|
tmux new-session -d -s kctl \
|
||||||
"kctl-tui panel --context=$CTX_A --ns=$NS --team=$TEAM" \; \
|
"kctl-tui panel --context=$CTX_A --ns=$NS --team=$TEAM" \; \
|
||||||
set-option -t kctl remain-on-exit on \; \
|
set-option -t kctl remain-on-exit on \; \
|
||||||
|
|||||||
+11
-1
@@ -1,7 +1,11 @@
|
|||||||
|
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
|
// Licensed under the MIT License. See LICENSE file in project root for details.
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
@@ -265,7 +269,10 @@ func (m *fullModel) loadNamespacesFor(teamValue string) tea.Cmd {
|
|||||||
// configured envs, resolved via the context template, so both are
|
// configured envs, resolved via the context template, so both are
|
||||||
// visible side by side.
|
// visible side by side.
|
||||||
func (m *fullModel) startTmuxSession() tea.Cmd {
|
func (m *fullModel) startTmuxSession() tea.Cmd {
|
||||||
selfPath := "kctl-tui"
|
selfPath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
selfPath = "kctl-tui" // fallback to PATH lookup
|
||||||
|
}
|
||||||
panelCmd := fmt.Sprintf("%s panel --context=%s --ns=%s --team=%s",
|
panelCmd := fmt.Sprintf("%s panel --context=%s --ns=%s --team=%s",
|
||||||
selfPath, m.selectedContext, m.selectedNamespace, m.selectedTeam)
|
selfPath, m.selectedContext, m.selectedNamespace, m.selectedTeam)
|
||||||
|
|
||||||
@@ -273,6 +280,9 @@ func (m *fullModel) startTmuxSession() tea.Cmd {
|
|||||||
ctxA := m.cfg.ResolveContext(envA, m.selectedContext)
|
ctxA := m.cfg.ResolveContext(envA, m.selectedContext)
|
||||||
k9sCmdA := fmt.Sprintf("k9s --context %s -n %s", ctxA, m.selectedNamespace)
|
k9sCmdA := fmt.Sprintf("k9s --context %s -n %s", ctxA, m.selectedNamespace)
|
||||||
|
|
||||||
|
// Kill stale session first (ignore error if none exists).
|
||||||
|
exec.Command("tmux", "kill-session", "-t", "kctl").Run()
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"new-session", "-d", "-s", "kctl",
|
"new-session", "-d", "-s", "kctl",
|
||||||
panelCmd, ";",
|
panelCmd, ";",
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
|
// Licensed under the MIT License. See LICENSE file in project root for details.
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "github.com/skoelle/kctl-tui/internal/kctl"
|
import "github.com/skoelle/kctl-tui/internal/kctl"
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
|
// Licensed under the MIT License. See LICENSE file in project root for details.
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
// simpleItem is a minimal implementation of list.Item used for all
|
// simpleItem is a minimal implementation of list.Item used for all
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
|
// Licensed under the MIT License. See LICENSE file in project root for details.
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -18,6 +21,7 @@ func main() {
|
|||||||
|
|
||||||
// Extract global flags before delegating to sub-commands.
|
// Extract global flags before delegating to sub-commands.
|
||||||
verbose := false
|
verbose := false
|
||||||
|
showHelp := false
|
||||||
filtered := make([]string, 0, len(args))
|
filtered := make([]string, 0, len(args))
|
||||||
for _, a := range args {
|
for _, a := range args {
|
||||||
switch a {
|
switch a {
|
||||||
@@ -26,6 +30,8 @@ func main() {
|
|||||||
case "--version", "-v":
|
case "--version", "-v":
|
||||||
fmt.Printf("kctl-tui %s\n", version)
|
fmt.Printf("kctl-tui %s\n", version)
|
||||||
return
|
return
|
||||||
|
case "--help", "-h":
|
||||||
|
showHelp = true
|
||||||
default:
|
default:
|
||||||
filtered = append(filtered, a)
|
filtered = append(filtered, a)
|
||||||
}
|
}
|
||||||
@@ -34,6 +40,14 @@ func main() {
|
|||||||
kubeexec.SetVerbose(true, os.Stderr)
|
kubeexec.SetVerbose(true, os.Stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if showHelp {
|
||||||
|
printUsage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(filtered) == 0 {
|
||||||
|
// No subcommand — start the full TUI.
|
||||||
|
}
|
||||||
|
|
||||||
if len(filtered) > 0 {
|
if len(filtered) > 0 {
|
||||||
switch filtered[0] {
|
switch filtered[0] {
|
||||||
case "panel":
|
case "panel":
|
||||||
@@ -48,6 +62,16 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
case "doctor":
|
||||||
|
if err := runDoctor(); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "kctl-tui doctor error:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "unknown command: %s\n\n", filtered[0])
|
||||||
|
printUsage()
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +83,28 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printUsage() {
|
||||||
|
fmt.Print(`kctl-tui — Kubernetes entry-point TUI
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
kctl-tui [flags] start the TUI (full navigation mode)
|
||||||
|
kctl-tui doctor check tools, config and connections
|
||||||
|
kctl-tui config check validate ~/.kctl-tui/config.yaml
|
||||||
|
kctl-tui panel [options] control pane (called internally by tmux)
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
--verbose log all kubectl/aws commands to stderr
|
||||||
|
--version print version
|
||||||
|
--help show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
kctl-tui # start the TUI
|
||||||
|
kctl-tui doctor # verify everything is installed
|
||||||
|
kctl-tui --verbose 2>debug.log # log commands to a file
|
||||||
|
kctl-tui config check # validate config
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
func runConfig(args []string) error {
|
func runConfig(args []string) error {
|
||||||
if len(args) == 0 || args[0] != "check" {
|
if len(args) == 0 || args[0] != "check" {
|
||||||
return fmt.Errorf("usage: kctl-tui config check")
|
return fmt.Errorf("usage: kctl-tui config check")
|
||||||
@@ -110,3 +156,112 @@ func runConfig(args []string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runDoctor() error {
|
||||||
|
pass := "ok"
|
||||||
|
fail := "FAIL"
|
||||||
|
warn := "WARN"
|
||||||
|
status := pass
|
||||||
|
errs := 0
|
||||||
|
|
||||||
|
check := func(label string, err error) {
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" [%s] %s: %v\n", fail, label, err)
|
||||||
|
status = fail
|
||||||
|
errs++
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" [%s] %s\n", pass, label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
warnCheck := func(label string, err error) {
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" [%s] %s: %v\n", warn, label, err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" [%s] %s\n", pass, label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tools ---
|
||||||
|
fmt.Println("\nTools:")
|
||||||
|
check("kubectl", kubeexec.CheckTool("kubectl"))
|
||||||
|
check("tmux/psmux", kubeexec.CheckTool("tmux"))
|
||||||
|
check("k9s", kubeexec.CheckTool("k9s"))
|
||||||
|
warnCheck("aws CLI (optional)", kubeexec.CheckTool("aws"))
|
||||||
|
|
||||||
|
// --- Config ---
|
||||||
|
fmt.Println("\nConfig:")
|
||||||
|
cfgPath, err := config.DefaultPath()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" [%s] config path: %v\n", fail, err)
|
||||||
|
errs++
|
||||||
|
status = fail
|
||||||
|
} else {
|
||||||
|
cfg, err := config.Load(cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" [%s] load config: %v\n", fail, err)
|
||||||
|
errs++
|
||||||
|
status = fail
|
||||||
|
} else {
|
||||||
|
check("config file exists", nil)
|
||||||
|
if len(cfg.Contexts) == 0 {
|
||||||
|
fmt.Printf(" [%s] contexts configured\n", fail)
|
||||||
|
errs++
|
||||||
|
status = fail
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" [%s] contexts configured (%d)\n", pass, len(cfg.Contexts))
|
||||||
|
}
|
||||||
|
if len(cfg.Envs) == 0 {
|
||||||
|
fmt.Printf(" [%s] envs configured\n", fail)
|
||||||
|
errs++
|
||||||
|
status = fail
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" [%s] envs configured (%d)\n", pass, len(cfg.Envs))
|
||||||
|
}
|
||||||
|
if cfg.ContextTemplate != "" {
|
||||||
|
ctx := cfg.ResolveContext(cfg.Envs[0], cfg.Contexts[0])
|
||||||
|
fmt.Printf(" [%s] context_template resolves to: %s\n", pass, ctx)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" [%s] context_template is empty\n", fail)
|
||||||
|
errs++
|
||||||
|
status = fail
|
||||||
|
}
|
||||||
|
if cfg.SecretNameTemplate != "" && len(cfg.Envs) > 0 {
|
||||||
|
secret := cfg.ResolveSecretName("example-ns", cfg.Envs[0])
|
||||||
|
fmt.Printf(" [%s] secret_name_template resolves to: %s\n", pass, secret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Connections ---
|
||||||
|
fmt.Println("\nConnections:")
|
||||||
|
if kubeexec.CheckTool("kubectl") == nil {
|
||||||
|
err := kubeexec.CheckAWSAuth()
|
||||||
|
if err == nil {
|
||||||
|
fmt.Printf(" [%s] kubectl cluster reachable\n", pass)
|
||||||
|
} else {
|
||||||
|
// Not fatal — cluster might be unreachable from this machine
|
||||||
|
fmt.Printf(" [%s] kubectl cluster: %v\n", warn, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if kubeexec.CheckTool("aws") == nil {
|
||||||
|
err := kubeexec.CheckAWSAuth()
|
||||||
|
if err == nil {
|
||||||
|
fmt.Printf(" [%s] AWS credentials valid\n", pass)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" [%s] AWS credentials: %v\n", warn, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" [%s] AWS credentials (aws CLI not installed)\n", warn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Summary ---
|
||||||
|
fmt.Println()
|
||||||
|
if status == pass {
|
||||||
|
fmt.Println("All checks passed. kctl-tui is ready to use.")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%d error(s) found. Fix the issues above and re-run: kctl-tui doctor\n", errs)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
|
// Licensed under the MIT License. See LICENSE file in project root for details.
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
|
# Licensed under the MIT License. See LICENSE file in project root for details.
|
||||||
# Install script for kctl-tui.
|
# Install script for kctl-tui.
|
||||||
# Downloads the latest GitHub release binary matching the current OS/arch
|
# Downloads the latest GitHub release binary matching the current OS/arch
|
||||||
# and installs it to /usr/local/bin (or $INSTALL_DIR if set).
|
# and installs it to /usr/local/bin (or $INSTALL_DIR if set).
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
|
// Licensed under the MIT License. See LICENSE file in project root for details.
|
||||||
|
|
||||||
// Package config loads the user-specific, non-versioned kctl-tui
|
// Package config loads the user-specific, non-versioned kctl-tui
|
||||||
// configuration (contexts, envs, templates) from a YAML file.
|
// configuration (contexts, envs, templates) from a YAML file.
|
||||||
package config
|
package config
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
|
// Licensed under the MIT License. See LICENSE file in project root for details.
|
||||||
|
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
|
// Licensed under the MIT License. See LICENSE file in project root for details.
|
||||||
|
|
||||||
package kctl
|
package kctl
|
||||||
|
|
||||||
import "sort"
|
import "sort"
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
|
// Licensed under the MIT License. See LICENSE file in project root for details.
|
||||||
|
|
||||||
package kctl
|
package kctl
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
|
// Licensed under the MIT License. See LICENSE file in project root for details.
|
||||||
|
|
||||||
package kctl
|
package kctl
|
||||||
|
|
||||||
import "sort"
|
import "sort"
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
|
// Licensed under the MIT License. See LICENSE file in project root for details.
|
||||||
|
|
||||||
package kctl
|
package kctl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
|
// Licensed under the MIT License. See LICENSE file in project root for details.
|
||||||
|
|
||||||
package kctl
|
package kctl
|
||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
|
// Licensed under the MIT License. See LICENSE file in project root for details.
|
||||||
|
|
||||||
package kctl
|
package kctl
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
|
// Licensed under the MIT License. See LICENSE file in project root for details.
|
||||||
|
|
||||||
// Package kubeexec wraps kubectl/aws-cli invocations used by kctl-tui.
|
// Package kubeexec wraps kubectl/aws-cli invocations used by kctl-tui.
|
||||||
// All functions here have side effects (they run external processes) and
|
// All functions here have side effects (they run external processes) and
|
||||||
// are therefore not covered by unit tests; the pure logic they depend on
|
// are therefore not covered by unit tests; the pure logic they depend on
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
|
// Licensed under the MIT License. See LICENSE file in project root for details.
|
||||||
|
|
||||||
package kubeexec
|
package kubeexec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
Reference in New Issue
Block a user