From 0cf91d294b0a403d0756b259302cb2c9cfb778b0 Mon Sep 17 00:00:00 2001 From: Mateusz Kubaczyk Date: Fri, 22 Oct 2021 12:05:47 +0200 Subject: [PATCH] Add namespaceLabelsAuthoritative settings option to allow removing undefined ns labels --- docs/desired_state_specification.md | 1 + internal/app/decision_maker.go | 6 +++--- internal/app/decision_maker_test.go | 4 ++-- internal/app/kube_helpers.go | 31 +++++++++++++++++++++++---- internal/app/plan.go | 4 ++-- internal/app/state.go | 33 +++++++++++++++-------------- 6 files changed, 52 insertions(+), 27 deletions(-) diff --git a/docs/desired_state_specification.md b/docs/desired_state_specification.md index 4be7b4cb..d7080652 100644 --- a/docs/desired_state_specification.md +++ b/docs/desired_state_specification.md @@ -112,6 +112,7 @@ The following options can be skipped if your kubectl context is already created - **slackWebhook** : a [Slack](http://slack.com) Webhook URL to receive Helmsman notifications. This can be passed directly or in an environment variable. - **msTeamsWebhook** : a [Microsoft Teams](https://www.microsoft.com/pl-pl/microsoft-teams/group-chat-software) Webhook URL to receive Helmsman notifications. This can be passed directly or in an environment variable. - **reverseDelete** : if set to `true` it will reverse the priority order whilst deleting. +- **namespaceLabelsAuthoritative** : if set to `true` it will remove all the namespace's labels that are not defined in DSL for particular namespace - **eyamlEnabled** : if set to `true` it will use [hiera-eyaml](https://github.com/voxpupuli/hiera-eyaml) to decrypt secret files instead of using default helm-secrets based on sops - **eyamlPrivateKeyPath** : if set with path to the eyaml private key file, it will use it instead of looking for default one in ./keys directory relative to where Helmsman were run. It needs to be defined in conjunction with eyamlPublicKeyPath. - **eyamlPublicKeyPath** : if set with path to the eyaml public key file, it will use it instead of looking for default one in ./keys directory relative to where Helmsman were run. It needs to be defined in conjunction with eyamlPrivateKeyPath. diff --git a/internal/app/decision_maker.go b/internal/app/decision_maker.go index dc1f7a84..51b14682 100644 --- a/internal/app/decision_maker.go +++ b/internal/app/decision_maker.go @@ -102,7 +102,7 @@ func (cs *currentState) decide(r *release, n *namespace, p *plan, c *chartInfo) if flags.destroy { if ok := cs.releaseExists(r, ""); ok { - p.addDecision("Release [ "+r.Name+" ] will be DELETED (destroy flag enabled).", r.Priority, delete) + p.addDecision("Release [ "+r.Name+" ] will be DELETED (destroy flag enabled).", r.Priority, remove) r.uninstall(p) } return @@ -110,7 +110,7 @@ func (cs *currentState) decide(r *release, n *namespace, p *plan, c *chartInfo) if !r.Enabled { if ok := cs.releaseExists(r, ""); ok { - p.addDecision("Release [ "+r.Name+" ] is desired to be DELETED.", r.Priority, delete) + p.addDecision("Release [ "+r.Name+" ] is desired to be DELETED.", r.Priority, remove) r.uninstall(p) } else { p.addDecision("Release [ "+r.Name+" ] disabled", r.Priority, noop) @@ -275,7 +275,7 @@ func (cs *currentState) cleanUntrackedReleases(s *state, p *plan) { if !tracked { toDelete++ r := cs.releases[name+"-"+ns] - p.addDecision("Untracked release [ "+r.Name+" ] found and it will be deleted", -1000, delete) + p.addDecision("Untracked release [ "+r.Name+" ] found and it will be deleted", -1000, remove) r.uninstall(p) } } diff --git a/internal/app/decision_maker_test.go b/internal/app/decision_maker_test.go index c165e360..01169293 100644 --- a/internal/app/decision_maker_test.go +++ b/internal/app/decision_maker_test.go @@ -317,8 +317,8 @@ func (dt decisionType) String() string { return "create" case change: return "change" - case delete: - return "delete" + case remove: + return "remove" case noop: return "noop" } diff --git a/internal/app/kube_helpers.go b/internal/app/kube_helpers.go index f93f1dd2..f175ea92 100644 --- a/internal/app/kube_helpers.go +++ b/internal/app/kube_helpers.go @@ -1,6 +1,7 @@ package app import ( + "encoding/json" "errors" "fmt" "io/ioutil" @@ -24,7 +25,7 @@ func addNamespaces(s *state) { go func(name string, cfg *namespace, wg *sync.WaitGroup) { defer wg.Done() createNamespace(name) - labelNamespace(name, cfg.Labels) + labelNamespace(name, cfg.Labels, s.Settings.NamespaceLabelsAuthoritative) annotateNamespace(name, cfg.Annotations) if !flags.dryRun { setLimits(name, cfg.Limits) @@ -60,12 +61,34 @@ func createNamespace(ns string) { } // labelNamespace labels a namespace with provided labels -func labelNamespace(ns string, labels map[string]string) { - if len(labels) == 0 { +func labelNamespace(ns string, labels map[string]string, authoritative bool) { + var nsLabels map[string]string + + args := []string{"label", "--overwrite", "namespace/" + ns, flags.getKubeDryRunFlag("label")} + + if authoritative { + cmdGetLabels := kubectl([]string{"get", "namespace", ns, "-o", "jsonpath='{.metadata.labels}'"}, "Getting namespace [ "+ns+" ] current labels") + res, err := cmdGetLabels.Exec() + if err != nil { + log.Error(fmt.Sprintf("Could not get namespace [ %s ] labels. Error message: %v", ns, err)) + } + if err := json.Unmarshal([]byte(strings.Trim(res.output, `'`)), &nsLabels); err != nil { + log.Fatal(fmt.Sprintf("failed to unmarshal kubectl get namespace labels output: %s, ended with error: %s", res.output, err)) + } + // ignore default k8s namespace label from being removed + delete(nsLabels, "kubernetes.io/metadata.name") + // ignore every label defined in DSF for the namespace from being removed + for definedLabelKey, _ := range labels { + delete(nsLabels, definedLabelKey) + } + for label, _ := range nsLabels { + args = append(args, label+"-") + } + } + if len(labels) == 0 && len(nsLabels) == 0 { return } - args := []string{"label", "--overwrite", "namespace/" + ns, flags.getKubeDryRunFlag("label")} for k, v := range labels { args = append(args, k+"="+v) } diff --git a/internal/app/plan.go b/internal/app/plan.go index 077709d1..1c8e6757 100644 --- a/internal/app/plan.go +++ b/internal/app/plan.go @@ -16,7 +16,7 @@ type decisionType int const ( create decisionType = iota + 1 change - delete + remove noop ignored ) @@ -219,7 +219,7 @@ func (p *plan) print() { for _, decision := range p.Decisions { if decision.Type == ignored || decision.Type == noop { log.Info(decision.Description + " -- priority: " + strconv.Itoa(decision.Priority)) - } else if decision.Type == delete { + } else if decision.Type == remove { log.Warning(decision.Description + " -- priority: " + strconv.Itoa(decision.Priority)) } else { log.Notice(decision.Description + " -- priority: " + strconv.Itoa(decision.Priority)) diff --git a/internal/app/state.go b/internal/app/state.go index 29a06869..4440d93b 100644 --- a/internal/app/state.go +++ b/internal/app/state.go @@ -12,22 +12,23 @@ import ( // config type represents the settings fields type config struct { - KubeContext string `yaml:"kubeContext"` - Username string `yaml:"username"` - Password string `yaml:"password"` - ClusterURI string `yaml:"clusterURI"` - ServiceAccount string `yaml:"serviceAccount"` - StorageBackend string `yaml:"storageBackend"` - SlackWebhook string `yaml:"slackWebhook"` - MSTeamsWebhook string `yaml:"msTeamsWebhook"` - ReverseDelete bool `yaml:"reverseDelete"` - BearerToken bool `yaml:"bearerToken"` - BearerTokenPath string `yaml:"bearerTokenPath"` - EyamlEnabled bool `yaml:"eyamlEnabled"` - EyamlPrivateKeyPath string `yaml:"eyamlPrivateKeyPath"` - EyamlPublicKeyPath string `yaml:"eyamlPublicKeyPath"` - GlobalHooks map[string]interface{} `yaml:"globalHooks"` - GlobalMaxHistory int `yaml:"globalMaxHistory"` + KubeContext string `yaml:"kubeContext"` + Username string `yaml:"username"` + Password string `yaml:"password"` + ClusterURI string `yaml:"clusterURI"` + ServiceAccount string `yaml:"serviceAccount"` + StorageBackend string `yaml:"storageBackend"` + SlackWebhook string `yaml:"slackWebhook"` + MSTeamsWebhook string `yaml:"msTeamsWebhook"` + ReverseDelete bool `yaml:"reverseDelete"` + BearerToken bool `yaml:"bearerToken"` + BearerTokenPath string `yaml:"bearerTokenPath"` + NamespaceLabelsAuthoritative bool `yaml:"namespaceLabelsAuthoritative"` + EyamlEnabled bool `yaml:"eyamlEnabled"` + EyamlPrivateKeyPath string `yaml:"eyamlPrivateKeyPath"` + EyamlPublicKeyPath string `yaml:"eyamlPublicKeyPath"` + GlobalHooks map[string]interface{} `yaml:"globalHooks"` + GlobalMaxHistory int `yaml:"globalMaxHistory"` } // state type represents the desired state of applications on a k8s cluster.