mirror of
https://github.com/skoelle/kctl-tui.git
synced 2026-09-17 20:10:24 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d05d35bdc1 | ||
|
|
f8679adf95 | ||
|
|
73cc510347 | ||
|
|
d17eb6971c | ||
|
|
fb4c1be12b | ||
|
|
9e3fefb71a | ||
|
|
2d382441a1 | ||
|
|
ad7d8534ab | ||
|
|
a0b937e882 | ||
|
|
d32ccd5952 | ||
|
|
3729a0301d | ||
|
|
a1760b7b01 | ||
|
|
bd23403e6c | ||
|
|
6a0405640d | ||
|
|
c6880e92ce | ||
|
|
209e93646c | ||
|
|
720064454b | ||
|
|
205f8906df | ||
|
|
c0ad7e803e | ||
|
|
7f2c9b2ba8 | ||
|
|
fed2b44c28 | ||
|
|
d89efc55d9 | ||
|
|
d743422d28 | ||
|
|
b9439e428a | ||
|
|
4afb20c740 |
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: "1.24"
|
||||
go-version: "1.25"
|
||||
cache: false
|
||||
|
||||
- name: Tidy dependencies (generates/updates go.sum)
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: "1.24"
|
||||
go-version: "1.25"
|
||||
cache: false
|
||||
|
||||
- name: Tidy dependencies (generates/updates go.sum)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
title: "kctl-tui"
|
||||
emoji: "🐳"
|
||||
category: code
|
||||
subcategory: "Dev Tools"
|
||||
status: active
|
||||
stack: [Go, Bubbletea, Bubbles, Lipgloss, YAML]
|
||||
@@ -29,7 +29,8 @@ Create a release tag and push it to origin. The GitHub Action will automatically
|
||||
5. Create annotated tag on that commit:
|
||||
- Run `git tag -a v$ARGUMENTS -m "Release v$ARGUMENTS"`
|
||||
|
||||
6. Push commit and tag:
|
||||
- Run `git push origin main --tags` (or current branch)
|
||||
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
|
||||
|
||||
@@ -6,6 +6,12 @@ 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
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ go mod download
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.22+
|
||||
- Go 1.25+
|
||||
- kubectl, k9s, tmux (or psmux on Windows)
|
||||
- An active Kubernetes cluster for integration testing
|
||||
|
||||
|
||||
@@ -77,6 +77,14 @@ is still open. For the full requirements, see [SPEC.md](SPEC.md).
|
||||
- [x] `--help` flag with full usage documentation.
|
||||
- [x] CHANGELOG.md, CONTRIBUTING.md, GitHub Issue/PR templates.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
# kctl-tui
|
||||
# 🚀 kctl-tui
|
||||
|
||||
A small terminal entry point for everyday Kubernetes work: pick a context
|
||||
and a namespace once, then drive rollout restarts and an AWS Secrets
|
||||
Manager <-> Kubernetes Secret diff/force-sync per environment from one
|
||||
place instead of retyping long `kubectl` commands.
|
||||
|
||||
## Why
|
||||
## ✨ Why
|
||||
|
||||
Working with several clusters, many namespaces per team, and paired
|
||||
environments (e.g. beta/prod) quickly turns into a lot of repeated typing
|
||||
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
|
||||
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.
|
||||
- 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
|
||||
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.
|
||||
- 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),
|
||||
instead of listing secrets or discovering contexts live from
|
||||
`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
|
||||
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.
|
||||
|
||||
See [SPEC.md](SPEC.md) for the full requirements and design rationale, and
|
||||
[PLAN.md](PLAN.md) for the implementation roadmap and current status.
|
||||
|
||||
## How it works
|
||||
## 🔧 How it works
|
||||
|
||||
```
|
||||
+--------------------------------------------------+
|
||||
@@ -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
|
||||
`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
|
||||
Configuration below - they must already exist in your kubeconfig, e.g.
|
||||
added via `aws eks update-kubeconfig`).
|
||||
- `k9s` (used for the two status panes).
|
||||
- `tmux` (used for the 3-pane layout). On **Linux/macOS**, install
|
||||
- 👀 `k9s` (used for the two status panes).
|
||||
- 📺 `tmux` (used for the 3-pane layout). On **Linux/macOS**, install
|
||||
`tmux` via your package manager. On **Windows**, install
|
||||
[psmux](https://github.com/marlocarlo/psmux) — a native,
|
||||
tmux-compatible terminal multiplexer:
|
||||
@@ -78,18 +78,18 @@ See [SPEC.md](SPEC.md) for the full requirements and design rationale, and
|
||||
cargo install psmux
|
||||
```
|
||||
psmux provides a `tmux` command, so kctl-tui works without changes.
|
||||
- `aws` CLI, configured with credentials, only needed for the secrets
|
||||
- ☁️ `aws` CLI, configured with credentials, only needed for the secrets
|
||||
workflow.
|
||||
|
||||
## Installation
|
||||
## 📥 Installation
|
||||
|
||||
### Quick install (Linux/macOS/WSL)
|
||||
### 🚀 Quick install (Linux/macOS/WSL)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/skoelle/kctl-tui/main/install.sh | bash
|
||||
```
|
||||
|
||||
### Quick install (Windows)
|
||||
### 🪟 Quick install (Windows)
|
||||
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/skoelle/kctl-tui/main/install.ps1 | iex
|
||||
@@ -98,7 +98,7 @@ irm https://raw.githubusercontent.com/skoelle/kctl-tui/main/install.ps1 | iex
|
||||
This downloads the latest release binary for your OS/architecture from
|
||||
GitHub Releases and installs it to `/usr/local/bin/kctl-tui`.
|
||||
|
||||
### From source
|
||||
### 🛠️ From source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/skoelle/kctl-tui.git
|
||||
@@ -107,19 +107,20 @@ go build -o kctl-tui ./cmd/kctl-tui
|
||||
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
|
||||
`windows`, each for `amd64` and `arm64`, via the GitHub Actions workflow in
|
||||
[.github/workflows/build.yml](.github/workflows/build.yml). Download the
|
||||
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`
|
||||
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
|
||||
contexts:
|
||||
@@ -136,40 +137,47 @@ aws_account_id: "123456789012"
|
||||
|
||||
secret_name_template: "tf-{namespace}-{env}-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"
|
||||
|
||||
team_label_key: "example.org/team"
|
||||
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
|
||||
clusters). This is the outermost navigation level, one `Esc` above team
|
||||
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
|
||||
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`.
|
||||
`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}`,
|
||||
`{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
|
||||
two sides commonly follow different naming conventions. Placeholders:
|
||||
`{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}`,
|
||||
`{account_id}`, `{env}`, `{context}`. Adjust the literal parts (`tf-`,
|
||||
`-1`, cluster naming, ARN shape) to match how your own clusters/contexts
|
||||
are actually named — the resolved value must match an existing context
|
||||
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
|
||||
entirely up to your organization's labeling convention; kctl-tui ships
|
||||
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
|
||||
SSO session). Defaults to `aws sso login`.
|
||||
|
||||
@@ -177,7 +185,7 @@ aws_sso_login_command: "aws sso login"
|
||||
that way — it typically contains your organization's internal account ID,
|
||||
context naming, and label names.
|
||||
|
||||
## Windows notes
|
||||
## 🪟 Windows notes
|
||||
|
||||
On native Windows (without WSL), install [psmux](https://github.com/marlocarlo/psmux)
|
||||
for the 3-pane layout. psmux is a native Windows terminal multiplexer
|
||||
@@ -196,19 +204,25 @@ mkdir -p ~/.kube
|
||||
ln -s /mnt/c/Users/<your-windows-username>/.kube/config ~/.kube/config
|
||||
```
|
||||
|
||||
## Usage
|
||||
## 📖 Usage
|
||||
|
||||
```bash
|
||||
kctl-tui # start the TUI (full navigation mode)
|
||||
kctl-tui --help # show all commands and flags
|
||||
kctl-tui --version # print version
|
||||
kctl-tui --verbose # enable debug logging to stderr
|
||||
kctl-tui update # update to the latest release
|
||||
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 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
|
||||
go test ./...
|
||||
@@ -222,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
|
||||
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)
|
||||
|
||||
+68
-19
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"sort"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
@@ -47,7 +48,7 @@ type fullModel struct {
|
||||
}
|
||||
|
||||
func newFullModel() *fullModel {
|
||||
l := list.New(nil, list.NewDefaultDelegate(), 0, 0)
|
||||
l := list.New(nil, newCompactDelegate(), 0, 0)
|
||||
l.Title = "kctl-tui"
|
||||
l.SetShowStatusBar(false)
|
||||
l.SetFilteringEnabled(false)
|
||||
@@ -197,7 +198,11 @@ func (m *fullModel) handleSelect() (tea.Model, tea.Cmd) {
|
||||
|
||||
case screenNamespace:
|
||||
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
|
||||
return m, nil
|
||||
}
|
||||
@@ -210,23 +215,21 @@ func (m *fullModel) handleSelect() (tea.Model, tea.Cmd) {
|
||||
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 {
|
||||
namespaces, err := kubeexec.GetNamespacesWithLabels(m.bootstrapContext())
|
||||
if err != nil {
|
||||
return errMsg{err}
|
||||
merged := map[string]map[string]string{}
|
||||
for _, env := range m.cfg.Envs {
|
||||
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 {
|
||||
@@ -264,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
|
||||
// action), and the two status panes run k9s against the first two
|
||||
// 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 {
|
||||
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()
|
||||
if err != nil {
|
||||
selfPath = "kctl-tui" // fallback to PATH lookup
|
||||
@@ -321,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 {
|
||||
view := m.list.View()
|
||||
if m.statusMessage != "" {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/charmbracelet/bubbles/list"
|
||||
|
||||
// simpleItem is a minimal implementation of list.Item used for all
|
||||
// selection screens (contexts, teams, namespaces, menu actions).
|
||||
type simpleItem struct {
|
||||
@@ -13,3 +15,13 @@ type simpleItem struct {
|
||||
func (i simpleItem) Title() string { return i.label }
|
||||
func (i simpleItem) Description() string { return "" }
|
||||
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
|
||||
}
|
||||
|
||||
+75
-2
@@ -6,6 +6,9 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
@@ -70,6 +73,12 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
case "update":
|
||||
if err := runUpdate(verbose); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "kctl-tui update error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command: %s\n\n", filtered[0])
|
||||
printUsage()
|
||||
@@ -77,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()
|
||||
p := tea.NewProgram(m, tea.WithAltScreen())
|
||||
if _, err := p.Run(); err != nil {
|
||||
@@ -92,6 +120,8 @@ https://github.com/skoelle/kctl-tui
|
||||
Usage:
|
||||
kctl-tui [flags] start the TUI (full navigation mode)
|
||||
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 panel [options] control pane (called internally by tmux)
|
||||
|
||||
@@ -103,15 +133,58 @@ Flags:
|
||||
Examples:
|
||||
kctl-tui # start the TUI
|
||||
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 config edit # open config in editor
|
||||
kctl-tui config check # validate config
|
||||
`)
|
||||
}
|
||||
|
||||
func runConfig(args []string) error {
|
||||
if len(args) == 0 || args[0] != "check" {
|
||||
return fmt.Errorf("usage: kctl-tui config check")
|
||||
if len(args) > 0 && args[0] == "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()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot determine config path: %w", err)
|
||||
|
||||
+24
-6
@@ -8,6 +8,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -49,8 +50,9 @@ type panelModel struct {
|
||||
|
||||
deploymentName string
|
||||
|
||||
awsSecretName string // resolved via secret_name_template (namespace + env)
|
||||
k8sSecretName string // resolved via k8s_secret_name_template (namespace only)
|
||||
awsSecretName string // resolved via secret_name_template (namespace + env)
|
||||
k8sSecretName string // resolved via k8s_secret_name_template (namespace only)
|
||||
externalSecretName string // resolved via external_secret_name_template (namespace only)
|
||||
awsValues map[string]string
|
||||
k8sValues map[string]string
|
||||
diffEntries []kctl.SecretDiffEntry
|
||||
@@ -60,6 +62,11 @@ type panelModel struct {
|
||||
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 {
|
||||
fs := flag.NewFlagSet("panel", flag.ContinueOnError)
|
||||
context := fs.String("context", "", "context (e.g. internal/external)")
|
||||
@@ -82,11 +89,15 @@ func newPanelModel(context, ns, team string) *panelModel {
|
||||
cfgPath, _ := config.DefaultPath()
|
||||
cfg, loadErr := config.Load(cfgPath)
|
||||
|
||||
l := list.New(nil, list.NewDefaultDelegate(), 0, 0)
|
||||
l := list.New(nil, newCompactDelegate(), 0, 0)
|
||||
l.SetShowStatusBar(false)
|
||||
l.SetFilteringEnabled(false)
|
||||
|
||||
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 {
|
||||
m.err = fmt.Errorf("config load failed: %w", loadErr)
|
||||
m.step = stepError
|
||||
@@ -98,7 +109,9 @@ func newPanelModel(context, ns, team string) *panelModel {
|
||||
|
||||
func (m *panelModel) showEnvMenu() {
|
||||
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 {
|
||||
items = append(items, simpleItem{label: env, value: env})
|
||||
}
|
||||
@@ -178,10 +191,14 @@ func (m *panelModel) usesTextInput() bool {
|
||||
// handleEsc navigates one level up: action menu -> env menu, most
|
||||
// sub-steps -> action menu. From the top-level env menu it closes the
|
||||
// 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) {
|
||||
switch m.step {
|
||||
case stepEnvMenu:
|
||||
if m.isWtMode() {
|
||||
return m, nil
|
||||
}
|
||||
exec.Command("tmux", "kill-session", "-t", "kctl").Run()
|
||||
return m, tea.Quit
|
||||
case stepActionMenu:
|
||||
@@ -316,6 +333,7 @@ func (m *panelModel) afterAWSLogin(execErr error) (tea.Model, tea.Cmd) {
|
||||
func (m *panelModel) startSecretsFlow() (tea.Model, tea.Cmd) {
|
||||
m.awsSecretName = m.cfg.ResolveSecretName(m.ns, m.currentEnv)
|
||||
m.k8sSecretName = m.cfg.ResolveK8sSecretName(m.ns)
|
||||
m.externalSecretName = m.cfg.ResolveExternalSecretName(m.ns)
|
||||
|
||||
raw, err := kubeexec.GetAWSSecretString(m.awsSecretName, m.cfg.AWSRegion)
|
||||
if err != nil {
|
||||
@@ -462,7 +480,7 @@ func (m *panelModel) fromForceSyncConfirm() (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
m.step = stepExternalSecretName
|
||||
m.input.SetValue(m.k8sSecretName)
|
||||
m.input.SetValue(m.externalSecretName)
|
||||
m.input.Placeholder = "ExternalSecret object name"
|
||||
return m, nil
|
||||
}
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
@@ -42,6 +42,14 @@ secret_name_template: "tf-{namespace}-{env}-secrets"
|
||||
# Available placeholders: {namespace}.
|
||||
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,
|
||||
# and context. Available placeholders: {region}, {account_id}, {env},
|
||||
# {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.:
|
||||
# aws_sso_login_command: "aws sso login --profile my-profile"
|
||||
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"
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
module github.com/skoelle/kctl-tui
|
||||
|
||||
go 1.24.2
|
||||
go 1.25.12
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver/v3 v3.5.0
|
||||
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
|
||||
)
|
||||
|
||||
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/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||
@@ -19,7 +24,14 @@ require (
|
||||
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/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-localereader v0.0.1 // indirect
|
||||
@@ -29,8 +41,12 @@ require (
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sahilm/fuzzy v0.1.1 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // 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
|
||||
)
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
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/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
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/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
|
||||
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
||||
github.com/charmbracelet/bubbles v0.21.1 h1:nj0decPiixaZeL9diI4uzzQTkkz1kYY8+jgzCZXSmW0=
|
||||
github.com/charmbracelet/bubbles v0.21.1/go.mod h1:HHvIYRCpbkCJw2yo0vNX1O5loCwSr9/mWS8GYSg50Sk=
|
||||
github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqKOCvppbPY=
|
||||
github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4=
|
||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
|
||||
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk=
|
||||
github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
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/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
|
||||
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
|
||||
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=
|
||||
@@ -32,47 +32,95 @@ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfa
|
||||
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/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/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.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
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/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/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
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/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
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/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
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/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
|
||||
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
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=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
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.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.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -50,6 +50,13 @@ type Config struct {
|
||||
// naming conventions.
|
||||
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
|
||||
// region, account_id, env, and context, e.g.
|
||||
// "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
|
||||
// before the secrets workflow (e.g. an expired SSO session).
|
||||
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
|
||||
@@ -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
|
||||
func DefaultPath() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
|
||||
@@ -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) {
|
||||
cfg := Config{}
|
||||
if got := cfg.LoginCommand(); got != DefaultAWSSSOLoginCommand {
|
||||
|
||||
Reference in New Issue
Block a user