Compare commits

...
44 Commits
Author SHA1 Message Date
stefankoelle d05d35bdc1 release v0.3.7
Windows Terminal maximized mode and quit keybinding fixes.

- Change wt -F (fullscreen) to wt -M (maximized) — keeps titlebar and taskbar visible
- Use DisableQuitKeybindings() in wt mode to fully disable q and ctrl+c in panel
- Hide help text in wt mode to remove visible 'q quit' hint
2026-09-17 06:52:25 +02:00
stefankoelle f8679adf95 fix: use maximized instead of fullscreen and disable quit keybindings
- Change wt -F (fullscreen) to wt -M (maximized) — keeps titlebar and taskbar
- Use DisableQuitKeybindings() in wt mode to fully disable q and ctrl+c
- Hide help text in wt mode to remove visible q quit hint
2026-09-17 06:51:09 +02:00
stefankoelle 73cc510347 release v0.3.6
Windows Terminal fullscreen and keybinding improvements.

- Add -F flag to wt.exe to start in fullscreen mode
- Disable 'q' keybinding in panel when using wt mode — prevents accidental quit (ctrl+c remains available)
2026-09-16 23:19:38 +02:00
stefankoelle d17eb6971c feat: add fullscreen and disable q key in Windows Terminal mode
- Add -F flag to wt.exe to start in fullscreen mode
- Disable 'q' keybinding in panel when using wt mode (ctrl+c remains)
2026-09-16 23:18:45 +02:00
stefankoelle fb4c1be12b release v0.3.5
Disable Quit option in Windows Terminal mode.

- Hide 'Quit (closes this tmux session)' in env menu when using wt mode — prevents confusion as wt windows are independent
- Esc on env menu stays on env menu in wt mode (no tmux kill attempted)
2026-09-16 22:46:26 +02:00
stefankoelle 9e3fefb71a feat: disable Quit option in Windows Terminal mode
- Hide 'Quit (closes this tmux session)' in env menu when using wt mode
- Esc on env menu stays on env menu in wt mode (no tmux kill attempted)
2026-09-16 22:41:34 +02:00
stefankoelle 2d382441a1 release v0.3.4
Improved Windows Terminal layout and update error handling.

- Split ratios: panel 25%, k9sA 37.5%, k9sB 37.5% (was 50/25/25) — k9s panes now get more space
- Update failures now exit with error code 1 instead of continuing silently with the old version
2026-09-16 22:21:56 +02:00
stefankoelle ad7d8534ab feat: improve Windows Terminal split ratios and update error handling
- Split ratios: panel 25%, k9sA 37.5%, k9sB 37.5% (was 50/25/25)
- Update failures now exit with error code 1 instead of continuing with old version
2026-09-16 22:20:42 +02:00
stefankoelle a0b937e882 release v0.3.3
Fix Windows Terminal mode issues.

- Fix split direction: use -H for top/bottom layout (opposite of tmux)
- Fix command quoting: use cmd /c to avoid Windows file-not-found errors
- Remove confusing tmux session closed message in wt mode
2026-09-16 21:44:37 +02:00
stefankoelle d32ccd5952 fix: avoid confusing tmux message in wt mode
Use c.Start() instead of tea.ExecProcess in startWtSession() to prevent
tmux session closed message from appearing in the main window.
2026-09-16 21:44:23 +02:00
stefankoelle 3729a0301d release v0.3.2
Windows Terminal native split-pane option.

- Add multiplexer config option to switch between tmux/psmux (default) and Windows Terminal's native split-pane
- Avoids the psmux focus-freeze issue when switching away from the terminal window on Windows
- Config: multiplexer: "tmux" (default) or "wt" for Windows Terminal
2026-09-16 21:12:26 +02:00
stefankoelle a1760b7b01 feat: add Windows Terminal native split-pane option
Add multiplexer config option to switch between tmux/psmux (default) and
Windows Terminal's native split-pane on Windows. This avoids the
psmux focus-freeze issue when switching away from the terminal window.

- Add Multiplexer field to config with MultiplexerBackend() getter
- Add startWtSession() using wt.exe split-pane command
- Dispatch startTmuxSession() based on OS and config
- Document option in config.example.yaml
2026-09-16 21:10:36 +02:00
stefankoelle bd23403e6c release v0.3.1
Compact menu layout and cross-env namespace discovery.

- Reduce menu item height from 3 rows to 1 (no description, no spacing) for denser display
- Merge namespaces from all configured envs so namespace-only-in-prod is no longer missing
2026-09-16 12:21:41 +02:00
stefankoelle 6a0405640d feat: compact menu layout and merge namespaces across all envs
- Reduce menu item height from 3 rows to 1 (no description, no spacing)
- Merge namespaces from all configured envs so namespace-only-in-prod is no longer missing
2026-09-16 12:18:17 +02:00
stefankoelle c6880e92ce release v0.3.0
Self-update support, config editing, and ExternalSecret CRD fixes.

- Self-update command (kctl-tui update): download and install the latest release directly from GitHub
- Startup update check: interactive prompt on launch when a newer version is available (opt-out via auto_update_check: false in config)
- Config edit command (kctl-tui config edit): open ~/.kctl-tui/config.yaml in your default editor
- ExternalSecret CRD fix: use the correct CRD name when force-syncing
- CI: upgrade to Go 1.25
2026-09-16 11:53:08 +02:00
stefankoelle 209e93646c feat: add config edit command to open config in editor
New subcommand 'kctl-tui config edit' opens ~/.kctl-tui/config.yaml
in the user's editor. Respects VISUAL/EDITOR env vars, falls back to
notepad on Windows and vim on Linux/macOS. Creates the config directory
and a starter file if they don't exist yet.
2026-09-16 11:49:40 +02:00
stefankoelle 720064454b fix: use correct ExternalSecret CRD name for force-sync annotation
The force-sync pre-filled the ExternalSecret object name with the
Kubernetes secret name (e.g. job-apply-common-secrets), but the
kubectl annotate command must target the ExternalSecret CRD object
(e.g. job-apply). Added external_secret_name_template config option
with fallback to k8s_secret_name_template.
2026-09-16 11:49:35 +02:00
Stefan Koelle 205f8906df Merge pull request #3 from skoelle/feature/self-update
feat: add self-update command
2026-09-16 11:20:45 +02:00
stefankoelle c0ad7e803e feat: add config opt-out, TTY check, and config-first update flow
- Add auto_update_check config field (defaults to true)
- Load config before update check, skip if auto_update_check is false
- Add TTY check before interactive prompt (skip in non-TTY environments)
- Update config.example.yaml with new option documentation
2026-08-22 10:44:01 +02:00
stefankoelle 7f2c9b2ba8 refactor: address code review findings for self-update
- go.mod: 1.25.12 → 1.25 (fix nonexistent patch version)
- Remove init() log mutation, pass logger explicitly via initUpdater()
- Add 10s HTTP timeout via context.WithTimeout on API calls
- Fix version comparison using proper semver (current.LessThan(newVer))
- Extract shared initUpdater() helper to eliminate duplicate creation
- Dev build warning now goes to stderr consistently
- Exit with code 0 after successful interactive update (binary replaced)
2026-08-22 10:08:48 +02:00
stefankoelle fed2b44c28 fix: upgrade CI to Go 1.25 and document self-update feature
- Upgrade go-version from 1.24 to 1.25 in build.yml (required by go-selfupdate v1.6.0)
- Add kctl-tui update command and interactive startup check to README.md
- Document new features in CHANGELOG.md
- Update Go version requirement to 1.25+ in README.md and CONTRIBUTING.md
- Add Phase 5 (Self-update) to PLAN.md roadmap
2026-08-22 09:50:27 +02:00
stefankoelle d89efc55d9 feat: add interactive update check on startup
When starting kctl-tui without a subcommand, check GitHub Releases
for a newer version and prompt the user to update.

