From 720064454b433bef7cf24a67c7a2dc17a49d9204 Mon Sep 17 00:00:00 2001 From: Stefan Koelle Date: Wed, 16 Sep 2026 11:49:35 +0200 Subject: [PATCH] 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. --- cmd/kctl-tui/panel.go | 8 +++++--- config.example.yaml | 8 ++++++++ internal/config/config.go | 24 ++++++++++++++++++++++++ internal/config/config_test.go | 27 +++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/cmd/kctl-tui/panel.go b/cmd/kctl-tui/panel.go index 4b5df77..32dc6a7 100644 --- a/cmd/kctl-tui/panel.go +++ b/cmd/kctl-tui/panel.go @@ -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 } diff --git a/config.example.yaml b/config.example.yaml index 8b98a44..855e4d2 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -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 diff --git a/internal/config/config.go b/internal/config/config.go index d2eb388..a769455 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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() diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 60c1cc7..1dc3df8 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -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 {