Compare commits

..
3 Commits
8 changed files with 224 additions and 53 deletions
+1 -1
View File
@@ -60,7 +60,7 @@ jobs:
ext="" ext=""
if [ "${{ matrix.goos }}" = "windows" ]; then ext=".exe"; fi if [ "${{ matrix.goos }}" = "windows" ]; then ext=".exe"; fi
out="dist/kctl-tui-${{ matrix.goos }}-${{ matrix.goarch }}${ext}" 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" echo "Built $out"
- name: Upload artifact - name: Upload artifact
+10 -9
View File
@@ -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 - [ ] Paginate/scroll the secrets diff table for secrets with many fields
instead of relying on terminal wrapping. 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 - [x] Windows support via [psmux](https://github.com/marlocarlo/psmux) —
`wt.exe split-pane` instead of `tmux` for the status panes. a native, tmux-compatible terminal multiplexer. kctl-tui works
- [ ] Document/implement that `Esc`-triggered session close is **not** without code changes; `CheckTool("tmux")` error message includes
available in the native Windows fallback — the panes must be closed Windows-specific install hint.
manually there. - [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) ## 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 - [ ] Optional direct use of `client-go` instead of shelling out to
`kubectl`, for faster context/namespace/label queries. `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 - [ ] Homebrew tap / `scoop` manifest as additional install options
alongside `install.sh`. alongside `install.sh`.
+41 -10
View File
@@ -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. Configuration below - they must already exist in your kubeconfig, e.g.
added via `aws eks update-kubeconfig`). added via `aws eks update-kubeconfig`).
- `k9s` (used for the two status panes). - `k9s` (used for the two status panes).
- `tmux` (used for the 3-pane layout). On Windows, this means running - `tmux` (used for the 3-pane layout). On **Linux/macOS**, install
kctl-tui inside **WSL**`tmux` has no native Windows port. Native `tmux` via your package manager. On **Windows**, install
Windows Terminal has its own split-pane feature, but it cannot be [psmux](https://github.com/marlocarlo/psmux) — a native,
scripted from inside a pane the way `tmux` can, so the automated 3-pane tmux-compatible terminal multiplexer:
layout and the `Esc` session handling described above are only fully ```powershell
supported under Linux/WSL. See SPEC.md section 3.6 for details. 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 - `aws` CLI, configured with credentials, only needed for the secrets
workflow. 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 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 This downloads the latest release binary for your OS/architecture from
GitHub Releases and installs it to `/usr/local/bin/kctl-tui`. GitHub Releases and installs it to `/usr/local/bin/kctl-tui`.
@@ -167,17 +180,35 @@ aws_sso_login_command: "aws sso login"
that way — it typically contains your organization's internal account ID, that way — it typically contains your organization's internal account ID,
context naming, and label names. context naming, and label names.
## WSL setup notes ## Windows notes
If `kubectx`/`kubens` or `kctl-tui` report a missing kubeconfig inside WSL, On native Windows (without WSL), install [psmux](https://github.com/marlocarlo/psmux)
your kubeconfig most likely only exists on the Windows side. Symlink it for the 3-pane layout. psmux is a native Windows terminal multiplexer
into WSL: 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 ```bash
mkdir -p ~/.kube mkdir -p ~/.kube
ln -s /mnt/c/Users/<your-windows-username>/.kube/config ~/.kube/config ln -s /mnt/c/Users/<your-windows-username>/.kube/config ~/.kube/config
``` ```
## Usage
```bash
kctl-tui # start the TUI (full navigation mode)
kctl-tui --version # print version
kctl-tui --verbose # enable debug logging to stderr
kctl-tui config check # validate ~/.kctl-tui/config.yaml
kctl-tui panel --context=... --ns=... --team=... # internal (called by tmux)
```
## Development ## Development
```bash ```bash
+6 -9
View File
@@ -167,18 +167,15 @@ The `context_template` replaces `{region}`, `{account_id}`, `{env}`, and
selected environment/context. The resolved value must match an existing selected environment/context. The resolved value must match an existing
context in your kubeconfig (e.g. added via `aws eks update-kubeconfig`). context in your kubeconfig (e.g. added via `aws eks update-kubeconfig`).
**Platform limitation on Windows without WSL:** `kill-session` is **Windows support:** On native Windows, install
tmux-specific. Windows Terminal (`wt.exe`) offers no equivalent scripting [psmux](https://github.com/marlocarlo/psmux) — a native, tmux-compatible
to end the session from inside a pane. On plain Windows (without WSL), terminal multiplexer. psmux provides a `tmux` command, so kctl-tui works
only a simplified flow is possible: k9s panes are closed manually (`q`, without code changes (including `Esc`-triggered session termination).
then `Ctrl+Shift+W`); automatic session termination is unavailable there. Alternatively, run kctl-tui inside WSL with standard `tmux`.
This limitation is the main reason the primary target system is set to
Linux/WSL.
## 4. Non-functional requirements ## 4. Non-functional requirements
- **Primary platform Linux/WSL**, secondary native Windows with reduced - **Primary platform Linux/WSL**, secondary native Windows (via psmux).
functionality.
- **Single-binary distribution** without external runtime dependency (Go - **Single-binary distribution** without external runtime dependency (Go
provides this natively). provides this natively).
- **External dependencies**: `kubectl` mandatory; `tmux`, `k9s`, `aws-cli` - **External dependencies**: `kubectl` mandatory; `tmux`, `k9s`, `aws-cli`
+9 -13
View File
@@ -192,6 +192,14 @@ func (m *fullModel) handleSelect() (tea.Model, tea.Cmd) {
case screenNamespace: case screenNamespace:
m.selectedNamespace = item.value 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, m.startTmuxSession()
} }
return m, nil return m, nil
@@ -257,19 +265,7 @@ 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 {
return func() tea.Msg { selfPath := "kctl-tui"
if err := kubeexec.CheckTool("tmux"); err != nil {
return errMsg{err}
}
if err := kubeexec.CheckTool("k9s"); err != nil {
return errMsg{err}
}
return m.buildAndRunTmux()
}
}
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", 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)
+77 -8
View File
@@ -6,19 +6,27 @@ import (
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/skoelle/kctl-tui/internal/config"
"github.com/skoelle/kctl-tui/internal/kubeexec" "github.com/skoelle/kctl-tui/internal/kubeexec"
) )
// version is set via -ldflags at build time.
var version = "dev"
func main() { func main() {
args := os.Args[1:] args := os.Args[1:]
// Extract --verbose before delegating to panel or full mode. // Extract global flags before delegating to sub-commands.
verbose := false verbose := false
filtered := make([]string, 0, len(args)) filtered := make([]string, 0, len(args))
for _, a := range args { for _, a := range args {
if a == "--verbose" { switch a {
case "--verbose":
verbose = true verbose = true
} else { case "--version", "-v":
fmt.Printf("kctl-tui %s\n", version)
return
default:
filtered = append(filtered, a) filtered = append(filtered, a)
} }
} }
@@ -26,12 +34,21 @@ func main() {
kubeexec.SetVerbose(true, os.Stderr) kubeexec.SetVerbose(true, os.Stderr)
} }
if len(filtered) > 0 && filtered[0] == "panel" { if len(filtered) > 0 {
if err := runPanel(filtered[1:]); err != nil { switch filtered[0] {
fmt.Fprintln(os.Stderr, "kctl-tui panel error:", err) case "panel":
os.Exit(1) 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
} }
return
} }
m := newFullModel() m := newFullModel()
@@ -41,3 +58,55 @@ func main() {
os.Exit(1) os.Exit(1)
} }
} }
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
}
+74
View File
@@ -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."
+6 -3
View File
@@ -182,8 +182,11 @@ func RunAWSLogin(loginCommand string) *exec.Cmd {
// Returns nil if found, or a descriptive error if not. // Returns nil if found, or a descriptive error if not.
func CheckTool(name string) error { func CheckTool(name string) error {
_, err := exec.LookPath(name) _, err := exec.LookPath(name)
if err != nil { if err == nil {
return fmt.Errorf("%q not found in PATH — please install it first", name) 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)
} }