mirror of
https://github.com/skoelle/kctl-tui.git
synced 2026-09-17 20:10:24 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6880e92ce | ||
|
|
209e93646c | ||
|
|
720064454b | ||
|
|
205f8906df |
@@ -119,7 +119,8 @@ matching asset from the [Releases page](https://github.com/skoelle/kctl-tui/rele
|
||||
## ⚙️ 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,6 +137,7 @@ 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"
|
||||
@@ -159,6 +161,12 @@ aws_sso_login_command: "aws sso login"
|
||||
chosen namespace. Kept separate from `secret_name_template` because the
|
||||
two sides commonly follow different naming conventions. Placeholders:
|
||||
`{namespace}`.
|
||||
- 🎯 `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-`,
|
||||
@@ -205,6 +213,7 @@ 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)
|
||||
```
|
||||
|
||||
+48
-2
@@ -6,6 +6,9 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
@@ -113,6 +116,7 @@ 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)
|
||||
|
||||
@@ -126,14 +130,56 @@ Examples:
|
||||
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)
|
||||
|
||||
@@ -49,8 +49,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
|
||||
@@ -316,6 +317,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 +464,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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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".
|
||||
@@ -134,6 +141,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