mirror of
https://github.com/skoelle/kctl-tui.git
synced 2026-09-18 04:20:25 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bb3507aec | ||
|
|
145288971a | ||
|
|
75e0b832a2 |
+19
-30
@@ -27,7 +27,6 @@ const (
|
|||||||
stepRedeployList
|
stepRedeployList
|
||||||
stepRedeployConfirm
|
stepRedeployConfirm
|
||||||
stepAWSAuthPrompt
|
stepAWSAuthPrompt
|
||||||
stepK8sSecretName
|
|
||||||
stepDiffResult
|
stepDiffResult
|
||||||
stepForceSyncConfirm
|
stepForceSyncConfirm
|
||||||
stepExternalSecretName
|
stepExternalSecretName
|
||||||
@@ -47,9 +46,9 @@ type panelModel struct {
|
|||||||
|
|
||||||
deploymentName string
|
deploymentName string
|
||||||
|
|
||||||
awsSecretID string
|
awsSecretName string // resolved via secret_name_template (namespace + env)
|
||||||
|
k8sSecretName string // resolved via k8s_secret_name_template (namespace only)
|
||||||
awsValues map[string]string
|
awsValues map[string]string
|
||||||
k8sSecretName string
|
|
||||||
k8sValues map[string]string
|
k8sValues map[string]string
|
||||||
diffEntries []kctl.SecretDiffEntry
|
diffEntries []kctl.SecretDiffEntry
|
||||||
|
|
||||||
@@ -155,11 +154,7 @@ func (m *panelModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *panelModel) usesTextInput() bool {
|
func (m *panelModel) usesTextInput() bool {
|
||||||
switch m.step {
|
return m.step == stepExternalSecretName
|
||||||
case stepK8sSecretName, stepExternalSecretName:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleEsc navigates one level up: action menu -> env menu, most
|
// handleEsc navigates one level up: action menu -> env menu, most
|
||||||
@@ -192,9 +187,6 @@ func (m *panelModel) handleEnter() (tea.Model, tea.Cmd) {
|
|||||||
return m.fromRedeployConfirm()
|
return m.fromRedeployConfirm()
|
||||||
case stepAWSAuthPrompt:
|
case stepAWSAuthPrompt:
|
||||||
return m.fromAWSAuthPrompt()
|
return m.fromAWSAuthPrompt()
|
||||||
case stepK8sSecretName:
|
|
||||||
m.k8sSecretName = m.input.Value()
|
|
||||||
return m.compareAllFields()
|
|
||||||
case stepForceSyncConfirm:
|
case stepForceSyncConfirm:
|
||||||
return m.fromForceSyncConfirm()
|
return m.fromForceSyncConfirm()
|
||||||
case stepExternalSecretName:
|
case stepExternalSecretName:
|
||||||
@@ -296,14 +288,17 @@ func (m *panelModel) afterAWSLogin(execErr error) (tea.Model, tea.Cmd) {
|
|||||||
return m.startSecretsFlow()
|
return m.startSecretsFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// startSecretsFlow computes the AWS secret ID from the configured
|
// startSecretsFlow computes the AWS secret ID (namespace + env) and the
|
||||||
// template (namespace + env) and fetches it directly - no more listing
|
// Kubernetes secret name (namespace only) from their respective
|
||||||
// secrets or asking for a region, both now driven by config.
|
// templates and fetches the AWS side directly - no manual input required
|
||||||
|
// for either name.
|
||||||
func (m *panelModel) startSecretsFlow() (tea.Model, tea.Cmd) {
|
func (m *panelModel) startSecretsFlow() (tea.Model, tea.Cmd) {
|
||||||
m.awsSecretID = m.cfg.ResolveSecretName(m.ns, m.currentEnv)
|
m.awsSecretName = m.cfg.ResolveSecretName(m.ns, m.currentEnv)
|
||||||
raw, err := kubeexec.GetAWSSecretString(m.awsSecretID, m.cfg.AWSRegion)
|
m.k8sSecretName = m.cfg.ResolveK8sSecretName(m.ns)
|
||||||
|
|
||||||
|
raw, err := kubeexec.GetAWSSecretString(m.awsSecretName, m.cfg.AWSRegion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return m.showError(fmt.Errorf("failed to fetch AWS secret %q: %w", m.awsSecretID, err))
|
return m.showError(fmt.Errorf("failed to fetch AWS secret %q: %w", m.awsSecretName, err))
|
||||||
}
|
}
|
||||||
var parsed map[string]interface{}
|
var parsed map[string]interface{}
|
||||||
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
|
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
|
||||||
@@ -316,10 +311,7 @@ func (m *panelModel) startSecretsFlow() (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.step = stepK8sSecretName
|
return m.compareAllFields()
|
||||||
m.input.SetValue("")
|
|
||||||
m.input.Placeholder = "Kubernetes secret name (in namespace " + m.ns + ")"
|
|
||||||
return m, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *panelModel) fromRedeployList() (tea.Model, tea.Cmd) {
|
func (m *panelModel) fromRedeployList() (tea.Model, tea.Cmd) {
|
||||||
@@ -358,15 +350,15 @@ func (m *panelModel) fromRedeployConfirm() (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// compareAllFields fetches every field of the Kubernetes secret and diffs
|
// compareAllFields fetches every field of the Kubernetes secret and diffs
|
||||||
// it against every key of the templated AWS secret in one go.
|
// it against every key of the AWS secret in one go.
|
||||||
func (m *panelModel) compareAllFields() (tea.Model, tea.Cmd) {
|
func (m *panelModel) compareAllFields() (tea.Model, tea.Cmd) {
|
||||||
k8sValues, err := kubeexec.GetSecretAllFields(m.resolvedContext(), m.ns, m.k8sSecretName)
|
k8sValues, err := kubeexec.GetSecretAllFields(m.resolvedContext(), m.ns, m.k8sSecretName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return m.showError(err)
|
return m.showError(fmt.Errorf("failed to fetch Kubernetes secret %q: %w", m.k8sSecretName, err))
|
||||||
}
|
}
|
||||||
m.k8sValues = k8sValues
|
m.k8sValues = k8sValues
|
||||||
m.diffEntries = diffSecretValues(m.awsValues, m.k8sValues)
|
m.diffEntries = diffSecretValues(m.awsValues, m.k8sValues)
|
||||||
m.message = renderDiffTable(m.currentEnv, m.awsSecretID, m.k8sSecretName, m.diffEntries)
|
m.message = renderDiffTable(m.currentEnv, m.awsSecretName, m.k8sSecretName, m.diffEntries)
|
||||||
|
|
||||||
if anyMismatch(m.diffEntries) {
|
if anyMismatch(m.diffEntries) {
|
||||||
m.list.SetItems([]list.Item{
|
m.list.SetItems([]list.Item{
|
||||||
@@ -381,9 +373,9 @@ func (m *panelModel) compareAllFields() (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderDiffTable(env, awsSecretID, k8sSecretName string, entries []kctl.SecretDiffEntry) string {
|
func renderDiffTable(env, awsSecretName, k8sSecretName string, entries []kctl.SecretDiffEntry) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
fmt.Fprintf(&b, "env: %s AWS secret: %s Kubernetes secret: %s\n\n", env, awsSecretID, k8sSecretName)
|
fmt.Fprintf(&b, "env: %s AWS secret: %s Kubernetes secret: %s\n\n", env, awsSecretName, k8sSecretName)
|
||||||
fmt.Fprintf(&b, "%-25s %-20s %-20s %s\n", "KEY", "AWS", "KUBERNETES", "STATUS")
|
fmt.Fprintf(&b, "%-25s %-20s %-20s %s\n", "KEY", "AWS", "KUBERNETES", "STATUS")
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
status := "OK"
|
status := "OK"
|
||||||
@@ -460,10 +452,7 @@ func (m *panelModel) View() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *panelModel) stepPrompt() string {
|
func (m *panelModel) stepPrompt() string {
|
||||||
switch m.step {
|
if m.step == stepExternalSecretName {
|
||||||
case stepK8sSecretName:
|
|
||||||
return fmt.Sprintf("Kubernetes secret name (env=%s, namespace=%s)", m.currentEnv, m.ns)
|
|
||||||
case stepExternalSecretName:
|
|
||||||
return "ExternalSecret object name to annotate"
|
return "ExternalSecret object name to annotate"
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -34,6 +34,14 @@ aws_account_id: "123456789012"
|
|||||||
# environment. Available placeholders: {namespace}, {env}.
|
# environment. Available placeholders: {namespace}, {env}.
|
||||||
secret_name_template: "tf-{namespace}-{env}-secrets"
|
secret_name_template: "tf-{namespace}-{env}-secrets"
|
||||||
|
|
||||||
|
# Builds the Kubernetes secret name from the chosen namespace. Kept as a
|
||||||
|
# separate template from secret_name_template above because the AWS side
|
||||||
|
# and the Kubernetes side commonly follow different naming conventions
|
||||||
|
# (e.g. the Kubernetes secret is per-namespace only, without an env
|
||||||
|
# segment, because each environment already has its own cluster).
|
||||||
|
# Available placeholders: {namespace}.
|
||||||
|
k8s_secret_name_template: "{namespace}-common-secrets"
|
||||||
|
|
||||||
# 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
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ type Config struct {
|
|||||||
// namespace and env, e.g. "tf-{namespace}-{env}-secrets".
|
// namespace and env, e.g. "tf-{namespace}-{env}-secrets".
|
||||||
SecretNameTemplate string `yaml:"secret_name_template"`
|
SecretNameTemplate string `yaml:"secret_name_template"`
|
||||||
|
|
||||||
|
// K8sSecretNameTemplate builds the Kubernetes secret name from a
|
||||||
|
// namespace, e.g. "{namespace}-common-secrets". Kept separate from
|
||||||
|
// SecretNameTemplate because the two sides commonly follow different
|
||||||
|
// naming conventions.
|
||||||
|
K8sSecretNameTemplate string `yaml:"k8s_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".
|
||||||
@@ -96,6 +102,22 @@ func (c Config) ResolveSecretName(namespace, env string) string {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolveK8sSecretName builds the Kubernetes secret name for a given
|
||||||
|
// namespace using K8sSecretNameTemplate. Falls back to
|
||||||
|
// SecretNameTemplate resolved without an env placeholder if
|
||||||
|
// K8sSecretNameTemplate is not configured, so existing configs keep
|
||||||
|
// working, though setting it explicitly is recommended since the two
|
||||||
|
// naming conventions usually differ.
|
||||||
|
func (c Config) ResolveK8sSecretName(namespace string) string {
|
||||||
|
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()
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ envs:
|
|||||||
aws_region: "eu-central-1"
|
aws_region: "eu-central-1"
|
||||||
aws_account_id: "123456789012"
|
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"
|
||||||
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"
|
||||||
`)
|
`)
|
||||||
@@ -46,6 +47,9 @@ team_label_key: "example.org/team"
|
|||||||
if len(cfg.Contexts) != 2 || len(cfg.Envs) != 2 {
|
if len(cfg.Contexts) != 2 || len(cfg.Envs) != 2 {
|
||||||
t.Fatalf("unexpected contexts/envs: %+v", cfg)
|
t.Fatalf("unexpected contexts/envs: %+v", cfg)
|
||||||
}
|
}
|
||||||
|
if cfg.K8sSecretNameTemplate != "{namespace}-common-secrets" {
|
||||||
|
t.Fatalf("unexpected k8s secret name template: %q", cfg.K8sSecretNameTemplate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEffectiveDefaultContext(t *testing.T) {
|
func TestEffectiveDefaultContext(t *testing.T) {
|
||||||
@@ -87,6 +91,24 @@ func TestResolveSecretName(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveK8sSecretName_ExplicitTemplate(t *testing.T) {
|
||||||
|
cfg := Config{K8sSecretNameTemplate: "{namespace}-common-secrets"}
|
||||||
|
got := cfg.ResolveK8sSecretName("example-ns")
|
||||||
|
want := "example-ns-common-secrets"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("got %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveK8sSecretName_FallsBackToSecretNameTemplate(t *testing.T) {
|
||||||
|
cfg := Config{SecretNameTemplate: "tf-{namespace}-{env}-secrets"}
|
||||||
|
got := cfg.ResolveK8sSecretName("example-ns")
|
||||||
|
want := "tf-example-ns-{env}-secrets" // {env} intentionally left unresolved here
|
||||||
|
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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user