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.
This commit is contained in:
2026-09-16 11:49:35 +02:00
parent 205f8906df
commit 720064454b
4 changed files with 64 additions and 3 deletions
+5 -3
View File
@@ -49,8 +49,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
@@ -316,6 +317,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 {
@@ -462,7 +464,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
} }
+8
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
+24
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".
@@ -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 // 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 {