mirror of
https://github.com/skoelle/kctl-tui.git
synced 2026-09-18 04:20:25 +00:00
Compare commits
20
Commits
v0.1.7
...
9521f47377
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9521f47377 | ||
|
|
a1e8894222 | ||
|
|
43aef3b76c | ||
|
|
84dbd9c230 | ||
|
|
2911f97991 | ||
|
|
7db58b6fe8 | ||
|
|
201168e8cd | ||
|
|
ffbf4341fa | ||
|
|
37be0707ad | ||
|
|
9e6acdedc2 | ||
|
|
07662dc9b7 | ||
|
|
38b28f64fb | ||
|
|
6a749811c7 | ||
|
|
313dffcbbf | ||
|
|
8546ac9bf2 | ||
|
|
4c0650c43b | ||
|
|
e36ac3568f | ||
|
|
697017e496 | ||
|
|
36a7cc5e3a | ||
|
|
e48bd790bd |
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
name: Bug Report
|
||||||
|
about: Report a bug to help us improve kctl-tui
|
||||||
|
title: ""
|
||||||
|
labels: bug
|
||||||
|
assignees: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
## Describe the Bug
|
||||||
|
|
||||||
|
A clear description of what the bug is.
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
|
||||||
|
1. Run `kctl-tui ...`
|
||||||
|
2. Select '...'
|
||||||
|
3. See error
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
What you expected to happen.
|
||||||
|
|
||||||
|
## Actual Behavior
|
||||||
|
|
||||||
|
What actually happened.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
- OS: [e.g. Windows 11, Ubuntu 24.04, macOS 15]
|
||||||
|
- Go version: [e.g. 1.22.5]
|
||||||
|
- kctl-tui version: [e.g. v0.2.0]
|
||||||
|
- kubectl version: [e.g. v1.30.0]
|
||||||
|
- tmux/psmux version: [e.g. tmux 3.4, psmux latest]
|
||||||
|
- Terminal: [e.g. Windows Terminal, iTerm2, GNOME Terminal]
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
Config file (without secrets), error logs, or screenshots.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
name: Feature Request
|
||||||
|
about: Suggest a new feature or improvement
|
||||||
|
title: ""
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
What problem does this feature solve?
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
|
||||||
|
Describe the solution you'd like.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
Any alternative solutions or workarounds you considered.
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
Any mockups, examples, or references.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
## Summary
|
||||||
|
|
||||||
|
Brief description of what this PR does.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- ...
|
||||||
|
|
||||||
|
## Related Issues
|
||||||
|
|
||||||
|
Closes #
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- [ ] `go vet ./...` passes
|
||||||
|
- [ ] `go test ./...` passes
|
||||||
|
- [ ] Manually tested on [OS]
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] Code follows existing style
|
||||||
|
- [ ] No new comments added (unless necessary)
|
||||||
|
- [ ] CHANGELOG.md updated (if applicable)
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to kctl-tui will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.2.0] - 2026-08-09
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Windows support** via [psmux](https://github.com/marlocarlo/psmux) as tmux-compatible multiplexer
|
||||||
|
- `install.ps1` PowerShell install script for Windows
|
||||||
|
- `--help` flag with full usage documentation
|
||||||
|
- `--version` / `-v` flag (set via `-ldflags` at build time)
|
||||||
|
- `kctl-tui doctor` command to verify tools, config and cluster connectivity
|
||||||
|
- `kctl-tui config check` command to validate `~/.kctl-tui/config.yaml`
|
||||||
|
- `--verbose` flag for debug logging of all kubectl/aws commands to stderr
|
||||||
|
- Binary secret value detection (`IsBinary`) — base64 or non-UTF-8 content is flagged
|
||||||
|
- ExternalSecret name validation before force-sync
|
||||||
|
- `CheckTool()` and `CheckAWSAuth()` helpers for pre-flight checks
|
||||||
|
- Diff table scroll support (j/k, up/down arrows)
|
||||||
|
- `--command pods` flag for k9s to start directly in pod view
|
||||||
|
- `--namespace` flag for k9s (Windows compatibility)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Tmux session cleanup: stale sessions are killed before creating new ones
|
||||||
|
- Panel path: uses `os.Executable()` instead of PATH lookup for correct binary
|
||||||
|
- k9s on Windows: `--namespace` instead of `-n`, direct pods view
|
||||||
|
- `--help` always shows usage and exits (no fallthrough to TUI)
|
||||||
|
- Unknown subcommands show error message + usage (exit 1)
|
||||||
|
- Panel quit: properly closes tmux session and exits
|
||||||
|
- Bubbles list filter disabled to prevent stuck filter state after tmux return
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Config uses `k8s_secret_name_template` for Kubernetes secret names (separate from AWS `secret_name_template`)
|
||||||
|
- Contexts resolved via template (`context_template`) instead of live kubectl discovery
|
||||||
|
- Panel redesigned: env-first menu (quit/beta/prod) with secrets sync + redeploy
|
||||||
|
- Full-mode navigation starts at team selection with default context
|
||||||
|
- `tea.ExecProcess` only used for `tmux attach` — setup commands run synchronously
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Context-pair logic (superseded by env-template based context resolution)
|
||||||
|
- Live kubectl discovery (superseded by config templates)
|
||||||
|
|
||||||
|
## [0.1.0] - 2026-07-XX
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release with Bubble Tea TUI
|
||||||
|
- 3-pane tmux orchestration (control panel + 2x k9s)
|
||||||
|
- Team/namespace navigation with kubectl context switching
|
||||||
|
- Panel mode with redeploy and secrets diff/force-sync
|
||||||
|
- AWS SSO integration with interactive login prompt
|
||||||
|
- CI/CD pipeline with Go build and release
|
||||||
|
- Linux/macOS install script
|
||||||
|
|
||||||
|
[Unreleased]: https://github.com/skoelle/kctl-tui/compare/v0.2.0...HEAD
|
||||||
|
[0.2.0]: https://github.com/skoelle/kctl-tui/compare/v0.1.0...v0.2.0
|
||||||
|
[0.1.0]: https://github.com/skoelle/kctl-tui/releases/tag/v0.1.0
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# Contributing to kctl-tui
|
||||||
|
|
||||||
|
Thanks for your interest in contributing! This document explains how to get
|
||||||
|
started.
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/skoelle/kctl-tui.git
|
||||||
|
cd kctl-tui
|
||||||
|
go mod download
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Go 1.22+
|
||||||
|
- kubectl, k9s, tmux (or psmux on Windows)
|
||||||
|
- An active Kubernetes cluster for integration testing
|
||||||
|
|
||||||
|
### Running Locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./cmd/kctl-tui
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o kctl-tui ./cmd/kctl-tui
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Version Tag
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -ldflags "-X main.version=v0.2.0" -o kctl-tui ./cmd/kctl-tui
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
cmd/kctl-tui/ Entry points (main, full mode, panel mode)
|
||||||
|
internal/config/ YAML config loading and template resolution
|
||||||
|
internal/kctl/ Pure logic (secret diffing, template engine)
|
||||||
|
internal/kubeexec/ kubectl/aws/tmux wrappers (side effects only)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Architecture Rules
|
||||||
|
|
||||||
|
- **Pure logic** goes into `internal/kctl` or `internal/config` — no exec, no I/O.
|
||||||
|
- **Side effects** (running kubectl, aws, tmux) go into `internal/kubeexec`.
|
||||||
|
- **UI** lives in `cmd/kctl-tui/` — Bubble Tea models, views, handlers.
|
||||||
|
- Unit tests cover pure logic only. Side-effect packages are tested via
|
||||||
|
integration/manual tests.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go vet ./... # static analysis
|
||||||
|
go test ./... # unit tests
|
||||||
|
```
|
||||||
|
|
||||||
|
There are no integration tests yet. Manual testing against a real cluster is
|
||||||
|
expected for UI and kubeexec changes.
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- Standard Go formatting (`gofmt`).
|
||||||
|
- No comments unless the logic is non-obvious.
|
||||||
|
- Error messages should be actionable — tell the user what to fix.
|
||||||
|
- Log commands with `kubeexec.VerboseLog()` when `--verbose` is active.
|
||||||
|
|
||||||
|
## Commits
|
||||||
|
|
||||||
|
- One logical change per commit.
|
||||||
|
- Imperative mood in commit messages ("Add ...", "Fix ...", "Remove ...").
|
||||||
|
- No co-authors in commits.
|
||||||
|
|
||||||
|
## Pull Requests
|
||||||
|
|
||||||
|
1. Fork the repo and create a feature branch.
|
||||||
|
2. Make your changes following the style guide above.
|
||||||
|
3. Run `go vet` and `go test`.
|
||||||
|
4. Open a PR against `main` with a clear description of what changed and why.
|
||||||
|
5. Reference any related issues.
|
||||||
|
|
||||||
|
## Issues
|
||||||
|
|
||||||
|
- Use the provided issue templates.
|
||||||
|
- Include your OS, Go version, and kctl-tui version.
|
||||||
|
- For bugs: steps to reproduce, expected vs actual behavior.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
By contributing, you agree that your contributions will be licensed under the
|
||||||
|
MIT License.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ is still open. For the full requirements, see [SPEC.md](SPEC.md).
|
|||||||
API is unreachable.
|
API is unreachable.
|
||||||
- [x] `README.md`, `config.example.yaml`.
|
- [x] `README.md`, `config.example.yaml`.
|
||||||
|
|
||||||
## Phase 1 — Core logic + navigation (done, initial version)
|
## Phase 1 — Core logic + navigation (done)
|
||||||
|
|
||||||
- [x] `internal/kctl`: pure, unit-tested logic —
|
- [x] `internal/kctl`: pure, unit-tested logic —
|
||||||
template resolution (`ResolveTemplate`), namespace/label filtering
|
template resolution (`ResolveTemplate`), namespace/label filtering
|
||||||
@@ -49,40 +49,101 @@ is still open. For the full requirements, see [SPEC.md](SPEC.md).
|
|||||||
**whole secret** (one ExternalSecret annotation).
|
**whole secret** (one ExternalSecret annotation).
|
||||||
- `Esc` closes the whole tmux session (`tmux kill-session`).
|
- `Esc` closes the whole tmux session (`tmux kill-session`).
|
||||||
|
|
||||||
## Phase 2 — Hardening (open)
|
## Phase 2 — Hardening (done)
|
||||||
|
|
||||||
- [ ] Handle non-JSON AWS secrets and Kubernetes secrets with binary
|
- [x] Handle non-JSON AWS secrets and Kubernetes secrets with binary
|
||||||
(non-UTF8) values more gracefully in the diff table (currently
|
(non-UTF8) values more gracefully in the diff table.
|
||||||
falls back to a single "value" key or may render oddly).
|
|
||||||
- [ ] Add integration-style tests against a local `kind`/`k3d` cluster in
|
- [ ] Add integration-style tests against a local `kind`/`k3d` cluster in
|
||||||
CI for the `kubeexec` wrappers currently excluded from automated
|
CI for the `kubeexec` wrappers currently excluded from automated
|
||||||
testing.
|
testing. *(Deferred — superseded by client-go in v0.3.0)*
|
||||||
- [ ] Input validation for the free-text steps in "panel" mode (empty
|
- [x] Input validation for the free-text steps in "panel" mode.
|
||||||
region/secret name, invalid characters).
|
- [x] Graceful handling when `tmux`, `k9s`, or `aws` are not installed.
|
||||||
- [ ] Graceful handling when `tmux`, `k9s`, or `aws` are not installed
|
- [x] Structured logging / `--verbose` flag for troubleshooting failed
|
||||||
(currently surfaces the raw exec error).
|
|
||||||
- [ ] Structured logging / `--verbose` flag for troubleshooting failed
|
|
||||||
`kubectl`/`aws` calls.
|
`kubectl`/`aws` calls.
|
||||||
- [ ] Paginate/scroll the secrets diff table for secrets with many fields
|
- [x] 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
|
- [x] Windows support via [psmux](https://github.com/marlocarlo/psmux).
|
||||||
`wt.exe split-pane` instead of `tmux` for the status panes.
|
- [x] `install.ps1` — PowerShell install script for Windows.
|
||||||
- [ ] Document/implement that `Esc`-triggered session close is **not**
|
- [x] Updated README and SPEC with Windows + psmux setup instructions.
|
||||||
available in the native Windows fallback — the panes must be closed
|
|
||||||
manually there.
|
|
||||||
|
|
||||||
## Phase 4 — Nice-to-haves (open, not committed)
|
## Phase 4 — Nice-to-haves (done for v0.2.0)
|
||||||
|
|
||||||
- [x] `--version` flag — prints version, set via `-ldflags` at build time.
|
- [x] `--version` flag — prints version, set via `-ldflags` at build time.
|
||||||
- [x] Config validation command (`kctl-tui config check`) — validates
|
- [x] Config validation command (`kctl-tui config check`) — validates
|
||||||
required fields and shows a resolved context example.
|
required fields and shows a resolved context example.
|
||||||
- [ ] Optional direct use of `client-go` instead of shelling out to
|
- [x] `kctl-tui doctor` — health check for tools, config, and connectivity.
|
||||||
`kubectl`, for faster context/namespace/label queries.
|
- [x] `--help` flag with full usage documentation.
|
||||||
- [ ] Homebrew tap / `scoop` manifest as additional install options
|
- [x] CHANGELOG.md, CONTRIBUTING.md, GitHub Issue/PR templates.
|
||||||
alongside `install.sh`.
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bugfix Sprint — between v0.2.0 and v0.3.0
|
||||||
|
|
||||||
|
- [ ] **Dead code** `cmd/kctl-tui/main.go:47-49` — empty
|
||||||
|
`if len(filtered) == 0` block with comment. Remove.
|
||||||
|
- [ ] **Redundant logic** `internal/kctl/diff.go:60-63` —
|
||||||
|
`if lb || rb { match = l == r }` is identical to the line above.
|
||||||
|
Either dead or misunderstood.
|
||||||
|
- [ ] **Diff-scroll is a no-op** `cmd/kctl-tui/panel.go:398-413` —
|
||||||
|
`renderDiffTable` is always called with `visibleHeight=0`, so
|
||||||
|
`end = len(entries)` is always true. j/k/arrows only change the
|
||||||
|
offset text but the table is always fully rendered. The CHANGELOG
|
||||||
|
promises "Diff table scroll support" but the feature is incomplete.
|
||||||
|
- [ ] **README duplicate** `README.md:98-102` — "This downloads the latest
|
||||||
|
release binary..." appears twice (once "to your PATH", once
|
||||||
|
"to /usr/local/bin"). Edit leftover.
|
||||||
|
- [ ] **go mod tidy in CI** `build.yml` — mutates `go.sum` during the
|
||||||
|
build instead of enforcing a tidy check. If someone forgets to tidy,
|
||||||
|
it's silently fixed instead of blocking the PR.
|
||||||
|
- [ ] **Bubbles filter disabled** `full.go:53`, `panel.go:87` — workaround
|
||||||
|
for the stuck-filter bug (commit 7db58b6). Users can no longer
|
||||||
|
type-to-filter. Worth restoring with a proper fix later.
|
||||||
|
- [ ] **Kleinkram:**
|
||||||
|
- `fmt.Errorf("%s", msg)` → `errors.New(msg)` in `kubeexec.go:41`
|
||||||
|
- `helpers.go` is a pointless 1:1 passthrough to the `kctl` package
|
||||||
|
- `IsBinary` also marks UTF-8 special chars (>0x7F) as "binary"
|
||||||
|
|
||||||
|
- [ ] **SECURITY.md** — fehlt, besonders wichtig für ein Tool mit
|
||||||
|
Secret-Workflows.
|
||||||
|
- [ ] **dependabot.yml** — automatische Dependency-Updates.
|
||||||
|
- [ ] **Checksummen für Release-Assets** — CI erzeugt Binaries aber keine
|
||||||
|
`.sha256`-Dateien; für `curl | bash`-Install wichtig.
|
||||||
|
- [ ] **Makefile / justfile** — Build/Test/Vet-Komfort.
|
||||||
|
- [ ] **golangci-lint** — `go vet` allein ist dünn; optional aber empfohlen.
|
||||||
|
- [ ] **PLAN.md aufräumen** — erledigte Phasen als „Done" markieren,
|
||||||
|
offene Items konsolidieren.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
### v0.3.0 — client-go integration
|
||||||
|
|
||||||
|
Replace kubectl shell-outs with direct API calls via `client-go`.
|
||||||
|
|
||||||
|
- [ ] Add `internal/kubeclient` package using `client-go` for:
|
||||||
|
- Context/namespace/label queries (faster than kubectl JSON parsing)
|
||||||
|
- Deployment list and rollout restart/status
|
||||||
|
- Secret fetch (AWS Secrets Manager via SDK, K8s secrets via API)
|
||||||
|
- ExternalSecret annotation update
|
||||||
|
- [ ] Keep `internal/kubeexec` as fallback for operations not yet
|
||||||
|
covered by `client-go`
|
||||||
|
- [ ] Remove `kind`/`k3d` integration test plan (client-go has its own
|
||||||
|
test coverage)
|
||||||
|
- [ ] Add unit tests with `fake.Clientset` for the new package
|
||||||
|
|
||||||
|
### v1.0 — Stable release
|
||||||
|
|
||||||
|
Production-ready with package manager support and documentation.
|
||||||
|
|
||||||
|
- [ ] Homebrew tap (`skoelle/homebrew-tap`) with `kctl-tui` formula
|
||||||
|
- [ ] Scoop manifest (`skoelle/scoop-bucket`) for Windows
|
||||||
|
- [ ] Full test coverage for `internal/kubeclient`
|
||||||
|
- [ ] Documentation: architecture diagram, config reference, troubleshooting
|
||||||
|
- [ ] Semantic versioning policy documented
|
||||||
|
- [ ] Deprecation policy for config schema changes
|
||||||
|
|
||||||
## Notes for contributors
|
## Notes for contributors
|
||||||
|
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ See [SPEC.md](SPEC.md) for the full requirements and design rationale, and
|
|||||||
| -> 1) Quit 2) beta 3) prod |
|
| -> 1) Quit 2) beta 3) prod |
|
||||||
| each with: a) Secrets sync b) Redeploy |
|
| each with: a) Secrets sync b) Redeploy |
|
||||||
+--------------------------------------------------+
|
+--------------------------------------------------+
|
||||||
| k9s --context <resolved beta context> -n <ns> |
|
| k9s --context <resolved beta context> --namespace <ns> --command pods |
|
||||||
+--------------------------------------------------+
|
+--------------------------------------------------+
|
||||||
| k9s --context <resolved prod context> -n <ns> |
|
| k9s --context <resolved prod context> --namespace <ns> --command pods |
|
||||||
+--------------------------------------------------+
|
+--------------------------------------------------+
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -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,12 @@ 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 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,11 +177,19 @@ 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
|
||||||
@@ -182,8 +200,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)
|
||||||
```
|
```
|
||||||
@@ -204,4 +224,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)
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ work, bundling the most common workflows currently done via long
|
|||||||
`kubectl`/`k9s`/`aws-cli` commands, operable through a text UI (arrow keys,
|
`kubectl`/`k9s`/`aws-cli` commands, operable through a text UI (arrow keys,
|
||||||
Esc) instead of long typed commands.
|
Esc) instead of long typed commands.
|
||||||
|
|
||||||
Target platform: **Linux / WSL** (primary usage scenario, since split
|
Target platform: **Linux / WSL / Windows** (primary usage scenario, since
|
||||||
panes require a real terminal multiplexer). Native Windows (without WSL)
|
split panes require a real terminal multiplexer). On Windows, use
|
||||||
is possible but with reduced split-view functionality (see 3.6).
|
[psmux](https://github.com/marlocarlo/psmux) as tmux-compatible
|
||||||
|
multiplexer.
|
||||||
|
|
||||||
**Technology decision: Go + Bubble Tea** (see section 5).
|
**Technology decision: Go + Bubble Tea** (see section 5).
|
||||||
|
|
||||||
@@ -75,9 +76,9 @@ Once start navigation is complete, the tool opens a tmux session with
|
|||||||
| -> runs the kctl-tui binary in "panel" mode |
|
| -> runs the kctl-tui binary in "panel" mode |
|
||||||
| -> menu: Redeploy, secrets diff |
|
| -> menu: Redeploy, secrets diff |
|
||||||
+--------------------------------------------------+
|
+--------------------------------------------------+
|
||||||
| Pane 1 (middle): k9s --context <context-a> -n <ns>|
|
| Pane 1 (middle): k9s --context <context-a> --namespace <ns> --command pods|
|
||||||
+--------------------------------------------------+
|
+--------------------------------------------------+
|
||||||
| Pane 2 (bottom): k9s --context <context-b> -n <ns>|
|
| Pane 2 (bottom): k9s --context <context-b> --namespace <ns> --command pods|
|
||||||
+--------------------------------------------------+
|
+--------------------------------------------------+
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -89,11 +90,13 @@ 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 \; \
|
||||||
split-window -v -t kctl:0.0 "k9s --context $CTX_A -n $NS" \; \
|
split-window -v -t kctl:0.0 "k9s --context $CTX_A --namespace $NS --command pods" \; \
|
||||||
split-window -v -t kctl:0.1 "k9s --context $CTX_B -n $NS" \; \
|
split-window -v -t kctl:0.1 "k9s --context $CTX_B --namespace $NS --command pods" \; \
|
||||||
select-layout -t kctl even-vertical \; \
|
select-layout -t kctl even-vertical \; \
|
||||||
select-pane -t kctl:0.0 \; \
|
select-pane -t kctl:0.0 \; \
|
||||||
attach -t kctl
|
attach -t kctl
|
||||||
@@ -167,22 +170,19 @@ 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
|
- **Platforms**: Linux, macOS, Windows (via psmux or WSL).
|
||||||
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`/`psmux`, `k9s`,
|
||||||
depending on the action used.
|
`aws-cli` depending on the action used.
|
||||||
- **Low startup time**, noticeably faster than the current `kubens`
|
- **Low startup time**, noticeably faster than the current `kubens`
|
||||||
experience.
|
experience.
|
||||||
- **No destructive actions without confirmation** (redeploy, force-sync).
|
- **No destructive actions without confirmation** (redeploy, force-sync).
|
||||||
@@ -244,16 +244,14 @@ Rejected options (see discussion history):
|
|||||||
- These fixes are a prerequisite before the tool can be meaningfully
|
- These fixes are a prerequisite before the tool can be meaningfully
|
||||||
tested, since it builds directly on `kubectl config`.
|
tested, since it builds directly on `kubectl config`.
|
||||||
|
|
||||||
## 8. Open items / out of scope (v1)
|
## 8. Open items / out of scope
|
||||||
|
|
||||||
- No automatic label setup for namespaces (migration is a separate,
|
- No automatic label setup for namespaces (migration is a separate,
|
||||||
one-time task).
|
one-time task).
|
||||||
- Split view v1 fixed at 2 status panes + 1 control pane (3 panes total).
|
- Split view fixed at 2 status panes + 1 control pane (3 panes total).
|
||||||
- No RBAC/permission checks before executing sensitive actions — the tool
|
- No RBAC/permission checks before executing sensitive actions — the tool
|
||||||
assumes existing kubectl permissions.
|
assumes existing kubectl permissions.
|
||||||
- Configuration file format (`config.yaml`) is defined and implemented;
|
- Configuration file format (`config.yaml`) is defined and implemented;
|
||||||
concrete label keys, context names, and namespace names are
|
concrete label keys, context names, and namespace names are
|
||||||
project-specific and belong exclusively in the user's local, unversioned
|
project-specific and belong exclusively in the user's local, unversioned
|
||||||
configuration, not in this document or the source code.
|
configuration, not in this document or the source code.
|
||||||
- Native Windows (without WSL) remains a secondary platform with manual
|
|
||||||
pane handling instead of an automated tmux lifecycle.
|
|
||||||
|
|||||||
+44
-24
@@ -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"
|
||||||
|
|
||||||
@@ -46,6 +50,7 @@ func newFullModel() *fullModel {
|
|||||||
l := list.New(nil, list.NewDefaultDelegate(), 0, 0)
|
l := list.New(nil, list.NewDefaultDelegate(), 0, 0)
|
||||||
l.Title = "kctl-tui"
|
l.Title = "kctl-tui"
|
||||||
l.SetShowStatusBar(false)
|
l.SetShowStatusBar(false)
|
||||||
|
l.SetFilteringEnabled(false)
|
||||||
return &fullModel{list: l}
|
return &fullModel{list: l}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +197,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,49 +270,56 @@ 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, err := os.Executable()
|
||||||
if err := kubeexec.CheckTool("tmux"); err != nil {
|
if err != nil {
|
||||||
return errMsg{err}
|
selfPath = "kctl-tui" // fallback to PATH lookup
|
||||||
}
|
}
|
||||||
if err := kubeexec.CheckTool("k9s"); err != nil {
|
|
||||||
return errMsg{err}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
envA := m.cfg.Envs[0]
|
envA := m.cfg.Envs[0]
|
||||||
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 --namespace %s --command pods", ctxA, m.selectedNamespace)
|
||||||
|
|
||||||
args := []string{
|
kubeexec.VerboseLog("[debug] selfPath=%s\n", selfPath)
|
||||||
"new-session", "-d", "-s", "kctl",
|
kubeexec.VerboseLog("[debug] panelCmd=%s\n", panelCmd)
|
||||||
panelCmd, ";",
|
kubeexec.VerboseLog("[debug] k9sCmdA=%s\n", k9sCmdA)
|
||||||
"set-option", "-t", "kctl", "remain-on-exit", "on", ";",
|
|
||||||
"split-window", "-v", "-t", "kctl:0.0", k9sCmdA, ";",
|
// 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 {
|
if len(m.cfg.Envs) > 1 {
|
||||||
envB := m.cfg.Envs[1]
|
envB := m.cfg.Envs[1]
|
||||||
ctxB := m.cfg.ResolveContext(envB, m.selectedContext)
|
ctxB := m.cfg.ResolveContext(envB, m.selectedContext)
|
||||||
k9sCmdB := fmt.Sprintf("k9s --context %s -n %s", ctxB, m.selectedNamespace)
|
k9sCmdB := fmt.Sprintf("k9s --context %s --namespace %s --command pods", ctxB, m.selectedNamespace)
|
||||||
args = append(args,
|
kubeexec.VerboseLog("[debug] k9sCmdB=%s\n", k9sCmdB)
|
||||||
"split-window", "-v", "-t", "kctl:0.1", k9sCmdB, ";",
|
setup = append(setup, []string{"split-window", "-v", "-t", "kctl:0.1", k9sCmdB})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
args = append(args,
|
setup = append(setup,
|
||||||
"select-layout", "-t", "kctl", "even-vertical", ";",
|
[]string{"select-layout", "-t", "kctl", "even-vertical"},
|
||||||
"select-pane", "-t", "kctl:0.0", ";",
|
[]string{"select-pane", "-t", "kctl:0.0"},
|
||||||
"attach", "-t", "kctl",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
c := exec.Command("tmux", args...)
|
for _, args := range setup {
|
||||||
|
if out, err := exec.Command("tmux", args...).CombinedOutput(); err != nil {
|
||||||
|
kubeexec.VerboseLog("[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.
|
||||||
|
c := exec.Command("tmux", "attach", "-t", "kctl")
|
||||||
return tea.ExecProcess(c, func(err error) tea.Msg {
|
return tea.ExecProcess(c, func(err error) tea.Msg {
|
||||||
return tmuxDoneMsg{err: err}
|
return tmuxDoneMsg{err: err}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fullModel) View() string {
|
func (m *fullModel) View() string {
|
||||||
view := m.list.View()
|
view := m.list.View()
|
||||||
|
|||||||
@@ -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 (
|
||||||
@@ -81,6 +84,7 @@ func newPanelModel(context, ns, team string) *panelModel {
|
|||||||
|
|
||||||
l := list.New(nil, list.NewDefaultDelegate(), 0, 0)
|
l := list.New(nil, list.NewDefaultDelegate(), 0, 0)
|
||||||
l.SetShowStatusBar(false)
|
l.SetShowStatusBar(false)
|
||||||
|
l.SetFilteringEnabled(false)
|
||||||
|
|
||||||
m := &panelModel{context: context, ns: ns, team: team, cfg: cfg, step: stepEnvMenu, list: l, input: ti}
|
m := &panelModel{context: context, ns: ns, team: team, cfg: cfg, step: stepEnvMenu, list: l, input: ti}
|
||||||
if loadErr != nil {
|
if loadErr != nil {
|
||||||
|
|||||||
+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
|
#!/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
|
||||||
@@ -182,8 +185,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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
@@ -26,6 +29,16 @@ func SetVerbose(enabled bool, w io.Writer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VerboseLog writes a message to the verbose log if enabled.
|
||||||
|
func VerboseLog(format string, args ...interface{}) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if !verbose {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(logOut, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
func logCmd(name string, args ...string) {
|
func logCmd(name string, args ...string) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
|||||||
Reference in New Issue
Block a user