mirror of
https://github.com/skoelle/kctl-tui.git
synced 2026-09-17 20:10:24 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37be0707ad | ||
|
|
9e6acdedc2 | ||
|
|
07662dc9b7 | ||
|
|
38b28f64fb | ||
|
|
6a749811c7 | ||
|
|
313dffcbbf | ||
|
|
8546ac9bf2 | ||
|
|
4c0650c43b | ||
|
|
e36ac3568f | ||
|
|
697017e496 | ||
|
|
36a7cc5e3a | ||
|
|
e48bd790bd | ||
|
|
6795ee1f4f |
@@ -60,7 +60,7 @@ jobs:
|
||||
ext=""
|
||||
if [ "${{ matrix.goos }}" = "windows" ]; then ext=".exe"; fi
|
||||
out="dist/kctl-tui-${{ matrix.goos }}-${{ matrix.goarch }}${ext}"
|
||||
go build -o "$out" -ldflags "-s -w" ./cmd/kctl-tui
|
||||
go build -o "$out" -ldflags "-s -w -X main.version=${GITHUB_REF_NAME}" ./cmd/kctl-tui
|
||||
echo "Built $out"
|
||||
|
||||
- name: Upload artifact
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -66,21 +66,22 @@ is still open. For the full requirements, see [SPEC.md](SPEC.md).
|
||||
- [ ] Paginate/scroll the secrets diff table for secrets with many fields
|
||||
instead of relying on terminal wrapping.
|
||||
|
||||
## Phase 3 — Windows-native support (open, secondary priority)
|
||||
## Phase 3 — Windows-native support (done)
|
||||
|
||||
- [ ] Detect OS at runtime; on native Windows (no WSL), fall back to
|
||||
`wt.exe split-pane` instead of `tmux` for the status panes.
|
||||
- [ ] Document/implement that `Esc`-triggered session close is **not**
|
||||
available in the native Windows fallback — the panes must be closed
|
||||
manually there.
|
||||
- [x] Windows support via [psmux](https://github.com/marlocarlo/psmux) —
|
||||
a native, tmux-compatible terminal multiplexer. kctl-tui works
|
||||
without code changes; `CheckTool("tmux")` error message includes
|
||||
Windows-specific install hint.
|
||||
- [x] `install.ps1` — PowerShell install script for Windows.
|
||||
- [x] Updated README and SPEC with Windows + psmux setup instructions.
|
||||
|
||||
## Phase 4 — Nice-to-haves (open, not committed)
|
||||
|
||||
- [x] `--version` flag — prints version, set via `-ldflags` at build time.
|
||||
- [x] Config validation command (`kctl-tui config check`) — validates
|
||||
required fields and shows a resolved context example.
|
||||
- [ ] Optional direct use of `client-go` instead of shelling out to
|
||||
`kubectl`, for faster context/namespace/label queries.
|
||||
- [ ] Config validation command (`kctl-tui config check`) that reports
|
||||
unknown label keys or context names not present in the current
|
||||
kubeconfig.
|
||||
- [ ] Homebrew tap / `scoop` manifest as additional install options
|
||||
alongside `install.sh`.
|
||||
|
||||
|
||||
@@ -68,12 +68,16 @@ See [SPEC.md](SPEC.md) for the full requirements and design rationale, and
|
||||
Configuration below - they must already exist in your kubeconfig, e.g.
|
||||
added via `aws eks update-kubeconfig`).
|
||||
- `k9s` (used for the two status panes).
|
||||
- `tmux` (used for the 3-pane layout). On Windows, this means running
|
||||
kctl-tui inside **WSL** — `tmux` has no native Windows port. Native
|
||||
Windows Terminal has its own split-pane feature, but it cannot be
|
||||
scripted from inside a pane the way `tmux` can, so the automated 3-pane
|
||||
layout and the `Esc` session handling described above are only fully
|
||||
supported under Linux/WSL. See SPEC.md section 3.6 for details.
|
||||
- `tmux` (used for the 3-pane layout). On **Linux/macOS**, install
|
||||
`tmux` via your package manager. On **Windows**, install
|
||||
[psmux](https://github.com/marlocarlo/psmux) — a native,
|
||||
tmux-compatible terminal multiplexer:
|
||||
```powershell
|
||||
scoop install psmux
|
||||
# or
|
||||
cargo install psmux
|
||||
```
|
||||
psmux provides a `tmux` command, so kctl-tui works without changes.
|
||||
- `aws` CLI, configured with credentials, only needed for the secrets
|
||||
workflow.
|
||||
|
||||
@@ -85,6 +89,15 @@ See [SPEC.md](SPEC.md) for the full requirements and design rationale, and
|
||||
curl -fsSL https://raw.githubusercontent.com/skoelle/kctl-tui/main/install.sh | bash
|
||||
```
|
||||
|
||||
### Quick install (Windows)
|
||||
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/skoelle/kctl-tui/main/install.ps1 | iex
|
||||
```
|
||||
|
||||
This downloads the latest release binary for your architecture from
|
||||
GitHub Releases and installs it to your PATH.
|
||||
|
||||
This downloads the latest release binary for your OS/architecture from
|
||||
GitHub Releases and installs it to `/usr/local/bin/kctl-tui`.
|
||||
|
||||
@@ -167,17 +180,37 @@ aws_sso_login_command: "aws sso login"
|
||||
that way — it typically contains your organization's internal account ID,
|
||||
context naming, and label names.
|
||||
|
||||
## WSL setup notes
|
||||
## Windows notes
|
||||
|
||||
If `kubectx`/`kubens` or `kctl-tui` report a missing kubeconfig inside WSL,
|
||||
your kubeconfig most likely only exists on the Windows side. Symlink it
|
||||
into WSL:
|
||||
On native Windows (without WSL), install [psmux](https://github.com/marlocarlo/psmux)
|
||||
for the 3-pane layout. psmux is a native Windows terminal multiplexer
|
||||
that is tmux-compatible — kctl-tui works without code changes:
|
||||
|
||||
```powershell
|
||||
scoop install psmux
|
||||
# or
|
||||
cargo install psmux
|
||||
```
|
||||
|
||||
If you prefer WSL, symlink your kubeconfig into WSL:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.kube
|
||||
ln -s /mnt/c/Users/<your-windows-username>/.kube/config ~/.kube/config
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
kctl-tui # start the TUI (full navigation mode)
|
||||
kctl-tui --help # show all commands and flags
|
||||
kctl-tui --version # print version
|
||||
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 panel --context=... --ns=... --team=... # internal (called by tmux)
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
@@ -194,4 +227,4 @@ and untested, since it has no meaningful behavior without a live cluster.
|
||||
|
||||
## 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):
|
||||
|
||||
```
|
||||
# Kill stale session first (separate command — tmux aborts on kill-session error).
|
||||
tmux kill-session -t kctl
|
||||
tmux new-session -d -s kctl \
|
||||
"kctl-tui panel --context=$CTX_A --ns=$NS --team=$TEAM" \; \
|
||||
set-option -t kctl remain-on-exit on \; \
|
||||
@@ -167,18 +169,15 @@ The `context_template` replaces `{region}`, `{account_id}`, `{env}`, and
|
||||
selected environment/context. The resolved value must match an existing
|
||||
context in your kubeconfig (e.g. added via `aws eks update-kubeconfig`).
|
||||
|
||||
**Platform limitation on Windows without WSL:** `kill-session` is
|
||||
tmux-specific. Windows Terminal (`wt.exe`) offers no equivalent scripting
|
||||
to end the session from inside a pane. On plain Windows (without WSL),
|
||||
only a simplified flow is possible: k9s panes are closed manually (`q`,
|
||||
then `Ctrl+Shift+W`); automatic session termination is unavailable there.
|
||||
This limitation is the main reason the primary target system is set to
|
||||
Linux/WSL.
|
||||
**Windows support:** On native Windows, install
|
||||
[psmux](https://github.com/marlocarlo/psmux) — a native, tmux-compatible
|
||||
terminal multiplexer. psmux provides a `tmux` command, so kctl-tui works
|
||||
without code changes (including `Esc`-triggered session termination).
|
||||
Alternatively, run kctl-tui inside WSL with standard `tmux`.
|
||||
|
||||
## 4. Non-functional requirements
|
||||
|
||||
- **Primary platform Linux/WSL**, secondary native Windows with reduced
|
||||
functionality.
|
||||
- **Primary platform Linux/WSL**, secondary native Windows (via psmux).
|
||||
- **Single-binary distribution** without external runtime dependency (Go
|
||||
provides this natively).
|
||||
- **External dependencies**: `kubectl` mandatory; `tmux`, `k9s`, `aws-cli`
|
||||
|
||||
+40
-25
@@ -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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
|
||||
@@ -192,6 +196,14 @@ func (m *fullModel) handleSelect() (tea.Model, tea.Cmd) {
|
||||
|
||||
case screenNamespace:
|
||||
m.selectedNamespace = item.value
|
||||
if err := kubeexec.CheckTool("tmux"); err != nil {
|
||||
m.err = err
|
||||
return m, nil
|
||||
}
|
||||
if err := kubeexec.CheckTool("k9s"); err != nil {
|
||||
m.err = err
|
||||
return m, nil
|
||||
}
|
||||
return m, m.startTmuxSession()
|
||||
}
|
||||
return m, nil
|
||||
@@ -257,19 +269,10 @@ func (m *fullModel) loadNamespacesFor(teamValue string) tea.Cmd {
|
||||
// configured envs, resolved via the context template, so both are
|
||||
// visible side by side.
|
||||
func (m *fullModel) startTmuxSession() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if err := kubeexec.CheckTool("tmux"); err != nil {
|
||||
return errMsg{err}
|
||||
}
|
||||
if err := kubeexec.CheckTool("k9s"); err != nil {
|
||||
return errMsg{err}
|
||||
}
|
||||
return m.buildAndRunTmux()
|
||||
selfPath, err := os.Executable()
|
||||
if err != nil {
|
||||
selfPath = "kctl-tui" // fallback to PATH lookup
|
||||
}
|
||||
}
|
||||
|
||||
func (m *fullModel) buildAndRunTmux() tea.Msg {
|
||||
selfPath := "kctl-tui" // resolved via PATH; see README for install instructions
|
||||
panelCmd := fmt.Sprintf("%s panel --context=%s --ns=%s --team=%s",
|
||||
selfPath, m.selectedContext, m.selectedNamespace, m.selectedTeam)
|
||||
|
||||
@@ -277,28 +280,40 @@ func (m *fullModel) buildAndRunTmux() tea.Msg {
|
||||
ctxA := m.cfg.ResolveContext(envA, m.selectedContext)
|
||||
k9sCmdA := fmt.Sprintf("k9s --context %s -n %s", ctxA, m.selectedNamespace)
|
||||
|
||||
args := []string{
|
||||
"new-session", "-d", "-s", "kctl",
|
||||
panelCmd, ";",
|
||||
"set-option", "-t", "kctl", "remain-on-exit", "on", ";",
|
||||
"split-window", "-v", "-t", "kctl:0.0", k9sCmdA, ";",
|
||||
fmt.Fprintf(os.Stderr, "[debug] selfPath=%s\n", selfPath)
|
||||
fmt.Fprintf(os.Stderr, "[debug] panelCmd=%s\n", panelCmd)
|
||||
|
||||
// Kill stale session first (ignore error if none exists).
|
||||
exec.Command("tmux", "kill-session", "-t", "kctl").Run()
|
||||
|
||||
// Run setup commands individually — this avoids the ; separator
|
||||
// issue on Windows where psmux doesn't handle chained args.
|
||||
setup := [][]string{
|
||||
{"new-session", "-d", "-s", "kctl", panelCmd},
|
||||
{"set-option", "-t", "kctl", "remain-on-exit", "on"},
|
||||
{"split-window", "-v", "-t", "kctl:0.0", k9sCmdA},
|
||||
}
|
||||
if len(m.cfg.Envs) > 1 {
|
||||
envB := m.cfg.Envs[1]
|
||||
ctxB := m.cfg.ResolveContext(envB, m.selectedContext)
|
||||
k9sCmdB := fmt.Sprintf("k9s --context %s -n %s", ctxB, m.selectedNamespace)
|
||||
args = append(args,
|
||||
"split-window", "-v", "-t", "kctl:0.1", k9sCmdB, ";",
|
||||
)
|
||||
setup = append(setup, []string{"split-window", "-v", "-t", "kctl:0.1", k9sCmdB})
|
||||
}
|
||||
args = append(args,
|
||||
"select-layout", "-t", "kctl", "even-vertical", ";",
|
||||
"select-pane", "-t", "kctl:0.0", ";",
|
||||
"attach", "-t", "kctl",
|
||||
setup = append(setup,
|
||||
[]string{"select-layout", "-t", "kctl", "even-vertical"},
|
||||
[]string{"select-pane", "-t", "kctl:0.0"},
|
||||
)
|
||||
|
||||
c := exec.Command("tmux", args...)
|
||||
for _, args := range setup {
|
||||
if out, err := exec.Command("tmux", args...).CombinedOutput(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[debug] tmux %s failed: %v\n%s\n", args[0], err, out)
|
||||
return func() tea.Msg { return tmuxDoneMsg{err: fmt.Errorf("tmux %s: %w", args[0], err)} }
|
||||
}
|
||||
}
|
||||
|
||||
// Only attach uses tea.ExecProcess so it takes over the terminal.
|
||||
fmt.Fprintf(os.Stderr, "[debug] tmux attach -t kctl\n")
|
||||
c := exec.Command("tmux", "attach", "-t", "kctl")
|
||||
return tea.ExecProcess(c, func(err error) tea.Msg {
|
||||
return tmuxDoneMsg{err: err}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
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
|
||||
|
||||
// simpleItem is a minimal implementation of list.Item used for all
|
||||
|
||||
+231
-7
@@ -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
|
||||
|
||||
import (
|
||||
@@ -6,19 +9,30 @@ import (
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"github.com/skoelle/kctl-tui/internal/config"
|
||||
"github.com/skoelle/kctl-tui/internal/kubeexec"
|
||||
)
|
||||
|
||||
// version is set via -ldflags at build time.
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
args := os.Args[1:]
|
||||
|
||||
// Extract --verbose before delegating to panel or full mode.
|
||||
// Extract global flags before delegating to sub-commands.
|
||||
verbose := false
|
||||
showHelp := false
|
||||
filtered := make([]string, 0, len(args))
|
||||
for _, a := range args {
|
||||
if a == "--verbose" {
|
||||
switch a {
|
||||
case "--verbose":
|
||||
verbose = true
|
||||
} else {
|
||||
case "--version", "-v":
|
||||
fmt.Printf("kctl-tui %s\n", version)
|
||||
return
|
||||
case "--help", "-h":
|
||||
showHelp = true
|
||||
default:
|
||||
filtered = append(filtered, a)
|
||||
}
|
||||
}
|
||||
@@ -26,12 +40,39 @@ func main() {
|
||||
kubeexec.SetVerbose(true, os.Stderr)
|
||||
}
|
||||
|
||||
if len(filtered) > 0 && filtered[0] == "panel" {
|
||||
if err := runPanel(filtered[1:]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "kctl-tui panel error:", err)
|
||||
if showHelp {
|
||||
printUsage()
|
||||
return
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
// No subcommand — start the full TUI.
|
||||
}
|
||||
|
||||
if len(filtered) > 0 {
|
||||
switch filtered[0] {
|
||||
case "panel":
|
||||
if err := runPanel(filtered[1:]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "kctl-tui panel error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
case "config":
|
||||
if err := runConfig(filtered[1:]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "kctl-tui config error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
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)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
m := newFullModel()
|
||||
@@ -41,3 +82,186 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
if len(args) == 0 || args[0] != "check" {
|
||||
return fmt.Errorf("usage: kctl-tui config check")
|
||||
}
|
||||
cfgPath, err := config.DefaultPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot determine config path: %w", err)
|
||||
}
|
||||
cfg, err := config.Load(cfgPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load %s: %w", cfgPath, err)
|
||||
}
|
||||
|
||||
ok := true
|
||||
if len(cfg.Contexts) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "ERROR: no 'contexts' configured")
|
||||
ok = false
|
||||
}
|
||||
if len(cfg.Envs) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "ERROR: no 'envs' configured")
|
||||
ok = false
|
||||
}
|
||||
if cfg.ContextTemplate == "" {
|
||||
fmt.Fprintln(os.Stderr, "ERROR: 'context_template' is empty")
|
||||
ok = false
|
||||
}
|
||||
if cfg.SecretNameTemplate == "" {
|
||||
fmt.Fprintln(os.Stderr, "ERROR: 'secret_name_template' is empty")
|
||||
ok = false
|
||||
}
|
||||
if cfg.TeamLabelKey == "" {
|
||||
fmt.Fprintln(os.Stderr, "WARNING: 'team_label_key' is empty — team selection will have no groups")
|
||||
}
|
||||
if cfg.AWSRegion == "" {
|
||||
fmt.Fprintln(os.Stderr, "WARNING: 'aws_region' is empty — secrets workflow will fail")
|
||||
}
|
||||
|
||||
// Try resolving one context to verify the template works.
|
||||
if len(cfg.Contexts) > 0 && len(cfg.Envs) > 0 && cfg.ContextTemplate != "" {
|
||||
ctx := cfg.ResolveContext(cfg.Envs[0], cfg.Contexts[0])
|
||||
fmt.Printf("Resolved context example: %s\n", ctx)
|
||||
}
|
||||
|
||||
if ok {
|
||||
fmt.Println("Config OK")
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, "Config has errors — see above")
|
||||
os.Exit(1)
|
||||
}
|
||||
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
|
||||
|
||||
import (
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
# Install script for kctl-tui on Windows.
|
||||
# Downloads the latest GitHub release binary matching the current architecture
|
||||
# and installs it to your PATH.
|
||||
#
|
||||
# Usage (PowerShell):
|
||||
# irm https://raw.githubusercontent.com/skoelle/kctl-tui/main/install.ps1 | iex
|
||||
#
|
||||
# Or save and run locally:
|
||||
# .\install.ps1
|
||||
#
|
||||
# Requires: PowerShell 5.1+, internet access.
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$Repo = "skoelle/kctl-tui"
|
||||
$BinName = "kctl-tui"
|
||||
|
||||
# --- Detect architecture ---
|
||||
$arch = $env:PROCESSOR_ARCHITECTURE
|
||||
switch ($arch) {
|
||||
"AMD64" { $goarch = "amd64" }
|
||||
"ARM64" { $goarch = "arm64" }
|
||||
default {
|
||||
Write-Error "Unsupported architecture: $arch"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# --- Determine install directory ---
|
||||
$installDir = "$env:USERPROFILE\bin"
|
||||
if (-not (Test-Path $installDir)) {
|
||||
New-Item -ItemType Directory -Path $installDir | Out-Null
|
||||
}
|
||||
|
||||
# Add to PATH if not already there
|
||||
$currentPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
||||
if ($currentPath -notlike "*$installDir*") {
|
||||
[Environment]::SetEnvironmentVariable("Path", "$currentPath;$installDir", "User")
|
||||
$env:Path = "$env:Path;$installDir"
|
||||
Write-Host "Added $installDir to your PATH."
|
||||
}
|
||||
|
||||
# --- Query GitHub API for latest release ---
|
||||
Write-Host "Detecting latest release for $Repo ..."
|
||||
try {
|
||||
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/latest" -UseBasicParsing
|
||||
} catch {
|
||||
Write-Error "Failed to reach the GitHub API (network error). Check your internet connection and try again."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$tag = $release.tag_name
|
||||
if (-not $tag) {
|
||||
Write-Error "Could not find a published release for $Repo. No release has been tagged yet."
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Latest release: $tag"
|
||||
|
||||
# --- Download binary ---
|
||||
$asset = "kctl-tui-windows-${goarch}.exe"
|
||||
$url = "https://github.com/$Repo/releases/download/$tag/$asset"
|
||||
$outFile = "$installDir\$BinName.exe"
|
||||
|
||||
Write-Host "Downloading $asset ($tag) ..."
|
||||
try {
|
||||
Invoke-WebRequest -Uri $url -OutFile $outFile -UseBasicParsing
|
||||
} catch {
|
||||
Write-Error "Download failed: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Installed $BinName to $outFile"
|
||||
Write-Host "Done. Run '$BinName' to get started."
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/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.
|
||||
# Downloads the latest GitHub release binary matching the current OS/arch
|
||||
# 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
|
||||
// configuration (contexts, envs, templates) from a YAML file.
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
// All functions here have side effects (they run external processes) and
|
||||
// are therefore not covered by unit tests; the pure logic they depend on
|
||||
@@ -182,8 +185,11 @@ func RunAWSLogin(loginCommand string) *exec.Cmd {
|
||||
// Returns nil if found, or a descriptive error if not.
|
||||
func CheckTool(name string) error {
|
||||
_, err := exec.LookPath(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%q not found in PATH — please install it first", name)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
if name == "tmux" {
|
||||
return fmt.Errorf("%q not found in PATH — install tmux (Linux/macOS) or psmux (Windows: scoop install psmux or cargo install psmux)", name)
|
||||
}
|
||||
return fmt.Errorf("%q not found in PATH — please install it first", name)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
import (
|
||||
|
||||
Reference in New Issue
Block a user