- Silent skip for dev builds and network errors
- Prompt: 'Update now? [y/N]'
- On 'y': apply update, then start TUI
- On 'N'/Enter: start TUI immediately
2026-08-22 09:33:41 +02:00
stefankoelle d743422d28 feat: add self-update command
Add 'kctl-tui update' subcommand that checks GitHub Releases for
new versions and replaces the running binary.

Uses github.com/creativeprojects/go-selfupdate for safe binary
replacement with automatic OS/arch detection.

Supports --verbose flag for detailed update progress logging.
2026-08-22 09:25:28 +02:00
stefankoelle b9439e428a README.md 2026-08-14 23:52:41 +02:00
stefankoelle 4afb20c740 create release command fix2 2026-08-11 14:20:00 +02:00
stefankoelle 10f996cfef release v0.2.1
This release adds --help and --version flags, updates dependencies,
improves documentation for Windows support, and refines CI pipelines.

- Add --help and --version flags to main binary
- Update GitHub Actions workflow to latest major versions
- Update Go dependencies to latest versions
- Fix SPEC.md and README.md for current codebase (Windows fully supported)
- Update PLAN.md with roadmap for v0.3.0 and v1.0
- Add release command automation
2026-08-11 14:08:27 +02:00
stefankoelle 15e4aa64f7 create release command fix 2026-08-11 14:06:00 +02:00
stefankoelle 50ad7d7335 create release command 2026-08-11 14:04:59 +02:00
stefankoelle 28eeeda58e Merge remote-tracking branch 'origin/main' 2026-08-11 13:08:38 +02:00
stefankoelle 0d33a57118 fix 2026-08-11 13:06:18 +02:00
Stefan Koelle 0dbce4769b Merge pull request #2 from skoelle/renovate/major-github-actions-(major)
Update GitHub Actions (major) (major)
2026-08-11 12:43:46 +02:00
Stefan Koelle 406d65ca33 Merge pull request #1 from skoelle/renovate/go-dependencies
Update Go dependencies
2026-08-11 12:43:36 +02:00
renovate[bot] d066733c20 Update GitHub Actions (major) 2026-08-11 10:41:01 +00:00
renovate[bot] 2e7d48a2bb Update Go dependencies 2026-08-11 10:40:54 +00:00
stefankoelle be4ad209db renovate update 2026-08-11 12:36:02 +02:00
stefankoelle d8d656504c --help and --version update 2026-08-11 12:31:39 +02:00
stefankoelle a3480f41bf renovate 2026-08-11 12:24:31 +02:00
stefankoelle 9521f47377 Fix SPEC.md and README.md for current codebase
- SPEC.md: Windows is now a fully supported platform (not secondary)
- SPEC.md: Remove outdated 'manual pane handling' claim for Windows
- SPEC.md: Update non-functional requirements (Linux/macOS/Windows)
- README.md: Remove duplicate 'This downloads...' paragraph
- README.md: Use --namespace instead of -n in k9s examples
2026-08-09 22:32:27 +02:00
stefankoelle a1e8894222 Add security, CI, and tooling items to bugfix sprint 2026-08-09 22:30:34 +02:00
stefankoelle 43aef3b76c Add bugfix sprint to PLAN.md between v0.2.0 and v0.3.0 2026-08-09 22:29:13 +02:00
stefankoelle 84dbd9c230 Update PLAN.md: mark v0.2.0 done, add roadmap for v0.3.0 and v1.0 2026-08-09 22:27:26 +02:00
stefankoelle 2911f97991 Add CHANGELOG, CONTRIBUTING, and GitHub Issue/PR templates for v0.2.0 2026-08-09 22:13:09 +02:00
stefankoelle 7db58b6fe8 Disable Bubbles list filter to prevent stuck filter state
The / filter in the list component captured keystrokes after returning
from tmux, showing random text like 'cc' and blocking cursor navigation.
Filtering is not needed for the 3-level context/team/namespace flow.
2026-08-09 22:07:49 +02:00
stefankoelle 201168e8cd Fix k9s on Windows: --namespace, --command pods
- -n flag not supported on Windows k9s, use --namespace instead
- Start k9s directly in pods view with --command pods
2026-08-09 21:57:24 +02:00
22 changed files with 1077 additions and 150 deletions
+38
View File
@@ -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.
+23
View File
@@ -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.
+23
View File
@@ -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)
+24 -10
View File
@@ -15,11 +15,11 @@ jobs:
name: Test name: Test
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- uses: actions/setup-go@v5 - uses: actions/setup-go@v7
with: with:
go-version: "1.22" go-version: "1.25"
cache: false cache: false
- name: Tidy dependencies (generates/updates go.sum) - name: Tidy dependencies (generates/updates go.sum)
@@ -40,11 +40,11 @@ jobs:
goos: [linux, windows, darwin] goos: [linux, windows, darwin]
goarch: [amd64, arm64] goarch: [amd64, arm64]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- uses: actions/setup-go@v5 - uses: actions/setup-go@v7
with: with:
go-version: "1.22" go-version: "1.25"
cache: false cache: false
- name: Tidy dependencies (generates/updates go.sum) - name: Tidy dependencies (generates/updates go.sum)
@@ -64,7 +64,7 @@ jobs:
echo "Built $out" echo "Built $out"
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: kctl-tui-${{ matrix.goos }}-${{ matrix.goarch }} name: kctl-tui-${{ matrix.goos }}-${{ matrix.goarch }}
path: dist/* path: dist/*
@@ -75,13 +75,27 @@ jobs:
needs: build needs: build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/download-artifact@v4 - uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Extract release notes from commit message
id: release
run: |
MSG=$(git log -1 --pretty=format:"%B" "${{ github.sha }}")
{
echo 'body<<EOF'
echo "$MSG"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
- uses: actions/download-artifact@v8
with: with:
path: dist path: dist
merge-multiple: true merge-multiple: true
- name: Create release and upload binaries - name: Create release and upload binaries
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v3
with: with:
body: ${{ steps.release.outputs.body }}
files: dist/* files: dist/*
generate_release_notes: true
+6
View File
@@ -0,0 +1,6 @@
title: "kctl-tui"
emoji: "🐳"
category: code
subcategory: "Dev Tools"
status: active
stack: [Go, Bubbletea, Bubbles, Lipgloss, YAML]
+36
View File
@@ -0,0 +1,36 @@
---
description: Create and push a release tag (e.g. /create-release 1.0.0)
---
Create a release tag and push it to origin. The GitHub Action will automatically build for all platforms and create the GitHub Release.
## Steps
1. Validate the version argument ($ARGUMENTS):
- Must be provided, otherwise show error and stop
- Must match semver format (e.g. 1.0.0, 0.9.1, 2.0.0-beta.1)
2. Check for uncommitted changes:
- Run `git status --porcelain`
- If any output, warn the user and stop (commit first)
3. Analyze changes since last release:
- Run `git log --oneline $(git describe --tags --abbrev=0 HEAD)..HEAD` to list all commits
- Read the changed files to understand context
- Write a concise, well-structured release summary in English with:
- A one-line overview
- Bullet points for each notable change (features, fixes, breaking changes)
- Keep it developer-friendly, no fluff
4. Create release commit with the summary as message:
- Run `git commit --allow-empty -m "release v$ARGUMENTS\n\n<summary>"`
- The commit message IS the release notes — the GitHub Action picks it up automatically
5. Create annotated tag on that commit:
- Run `git tag -a v$ARGUMENTS -m "Release v$ARGUMENTS"`
6. Push commit and tag separately (pushing both in one step causes GitHub Actions to skip the release workflow):
- Run `git push origin main` (or current branch)
- Run `git push origin v$ARGUMENTS`
7. Confirm success with the version number
+65
View File
@@ -0,0 +1,65 @@
# 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]
### Added
- `kctl-tui update` — self-update command that downloads the latest release
from GitHub and replaces the running binary
- Automatic update check on startup — prompts to update interactively when
a newer version is available
## [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
+95
View File
@@ -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.25+
- 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.
+90 -22
View File
@@ -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,41 +49,109 @@ 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 (done) ## Phase 3 — Windows-native support (done)
- [x] Windows support via [psmux](https://github.com/marlocarlo/psmux) - [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] `install.ps1` — PowerShell install script for Windows.
- [x] Updated README and SPEC with Windows + psmux setup instructions. - [x] Updated README and SPEC with Windows + psmux setup instructions.
## 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`.
## Phase 5 — Self-update (done)
- [x] `kctl-tui update` — self-update command using `go-selfupdate` library.
Downloads the matching OS/arch asset from GitHub Releases and replaces
the running binary atomically.
- [x] Interactive update check on startup — queries GitHub Releases in the
background, prompts the user to update when a newer version is found.
---
## 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
+51 -40
View File
@@ -1,40 +1,40 @@
# kctl-tui # 🚀 kctl-tui
A small terminal entry point for everyday Kubernetes work: pick a context A small terminal entry point for everyday Kubernetes work: pick a context
and a namespace once, then drive rollout restarts and an AWS Secrets and a namespace once, then drive rollout restarts and an AWS Secrets
Manager <-> Kubernetes Secret diff/force-sync per environment from one Manager <-> Kubernetes Secret diff/force-sync per environment from one
place instead of retyping long `kubectl` commands. place instead of retyping long `kubectl` commands.
## Why ## Why
Working with several clusters, many namespaces per team, and paired Working with several clusters, many namespaces per team, and paired
environments (e.g. beta/prod) quickly turns into a lot of repeated typing environments (e.g. beta/prod) quickly turns into a lot of repeated typing
with plain `kubectl`/`k9s`. kctl-tui adds: with plain `kubectl`/`k9s`. kctl-tui adds:
- A guided **context -> team -> namespace** selection that starts - 🎯 A guided **context -> team -> namespace** selection that starts
directly at team selection (using a configured default context), with directly at team selection (using a configured default context), with
the context screen just one `Esc` away. the context screen just one `Esc` away.
- Namespace grouping by an arbitrary, configurable **label** instead of - 🏷️ Namespace grouping by an arbitrary, configurable **label** instead of
scrolling through every namespace in the cluster. scrolling through every namespace in the cluster.
- A **3-pane view** (via `tmux`): one control pane for actions, two status - 🖥️ A **3-pane view** (via `tmux`): one control pane for actions, two status
panes running `k9s` for the current namespace across your two panes running `k9s` for the current namespace across your two
configured environments (e.g. beta/prod), shown side by side. configured environments (e.g. beta/prod), shown side by side.
- A control-pane menu organized **by environment**: pick beta or prod, - 📋 A control-pane menu organized **by environment**: pick beta or prod,
then Secrets sync or Redeploy for that environment specifically. then Secrets sync or Redeploy for that environment specifically.
- AWS Secrets Manager secret IDs and Kubernetes context names/ARNs are - 🔐 AWS Secrets Manager secret IDs and Kubernetes context names/ARNs are
**computed from configurable templates** (namespace + environment), **computed from configurable templates** (namespace + environment),
instead of listing secrets or discovering contexts live from instead of listing secrets or discovering contexts live from
`kubectl`/`aws-cli`. `kubectl`/`aws-cli`.
- A guided **AWS Secrets Manager vs. Kubernetes Secret** comparison of - 🔄 A guided **AWS Secrets Manager vs. Kubernetes Secret** comparison of
every field at once, with a force-sync request for the whole secret if every field at once, with a force-sync request for the whole secret if
anything differs. anything differs.
- An **AWS auth check** before the secrets workflow, offering to run your - 🔑 An **AWS auth check** before the secrets workflow, offering to run your
configured SSO login command interactively if the session has expired. configured SSO login command interactively if the session has expired.
See [SPEC.md](SPEC.md) for the full requirements and design rationale, and See [SPEC.md](SPEC.md) for the full requirements and design rationale, and
[PLAN.md](PLAN.md) for the implementation roadmap and current status. [PLAN.md](PLAN.md) for the implementation roadmap and current status.
## How it works ## 🔧 How it works
``` ```
+--------------------------------------------------+ +--------------------------------------------------+
@@ -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 |
+--------------------------------------------------+ +--------------------------------------------------+
``` ```
@@ -61,14 +61,14 @@ See [SPEC.md](SPEC.md) for the full requirements and design rationale, and
-> environment menu -> closes the whole tmux session, including both -> environment menu -> closes the whole tmux session, including both
`k9s` panes, and returns you to namespace selection). `k9s` panes, and returns you to namespace selection).
## Requirements ## 📋 Requirements
- `kubectl`, configured with access to your cluster(s) (the actual - 🐳 `kubectl`, configured with access to your cluster(s) (the actual
context names/ARNs are resolved from your `context_template`, see context names/ARNs are resolved from your `context_template`, see
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 **Linux/macOS**, install - 📺 `tmux` (used for the 3-pane layout). On **Linux/macOS**, install
`tmux` via your package manager. On **Windows**, install `tmux` via your package manager. On **Windows**, install
[psmux](https://github.com/marlocarlo/psmux) — a native, [psmux](https://github.com/marlocarlo/psmux) — a native,
tmux-compatible terminal multiplexer: tmux-compatible terminal multiplexer:
@@ -78,30 +78,27 @@ See [SPEC.md](SPEC.md) for the full requirements and design rationale, and
cargo install psmux cargo install psmux
``` ```
psmux provides a `tmux` command, so kctl-tui works without changes. 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.
## Installation ## 📥 Installation
### Quick install (Linux/macOS/WSL) ### 🚀 Quick install (Linux/macOS/WSL)
```bash ```bash
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) ### 🪟 Quick install (Windows)
```powershell ```powershell
irm https://raw.githubusercontent.com/skoelle/kctl-tui/main/install.ps1 | iex 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`.
### From source ### 🛠️ From source
```bash ```bash
git clone https://github.com/skoelle/kctl-tui.git git clone https://github.com/skoelle/kctl-tui.git
@@ -110,19 +107,20 @@ go build -o kctl-tui ./cmd/kctl-tui
sudo mv kctl-tui /usr/local/bin/ sudo mv kctl-tui /usr/local/bin/
``` ```
Requires Go 1.22+. Requires Go 1.25+.
### Prebuilt binaries ### 📦 Prebuilt binaries
Every tagged release (`vX.Y.Z`) is built for `linux`, `darwin`, and Every tagged release (`vX.Y.Z`) is built for `linux`, `darwin`, and
`windows`, each for `amd64` and `arm64`, via the GitHub Actions workflow in `windows`, each for `amd64` and `arm64`, via the GitHub Actions workflow in
[.github/workflows/build.yml](.github/workflows/build.yml). Download the [.github/workflows/build.yml](.github/workflows/build.yml). Download the
matching asset from the [Releases page](https://github.com/skoelle/kctl-tui/releases). matching asset from the [Releases page](https://github.com/skoelle/kctl-tui/releases).
## Configuration ## ⚙️ Configuration
Copy [config.example.yaml](config.example.yaml) to `~/.kctl-tui/config.yaml` Copy [config.example.yaml](config.example.yaml) to `~/.kctl-tui/config.yaml`
and adjust it to your own setup: and adjust it to your own setup — or use `kctl-tui config edit` to open the
file directly in your editor (creates the file and directory if needed):
```yaml ```yaml
contexts: contexts:
@@ -139,40 +137,47 @@ aws_account_id: "123456789012"
secret_name_template: "tf-{namespace}-{env}-secrets" secret_name_template: "tf-{namespace}-{env}-secrets"
k8s_secret_name_template: "{namespace}-common-secrets" k8s_secret_name_template: "{namespace}-common-secrets"
external_secret_name_template: "{namespace}"
context_template: "arn:aws:eks:{region}:{account_id}:cluster/tf-{env}-{context}-1" context_template: "arn:aws:eks:{region}:{account_id}:cluster/tf-{env}-{context}-1"
team_label_key: "example.org/team" team_label_key: "example.org/team"
aws_sso_login_command: "aws sso login" aws_sso_login_command: "aws sso login"
``` ```
- `contexts` / `default_context`: the top-level grouping the tool starts - 🌐 `contexts` / `default_context`: the top-level grouping the tool starts
from (e.g. a network boundary such as internal/external-facing from (e.g. a network boundary such as internal/external-facing
clusters). This is the outermost navigation level, one `Esc` above team clusters). This is the outermost navigation level, one `Esc` above team
selection. selection.
- `envs`: the environments switchable from the control panel (e.g. - 🎛️ `envs`: the environments switchable from the control panel (e.g.
"beta"/"prod"). The **first two** entries are also used for the two k9s "beta"/"prod"). The **first two** entries are also used for the two k9s
status panes shown side by side. status panes shown side by side.
- `aws_region` / `aws_account_id`: used for AWS Secrets Manager calls and - 🌍 `aws_region` / `aws_account_id`: used for AWS Secrets Manager calls and
to fill the `{account_id}` placeholder in `context_template`. to fill the `{account_id}` placeholder in `context_template`.
`123456789012` is a placeholder, not a real account. `123456789012` is a placeholder, not a real account.
- `secret_name_template`: builds the AWS Secrets Manager secret ID from - 🔑 `secret_name_template`: builds the AWS Secrets Manager secret ID from
the chosen namespace and environment. Placeholders: `{namespace}`, the chosen namespace and environment. Placeholders: `{namespace}`,
`{env}`. `{env}`.
- `k8s_secret_name_template`: builds the Kubernetes secret name from the - 🏷️ `k8s_secret_name_template`: builds the Kubernetes secret name from the
chosen namespace. Kept separate from `secret_name_template` because the chosen namespace. Kept separate from `secret_name_template` because the
two sides commonly follow different naming conventions. Placeholders: two sides commonly follow different naming conventions. Placeholders:
`{namespace}`. `{namespace}`.
- `context_template`: builds the actual kubectl context name/ARN from - 🎯 `external_secret_name_template`: builds the ExternalSecret CRD object
name to annotate when a force-sync is requested. This is often different
from the Kubernetes secret name because the ExternalSecret CRD and the
resulting Secret are separate objects (e.g. ExternalSecret `"job-apply"`
produces Secret `"job-apply-common-secrets"`). Falls back to
`k8s_secret_name_template` if omitted. Placeholders: `{namespace}`.
- 🔗 `context_template`: builds the actual kubectl context name/ARN from
region, account ID, environment, and context. Placeholders: `{region}`, region, account ID, environment, and context. Placeholders: `{region}`,
`{account_id}`, `{env}`, `{context}`. Adjust the literal parts (`tf-`, `{account_id}`, `{env}`, `{context}`. Adjust the literal parts (`tf-`,
`-1`, cluster naming, ARN shape) to match how your own clusters/contexts `-1`, cluster naming, ARN shape) to match how your own clusters/contexts
are actually named — the resolved value must match an existing context are actually named — the resolved value must match an existing context
in your kubeconfig. in your kubeconfig.
- `team_label_key`: the Kubernetes namespace label used to group - 👥 `team_label_key`: the Kubernetes namespace label used to group
namespaces by team/ownership in the team-selection screen. This is namespaces by team/ownership in the team-selection screen. This is
entirely up to your organization's labeling convention; kctl-tui ships entirely up to your organization's labeling convention; kctl-tui ships
with no default team label of its own. with no default team label of its own.
- `aws_sso_login_command`: run interactively if `aws sts - 🔐 `aws_sso_login_command`: run interactively if `aws sts
get-caller-identity` fails before the secrets workflow (e.g. an expired get-caller-identity` fails before the secrets workflow (e.g. an expired
SSO session). Defaults to `aws sso login`. SSO session). Defaults to `aws sso login`.
@@ -180,7 +185,7 @@ 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.
## Windows notes ## 🪟 Windows notes
On native Windows (without WSL), install [psmux](https://github.com/marlocarlo/psmux) On native Windows (without WSL), install [psmux](https://github.com/marlocarlo/psmux)
for the 3-pane layout. psmux is a native Windows terminal multiplexer for the 3-pane layout. psmux is a native Windows terminal multiplexer
@@ -199,19 +204,25 @@ 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 ## 📖 Usage
```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 --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 update # update to the latest release
kctl-tui doctor # check if all tools, config and connections are OK kctl-tui doctor # check if all tools, config and connections are OK
kctl-tui config edit # open ~/.kctl-tui/config.yaml in your editor
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)
``` ```
## Development When starting `kctl-tui` without a subcommand, a background check queries
GitHub Releases for a newer version. If one is found, you are prompted to
update interactively before the TUI starts.
## 🛠️ Development
```bash ```bash
go test ./... go test ./...
@@ -225,6 +236,6 @@ unit tests. Code that shells out to `kubectl`/`aws`/`tmux` lives in
`internal/kubeexec` and in `cmd/kctl-tui` and is intentionally kept thin `internal/kubeexec` and in `cmd/kctl-tui` and is intentionally kept thin
and untested, since it has no meaningful behavior without a live cluster. and untested, since it has no meaningful behavior without a live cluster.
## License ## 📄 License
Licensed under the [MIT License](LICENSE) - Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de) Licensed under the [MIT License](LICENSE) - Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
+13 -14
View File
@@ -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|
+--------------------------------------------------+ +--------------------------------------------------+
``` ```
@@ -94,8 +95,8 @@ 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
@@ -177,11 +178,11 @@ Alternatively, run kctl-tui inside WSL with standard `tmux`.
## 4. Non-functional requirements ## 4. Non-functional requirements
- **Primary platform Linux/WSL**, secondary native Windows (via psmux). - **Platforms**: Linux, macOS, Windows (via psmux or WSL).
- **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).
@@ -243,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.
+71 -21
View File
@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"runtime"
"sort" "sort"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
@@ -47,9 +48,10 @@ type fullModel struct {
} }
func newFullModel() *fullModel { func newFullModel() *fullModel {
l := list.New(nil, list.NewDefaultDelegate(), 0, 0) l := list.New(nil, newCompactDelegate(), 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}
} }
@@ -196,7 +198,11 @@ 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 { tool := "tmux"
if runtime.GOOS == "windows" && m.cfg.MultiplexerBackend() == "wt" {
tool = "wt"
}
if err := kubeexec.CheckTool(tool); err != nil {
m.err = err m.err = err
return m, nil return m, nil
} }
@@ -209,23 +215,21 @@ func (m *fullModel) handleSelect() (tea.Model, tea.Cmd) {
return m, nil return m, nil
} }
// bootstrapContext resolves a kubectl context purely to discover
// namespaces/labels for the team/namespace screens. The first configured
// env is used as a stable default for this discovery step, since
// namespace names are assumed to be identical across envs.
func (m *fullModel) bootstrapContext() string {
if len(m.cfg.Envs) == 0 {
return ""
}
return m.cfg.ResolveContext(m.cfg.Envs[0], m.selectedContext)
}
func (m *fullModel) loadTeams() tea.Msg { func (m *fullModel) loadTeams() tea.Msg {
namespaces, err := kubeexec.GetNamespacesWithLabels(m.bootstrapContext()) merged := map[string]map[string]string{}
if err != nil { for _, env := range m.cfg.Envs {
return errMsg{err} ctx := m.cfg.ResolveContext(env, m.selectedContext)
namespaces, err := kubeexec.GetNamespacesWithLabels(ctx)
if err != nil {
continue
}
for ns, labels := range namespaces {
if _, exists := merged[ns]; !exists {
merged[ns] = labels
}
}
} }
return *toTeamsLoadedMsg(namespaces, m.cfg.TeamLabelKey) return *toTeamsLoadedMsg(merged, m.cfg.TeamLabelKey)
} }
func (m *fullModel) loadTeamsFor(namespaces map[string]map[string]string) tea.Cmd { func (m *fullModel) loadTeamsFor(namespaces map[string]map[string]string) tea.Cmd {
@@ -263,12 +267,21 @@ func (m *fullModel) loadNamespacesFor(teamValue string) tea.Cmd {
} }
} }
// startTmuxSession builds the 3-pane tmux command: the control pane runs // startTmuxSession builds the 3-pane session: the control pane runs
// this binary in "panel" mode (letting the user pick an env and an // this binary in "panel" mode (letting the user pick an env and an
// action), and the two status panes run k9s against the first two // action), and the two status panes run k9s against the first two
// 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. On Windows with multiplexer: "wt", this uses
// Windows Terminal's native split-pane instead of tmux/psmux.
func (m *fullModel) startTmuxSession() tea.Cmd { func (m *fullModel) startTmuxSession() tea.Cmd {
if runtime.GOOS == "windows" && m.cfg.MultiplexerBackend() == "wt" {
return m.startWtSession()
}
return m.startTmuxSessionTmux()
}
// startTmuxSessionTmux creates the session using tmux/psmux.
func (m *fullModel) startTmuxSessionTmux() tea.Cmd {
selfPath, err := os.Executable() selfPath, err := os.Executable()
if err != nil { if err != nil {
selfPath = "kctl-tui" // fallback to PATH lookup selfPath = "kctl-tui" // fallback to PATH lookup
@@ -278,7 +291,7 @@ func (m *fullModel) startTmuxSession() tea.Cmd {
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)
kubeexec.VerboseLog("[debug] selfPath=%s\n", selfPath) kubeexec.VerboseLog("[debug] selfPath=%s\n", selfPath)
kubeexec.VerboseLog("[debug] panelCmd=%s\n", panelCmd) kubeexec.VerboseLog("[debug] panelCmd=%s\n", panelCmd)
@@ -297,7 +310,7 @@ func (m *fullModel) startTmuxSession() tea.Cmd {
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)
kubeexec.VerboseLog("[debug] k9sCmdB=%s\n", k9sCmdB) kubeexec.VerboseLog("[debug] k9sCmdB=%s\n", k9sCmdB)
setup = append(setup, []string{"split-window", "-v", "-t", "kctl:0.1", k9sCmdB}) setup = append(setup, []string{"split-window", "-v", "-t", "kctl:0.1", k9sCmdB})
} }
@@ -320,6 +333,43 @@ func (m *fullModel) startTmuxSession() tea.Cmd {
}) })
} }
// startWtSession creates the session using Windows Terminal's native
// split-pane feature. This avoids the psmux focus-freeze issue on Windows.
// Note: In Windows Terminal, -H (horizontal) stacks panes top/bottom,
// while -V (vertical) places them side by side — opposite of tmux.
// The -s flag controls the split ratio: first split gives k9sA 75%
// (panel keeps 25%), second split divides k9sA equally (37.5% each).
func (m *fullModel) startWtSession() tea.Cmd {
selfPath, err := os.Executable()
if err != nil {
selfPath = "kctl-tui"
}
panelCmd := fmt.Sprintf("%s panel --context=%s --ns=%s --team=%s",
selfPath, m.selectedContext, m.selectedNamespace, m.selectedTeam)
envA := m.cfg.Envs[0]
ctxA := m.cfg.ResolveContext(envA, m.selectedContext)
k9sCmdA := fmt.Sprintf("k9s --context %s --namespace %s --command pods", ctxA, m.selectedNamespace)
kubeexec.VerboseLog("[debug] selfPath=%s\n", selfPath)
kubeexec.VerboseLog("[debug] panelCmd=%s\n", panelCmd)
kubeexec.VerboseLog("[debug] k9sCmdA=%s\n", k9sCmdA)
wtCmd := fmt.Sprintf("wt -M new-tab %s ; split-pane -H -s 0.75 %s", panelCmd, 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 --namespace %s --command pods", ctxB, m.selectedNamespace)
kubeexec.VerboseLog("[debug] k9sCmdB=%s\n", k9sCmdB)
wtCmd += fmt.Sprintf(" ; split-pane -H -s 0.5 %s", k9sCmdB)
}
c := exec.Command("cmd", "/c", wtCmd)
_ = c.Start()
return nil
}
func (m *fullModel) View() string { func (m *fullModel) View() string {
view := m.list.View() view := m.list.View()
if m.statusMessage != "" { if m.statusMessage != "" {
+12
View File
@@ -3,6 +3,8 @@
package main package main
import "github.com/charmbracelet/bubbles/list"
// simpleItem is a minimal implementation of list.Item used for all // simpleItem is a minimal implementation of list.Item used for all
// selection screens (contexts, teams, namespaces, menu actions). // selection screens (contexts, teams, namespaces, menu actions).
type simpleItem struct { type simpleItem struct {
@@ -13,3 +15,13 @@ type simpleItem struct {
func (i simpleItem) Title() string { return i.label } func (i simpleItem) Title() string { return i.label }
func (i simpleItem) Description() string { return "" } func (i simpleItem) Description() string { return "" }
func (i simpleItem) FilterValue() string { return i.label } func (i simpleItem) FilterValue() string { return i.label }
// newCompactDelegate returns a list delegate that renders each item as a
// single line with no extra spacing, maximizing the number of visible
// entries in the terminal.
func newCompactDelegate() list.DefaultDelegate {
d := list.NewDefaultDelegate()
d.ShowDescription = false
d.SetSpacing(0)
return d
}
+78 -2
View File
@@ -6,6 +6,9 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath"
"runtime"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
@@ -29,6 +32,8 @@ func main() {
verbose = true verbose = true
case "--version", "-v": case "--version", "-v":
fmt.Printf("kctl-tui %s\n", version) fmt.Printf("kctl-tui %s\n", version)
fmt.Println("Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)")
fmt.Println("Licensed under the MIT License")
return return
case "--help", "-h": case "--help", "-h":
showHelp = true showHelp = true
@@ -68,6 +73,12 @@ func main() {
os.Exit(1) os.Exit(1)
} }
return return
case "update":
if err := runUpdate(verbose); err != nil {
fmt.Fprintln(os.Stderr, "kctl-tui update error:", err)
os.Exit(1)
}
return
default: default:
fmt.Fprintf(os.Stderr, "unknown command: %s\n\n", filtered[0]) fmt.Fprintf(os.Stderr, "unknown command: %s\n\n", filtered[0])
printUsage() printUsage()
@@ -75,6 +86,25 @@ func main() {
} }
} }
// Load config early to check auto_update_update and validate.
cfgPath, err := config.DefaultPath()
if err == nil {
cfg, cfgErr := config.Load(cfgPath)
if cfgErr != nil {
fmt.Fprintf(os.Stderr, "WARNING: failed to load config: %v\n", cfgErr)
}
if cfgErr == nil && cfg.IsAutoUpdateCheckEnabled() {
updated, err := checkForUpdateInteractive(verbose)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if updated {
os.Exit(0)
}
}
}
m := newFullModel() m := newFullModel()
p := tea.NewProgram(m, tea.WithAltScreen()) p := tea.NewProgram(m, tea.WithAltScreen())
if _, err := p.Run(); err != nil { if _, err := p.Run(); err != nil {
@@ -85,10 +115,13 @@ func main() {
func printUsage() { func printUsage() {
fmt.Print(`kctl-tui — Kubernetes entry-point TUI fmt.Print(`kctl-tui — Kubernetes entry-point TUI
https://github.com/skoelle/kctl-tui
Usage: Usage:
kctl-tui [flags] start the TUI (full navigation mode) kctl-tui [flags] start the TUI (full navigation mode)
kctl-tui doctor check tools, config and connections kctl-tui doctor check tools, config and connections
kctl-tui update update to the latest release
kctl-tui config edit open ~/.kctl-tui/config.yaml in editor
kctl-tui config check validate ~/.kctl-tui/config.yaml kctl-tui config check validate ~/.kctl-tui/config.yaml
kctl-tui panel [options] control pane (called internally by tmux) kctl-tui panel [options] control pane (called internally by tmux)
@@ -100,15 +133,58 @@ Flags:
Examples: Examples:
kctl-tui # start the TUI kctl-tui # start the TUI
kctl-tui doctor # verify everything is installed kctl-tui doctor # verify everything is installed
kctl-tui update # update to the latest version
kctl-tui --verbose 2>debug.log # log commands to a file kctl-tui --verbose 2>debug.log # log commands to a file
kctl-tui config edit # open config in editor
kctl-tui config check # validate config 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 runConfigCheck()
} }
if len(args) == 0 || args[0] == "edit" {
return runConfigEdit()
}
return fmt.Errorf("usage: kctl-tui config [check|edit]")
}
func runConfigEdit() error {
cfgPath, err := config.DefaultPath()
if err != nil {
return fmt.Errorf("cannot determine config path: %w", err)
}
dir := filepath.Dir(cfgPath)
if err := os.MkdirAll(dir, 0o700); err != nil {
return fmt.Errorf("cannot create config directory %s: %w", dir, err)
}
if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
if err := os.WriteFile(cfgPath, []byte("# kctl-tui configuration\n# See https://github.com/skoelle/kctl-tui for examples.\n"), 0o600); err != nil {
return fmt.Errorf("cannot create config file %s: %w", cfgPath, err)
}
fmt.Printf("Created new config file: %s\n", cfgPath)
}
editor := os.Getenv("VISUAL")
if editor == "" {
editor = os.Getenv("EDITOR")
}
if editor == "" {
switch runtime.GOOS {
case "windows":
editor = "notepad"
default:
editor = "vim"
}
}
cmd := exec.Command(editor, cfgPath)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func runConfigCheck() error {
cfgPath, err := config.DefaultPath() cfgPath, err := config.DefaultPath()
if err != nil { if err != nil {
return fmt.Errorf("cannot determine config path: %w", err) return fmt.Errorf("cannot determine config path: %w", err)
+25 -6
View File
@@ -8,6 +8,7 @@ import (
"flag" "flag"
"fmt" "fmt"
"os/exec" "os/exec"
"runtime"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -49,8 +50,9 @@ type panelModel struct {
deploymentName string deploymentName string
awsSecretName string // resolved via secret_name_template (namespace + env) awsSecretName string // resolved via secret_name_template (namespace + env)
k8sSecretName string // resolved via k8s_secret_name_template (namespace only) k8sSecretName string // resolved via k8s_secret_name_template (namespace only)
externalSecretName string // resolved via external_secret_name_template (namespace only)
awsValues map[string]string awsValues map[string]string
k8sValues map[string]string k8sValues map[string]string
diffEntries []kctl.SecretDiffEntry diffEntries []kctl.SecretDiffEntry
@@ -60,6 +62,11 @@ type panelModel struct {
err error err error
} }
// isWtMode returns true when running on Windows with multiplexer: "wt".
func (m *panelModel) isWtMode() bool {
return runtime.GOOS == "windows" && m.cfg.MultiplexerBackend() == "wt"
}
func runPanel(args []string) error { func runPanel(args []string) error {
fs := flag.NewFlagSet("panel", flag.ContinueOnError) fs := flag.NewFlagSet("panel", flag.ContinueOnError)
context := fs.String("context", "", "context (e.g. internal/external)") context := fs.String("context", "", "context (e.g. internal/external)")
@@ -82,10 +89,15 @@ func newPanelModel(context, ns, team string) *panelModel {
cfgPath, _ := config.DefaultPath() cfgPath, _ := config.DefaultPath()
cfg, loadErr := config.Load(cfgPath) cfg, loadErr := config.Load(cfgPath)
l := list.New(nil, list.NewDefaultDelegate(), 0, 0) l := list.New(nil, newCompactDelegate(), 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 m.isWtMode() {
l.DisableQuitKeybindings()
l.SetShowHelp(false)
}
if loadErr != nil { if loadErr != nil {
m.err = fmt.Errorf("config load failed: %w", loadErr) m.err = fmt.Errorf("config load failed: %w", loadErr)
m.step = stepError m.step = stepError
@@ -97,7 +109,9 @@ func newPanelModel(context, ns, team string) *panelModel {
func (m *panelModel) showEnvMenu() { func (m *panelModel) showEnvMenu() {
items := make([]list.Item, 0, len(m.cfg.Envs)+1) items := make([]list.Item, 0, len(m.cfg.Envs)+1)
items = append(items, simpleItem{label: "Quit (closes this tmux session)", value: "quit"}) if !m.isWtMode() {
items = append(items, simpleItem{label: "Quit (closes this tmux session)", value: "quit"})
}
for _, env := range m.cfg.Envs { for _, env := range m.cfg.Envs {
items = append(items, simpleItem{label: env, value: env}) items = append(items, simpleItem{label: env, value: env})
} }
@@ -177,10 +191,14 @@ func (m *panelModel) usesTextInput() bool {
// handleEsc navigates one level up: action menu -> env menu, most // handleEsc navigates one level up: action menu -> env menu, most
// sub-steps -> action menu. From the top-level env menu it closes the // sub-steps -> action menu. From the top-level env menu it closes the
// whole tmux session (all panes, including the two k9s status panes) // whole tmux session (all panes, including the two k9s status panes)
// before quitting this program, per SPEC.md 3.6. // before quitting this program, per SPEC.md 3.6. In wt mode, Esc on
// the env menu does nothing — the user closes the wt window manually.
func (m *panelModel) handleEsc() (tea.Model, tea.Cmd) { func (m *panelModel) handleEsc() (tea.Model, tea.Cmd) {
switch m.step { switch m.step {
case stepEnvMenu: case stepEnvMenu:
if m.isWtMode() {
return m, nil
}
exec.Command("tmux", "kill-session", "-t", "kctl").Run() exec.Command("tmux", "kill-session", "-t", "kctl").Run()
return m, tea.Quit return m, tea.Quit
case stepActionMenu: case stepActionMenu:
@@ -315,6 +333,7 @@ func (m *panelModel) afterAWSLogin(execErr error) (tea.Model, tea.Cmd) {
func (m *panelModel) startSecretsFlow() (tea.Model, tea.Cmd) { func (m *panelModel) startSecretsFlow() (tea.Model, tea.Cmd) {
m.awsSecretName = m.cfg.ResolveSecretName(m.ns, m.currentEnv) m.awsSecretName = m.cfg.ResolveSecretName(m.ns, m.currentEnv)
m.k8sSecretName = m.cfg.ResolveK8sSecretName(m.ns) m.k8sSecretName = m.cfg.ResolveK8sSecretName(m.ns)
m.externalSecretName = m.cfg.ResolveExternalSecretName(m.ns)
raw, err := kubeexec.GetAWSSecretString(m.awsSecretName, m.cfg.AWSRegion) raw, err := kubeexec.GetAWSSecretString(m.awsSecretName, m.cfg.AWSRegion)
if err != nil { if err != nil {
@@ -461,7 +480,7 @@ func (m *panelModel) fromForceSyncConfirm() (tea.Model, tea.Cmd) {
return m, nil return m, nil
} }
m.step = stepExternalSecretName m.step = stepExternalSecretName
m.input.SetValue(m.k8sSecretName) m.input.SetValue(m.externalSecretName)
m.input.Placeholder = "ExternalSecret object name" m.input.Placeholder = "ExternalSecret object name"
return m, nil return m, nil
} }
+155
View File
@@ -0,0 +1,155 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
package main
import (
"bufio"
"context"
"fmt"
"os"
"strings"
"time"
"github.com/Masterminds/semver/v3"
"github.com/creativeprojects/go-selfupdate"
"golang.org/x/term"
)
const (
githubSlug = "skoelle/kctl-tui"
updateTimeout = 10 * time.Second
)
func initUpdater(verbose bool) (*selfupdate.Updater, error) {
if verbose {
selfupdate.SetLogger(&verboseLogger{})
}
source, err := selfupdate.NewGitHubSource(selfupdate.GitHubConfig{})
if err != nil {
return nil, fmt.Errorf("failed to init GitHub source: %w", err)
}
return selfupdate.NewUpdater(selfupdate.Config{
Source: source,
})
}
func runUpdate(verbose bool) error {
if version == "dev" {
fmt.Fprintln(os.Stderr, "WARNING: running dev build — cannot compare versions")
fmt.Fprintln(os.Stderr, "Skipping version check. Build from a tagged release to enable self-update.")
return nil
}
updater, err := initUpdater(verbose)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), updateTimeout)
defer cancel()
repo := selfupdate.ParseSlug(githubSlug)
rel, found, err := updater.DetectLatest(ctx, repo)
if err != nil {
return fmt.Errorf("failed to check for updates: %w", err)
}
if !found {
fmt.Println("Already up-to-date.")
return nil
}
current, _ := semver.NewVersion(version)
newVersion := rel.Version()
newVer, _ := semver.NewVersion(newVersion)
if current != nil && !current.LessThan(newVer) {
fmt.Println("Already up-to-date.")
return nil
}
fmt.Printf("Current version: %s\n", version)
fmt.Printf("Found version %s. Updating...\n", newVersion)
if err := updater.UpdateTo(ctx, rel, ""); err != nil {
return fmt.Errorf("update failed: %w", err)
}
fmt.Printf("Updated from %s to %s\n", current, newVersion)
return nil
}
// checkForUpdateInteractive checks for a new version and prompts the user to update.
// Returns (true, nil) if an update was applied successfully, (false, nil) if no
// update was needed or the user declined, and (false, err) if the update failed.
func checkForUpdateInteractive(verbose bool) (bool, error) {
if version == "dev" {
return false, nil
}
if !term.IsTerminal(int(os.Stdin.Fd())) {
return false, nil
}
updater, err := initUpdater(verbose)
if err != nil {
if verbose {
fmt.Fprintf(os.Stderr, "Update check failed: %v\n", err)
}
return false, nil
}
ctx, cancel := context.WithTimeout(context.Background(), updateTimeout)
defer cancel()
repo := selfupdate.ParseSlug(githubSlug)
rel, found, err := updater.DetectLatest(ctx, repo)
if err != nil {
if verbose {
fmt.Fprintf(os.Stderr, "Update check failed: %v\n", err)
}
return false, nil
}
if !found {
return false, nil
}
current, _ := semver.NewVersion(version)
newVersion := rel.Version()
newVer, _ := semver.NewVersion(newVersion)
if current != nil && !current.LessThan(newVer) {
return false, nil
}
fmt.Printf("New version %s available (current: %s). Update now? [y/N] ", newVersion, version)
reader := bufio.NewReader(os.Stdin)
answer, _ := reader.ReadString('\n')
answer = strings.TrimSpace(strings.ToLower(answer))
if answer != "y" && answer != "yes" {
return false, nil
}
fmt.Println("Updating...")
if err := updater.UpdateTo(ctx, rel, ""); err != nil {
return false, fmt.Errorf("update failed: %w", err)
}
fmt.Printf("Updated to %s. Please restart kctl-tui.\n", newVersion)
return true, nil
}
type verboseLogger struct{}
func (l *verboseLogger) Print(v ...any) {
fmt.Fprint(os.Stderr, v...)
}
func (l *verboseLogger) Printf(format string, v ...any) {
fmt.Fprintf(os.Stderr, format, v...)
}
+17
View File
@@ -42,6 +42,14 @@ secret_name_template: "tf-{namespace}-{env}-secrets"
# Available placeholders: {namespace}. # Available placeholders: {namespace}.
k8s_secret_name_template: "{namespace}-common-secrets" k8s_secret_name_template: "{namespace}-common-secrets"
# Builds the ExternalSecret CRD object name to annotate when a force-sync
# is requested. This is often different from the Kubernetes secret name
# because the ExternalSecret CRD and the resulting Secret are separate
# objects (e.g. ExternalSecret "job-apply" produces Secret
# "job-apply-common-secrets"). Falls back to k8s_secret_name_template
# if omitted. Available placeholders: {namespace}.
external_secret_name_template: "{namespace}"
# Builds the actual kubectl context name/ARN from region, account ID, env, # Builds the actual kubectl context name/ARN from region, account ID, env,
# and context. Available placeholders: {region}, {account_id}, {env}, # and context. Available placeholders: {region}, {account_id}, {env},
# {context}. Adjust the literal parts ("tf-", "-1", cluster naming, ARN # {context}. Adjust the literal parts ("tf-", "-1", cluster naming, ARN
@@ -60,3 +68,12 @@ team_label_key: "example.org/team"
# wraps SSO login in a custom script or needs a specific --profile, e.g.: # wraps SSO login in a custom script or needs a specific --profile, e.g.:
# aws_sso_login_command: "aws sso login --profile my-profile" # aws_sso_login_command: "aws sso login --profile my-profile"
aws_sso_login_command: "aws sso login" aws_sso_login_command: "aws sso login"
# Check for updates on startup and prompt to update if a newer version is
# available. Set to false to disable. Defaults to true if omitted.
# auto_update_check: true
# Terminal multiplexer backend. "tmux" (default) uses tmux/psmux.
# "wt" uses Windows Terminal's native split-pane — avoids the psmux
# focus-freeze issue on Windows. Only effective on Windows.
# multiplexer: "tmux"
+34 -12
View File
@@ -1,30 +1,52 @@
module github.com/skoelle/kctl-tui module github.com/skoelle/kctl-tui
go 1.22 go 1.25.12
require ( require (
github.com/charmbracelet/bubbles v0.20.0 github.com/Masterminds/semver/v3 v3.5.0
github.com/charmbracelet/bubbletea v1.1.1 github.com/charmbracelet/bubbles v0.21.1
github.com/charmbracelet/bubbletea v1.3.10
github.com/creativeprojects/go-selfupdate v1.6.0
golang.org/x/term v0.44.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
code.gitea.io/sdk/gitea v0.23.2 // indirect
github.com/42wim/httpsig v1.2.4 // indirect
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/lipgloss v1.0.0 // indirect github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/x/ansi v0.4.2 // indirect github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/term v0.2.0 // indirect github.com/charmbracelet/x/ansi v0.11.5 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/go-fed/httpsig v1.1.0 // indirect
github.com/google/go-github/v86 v86.0.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/hashicorp/go-version v1.9.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.15.2 // indirect github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect github.com/sahilm/fuzzy v0.1.1 // indirect
golang.org/x/sync v0.8.0 // indirect github.com/ulikunitz/xz v0.5.15 // indirect
golang.org/x/sys v0.24.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/text v0.3.8 // indirect gitlab.com/gitlab-org/api/client-go v1.46.0 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.38.0 // indirect
golang.org/x/time v0.15.0 // indirect
) )
+101 -23
View File
@@ -1,48 +1,126 @@
code.gitea.io/sdk/gitea v0.23.2 h1:iJB1FDmLegwfwjX8gotBDHdPSbk/ZR8V9VmEJaVsJYg=
code.gitea.io/sdk/gitea v0.23.2/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU=
github.com/42wim/httpsig v1.2.4/go.mod h1:yKsYfSyTBEohkPik224QPFylmzEBtda/kjyIAJjh3ps=
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqKOCvppbPY= github.com/charmbracelet/bubbles v0.21.1 h1:nj0decPiixaZeL9diI4uzzQTkkz1kYY8+jgzCZXSmW0=
github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= github.com/charmbracelet/bubbles v0.21.1/go.mod h1:HHvIYRCpbkCJw2yo0vNX1O5loCwSr9/mWS8GYSg50Sk=
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk= github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.11.5 h1:NBWeBpj/lJPE3Q5l+Lusa4+mH6v7487OP8K0r1IhRg4=
github.com/charmbracelet/x/ansi v0.11.5/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/creativeprojects/go-selfupdate v1.6.0 h1:Bu3cIgdyfI1Pg8XsL8nbaT2uMjfZ8HIoxnBmPJbN0sw=
github.com/creativeprojects/go-selfupdate v1.6.0/go.mod h1:Ids8O474XGQG0jZ5vpBIhWffcGYjUP6ccOI0mMcvQbI=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v86 v86.0.0 h1:S/6aANJhwRm8EQmGKVML3j41yq0h2BsTP8FnDkO7kcA=
github.com/google/go-github/v86 v86.0.0/go.mod h1:zKv1l4SwDXNFMGByi2FWkq71KwSXqj/eQRZuqtmcot8=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/graph-gophers/graphql-go v1.9.0 h1:yu0ucKHLc5qGpRwLYKIWtr9bOoxovkWasuBrPQwlHls=
github.com/graph-gophers/graphql-go v1.9.0/go.mod h1:23olKZ7duEvHlF/2ELEoSZaY1aNPfShjP782SOoNTyM=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
gitlab.com/gitlab-org/api/client-go v1.46.0 h1:YxBWFZIFYKcGESCb9fpkwzouo+apyB9pr/XTWzNoL24=
gitlab.com/gitlab-org/api/client-go v1.46.0/go.mod h1:FtgyU6g2HS5+fMhw6nLK96GBEEBx5MzntOiJWfIaiN8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+51
View File
@@ -50,6 +50,13 @@ type Config struct {
// naming conventions. // naming conventions.
K8sSecretNameTemplate string `yaml:"k8s_secret_name_template"` K8sSecretNameTemplate string `yaml:"k8s_secret_name_template"`
// ExternalSecretNameTemplate builds the ExternalSecret CRD object
// name that should be annotated when a force-sync is requested. This
// is often different from the Kubernetes secret name because the
// ExternalSecret CRD and the resulting Secret are separate objects.
// Falls back to K8sSecretNameTemplate if empty.
ExternalSecretNameTemplate string `yaml:"external_secret_name_template"`
// ContextTemplate builds the actual kubectl context name/ARN from // ContextTemplate builds the actual kubectl context name/ARN from
// region, account_id, env, and context, e.g. // region, account_id, env, and context, e.g.
// "arn:aws:eks:{region}:{account_id}:cluster/tf-{env}-{context}-1". // "arn:aws:eks:{region}:{account_id}:cluster/tf-{env}-{context}-1".
@@ -62,6 +69,33 @@ type Config struct {
// AWSSSOLoginCommand is run interactively if an AWS auth check fails // AWSSSOLoginCommand is run interactively if an AWS auth check fails
// before the secrets workflow (e.g. an expired SSO session). // before the secrets workflow (e.g. an expired SSO session).
AWSSSOLoginCommand string `yaml:"aws_sso_login_command"` AWSSSOLoginCommand string `yaml:"aws_sso_login_command"`
// AutoUpdateCheck controls whether kctl-tui checks for updates on
// startup. Defaults to true when omitted.
AutoUpdateCheck *bool `yaml:"auto_update_check"`
// Multiplexer selects the terminal multiplexer for the session.
// "tmux" (default) uses tmux/psmux. "wt" uses Windows Terminal's
// native split-pane (only effective on Windows).
Multiplexer string `yaml:"multiplexer"`
}
// IsAutoUpdateCheckEnabled returns true unless the user has explicitly set
// auto_update_check to false in their config.
func (c Config) IsAutoUpdateCheckEnabled() bool {
if c.AutoUpdateCheck == nil {
return true
}
return *c.AutoUpdateCheck
}
// MultiplexerBackend returns the configured multiplexer backend,
// falling back to "tmux" if not set.
func (c Config) MultiplexerBackend() string {
if c.Multiplexer == "" {
return "tmux"
}
return c.Multiplexer
} }
// LoginCommand returns the configured AWS SSO login command, falling back // LoginCommand returns the configured AWS SSO login command, falling back
@@ -121,6 +155,23 @@ func (c Config) ResolveK8sSecretName(namespace string) string {
}) })
} }
// ResolveExternalSecretName builds the ExternalSecret CRD object name for
// a given namespace using ExternalSecretNameTemplate. Falls back to
// K8sSecretNameTemplate (or SecretNameTemplate if that is also empty) so
// that existing configs keep working without changes.
func (c Config) ResolveExternalSecretName(namespace string) string {
template := c.ExternalSecretNameTemplate
if template == "" {
template = c.K8sSecretNameTemplate
}
if template == "" {
template = c.SecretNameTemplate
}
return kctl.ResolveTemplate(template, map[string]string{
"namespace": namespace,
})
}
// DefaultPath returns the default config file location: ~/.kctl-tui/config.yaml // DefaultPath returns the default config file location: ~/.kctl-tui/config.yaml
func DefaultPath() (string, error) { func DefaultPath() (string, error) {
home, err := os.UserHomeDir() home, err := os.UserHomeDir()
+27
View File
@@ -112,6 +112,33 @@ func TestResolveK8sSecretName_FallsBackToSecretNameTemplate(t *testing.T) {
} }
} }
func TestResolveExternalSecretName_ExplicitTemplate(t *testing.T) {
cfg := Config{ExternalSecretNameTemplate: "{namespace}"}
got := cfg.ResolveExternalSecretName("job-apply")
want := "job-apply"
if got != want {
t.Fatalf("got %q, want %q", got, want)
}
}
func TestResolveExternalSecretName_FallsBackToK8sSecretNameTemplate(t *testing.T) {
cfg := Config{K8sSecretNameTemplate: "{namespace}-common-secrets"}
got := cfg.ResolveExternalSecretName("job-apply")
want := "job-apply-common-secrets"
if got != want {
t.Fatalf("got %q, want %q", got, want)
}
}
func TestResolveExternalSecretName_FallsBackToSecretNameTemplate(t *testing.T) {
cfg := Config{SecretNameTemplate: "tf-{namespace}-{env}-secrets"}
got := cfg.ResolveExternalSecretName("job-apply")
want := "tf-job-apply-{env}-secrets" // {env} intentionally left unresolved
if got != want {
t.Fatalf("got %q, want %q", got, want)
}
}
func TestLoginCommand_DefaultsWhenUnset(t *testing.T) { func TestLoginCommand_DefaultsWhenUnset(t *testing.T) {
cfg := Config{} cfg := Config{}
if got := cfg.LoginCommand(); got != DefaultAWSSSOLoginCommand { if got := cfg.LoginCommand(); got != DefaultAWSSSOLoginCommand {
+42
View File
@@ -0,0 +1,42 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"schedule": ["before 6am on Monday"],
"packageRules": [
{
"matchManagers": ["github-actions"],
"matchUpdateTypes": ["minor", "patch"],
"groupName": "GitHub Actions",
"automerge": true
},
{
"matchManagers": ["github-actions"],
"matchUpdateTypes": ["major"],
"groupName": "GitHub Actions (major)",
"labels": ["major-update"],
"automerge": false
},
{
"matchManagers": ["gomod"],
"matchUpdateTypes": ["minor", "patch"],
"groupName": "Go dependencies",
"automerge": true
},
{
"matchManagers": ["gomod"],
"matchUpdateTypes": ["major"],
"groupName": "Go dependencies (major)",
"labels": ["major-update"],
"automerge": false
},
{
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
},
{
"matchUpdateTypes": ["major"],
"labels": ["major-update"],
"automerge": false
}
]
}