From 562003e3fcb18e8d586951053948e9318b9979e9 Mon Sep 17 00:00:00 2001 From: Joshua Knarr Date: Fri, 25 May 2018 15:56:49 -0400 Subject: [PATCH 1/5] trying to enable environmental parsing across the board in helmsman --- state.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/state.go b/state.go index 75a079ef..a9eb92b8 100644 --- a/state.go +++ b/state.go @@ -159,8 +159,8 @@ func (s state) validate() (bool, string) { // if the env variable is empty or unset, an empty string is returned // if the string does not start with '$', it is returned as is. func subsituteEnv(name string) string { - if strings.HasPrefix(name, "$") { - return os.Getenv(strings.SplitAfterN(name, "$", 2)[1]) + if strings.Contains(name, "$") { + return os.ExpandEnv(name) } return name } From c83a21b4e12513f7d8b7a974a53f41709c572d13 Mon Sep 17 00:00:00 2001 From: Joshua Knarr Date: Sat, 26 May 2018 08:12:37 -0400 Subject: [PATCH 2/5] why not expand them all the time? --- state.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/state.go b/state.go index a9eb92b8..c486791b 100644 --- a/state.go +++ b/state.go @@ -155,14 +155,9 @@ func (s state) validate() (bool, string) { return true, "" } -// substitueEnv checks if a string is an env variable (starts with '$'), then it returns its value -// if the env variable is empty or unset, an empty string is returned -// if the string does not start with '$', it is returned as is. +// expand the environment variables if present. Useful for pipelines and gitlab runners. func subsituteEnv(name string) string { - if strings.Contains(name, "$") { - return os.ExpandEnv(name) - } - return name + return os.ExpandEnv(name) } // isValidCert checks if a certificate/key path/URI is valid From 8d6f68cce6a147514a75491d3cdf7912591d575a Mon Sep 17 00:00:00 2001 From: Joshua Knarr Date: Sun, 27 May 2018 19:03:13 -0400 Subject: [PATCH 3/5] fixes for naemspaces --- state.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/state.go b/state.go index c486791b..d4ee6210 100644 --- a/state.go +++ b/state.go @@ -97,7 +97,7 @@ func (s state) validate() (bool, string) { for k, v := range s.Namespaces { if !v.InstallTiller && k != "kube-system" { - log.Println("INFO: naemspace validation -- Tiller is not desired to be deployed in namespace [ " + k + " ].") + log.Println("INFO: namespace validation -- Tiller is not desired to be deployed in namespace [ " + k + " ].") } else { if tillerTLSEnabled(k) { // validating the TLS certs and keys for Tiller From 31a808e92ad608c234b6807fc3bba2775c2bb2ed Mon Sep 17 00:00:00 2001 From: Joshua Knarr Date: Tue, 29 May 2018 10:47:58 -0400 Subject: [PATCH 4/5] Thanks to Dave for the fixups --- .utils.go.swp | Bin 0 -> 16384 bytes decision_maker.go | 428 +++++++++++++++++++++++----------------------- release.go | 127 +++++++------- state.go | 346 ++++++++++++++++++------------------- utils.go | 292 +++++++++++++++---------------- 5 files changed, 586 insertions(+), 607 deletions(-) create mode 100644 .utils.go.swp diff --git a/.utils.go.swp b/.utils.go.swp new file mode 100644 index 0000000000000000000000000000000000000000..764c0c5349a48e115d957e3b1c188e915e93490a GIT binary patch literal 16384 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 C{JvNK literal 0 HcmV?d00001 diff --git a/decision_maker.go b/decision_maker.go index a8cfe3e9..60765d62 100644 --- a/decision_maker.go +++ b/decision_maker.go @@ -1,7 +1,8 @@ package main import ( - "strings" + "os" + "strings" ) var outcome plan @@ -9,112 +10,112 @@ var releases string // makePlan creates a plan of the actions needed to make the desired state come true. func makePlan(s *state) *plan { - outcome = createPlan() - buildState() + outcome = createPlan() + buildState() - for _, r := range s.Apps { - decide(r, s) - } + for _, r := range s.Apps { + decide(r, s) + } - return &outcome + return &outcome } // decide makes a decision about what commands (actions) need to be executed // to make a release section of the desired state come true. func decide(r *release, s *state) { - // check for deletion - if !r.Enabled { - if !isProtected(r) { - inspectDeleteScenario(getDesiredNamespace(r), r) - } else { - logDecision("DECISION: release "+r.Name+" is PROTECTED. Operations are not allowed on this release until "+ - "you remove its protection.", r.Priority) - } + // check for deletion + if !r.Enabled { + if !isProtected(r) { + inspectDeleteScenario(getDesiredNamespace(r), r) + } else { + logDecision("DECISION: release "+r.Name+" is PROTECTED. Operations are not allowed on this release until "+ + "you remove its protection.", r.Priority) + } - } else { // check for install/upgrade/rollback - if helmReleaseExists(getDesiredNamespace(r), r.Name, "deployed") { - if !isProtected(r) { - inspectUpgradeScenario(getDesiredNamespace(r), r) // upgrade - } else { - logDecision("DECISION: release "+r.Name+" is PROTECTED. Operations are not allowed on this release until "+ - "you remove its protection.", r.Priority) - } + } else { // check for install/upgrade/rollback + if helmReleaseExists(getDesiredNamespace(r), r.Name, "deployed") { + if !isProtected(r) { + inspectUpgradeScenario(getDesiredNamespace(r), r) // upgrade + } else { + logDecision("DECISION: release "+r.Name+" is PROTECTED. Operations are not allowed on this release until "+ + "you remove its protection.", r.Priority) + } - } else if helmReleaseExists("", r.Name, "deleted") { - if !isProtected(r) { + } else if helmReleaseExists("", r.Name, "deleted") { + if !isProtected(r) { - rollbackRelease(getDesiredNamespace(r), r) // rollback + rollbackRelease(getDesiredNamespace(r), r) // rollback - } else { - logDecision("DECISION: release "+r.Name+" is PROTECTED. Operations are not allowed on this release until "+ - "you remove its protection.", r.Priority) - } + } else { + logDecision("DECISION: release "+r.Name+" is PROTECTED. Operations are not allowed on this release until "+ + "you remove its protection.", r.Priority) + } - } else if helmReleaseExists("", r.Name, "failed") { + } else if helmReleaseExists("", r.Name, "failed") { - if !isProtected(r) { + if !isProtected(r) { - reInstallRelease(getDesiredNamespace(r), r) // re-install failed release + reInstallRelease(getDesiredNamespace(r), r) // re-install failed release - } else { - logDecision("DECISION: release "+r.Name+" is PROTECTED. Operations are not allowed on this release until "+ - "you remove its protection.", r.Priority) - } + } else { + logDecision("DECISION: release "+r.Name+" is PROTECTED. Operations are not allowed on this release until "+ + "you remove its protection.", r.Priority) + } - } else if helmReleaseExists("", r.Name, "") { // not deployed in the desired namespace but deployed elsewhere + } else if helmReleaseExists("", r.Name, "") { // not deployed in the desired namespace but deployed elsewhere - if !isProtected(r) { + if !isProtected(r) { - reInstallRelease(getDesiredNamespace(r), r) // move the release to a new (the desired) namespace - logDecision("WARNING: moving release [ "+r.Name+" ] from [[ "+getReleaseNamespace(r.Name)+" ]] to [[ "+getDesiredNamespace(r)+ - " ]] might not correctly connect to existing volumes. Check https://github.com/Praqma/helmsman/blob/master/docs/how_to/move_charts_across_namespaces.md"+ - " for details if this release uses PV and PVC.", r.Priority) + reInstallRelease(getDesiredNamespace(r), r) // move the release to a new (the desired) namespace + logDecision("WARNING: moving release [ "+r.Name+" ] from [[ "+getReleaseNamespace(r.Name)+" ]] to [[ "+getDesiredNamespace(r)+ + " ]] might not correctly connect to existing volumes. Check https://github.com/Praqma/helmsman/blob/master/docs/how_to/move_charts_across_namespaces.md"+ + " for details if this release uses PV and PVC.", r.Priority) - } else { - logDecision("DECISION: release "+r.Name+" is PROTECTED. Operations are not allowed on this release until "+ - "you remove its protection.", r.Priority) - } + } else { + logDecision("DECISION: release "+r.Name+" is PROTECTED. Operations are not allowed on this release until "+ + "you remove its protection.", r.Priority) + } - } else { + } else { - installRelease(getDesiredNamespace(r), r) // install a new release + installRelease(getDesiredNamespace(r), r) // install a new release - } + } - } + } } // testRelease creates a Helm command to test a particular release. func testRelease(r *release) { - cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm test " + r.Name + getDesiredTillerNamespace(r) + getTLSFlags(r)}, - Description: "running tests for release [ " + r.Name + " ]", - } - outcome.addCommand(cmd, r.Priority) - logDecision("DECISION: release [ "+r.Name+" ] is required to be tested when installed. Got it!", r.Priority) + cmd := command{ + Cmd: "bash", + Args: []string{"-c", "helm test " + r.Name + getDesiredTillerNamespace(r) + getTLSFlags(r)}, + Description: "running tests for release [ " + r.Name + " ]", + } + outcome.addCommand(cmd, r.Priority) + logDecision("DECISION: release [ "+r.Name+" ] is required to be tested when installed. Got it!", r.Priority) } // installRelease creates a Helm command to install a particular release in a particular namespace. func installRelease(namespace string, r *release) { - releaseName := r.Name - cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm install " + r.Chart + " -n " + releaseName + " --namespace " + namespace + getValuesFile(r) + " --version " + r.Version + getSetValues(r) + getWait(r) + getDesiredTillerNamespace(r) + getTLSFlags(r)}, - Description: "installing release [ " + releaseName + " ] in namespace [[ " + namespace + " ]]", - } - outcome.addCommand(cmd, r.Priority) - logDecision("DECISION: release [ "+releaseName+" ] is not present in the current k8s context. Will install it in namespace [[ "+ - namespace+" ]]", r.Priority) - - if r.Test { - testRelease(r) - } + releaseName := r.Name + cmd := command{ + Cmd: "bash", + Args: []string{"-c", "helm install " + r.Chart + " -n " + releaseName + " --namespace " + namespace + getValuesFile(r) + " --version " + r.Version + getSetValues(r) + getWait(r) + getDesiredTillerNamespace(r) + getTLSFlags(r)}, + Description: "installing release [ " + releaseName + " ] in namespace [[ " + namespace + " ]]", + } + outcome.addCommand(cmd, r.Priority) + logDecision("DECISION: release [ "+releaseName+" ] is not present in the current k8s context. Will install it in namespace [[ "+ + namespace+" ]]", r.Priority) + + if r.Test { + testRelease(r) + } } // rollbackRelease evaluates if a rollback action needs to be taken for a given release. @@ -122,29 +123,29 @@ func installRelease(namespace string, r *release) { // it purge deletes it and create it in the spcified namespace. func rollbackRelease(namespace string, r *release) { - releaseName := r.Name - if getReleaseNamespace(r.Name) == namespace { - - cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm rollback " + releaseName + " " + getReleaseRevision(releaseName, "deleted") + getWait(r) + getDesiredTillerNamespace(r) + getTLSFlags(r)}, - Description: "rolling back release [ " + releaseName + " ]", - } - outcome.addCommand(cmd, r.Priority) - upgradeRelease(r) - logDecision("DECISION: release [ "+releaseName+" ] is currently deleted and is desired to be rolledback to "+ - "namespace [[ "+namespace+" ]] . It will also be upgraded in case values have changed.", r.Priority) - - } else { - - reInstallRelease(namespace, r) - logDecision("DECISION: release [ "+releaseName+" ] is deleted BUT from namespace [[ "+getReleaseNamespace(releaseName)+ - " ]]. Will purge delete it from there and install it in namespace [[ "+namespace+" ]]", r.Priority) - logDecision("WARNING: rolling back release [ "+releaseName+" ] from [[ "+getReleaseNamespace(releaseName)+" ]] to [[ "+namespace+ - " ]] might not correctly connect to existing volumes. Check https://github.com/Praqma/helmsman/blob/master/docs/how_to/move_charts_across_namespaces.md"+ - " for details if this release uses PV and PVC.", r.Priority) - - } + releaseName := r.Name + if getReleaseNamespace(r.Name) == namespace { + + cmd := command{ + Cmd: "bash", + Args: []string{"-c", "helm rollback " + releaseName + " " + getReleaseRevision(releaseName, "deleted") + getWait(r) + getDesiredTillerNamespace(r) + getTLSFlags(r)}, + Description: "rolling back release [ " + releaseName + " ]", + } + outcome.addCommand(cmd, r.Priority) + upgradeRelease(r) + logDecision("DECISION: release [ "+releaseName+" ] is currently deleted and is desired to be rolledback to "+ + "namespace [[ "+namespace+" ]] . It will also be upgraded in case values have changed.", r.Priority) + + } else { + + reInstallRelease(namespace, r) + logDecision("DECISION: release [ "+releaseName+" ] is deleted BUT from namespace [[ "+getReleaseNamespace(releaseName)+ + " ]]. Will purge delete it from there and install it in namespace [[ "+namespace+" ]]", r.Priority) + logDecision("WARNING: rolling back release [ "+releaseName+" ] from [[ "+getReleaseNamespace(releaseName)+" ]] to [[ "+namespace+ + " ]] might not correctly connect to existing volumes. Check https://github.com/Praqma/helmsman/blob/master/docs/how_to/move_charts_across_namespaces.md"+ + " for details if this release uses PV and PVC.", r.Priority) + + } } // inspectDeleteScenario evaluates if a delete action needs to be taken for a given release. @@ -152,33 +153,33 @@ func rollbackRelease(namespace string, r *release) { // If the release is not deployed, it will be skipped. func inspectDeleteScenario(namespace string, r *release) { - releaseName := r.Name - //if it exists in helm list , add command to delete it, else log that it is skipped - if helmReleaseExists(namespace, releaseName, "deployed") { - // delete it - deleteRelease(r) + releaseName := r.Name + //if it exists in helm list , add command to delete it, else log that it is skipped + if helmReleaseExists(namespace, releaseName, "deployed") { + // delete it + deleteRelease(r) - } else { - logDecision("DECISION: release [ "+releaseName+" ] is set to be disabled but is not yet deployed. Skipping.", r.Priority) - } + } else { + logDecision("DECISION: release [ "+releaseName+" ] is set to be disabled but is not yet deployed. Skipping.", r.Priority) + } } // deleteRelease deletes a release from a k8s cluster func deleteRelease(r *release) { - p := "" - purgeDesc := "" - if r.Purge { - p = "--purge" - purgeDesc = "and purged!" - } - - cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm delete " + p + " " + r.Name + getCurrentTillerNamespace(r) + getTLSFlags(r)}, - Description: "deleting release [ " + r.Name + " ]", - } - outcome.addCommand(cmd, r.Priority) - logDecision("DECISION: release [ "+r.Name+" ] is desired to be deleted "+purgeDesc+". Planing this for you!", r.Priority) + p := "" + purgeDesc := "" + if r.Purge { + p = "--purge" + purgeDesc = "and purged!" + } + + cmd := command{ + Cmd: "bash", + Args: []string{"-c", "helm delete " + p + " " + r.Name + getCurrentTillerNamespace(r) + getTLSFlags(r)}, + Description: "deleting release [ " + r.Name + " ]", + } + outcome.addCommand(cmd, r.Priority) + logDecision("DECISION: release [ "+r.Name+" ] is desired to be deleted "+purgeDesc+". Planing this for you!", r.Priority) } // inspectUpgradeScenario evaluates if a release should be upgraded. @@ -190,65 +191,65 @@ func deleteRelease(r *release) { // it will be purge deleted and installed in the new namespace. func inspectUpgradeScenario(namespace string, r *release) { - releaseName := r.Name - if getReleaseNamespace(releaseName) == namespace { - if extractChartName(r.Chart) == getReleaseChartName(releaseName) && r.Version != getReleaseChartVersion(releaseName) { - // upgrade - upgradeRelease(r) - logDecision("DECISION: release [ "+releaseName+" ] is desired to be upgraded. Planing this for you!", r.Priority) - - } else if extractChartName(r.Chart) != getReleaseChartName(releaseName) { - reInstallRelease(namespace, r) - logDecision("DECISION: release [ "+releaseName+" ] is desired to use a new Chart [ "+r.Chart+ - " ]. I am planning a purge delete of the current release and will install it with the new chart in namespace [[ "+ - namespace+" ]]", r.Priority) - - } else { - upgradeRelease(r) - logDecision("DECISION: release [ "+releaseName+" ] is desired to be enabled and is currently enabled."+ - "I will upgrade it in case you changed your values.yaml!", r.Priority) - } - } else { - reInstallRelease(namespace, r) - logDecision("DECISION: release [ "+releaseName+" ] is desired to be enabled in a new namespace [[ "+namespace+ - " ]]. I am planning a purge delete of the current release from namespace [[ "+getReleaseNamespace(releaseName)+" ]] "+ - "and will install it for you in namespace [[ "+namespace+" ]]", r.Priority) - logDecision("WARNING: moving release [ "+releaseName+" ] from [[ "+getReleaseNamespace(releaseName)+" ]] to [[ "+namespace+ - " ]] might not correctly connect to existing volumes. Check https://github.com/Praqma/helmsman/blob/master/docs/how_to/move_charts_across_namespaces.md"+ - " for details if this release uses PV and PVC.", r.Priority) - } + releaseName := r.Name + if getReleaseNamespace(releaseName) == namespace { + if extractChartName(r.Chart) == getReleaseChartName(releaseName) && r.Version != getReleaseChartVersion(releaseName) { + // upgrade + upgradeRelease(r) + logDecision("DECISION: release [ "+releaseName+" ] is desired to be upgraded. Planing this for you!", r.Priority) + + } else if extractChartName(r.Chart) != getReleaseChartName(releaseName) { + reInstallRelease(namespace, r) + logDecision("DECISION: release [ "+releaseName+" ] is desired to use a new Chart [ "+r.Chart+ + " ]. I am planning a purge delete of the current release and will install it with the new chart in namespace [[ "+ + namespace+" ]]", r.Priority) + + } else { + upgradeRelease(r) + logDecision("DECISION: release [ "+releaseName+" ] is desired to be enabled and is currently enabled."+ + "I will upgrade it in case you changed your values.yaml!", r.Priority) + } + } else { + reInstallRelease(namespace, r) + logDecision("DECISION: release [ "+releaseName+" ] is desired to be enabled in a new namespace [[ "+namespace+ + " ]]. I am planning a purge delete of the current release from namespace [[ "+getReleaseNamespace(releaseName)+" ]] "+ + "and will install it for you in namespace [[ "+namespace+" ]]", r.Priority) + logDecision("WARNING: moving release [ "+releaseName+" ] from [[ "+getReleaseNamespace(releaseName)+" ]] to [[ "+namespace+ + " ]] might not correctly connect to existing volumes. Check https://github.com/Praqma/helmsman/blob/master/docs/how_to/move_charts_across_namespaces.md"+ + " for details if this release uses PV and PVC.", r.Priority) + } } // upgradeRelease upgrades an existing release with the specified values.yaml func upgradeRelease(r *release) { - cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm upgrade " + r.Name + " " + r.Chart + getValuesFile(r) + " --version " + r.Version + " --force " + getSetValues(r) + getWait(r) + getDesiredTillerNamespace(r) + getTLSFlags(r)}, - Description: "upgrading release [ " + r.Name + " ]", - } + cmd := command{ + Cmd: "bash", + Args: []string{"-c", "helm upgrade " + r.Name + " " + r.Chart + getValuesFile(r) + " --version " + r.Version + " --force " + getSetValues(r) + getWait(r) + getDesiredTillerNamespace(r) + getTLSFlags(r)}, + Description: "upgrading release [ " + r.Name + " ]", + } - outcome.addCommand(cmd, r.Priority) + outcome.addCommand(cmd, r.Priority) } // reInstallRelease purge deletes a release and reinstalls it. // This is used when moving a release to another namespace or when changing the chart used for it. func reInstallRelease(namespace string, r *release) { - releaseName := r.Name - delCmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm delete --purge " + releaseName + getCurrentTillerNamespace(r) + getTLSFlags(r)}, - Description: "deleting release [ " + releaseName + " ]", - } - outcome.addCommand(delCmd, r.Priority) - - installCmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm install " + r.Chart + " --version " + r.Version + " -n " + releaseName + " --namespace " + namespace + getValuesFile(r) + getSetValues(r) + getWait(r) + getDesiredTillerNamespace(r) + getTLSFlags(r)}, - Description: "installing release [ " + releaseName + " ] in namespace [[ " + namespace + " ]]", - } - outcome.addCommand(installCmd, r.Priority) - logDecision("DECISION: release [ "+releaseName+" ] will be deleted from namespace [[ "+getReleaseNamespace(releaseName)+" ]] and reinstalled in [[ "+namespace+"]].", r.Priority) + releaseName := r.Name + delCmd := command{ + Cmd: "bash", + Args: []string{"-c", "helm delete --purge " + releaseName + getCurrentTillerNamespace(r) + getTLSFlags(r)}, + Description: "deleting release [ " + releaseName + " ]", + } + outcome.addCommand(delCmd, r.Priority) + + installCmd := command{ + Cmd: "bash", + Args: []string{"-c", "helm install " + r.Chart + " --version " + r.Version + " -n " + releaseName + " --namespace " + namespace + getValuesFile(r) + getSetValues(r) + getWait(r) + getDesiredTillerNamespace(r) + getTLSFlags(r)}, + Description: "installing release [ " + releaseName + " ] in namespace [[ " + namespace + " ]]", + } + outcome.addCommand(installCmd, r.Priority) + logDecision("DECISION: release [ "+releaseName+" ] will be deleted from namespace [[ "+getReleaseNamespace(releaseName)+" ]] and reinstalled in [[ "+namespace+"]].", r.Priority) } @@ -256,7 +257,7 @@ func reInstallRelease(namespace string, r *release) { // Depending on the debug flag being set or not, it will either log the the decision to output or not. func logDecision(decision string, priority int) { - outcome.addDecision(decision, priority) + outcome.addDecision(decision, priority) } @@ -264,49 +265,49 @@ func logDecision(decision string, priority int) { // example: it extracts "chartY" from "repoX/chartY" func extractChartName(releaseChart string) string { - return strings.TrimSpace(strings.Split(releaseChart, "/")[1]) + return strings.TrimSpace(strings.Split(releaseChart, "/")[1]) } // getValuesFile return partial install/upgrade release command to substitute the -f flag in Helm. func getValuesFile(r *release) string { - if r.ValuesFile != "" { - return " -f " + r.ValuesFile - } - return "" + if r.ValuesFile != "" { + return " -f " + r.ValuesFile + } + return "" } // getSetValues returns --set params to be used with helm install/upgrade commands func getSetValues(r *release) string { - result := "" - for k, v := range r.Set { - _, value := envVarExists(v) - result = result + " --set " + k + "=\"" + strings.Replace(value, ",", "\\,", -1) + "\"" - } - return result + result := "" + for k, v := range r.Set { + value := os.ExpandEnv(v) + result = result + " --set " + k + "=\"" + strings.Replace(value, ",", "\\,", -1) + "\"" + } + return result } // getWait returns a partial helm command containing the helm wait flag (--wait) if the wait flag for the release was set to true // Otherwise, retruns an empty string func getWait(r *release) string { - result := "" - if r.Wait { - result = " --wait" - } - return result + result := "" + if r.Wait { + result = " --wait" + } + return result } // getDesiredNamespace returns the namespace of a release func getDesiredNamespace(r *release) string { - return r.Namespace + return r.Namespace } // getCurrentNamespaceProtection returns the protection state for the namespace where a release is currently installed. // It returns true if a namespace is defined as protected in the desired state file, false otherwise. func getCurrentNamespaceProtection(r *release) bool { - return s.Namespaces[getReleaseNamespace(r.Name)].Protected + return s.Namespaces[getReleaseNamespace(r.Name)].Protected } // isProtected checks if a release is protected or not. @@ -315,57 +316,58 @@ func getCurrentNamespaceProtection(r *release) bool { // returns true if a release is protected, false otherwise func isProtected(r *release) bool { - // if the release does not exist in the cluster, it is not protected - if !helmReleaseExists("", r.Name, "") { - return false - } + // if the release does not exist in the cluster, it is not protected + if !helmReleaseExists("", r.Name, "") { + return false + } - if getCurrentNamespaceProtection(r) { - return true - } + if getCurrentNamespaceProtection(r) { + return true + } - if r.Protected { - return true - } + if r.Protected { + return true + } - return false + return false } // getDesiredTillerNamespace returns a tiller-namespace flag with which a release is desired to be maintained func getDesiredTillerNamespace(r *release) string { - if s.Namespaces[r.Namespace].InstallTiller { - return " --tiller-namespace " + r.Namespace - } + if s.Namespaces[r.Namespace].InstallTiller { + return " --tiller-namespace " + r.Namespace + } - return "" // same as return " --tiller-namespace kube-system" + return "" // same as return " --tiller-namespace kube-system" } // getCurrentTillerNamespace returns the tiller-namespace with which a release is currently maintained func getCurrentTillerNamespace(r *release) string { - if v, ok := currentState[r.Name]; ok { - return " --tiller-namespace " + v.TillerNamespace - } - return "" + if v, ok := currentState[r.Name]; ok { + return " --tiller-namespace " + v.TillerNamespace + } + return "" } // getTLSFlags returns TLS flags with which a release is maintained // If the release where the namespace is to be deployed has Tiller deployed, the TLS flages will use certs/keys for that namespace (if any) // otherwise, it will be the certs/keys for the kube-system namespace. func getTLSFlags(r *release) string { - tls := "" - if s.Namespaces[r.Namespace].InstallTiller { - if tillerTLSEnabled(r.Namespace) { + tls := "" + if s.Namespaces[r.Namespace].InstallTiller { + if tillerTLSEnabled(r.Namespace) { - tls = " --tls --tls-ca-cert " + r.Namespace + "-ca.cert --tls-cert " + r.Namespace + "-client.cert --tls-key " + r.Namespace + "-client.key " - } - } else { - if tillerTLSEnabled("kube-system") { + tls = " --tls --tls-ca-cert " + r.Namespace + "-ca.cert --tls-cert " + r.Namespace + "-client.cert --tls-key " + r.Namespace + "-client.key " + } + } else { + if tillerTLSEnabled("kube-system") { - tls = " --tls --tls-ca-cert kube-system-ca.cert --tls-cert kube-system-client.cert --tls-key kube-system-client.key " - } - } + tls = " --tls --tls-ca-cert kube-system-ca.cert --tls-cert kube-system-client.cert --tls-key kube-system-client.key " + } + } - return tls + return tls } + diff --git a/release.go b/release.go index 5052f6a6..757518bb 100644 --- a/release.go +++ b/release.go @@ -1,93 +1,86 @@ package main import ( - "fmt" - "log" - "os" - "strings" + "fmt" + "log" + "os" + "strings" ) // release type representing Helm releases which are described in the desired state type release struct { - Name string - Description string - Namespace string - Enabled bool - Chart string - Version string - ValuesFile string - Purge bool - Test bool - Protected bool - Wait bool - Priority int - Set map[string]string + Name string + Description string + Namespace string + Enabled bool + Chart string + Version string + ValuesFile string + Purge bool + Test bool + Protected bool + Wait bool + Priority int + Set map[string]string } // validateRelease validates if a release inside a desired state meets the specifications or not. // check the full specification @ https://github.com/Praqma/helmsman/docs/desired_state_spec.md func validateRelease(r *release, names map[string]bool, s state) (bool, string) { - _, err := os.Stat(r.ValuesFile) - if r.Name == "" || names[r.Name] { - return false, "release name can't be empty and must be unique." - } else if nsOverride == "" && r.Namespace == "" { - return false, "release targeted namespace can't be empty." - } else if nsOverride == "" && r.Namespace != "" && !checkNamespaceDefined(r.Namespace, s) { - return false, "release " + r.Name + " is using namespace [ " + r.Namespace + " ] which is not defined in the Namespaces section of your desired state file." + - " Release [ " + r.Name + " ] can't be installed in that Namespace until its defined." - } else if r.Chart == "" || !strings.ContainsAny(r.Chart, "/") { - return false, "chart can't be empty and must be of the format: repo/chart." - } else if r.Version == "" { - return false, "version can't be empty." - } else if r.ValuesFile != "" && (!isOfType(r.ValuesFile, ".yaml") || err != nil) { - return false, "valuesFile must be a valid file path for a yaml file, Or can be left empty." - } else if len(r.Set) > 0 { - for k, v := range r.Set { - if !strings.HasPrefix(v, "$") { - return false, "the value for variable [ " + k + " ] must be an environment variable name and start with '$'." - } else if ok, _ := envVarExists(v); !ok { - return false, "env variable [ " + v + " ] is not found in the environment." - } - } - } else if r.Priority != 0 && r.Priority > 0 { - return false, "priority can only be 0 or negative value, positive values are not allowed." - } + _, err := os.Stat(r.ValuesFile) + if r.Name == "" || names[r.Name] { + return false, "release name can't be empty and must be unique." + } else if nsOverride == "" && r.Namespace == "" { + return false, "release targeted namespace can't be empty." + } else if nsOverride == "" && r.Namespace != "" && !checkNamespaceDefined(r.Namespace, s) { + return false, "release " + r.Name + " is using namespace [ " + r.Namespace + " ] which is not defined in the Namespaces section of your desired state file." + + " Release [ " + r.Name + " ] can't be installed in that Namespace until its defined." + } else if r.Chart == "" || !strings.ContainsAny(r.Chart, "/") { + return false, "chart can't be empty and must be of the format: repo/chart." + } else if r.Version == "" { + return false, "version can't be empty." + } else if r.ValuesFile != "" && (!isOfType(r.ValuesFile, ".yaml") || err != nil) { + return false, "valuesFile must be a valid file path for a yaml file, Or can be left empty." + } else if r.Priority != 0 && r.Priority > 0 { + return false, "priority can only be 0 or negative value, positive values are not allowed." + } - names[r.Name] = true - return true, "" + names[r.Name] = true + return true, "" } // checkNamespaceDefined checks if a given namespace is defined in the namespaces section of the desired state file func checkNamespaceDefined(ns string, s state) bool { - _, ok := s.Namespaces[ns] - if !ok { - return false - } - return true + _, ok := s.Namespaces[ns] + if !ok { + return false + } + return true } // overrideNamespace overrides a release defined namespace with a new given one func overrideNamespace(r *release, newNs string) { - log.Println("INFO: overriding namespace for app: " + r.Name) - r.Namespace = newNs + log.Println("INFO: overriding namespace for app: " + r.Name) + r.Namespace = newNs } // print prints the details of the release func (r release) print() { - fmt.Println("") - fmt.Println("\tname : ", r.Name) - fmt.Println("\tdescription : ", r.Description) - fmt.Println("\tnamespace : ", r.Namespace) - fmt.Println("\tenabled : ", r.Enabled) - fmt.Println("\tchart : ", r.Chart) - fmt.Println("\tversion : ", r.Version) - fmt.Println("\tvaluesFile : ", r.ValuesFile) - fmt.Println("\tpurge : ", r.Purge) - fmt.Println("\ttest : ", r.Test) - fmt.Println("\tprotected : ", r.Protected) - fmt.Println("\twait : ", r.Wait) - fmt.Println("\tpriority : ", r.Priority) - fmt.Println("\tvalues to override from env:") - printMap(r.Set) - fmt.Println("------------------- ") + fmt.Println("") + fmt.Println("\tname : ", r.Name) + fmt.Println("\tdescription : ", r.Description) + fmt.Println("\tnamespace : ", r.Namespace) + fmt.Println("\tenabled : ", r.Enabled) + fmt.Println("\tchart : ", r.Chart) + fmt.Println("\tversion : ", r.Version) + fmt.Println("\tvaluesFile : ", r.ValuesFile) + fmt.Println("\tpurge : ", r.Purge) + fmt.Println("\ttest : ", r.Test) + fmt.Println("\tprotected : ", r.Protected) + fmt.Println("\twait : ", r.Wait) + fmt.Println("\tpriority : ", r.Priority) + fmt.Println("\tvalues to override from env:") + printMap(r.Set) + fmt.Println("------------------- ") } + diff --git a/state.go b/state.go index d4ee6210..404281aa 100644 --- a/state.go +++ b/state.go @@ -1,208 +1,204 @@ package main import ( - "fmt" - "log" - "net/url" - "os" - "strings" + "fmt" + "log" + "net/url" + "os" + "strings" ) // namespace type represents the fields of a namespace type namespace struct { - Protected bool - InstallTiller bool - TillerServiceAccount string - CaCert string - TillerCert string - TillerKey string - ClientCert string - ClientKey string + Protected bool + InstallTiller bool + TillerServiceAccount string + CaCert string + TillerCert string + TillerKey string + ClientCert string + ClientKey string } // state type represents the desired state of applications on a k8s cluster. type state struct { - Metadata map[string]string - Certificates map[string]string - Settings map[string]string - Namespaces map[string]namespace - HelmRepos map[string]string - Apps map[string]*release + Metadata map[string]string + Certificates map[string]string + Settings map[string]string + Namespaces map[string]namespace + HelmRepos map[string]string + Apps map[string]*release } // validate validates that the values specified in the desired state are valid according to the desired state spec. // check https://github.com/Praqma/Helmsman/docs/desired_state_spec.md for the detailed specification func (s state) validate() (bool, string) { - // settings - if s.Settings == nil || len(s.Settings) == 0 { - return false, "ERROR: settings validation failed -- no settings table provided in TOML." - } else if value, ok := s.Settings["kubeContext"]; !ok || value == "" { - return false, "ERROR: settings validation failed -- you have not provided a " + - "kubeContext to use. Can't work without it. Sorry!" - } else if value, ok = s.Settings["clusterURI"]; ok { - - s.Settings["clusterURI"] = subsituteEnv(value) - if _, err := url.ParseRequestURI(s.Settings["clusterURI"]); err != nil { - return false, "ERROR: settings validation failed -- clusterURI must have a valid URL set in an env varibale or passed directly. Either the env var is missing/empty or the URL is invalid." - } - - if _, ok = s.Settings["username"]; !ok { - return false, "ERROR: settings validation failed -- username must be provided if clusterURI is defined." - } - if value, ok = s.Settings["password"]; ok { - s.Settings["password"] = subsituteEnv(value) - } else { - return false, "ERROR: settings validation failed -- password must be provided if clusterURI is defined." - } - - if s.Settings["password"] == "" { - return false, "ERROR: settings validation failed -- password should be set as an env variable. It is currently missing or empty. " - } - } - - // certificates - if s.Certificates != nil && len(s.Certificates) != 0 { - _, ok1 := s.Settings["clusterURI"] - _, ok2 := s.Certificates["caCrt"] - _, ok3 := s.Certificates["caKey"] - if ok1 && (!ok2 || !ok3) { - return false, "ERROR: certifications validation failed -- You want me to connect to your cluster for you " + - "but have not given me the cert/key to do so. Please add [caCrt] and [caKey] under Certifications. You might also need to provide [clientCrt]." - } else if ok1 { - for key, value := range s.Certificates { - r, path := isValidCert(value) - if !r { - return false, "ERROR: certifications validation failed -- [ " + key + " ] must be a valid S3 or GCS bucket URL or a valid relative file path." - } - s.Certificates[key] = path - } - } else { - log.Println("INFO: certificates provided but not needed. Skipping certificates validation.") - } - - } else { - if _, ok := s.Settings["clusterURI"]; ok { - return false, "ERROR: certifications validation failed -- You want me to connect to your cluster for you " + - "but have not given me the cert/key to do so. Please add [caCrt] and [caKey] under Certifications. You might also need to provide [clientCrt]." - } - } - - // namespaces - if nsOverride == "" { - if s.Namespaces == nil || len(s.Namespaces) == 0 { - return false, "ERROR: namespaces validation failed -- I need at least one namespace " + - "to work with!" - } - - for k, v := range s.Namespaces { - if !v.InstallTiller && k != "kube-system" { - log.Println("INFO: namespace validation -- Tiller is not desired to be deployed in namespace [ " + k + " ].") - } else { - if tillerTLSEnabled(k) { - // validating the TLS certs and keys for Tiller - // if they are valid, their values (if they are env vars) are substituted - var ok1, ok2, ok3, ok4, ok5 bool - ok1, v.CaCert = isValidCert(v.CaCert) - ok2, v.ClientCert = isValidCert(v.ClientCert) - ok3, v.ClientKey = isValidCert(v.ClientKey) - ok4, v.TillerCert = isValidCert(v.TillerCert) - ok5, v.TillerKey = isValidCert(v.TillerKey) - if !ok1 || !ok2 || !ok3 || !ok4 || !ok5 { - return false, "ERROR: namespaces validation failed -- some certs/keys are not valid for Tiller TLS in namespace [ " + k + " ]." - } - log.Println("INFO: namespace validation -- Tiller is desired to be deployed with TLS in namespace [ " + k + " ]. ") - } else { - log.Println("INFO: namespace validation -- Tiller is desired to be deployed WITHOUT TLS in namespace [ " + k + " ]. ") - } - } - } - } else { - log.Println("INFO: ns-override is used to override all namespaces with [ " + nsOverride + " ] Skipping defined namespaces validation.") - } - - // repos - if s.HelmRepos == nil || len(s.HelmRepos) == 0 { - return false, "ERROR: repos validation failed -- I need at least one helm repo " + - "to work with!" - } - for k, v := range s.HelmRepos { - _, err := url.ParseRequestURI(v) - if err != nil { - return false, "ERROR: repos validation failed -- repo [" + k + " ] " + - "must have a valid URL." - } - - continue - - } - - // apps - if s.Apps == nil { - log.Println("INFO: You have not specified any apps. I have nothing to do. ", - "Horraayyy!.") - os.Exit(0) - } - - names := make(map[string]bool) - for appLabel, r := range s.Apps { - result, errMsg := validateRelease(r, names, s) - if !result { - return false, "ERROR: apps validation failed -- for app [" + appLabel + " ]. " + errMsg - } - } - - return true, "" -} - -// expand the environment variables if present. Useful for pipelines and gitlab runners. -func subsituteEnv(name string) string { - return os.ExpandEnv(name) + // settings + if s.Settings == nil || len(s.Settings) == 0 { + return false, "ERROR: settings validation failed -- no settings table provided in TOML." + } else if value, ok := s.Settings["kubeContext"]; !ok || value == "" { + return false, "ERROR: settings validation failed -- you have not provided a " + + "kubeContext to use. Can't work without it. Sorry!" + } else if value, ok = s.Settings["clusterURI"]; ok { + + s.Settings["clusterURI"] = os.ExpandEnv(value) + if _, err := url.ParseRequestURI(s.Settings["clusterURI"]); err != nil { + return false, "ERROR: settings validation failed -- clusterURI must have a valid URL set in an env varibale or passed directly. Either the env var is missing/empty or the URL is invalid." + } + + if _, ok = s.Settings["username"]; !ok { + return false, "ERROR: settings validation failed -- username must be provided if clusterURI is defined." + } + if value, ok = s.Settings["password"]; ok { + s.Settings["password"] = os.ExpandEnv(value) + } else { + return false, "ERROR: settings validation failed -- password must be provided if clusterURI is defined." + } + + if s.Settings["password"] == "" { + return false, "ERROR: settings validation failed -- password should be set as an env variable. It is currently missing or empty. " + } + } + + // certificates + if s.Certificates != nil && len(s.Certificates) != 0 { + _, ok1 := s.Settings["clusterURI"] + _, ok2 := s.Certificates["caCrt"] + _, ok3 := s.Certificates["caKey"] + if ok1 && (!ok2 || !ok3) { + return false, "ERROR: certifications validation failed -- You want me to connect to your cluster for you " + + "but have not given me the cert/key to do so. Please add [caCrt] and [caKey] under Certifications. You might also need to provide [clientCrt]." + } else if ok1 { + for key, value := range s.Certificates { + r, path := isValidCert(value) + if !r { + return false, "ERROR: certifications validation failed -- [ " + key + " ] must be a valid S3 or GCS bucket URL or a valid relative file path." + } + s.Certificates[key] = path + } + } else { + log.Println("INFO: certificates provided but not needed. Skipping certificates validation.") + } + + } else { + if _, ok := s.Settings["clusterURI"]; ok { + return false, "ERROR: certifications validation failed -- You want me to connect to your cluster for you " + + "but have not given me the cert/key to do so. Please add [caCrt] and [caKey] under Certifications. You might also need to provide [clientCrt]." + } + } + + // namespaces + if nsOverride == "" { + if s.Namespaces == nil || len(s.Namespaces) == 0 { + return false, "ERROR: namespaces validation failed -- I need at least one namespace " + + "to work with!" + } + + for k, v := range s.Namespaces { + if !v.InstallTiller && k != "kube-system" { + log.Println("INFO: naemspace validation -- Tiller is not desired to be deployed in namespace [ " + k + " ].") + } else { + if tillerTLSEnabled(k) { + // validating the TLS certs and keys for Tiller + // if they are valid, their values (if they are env vars) are substituted + var ok1, ok2, ok3, ok4, ok5 bool + ok1, v.CaCert = isValidCert(v.CaCert) + ok2, v.ClientCert = isValidCert(v.ClientCert) + ok3, v.ClientKey = isValidCert(v.ClientKey) + ok4, v.TillerCert = isValidCert(v.TillerCert) + ok5, v.TillerKey = isValidCert(v.TillerKey) + if !ok1 || !ok2 || !ok3 || !ok4 || !ok5 { + return false, "ERROR: namespaces validation failed -- some certs/keys are not valid for Tiller TLS in namespace [ " + k + " ]." + } + log.Println("INFO: namespace validation -- Tiller is desired to be deployed with TLS in namespace [ " + k + " ]. ") + } else { + log.Println("INFO: namespace validation -- Tiller is desired to be deployed WITHOUT TLS in namespace [ " + k + " ]. ") + } + } + } + } else { + log.Println("INFO: ns-override is used to override all namespaces with [ " + nsOverride + " ] Skipping defined namespaces validation.") + } + + // repos + if s.HelmRepos == nil || len(s.HelmRepos) == 0 { + return false, "ERROR: repos validation failed -- I need at least one helm repo " + + "to work with!" + } + for k, v := range s.HelmRepos { + _, err := url.ParseRequestURI(v) + if err != nil { + return false, "ERROR: repos validation failed -- repo [" + k + " ] " + + "must have a valid URL." + } + + continue + + } + + // apps + if s.Apps == nil { + log.Println("INFO: You have not specified any apps. I have nothing to do. ", + "Horraayyy!.") + os.Exit(0) + } + + names := make(map[string]bool) + for appLabel, r := range s.Apps { + result, errMsg := validateRelease(r, names, s) + if !result { + return false, "ERROR: apps validation failed -- for app [" + appLabel + " ]. " + errMsg + } + } + + return true, "" } // isValidCert checks if a certificate/key path/URI is valid func isValidCert(value string) (bool, string) { - tmp := subsituteEnv(value) - _, err1 := url.ParseRequestURI(tmp) - _, err2 := os.Stat(tmp) - if err2 != nil && (err1 != nil || (!strings.HasPrefix(tmp, "s3://") && !strings.HasPrefix(tmp, "gs://"))) { - return false, "" - } - return true, tmp + tmp := os.ExpandEnv(value) + _, err1 := url.ParseRequestURI(tmp) + _, err2 := os.Stat(tmp) + if err2 != nil && (err1 != nil || (!strings.HasPrefix(tmp, "s3://") && !strings.HasPrefix(tmp, "gs://"))) { + return false, "" + } + return true, tmp } // tillerTLSEnabled checks if Tiller is desired to be deployed with TLS enabled for a given namespace // TLS is considered desired ONLY if all certs and keys for both Tiller and the Helm client are defined. func tillerTLSEnabled(namespace string) bool { - ns := s.Namespaces[namespace] - if ns.CaCert != "" && ns.TillerCert != "" && ns.TillerKey != "" && ns.ClientCert != "" && ns.ClientKey != "" { - return true - } - return false + ns := s.Namespaces[namespace] + if ns.CaCert != "" && ns.TillerCert != "" && ns.TillerKey != "" && ns.ClientCert != "" && ns.ClientKey != "" { + return true + } + return false } // print prints the desired state func (s state) print() { - fmt.Println("\nMetadata: ") - fmt.Println("--------- ") - printMap(s.Metadata) - fmt.Println("\nCertificates: ") - fmt.Println("--------- ") - printMap(s.Certificates) - fmt.Println("\nSettings: ") - fmt.Println("--------- ") - printMap(s.Settings) - fmt.Println("\nNamespaces: ") - fmt.Println("------------- ") - printNamespacesMap(s.Namespaces) - fmt.Println("\nRepositories: ") - fmt.Println("------------- ") - printMap(s.HelmRepos) - fmt.Println("\nApplications: ") - fmt.Println("--------------- ") - for _, r := range s.Apps { - r.print() - } + fmt.Println("\nMetadata: ") + fmt.Println("--------- ") + printMap(s.Metadata) + fmt.Println("\nCertificates: ") + fmt.Println("--------- ") + printMap(s.Certificates) + fmt.Println("\nSettings: ") + fmt.Println("--------- ") + printMap(s.Settings) + fmt.Println("\nNamespaces: ") + fmt.Println("------------- ") + printNamespacesMap(s.Namespaces) + fmt.Println("\nRepositories: ") + fmt.Println("------------- ") + printMap(s.HelmRepos) + fmt.Println("\nApplications: ") + fmt.Println("--------------- ") + for _, r := range s.Apps { + r.print() + } } + diff --git a/utils.go b/utils.go index 1ad40fb9..f1037a7a 100644 --- a/utils.go +++ b/utils.go @@ -1,220 +1,208 @@ package main import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "log" - "os" - "path/filepath" - "strconv" - "strings" - - "github.com/BurntSushi/toml" - "github.com/Praqma/helmsman/aws" - "github.com/Praqma/helmsman/gcs" + "bytes" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/BurntSushi/toml" + "github.com/Praqma/helmsman/aws" + "github.com/Praqma/helmsman/gcs" ) // printMap prints to the console any map of string keys and values. func printMap(m map[string]string) { - for key, value := range m { - fmt.Println(key, " : ", value) - } + for key, value := range m { + fmt.Println(key, " : ", value) + } } // printObjectMap prints to the console any map of string keys and object values. func printNamespacesMap(m map[string]namespace) { - for key, value := range m { - fmt.Println(key, " : protected = ", value) - } + for key, value := range m { + fmt.Println(key, " : protected = ", value) + } } // fromTOML reads a toml file and decodes it to a state type. // It uses the BurntSuchi TOML parser which throws an error if the TOML file is not valid. func fromTOML(file string, s *state) (bool, string) { - if _, err := toml.DecodeFile(file, s); err != nil { - return false, err.Error() - } - return true, "INFO: Parsed [[ " + file + " ]] successfully and found [ " + strconv.Itoa(len(s.Apps)) + " ] apps." + if _, err := toml.DecodeFile(file, s); err != nil { + return false, err.Error() + } + return true, "INFO: Parsed [[ " + file + " ]] successfully and found [ " + strconv.Itoa(len(s.Apps)) + " ] apps." } // toTOML encodes a state type into a TOML file. // It uses the BurntSuchi TOML parser. func toTOML(file string, s *state) { - log.Println("printing generated toml ... ") - var buff bytes.Buffer - var ( - newFile *os.File - err error - ) - - if err := toml.NewEncoder(&buff).Encode(s); err != nil { - log.Fatal(err) - os.Exit(1) - } - newFile, err = os.Create(file) - if err != nil { - log.Fatal(err) - } - bytesWritten, err := newFile.Write(buff.Bytes()) - if err != nil { - log.Fatal(err) - } - log.Printf("Wrote %d bytes.\n", bytesWritten) - newFile.Close() + log.Println("printing generated toml ... ") + var buff bytes.Buffer + var ( + newFile *os.File + err error + ) + + if err := toml.NewEncoder(&buff).Encode(s); err != nil { + log.Fatal(err) + os.Exit(1) + } + newFile, err = os.Create(file) + if err != nil { + log.Fatal(err) + } + bytesWritten, err := newFile.Write(buff.Bytes()) + if err != nil { + log.Fatal(err) + } + log.Printf("Wrote %d bytes.\n", bytesWritten) + newFile.Close() } // isOfType checks if the file extension of a filename/path is the same as "filetype". // isisOfType is case insensitive. filetype should contain the "." e.g. ".yaml" func isOfType(filename string, filetype string) bool { - return filepath.Ext(strings.ToLower(filename)) == strings.ToLower(filetype) + return filepath.Ext(strings.ToLower(filename)) == strings.ToLower(filetype) } // readFile returns the content of a file as a string. // takes a file path as input. It throws an error and breaks the program execution if it failes to read the file. func readFile(filepath string) string { - data, err := ioutil.ReadFile(filepath) - if err != nil { - log.Fatal("ERROR: failed to read [ " + filepath + " ] file content: " + err.Error()) - } - return string(data) + data, err := ioutil.ReadFile(filepath) + if err != nil { + log.Fatal("ERROR: failed to read [ " + filepath + " ] file content: " + err.Error()) + } + return string(data) } // printHelp prints Helmsman commands func printHelp() { - fmt.Println("Helmsman version: " + version) - fmt.Println("Helmsman is a Helm Charts as Code tool which allows you to automate the deployment/management of your Helm charts.") - fmt.Println("Usage: helmsman [options]") - fmt.Println() - fmt.Println("Options:") - fmt.Println("--f specifies the desired state TOML file.") - fmt.Println("--debug prints basic logs during execution.") - fmt.Println("--apply generates and applies an action plan.") - fmt.Println("--verbose prints more verbose logs during execution.") - fmt.Println("--ns-override override defined namespaces with a provided one.") - fmt.Println("--skip-validation generates and applies an action plan.") - fmt.Println("--help prints Helmsman help.") - fmt.Println("--v prints Helmsman version.") + fmt.Println("Helmsman version: " + version) + fmt.Println("Helmsman is a Helm Charts as Code tool which allows you to automate the deployment/management of your Helm charts.") + fmt.Println("Usage: helmsman [options]") + fmt.Println() + fmt.Println("Options:") + fmt.Println("--f specifies the desired state TOML file.") + fmt.Println("--debug prints basic logs during execution.") + fmt.Println("--apply generates and applies an action plan.") + fmt.Println("--verbose prints more verbose logs during execution.") + fmt.Println("--ns-override override defined namespaces with a provided one.") + fmt.Println("--skip-validation generates and applies an action plan.") + fmt.Println("--help prints Helmsman help.") + fmt.Println("--v prints Helmsman version.") } // logVersions prints the versions of kubectl and helm to the logs func logVersions() { - cmd := command{ - Cmd: "bash", - Args: []string{"-c", "kubectl version"}, - Description: "Kubectl version: ", - } - - exitCode, result := cmd.exec(debug, false) - if exitCode != 0 { - log.Fatal("ERROR: while checking kubectl version: " + result) - } - - log.Println("VERBOSE: kubectl version: \n " + result + "\n") - - cmd = command{ - Cmd: "bash", - Args: []string{"-c", "helm version"}, - Description: "Helm version: ", - } - - exitCode, result = cmd.exec(debug, false) - if exitCode != 0 { - log.Fatal("ERROR: while checking helm version: " + result) - } - log.Println("VERBOSE: helm version: \n" + result + "\n") -} - -// envVarExists checks if an environment variable is set or not and returns it. -// empty string is returned for unset env vars -// it accepts env var with/without '$' at the beginning -func envVarExists(v string) (bool, string) { - - if strings.HasPrefix(v, "$") { - v = strings.SplitAfter(v, "$")[1] - } - - value, ok := os.LookupEnv(v) - return ok, value + cmd := command{ + Cmd: "bash", + Args: []string{"-c", "kubectl version"}, + Description: "Kubectl version: ", + } + + exitCode, result := cmd.exec(debug, false) + if exitCode != 0 { + log.Fatal("ERROR: while checking kubectl version: " + result) + } + + log.Println("VERBOSE: kubectl version: \n " + result + "\n") + + cmd = command{ + Cmd: "bash", + Args: []string{"-c", "helm version"}, + Description: "Helm version: ", + } + + exitCode, result = cmd.exec(debug, false) + if exitCode != 0 { + log.Fatal("ERROR: while checking helm version: " + result) + } + log.Println("VERBOSE: helm version: \n" + result + "\n") } // sliceContains checks if a string slice contains a given string func sliceContains(slice []string, s string) bool { - for _, a := range slice { - if strings.TrimSpace(a) == s { - return true - } - } - return false + for _, a := range slice { + if strings.TrimSpace(a) == s { + return true + } + } + return false } // validateServiceAccount checks if k8s service account exists in a given namespace func validateServiceAccount(sa string, namespace string) (bool, string) { - if namespace == "" { - namespace = "default" - } - ns := " -n " + namespace - - cmd := command{ - Cmd: "bash", - Args: []string{"-c", "kubectl get serviceaccount " + sa + ns}, - Description: "validating that serviceaccount [ " + sa + " ] exists in namespace [ " + namespace + " ].", - } - - if exitCode, err := cmd.exec(debug, verbose); exitCode != 0 { - return false, err - } - return true, "" + if namespace == "" { + namespace = "default" + } + ns := " -n " + namespace + + cmd := command{ + Cmd: "bash", + Args: []string{"-c", "kubectl get serviceaccount " + sa + ns}, + Description: "validating that serviceaccount [ " + sa + " ] exists in namespace [ " + namespace + " ].", + } + + if exitCode, err := cmd.exec(debug, verbose); exitCode != 0 { + return false, err + } + return true, "" } // downloadFile downloads a file from GCS or AWS buckets and name it with a given outfile // if downloaded, returns the outfile name. If the file path is local file system path, it is returned as is. func downloadFile(path string, outfile string) string { - if strings.HasPrefix(path, "s3") { + if strings.HasPrefix(path, "s3") { - tmp := getBucketElements(path) - aws.ReadFile(tmp["bucketName"], tmp["filePath"], outfile) + tmp := getBucketElements(path) + aws.ReadFile(tmp["bucketName"], tmp["filePath"], outfile) - } else if strings.HasPrefix(path, "gs") { + } else if strings.HasPrefix(path, "gs") { - tmp := getBucketElements(path) - gcs.ReadFile(tmp["bucketName"], tmp["filePath"], outfile) + tmp := getBucketElements(path) + gcs.ReadFile(tmp["bucketName"], tmp["filePath"], outfile) - } else { + } else { - log.Println("INFO: " + outfile + " will be used from local file system.") - copyFile(path, outfile) - } - return outfile + log.Println("INFO: " + outfile + " will be used from local file system.") + copyFile(path, outfile) + } + return outfile } // copyFile copies a file from source to destination func copyFile(source string, destination string) { - from, err := os.Open(source) - if err != nil { - log.Fatal("ERROR: while copying " + source + " to " + destination + " : " + err.Error()) - } - defer from.Close() - - to, err := os.OpenFile(destination, os.O_RDWR|os.O_CREATE, 0666) - if err != nil { - log.Fatal("ERROR: while copying " + source + " to " + destination + " : " + err.Error()) - } - defer to.Close() - - _, err = io.Copy(to, from) - if err != nil { - log.Fatal("ERROR: while copying " + source + " to " + destination + " : " + err.Error()) - } + from, err := os.Open(source) + if err != nil { + log.Fatal("ERROR: while copying " + source + " to " + destination + " : " + err.Error()) + } + defer from.Close() + + to, err := os.OpenFile(destination, os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + log.Fatal("ERROR: while copying " + source + " to " + destination + " : " + err.Error()) + } + defer to.Close() + + _, err = io.Copy(to, from) + if err != nil { + log.Fatal("ERROR: while copying " + source + " to " + destination + " : " + err.Error()) + } } // deleteFile deletes a file func deleteFile(path string) { - log.Println("INFO: cleaning up ... deleting " + path) - if err := os.Remove(path); err != nil { - log.Fatal("ERROR: could not delete file: " + path) - } + log.Println("INFO: cleaning up ... deleting " + path) + if err := os.Remove(path); err != nil { + log.Fatal("ERROR: could not delete file: " + path) + } } + From fedae2e7d876090768745245b4c588ae500db223 Mon Sep 17 00:00:00 2001 From: Joshua Knarr Date: Tue, 29 May 2018 10:54:48 -0400 Subject: [PATCH 5/5] cherry pick from internal working repo --- .utils.go.swp | Bin 16384 -> 0 bytes decision_maker.go | 428 +++++++++++++++++++++++----------------------- release.go | 127 +++++++------- state.go | 351 +++++++++++++++++++------------------ utils.go | 292 ++++++++++++++++--------------- 5 files changed, 612 insertions(+), 586 deletions(-) delete mode 100644 .utils.go.swp diff --git a/.utils.go.swp b/.utils.go.swp deleted file mode 100644 index 764c0c5349a48e115d957e3b1c188e915e93490a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 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 C{JvNK diff --git a/decision_maker.go b/decision_maker.go index 60765d62..a8cfe3e9 100644 --- a/decision_maker.go +++ b/decision_maker.go @@ -1,8 +1,7 @@ package main import ( - "os" - "strings" + "strings" ) var outcome plan @@ -10,112 +9,112 @@ var releases string // makePlan creates a plan of the actions needed to make the desired state come true. func makePlan(s *state) *plan { - outcome = createPlan() - buildState() + outcome = createPlan() + buildState() - for _, r := range s.Apps { - decide(r, s) - } + for _, r := range s.Apps { + decide(r, s) + } - return &outcome + return &outcome } // decide makes a decision about what commands (actions) need to be executed // to make a release section of the desired state come true. func decide(r *release, s *state) { - // check for deletion - if !r.Enabled { - if !isProtected(r) { - inspectDeleteScenario(getDesiredNamespace(r), r) - } else { - logDecision("DECISION: release "+r.Name+" is PROTECTED. Operations are not allowed on this release until "+ - "you remove its protection.", r.Priority) - } + // check for deletion + if !r.Enabled { + if !isProtected(r) { + inspectDeleteScenario(getDesiredNamespace(r), r) + } else { + logDecision("DECISION: release "+r.Name+" is PROTECTED. Operations are not allowed on this release until "+ + "you remove its protection.", r.Priority) + } - } else { // check for install/upgrade/rollback - if helmReleaseExists(getDesiredNamespace(r), r.Name, "deployed") { - if !isProtected(r) { - inspectUpgradeScenario(getDesiredNamespace(r), r) // upgrade - } else { - logDecision("DECISION: release "+r.Name+" is PROTECTED. Operations are not allowed on this release until "+ - "you remove its protection.", r.Priority) - } + } else { // check for install/upgrade/rollback + if helmReleaseExists(getDesiredNamespace(r), r.Name, "deployed") { + if !isProtected(r) { + inspectUpgradeScenario(getDesiredNamespace(r), r) // upgrade + } else { + logDecision("DECISION: release "+r.Name+" is PROTECTED. Operations are not allowed on this release until "+ + "you remove its protection.", r.Priority) + } - } else if helmReleaseExists("", r.Name, "deleted") { - if !isProtected(r) { + } else if helmReleaseExists("", r.Name, "deleted") { + if !isProtected(r) { - rollbackRelease(getDesiredNamespace(r), r) // rollback + rollbackRelease(getDesiredNamespace(r), r) // rollback - } else { - logDecision("DECISION: release "+r.Name+" is PROTECTED. Operations are not allowed on this release until "+ - "you remove its protection.", r.Priority) - } + } else { + logDecision("DECISION: release "+r.Name+" is PROTECTED. Operations are not allowed on this release until "+ + "you remove its protection.", r.Priority) + } - } else if helmReleaseExists("", r.Name, "failed") { + } else if helmReleaseExists("", r.Name, "failed") { - if !isProtected(r) { + if !isProtected(r) { - reInstallRelease(getDesiredNamespace(r), r) // re-install failed release + reInstallRelease(getDesiredNamespace(r), r) // re-install failed release - } else { - logDecision("DECISION: release "+r.Name+" is PROTECTED. Operations are not allowed on this release until "+ - "you remove its protection.", r.Priority) - } + } else { + logDecision("DECISION: release "+r.Name+" is PROTECTED. Operations are not allowed on this release until "+ + "you remove its protection.", r.Priority) + } - } else if helmReleaseExists("", r.Name, "") { // not deployed in the desired namespace but deployed elsewhere + } else if helmReleaseExists("", r.Name, "") { // not deployed in the desired namespace but deployed elsewhere - if !isProtected(r) { + if !isProtected(r) { - reInstallRelease(getDesiredNamespace(r), r) // move the release to a new (the desired) namespace - logDecision("WARNING: moving release [ "+r.Name+" ] from [[ "+getReleaseNamespace(r.Name)+" ]] to [[ "+getDesiredNamespace(r)+ - " ]] might not correctly connect to existing volumes. Check https://github.com/Praqma/helmsman/blob/master/docs/how_to/move_charts_across_namespaces.md"+ - " for details if this release uses PV and PVC.", r.Priority) + reInstallRelease(getDesiredNamespace(r), r) // move the release to a new (the desired) namespace + logDecision("WARNING: moving release [ "+r.Name+" ] from [[ "+getReleaseNamespace(r.Name)+" ]] to [[ "+getDesiredNamespace(r)+ + " ]] might not correctly connect to existing volumes. Check https://github.com/Praqma/helmsman/blob/master/docs/how_to/move_charts_across_namespaces.md"+ + " for details if this release uses PV and PVC.", r.Priority) - } else { - logDecision("DECISION: release "+r.Name+" is PROTECTED. Operations are not allowed on this release until "+ - "you remove its protection.", r.Priority) - } + } else { + logDecision("DECISION: release "+r.Name+" is PROTECTED. Operations are not allowed on this release until "+ + "you remove its protection.", r.Priority) + } - } else { + } else { - installRelease(getDesiredNamespace(r), r) // install a new release + installRelease(getDesiredNamespace(r), r) // install a new release - } + } - } + } } // testRelease creates a Helm command to test a particular release. func testRelease(r *release) { - cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm test " + r.Name + getDesiredTillerNamespace(r) + getTLSFlags(r)}, - Description: "running tests for release [ " + r.Name + " ]", - } - outcome.addCommand(cmd, r.Priority) - logDecision("DECISION: release [ "+r.Name+" ] is required to be tested when installed. Got it!", r.Priority) + cmd := command{ + Cmd: "bash", + Args: []string{"-c", "helm test " + r.Name + getDesiredTillerNamespace(r) + getTLSFlags(r)}, + Description: "running tests for release [ " + r.Name + " ]", + } + outcome.addCommand(cmd, r.Priority) + logDecision("DECISION: release [ "+r.Name+" ] is required to be tested when installed. Got it!", r.Priority) } // installRelease creates a Helm command to install a particular release in a particular namespace. func installRelease(namespace string, r *release) { - releaseName := r.Name - cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm install " + r.Chart + " -n " + releaseName + " --namespace " + namespace + getValuesFile(r) + " --version " + r.Version + getSetValues(r) + getWait(r) + getDesiredTillerNamespace(r) + getTLSFlags(r)}, - Description: "installing release [ " + releaseName + " ] in namespace [[ " + namespace + " ]]", - } - outcome.addCommand(cmd, r.Priority) - logDecision("DECISION: release [ "+releaseName+" ] is not present in the current k8s context. Will install it in namespace [[ "+ - namespace+" ]]", r.Priority) - - if r.Test { - testRelease(r) - } + releaseName := r.Name + cmd := command{ + Cmd: "bash", + Args: []string{"-c", "helm install " + r.Chart + " -n " + releaseName + " --namespace " + namespace + getValuesFile(r) + " --version " + r.Version + getSetValues(r) + getWait(r) + getDesiredTillerNamespace(r) + getTLSFlags(r)}, + Description: "installing release [ " + releaseName + " ] in namespace [[ " + namespace + " ]]", + } + outcome.addCommand(cmd, r.Priority) + logDecision("DECISION: release [ "+releaseName+" ] is not present in the current k8s context. Will install it in namespace [[ "+ + namespace+" ]]", r.Priority) + + if r.Test { + testRelease(r) + } } // rollbackRelease evaluates if a rollback action needs to be taken for a given release. @@ -123,29 +122,29 @@ func installRelease(namespace string, r *release) { // it purge deletes it and create it in the spcified namespace. func rollbackRelease(namespace string, r *release) { - releaseName := r.Name - if getReleaseNamespace(r.Name) == namespace { - - cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm rollback " + releaseName + " " + getReleaseRevision(releaseName, "deleted") + getWait(r) + getDesiredTillerNamespace(r) + getTLSFlags(r)}, - Description: "rolling back release [ " + releaseName + " ]", - } - outcome.addCommand(cmd, r.Priority) - upgradeRelease(r) - logDecision("DECISION: release [ "+releaseName+" ] is currently deleted and is desired to be rolledback to "+ - "namespace [[ "+namespace+" ]] . It will also be upgraded in case values have changed.", r.Priority) - - } else { - - reInstallRelease(namespace, r) - logDecision("DECISION: release [ "+releaseName+" ] is deleted BUT from namespace [[ "+getReleaseNamespace(releaseName)+ - " ]]. Will purge delete it from there and install it in namespace [[ "+namespace+" ]]", r.Priority) - logDecision("WARNING: rolling back release [ "+releaseName+" ] from [[ "+getReleaseNamespace(releaseName)+" ]] to [[ "+namespace+ - " ]] might not correctly connect to existing volumes. Check https://github.com/Praqma/helmsman/blob/master/docs/how_to/move_charts_across_namespaces.md"+ - " for details if this release uses PV and PVC.", r.Priority) - - } + releaseName := r.Name + if getReleaseNamespace(r.Name) == namespace { + + cmd := command{ + Cmd: "bash", + Args: []string{"-c", "helm rollback " + releaseName + " " + getReleaseRevision(releaseName, "deleted") + getWait(r) + getDesiredTillerNamespace(r) + getTLSFlags(r)}, + Description: "rolling back release [ " + releaseName + " ]", + } + outcome.addCommand(cmd, r.Priority) + upgradeRelease(r) + logDecision("DECISION: release [ "+releaseName+" ] is currently deleted and is desired to be rolledback to "+ + "namespace [[ "+namespace+" ]] . It will also be upgraded in case values have changed.", r.Priority) + + } else { + + reInstallRelease(namespace, r) + logDecision("DECISION: release [ "+releaseName+" ] is deleted BUT from namespace [[ "+getReleaseNamespace(releaseName)+ + " ]]. Will purge delete it from there and install it in namespace [[ "+namespace+" ]]", r.Priority) + logDecision("WARNING: rolling back release [ "+releaseName+" ] from [[ "+getReleaseNamespace(releaseName)+" ]] to [[ "+namespace+ + " ]] might not correctly connect to existing volumes. Check https://github.com/Praqma/helmsman/blob/master/docs/how_to/move_charts_across_namespaces.md"+ + " for details if this release uses PV and PVC.", r.Priority) + + } } // inspectDeleteScenario evaluates if a delete action needs to be taken for a given release. @@ -153,33 +152,33 @@ func rollbackRelease(namespace string, r *release) { // If the release is not deployed, it will be skipped. func inspectDeleteScenario(namespace string, r *release) { - releaseName := r.Name - //if it exists in helm list , add command to delete it, else log that it is skipped - if helmReleaseExists(namespace, releaseName, "deployed") { - // delete it - deleteRelease(r) + releaseName := r.Name + //if it exists in helm list , add command to delete it, else log that it is skipped + if helmReleaseExists(namespace, releaseName, "deployed") { + // delete it + deleteRelease(r) - } else { - logDecision("DECISION: release [ "+releaseName+" ] is set to be disabled but is not yet deployed. Skipping.", r.Priority) - } + } else { + logDecision("DECISION: release [ "+releaseName+" ] is set to be disabled but is not yet deployed. Skipping.", r.Priority) + } } // deleteRelease deletes a release from a k8s cluster func deleteRelease(r *release) { - p := "" - purgeDesc := "" - if r.Purge { - p = "--purge" - purgeDesc = "and purged!" - } - - cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm delete " + p + " " + r.Name + getCurrentTillerNamespace(r) + getTLSFlags(r)}, - Description: "deleting release [ " + r.Name + " ]", - } - outcome.addCommand(cmd, r.Priority) - logDecision("DECISION: release [ "+r.Name+" ] is desired to be deleted "+purgeDesc+". Planing this for you!", r.Priority) + p := "" + purgeDesc := "" + if r.Purge { + p = "--purge" + purgeDesc = "and purged!" + } + + cmd := command{ + Cmd: "bash", + Args: []string{"-c", "helm delete " + p + " " + r.Name + getCurrentTillerNamespace(r) + getTLSFlags(r)}, + Description: "deleting release [ " + r.Name + " ]", + } + outcome.addCommand(cmd, r.Priority) + logDecision("DECISION: release [ "+r.Name+" ] is desired to be deleted "+purgeDesc+". Planing this for you!", r.Priority) } // inspectUpgradeScenario evaluates if a release should be upgraded. @@ -191,65 +190,65 @@ func deleteRelease(r *release) { // it will be purge deleted and installed in the new namespace. func inspectUpgradeScenario(namespace string, r *release) { - releaseName := r.Name - if getReleaseNamespace(releaseName) == namespace { - if extractChartName(r.Chart) == getReleaseChartName(releaseName) && r.Version != getReleaseChartVersion(releaseName) { - // upgrade - upgradeRelease(r) - logDecision("DECISION: release [ "+releaseName+" ] is desired to be upgraded. Planing this for you!", r.Priority) - - } else if extractChartName(r.Chart) != getReleaseChartName(releaseName) { - reInstallRelease(namespace, r) - logDecision("DECISION: release [ "+releaseName+" ] is desired to use a new Chart [ "+r.Chart+ - " ]. I am planning a purge delete of the current release and will install it with the new chart in namespace [[ "+ - namespace+" ]]", r.Priority) - - } else { - upgradeRelease(r) - logDecision("DECISION: release [ "+releaseName+" ] is desired to be enabled and is currently enabled."+ - "I will upgrade it in case you changed your values.yaml!", r.Priority) - } - } else { - reInstallRelease(namespace, r) - logDecision("DECISION: release [ "+releaseName+" ] is desired to be enabled in a new namespace [[ "+namespace+ - " ]]. I am planning a purge delete of the current release from namespace [[ "+getReleaseNamespace(releaseName)+" ]] "+ - "and will install it for you in namespace [[ "+namespace+" ]]", r.Priority) - logDecision("WARNING: moving release [ "+releaseName+" ] from [[ "+getReleaseNamespace(releaseName)+" ]] to [[ "+namespace+ - " ]] might not correctly connect to existing volumes. Check https://github.com/Praqma/helmsman/blob/master/docs/how_to/move_charts_across_namespaces.md"+ - " for details if this release uses PV and PVC.", r.Priority) - } + releaseName := r.Name + if getReleaseNamespace(releaseName) == namespace { + if extractChartName(r.Chart) == getReleaseChartName(releaseName) && r.Version != getReleaseChartVersion(releaseName) { + // upgrade + upgradeRelease(r) + logDecision("DECISION: release [ "+releaseName+" ] is desired to be upgraded. Planing this for you!", r.Priority) + + } else if extractChartName(r.Chart) != getReleaseChartName(releaseName) { + reInstallRelease(namespace, r) + logDecision("DECISION: release [ "+releaseName+" ] is desired to use a new Chart [ "+r.Chart+ + " ]. I am planning a purge delete of the current release and will install it with the new chart in namespace [[ "+ + namespace+" ]]", r.Priority) + + } else { + upgradeRelease(r) + logDecision("DECISION: release [ "+releaseName+" ] is desired to be enabled and is currently enabled."+ + "I will upgrade it in case you changed your values.yaml!", r.Priority) + } + } else { + reInstallRelease(namespace, r) + logDecision("DECISION: release [ "+releaseName+" ] is desired to be enabled in a new namespace [[ "+namespace+ + " ]]. I am planning a purge delete of the current release from namespace [[ "+getReleaseNamespace(releaseName)+" ]] "+ + "and will install it for you in namespace [[ "+namespace+" ]]", r.Priority) + logDecision("WARNING: moving release [ "+releaseName+" ] from [[ "+getReleaseNamespace(releaseName)+" ]] to [[ "+namespace+ + " ]] might not correctly connect to existing volumes. Check https://github.com/Praqma/helmsman/blob/master/docs/how_to/move_charts_across_namespaces.md"+ + " for details if this release uses PV and PVC.", r.Priority) + } } // upgradeRelease upgrades an existing release with the specified values.yaml func upgradeRelease(r *release) { - cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm upgrade " + r.Name + " " + r.Chart + getValuesFile(r) + " --version " + r.Version + " --force " + getSetValues(r) + getWait(r) + getDesiredTillerNamespace(r) + getTLSFlags(r)}, - Description: "upgrading release [ " + r.Name + " ]", - } + cmd := command{ + Cmd: "bash", + Args: []string{"-c", "helm upgrade " + r.Name + " " + r.Chart + getValuesFile(r) + " --version " + r.Version + " --force " + getSetValues(r) + getWait(r) + getDesiredTillerNamespace(r) + getTLSFlags(r)}, + Description: "upgrading release [ " + r.Name + " ]", + } - outcome.addCommand(cmd, r.Priority) + outcome.addCommand(cmd, r.Priority) } // reInstallRelease purge deletes a release and reinstalls it. // This is used when moving a release to another namespace or when changing the chart used for it. func reInstallRelease(namespace string, r *release) { - releaseName := r.Name - delCmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm delete --purge " + releaseName + getCurrentTillerNamespace(r) + getTLSFlags(r)}, - Description: "deleting release [ " + releaseName + " ]", - } - outcome.addCommand(delCmd, r.Priority) - - installCmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm install " + r.Chart + " --version " + r.Version + " -n " + releaseName + " --namespace " + namespace + getValuesFile(r) + getSetValues(r) + getWait(r) + getDesiredTillerNamespace(r) + getTLSFlags(r)}, - Description: "installing release [ " + releaseName + " ] in namespace [[ " + namespace + " ]]", - } - outcome.addCommand(installCmd, r.Priority) - logDecision("DECISION: release [ "+releaseName+" ] will be deleted from namespace [[ "+getReleaseNamespace(releaseName)+" ]] and reinstalled in [[ "+namespace+"]].", r.Priority) + releaseName := r.Name + delCmd := command{ + Cmd: "bash", + Args: []string{"-c", "helm delete --purge " + releaseName + getCurrentTillerNamespace(r) + getTLSFlags(r)}, + Description: "deleting release [ " + releaseName + " ]", + } + outcome.addCommand(delCmd, r.Priority) + + installCmd := command{ + Cmd: "bash", + Args: []string{"-c", "helm install " + r.Chart + " --version " + r.Version + " -n " + releaseName + " --namespace " + namespace + getValuesFile(r) + getSetValues(r) + getWait(r) + getDesiredTillerNamespace(r) + getTLSFlags(r)}, + Description: "installing release [ " + releaseName + " ] in namespace [[ " + namespace + " ]]", + } + outcome.addCommand(installCmd, r.Priority) + logDecision("DECISION: release [ "+releaseName+" ] will be deleted from namespace [[ "+getReleaseNamespace(releaseName)+" ]] and reinstalled in [[ "+namespace+"]].", r.Priority) } @@ -257,7 +256,7 @@ func reInstallRelease(namespace string, r *release) { // Depending on the debug flag being set or not, it will either log the the decision to output or not. func logDecision(decision string, priority int) { - outcome.addDecision(decision, priority) + outcome.addDecision(decision, priority) } @@ -265,49 +264,49 @@ func logDecision(decision string, priority int) { // example: it extracts "chartY" from "repoX/chartY" func extractChartName(releaseChart string) string { - return strings.TrimSpace(strings.Split(releaseChart, "/")[1]) + return strings.TrimSpace(strings.Split(releaseChart, "/")[1]) } // getValuesFile return partial install/upgrade release command to substitute the -f flag in Helm. func getValuesFile(r *release) string { - if r.ValuesFile != "" { - return " -f " + r.ValuesFile - } - return "" + if r.ValuesFile != "" { + return " -f " + r.ValuesFile + } + return "" } // getSetValues returns --set params to be used with helm install/upgrade commands func getSetValues(r *release) string { - result := "" - for k, v := range r.Set { - value := os.ExpandEnv(v) - result = result + " --set " + k + "=\"" + strings.Replace(value, ",", "\\,", -1) + "\"" - } - return result + result := "" + for k, v := range r.Set { + _, value := envVarExists(v) + result = result + " --set " + k + "=\"" + strings.Replace(value, ",", "\\,", -1) + "\"" + } + return result } // getWait returns a partial helm command containing the helm wait flag (--wait) if the wait flag for the release was set to true // Otherwise, retruns an empty string func getWait(r *release) string { - result := "" - if r.Wait { - result = " --wait" - } - return result + result := "" + if r.Wait { + result = " --wait" + } + return result } // getDesiredNamespace returns the namespace of a release func getDesiredNamespace(r *release) string { - return r.Namespace + return r.Namespace } // getCurrentNamespaceProtection returns the protection state for the namespace where a release is currently installed. // It returns true if a namespace is defined as protected in the desired state file, false otherwise. func getCurrentNamespaceProtection(r *release) bool { - return s.Namespaces[getReleaseNamespace(r.Name)].Protected + return s.Namespaces[getReleaseNamespace(r.Name)].Protected } // isProtected checks if a release is protected or not. @@ -316,58 +315,57 @@ func getCurrentNamespaceProtection(r *release) bool { // returns true if a release is protected, false otherwise func isProtected(r *release) bool { - // if the release does not exist in the cluster, it is not protected - if !helmReleaseExists("", r.Name, "") { - return false - } + // if the release does not exist in the cluster, it is not protected + if !helmReleaseExists("", r.Name, "") { + return false + } - if getCurrentNamespaceProtection(r) { - return true - } + if getCurrentNamespaceProtection(r) { + return true + } - if r.Protected { - return true - } + if r.Protected { + return true + } - return false + return false } // getDesiredTillerNamespace returns a tiller-namespace flag with which a release is desired to be maintained func getDesiredTillerNamespace(r *release) string { - if s.Namespaces[r.Namespace].InstallTiller { - return " --tiller-namespace " + r.Namespace - } + if s.Namespaces[r.Namespace].InstallTiller { + return " --tiller-namespace " + r.Namespace + } - return "" // same as return " --tiller-namespace kube-system" + return "" // same as return " --tiller-namespace kube-system" } // getCurrentTillerNamespace returns the tiller-namespace with which a release is currently maintained func getCurrentTillerNamespace(r *release) string { - if v, ok := currentState[r.Name]; ok { - return " --tiller-namespace " + v.TillerNamespace - } - return "" + if v, ok := currentState[r.Name]; ok { + return " --tiller-namespace " + v.TillerNamespace + } + return "" } // getTLSFlags returns TLS flags with which a release is maintained // If the release where the namespace is to be deployed has Tiller deployed, the TLS flages will use certs/keys for that namespace (if any) // otherwise, it will be the certs/keys for the kube-system namespace. func getTLSFlags(r *release) string { - tls := "" - if s.Namespaces[r.Namespace].InstallTiller { - if tillerTLSEnabled(r.Namespace) { + tls := "" + if s.Namespaces[r.Namespace].InstallTiller { + if tillerTLSEnabled(r.Namespace) { - tls = " --tls --tls-ca-cert " + r.Namespace + "-ca.cert --tls-cert " + r.Namespace + "-client.cert --tls-key " + r.Namespace + "-client.key " - } - } else { - if tillerTLSEnabled("kube-system") { + tls = " --tls --tls-ca-cert " + r.Namespace + "-ca.cert --tls-cert " + r.Namespace + "-client.cert --tls-key " + r.Namespace + "-client.key " + } + } else { + if tillerTLSEnabled("kube-system") { - tls = " --tls --tls-ca-cert kube-system-ca.cert --tls-cert kube-system-client.cert --tls-key kube-system-client.key " - } - } + tls = " --tls --tls-ca-cert kube-system-ca.cert --tls-cert kube-system-client.cert --tls-key kube-system-client.key " + } + } - return tls + return tls } - diff --git a/release.go b/release.go index 757518bb..5052f6a6 100644 --- a/release.go +++ b/release.go @@ -1,86 +1,93 @@ package main import ( - "fmt" - "log" - "os" - "strings" + "fmt" + "log" + "os" + "strings" ) // release type representing Helm releases which are described in the desired state type release struct { - Name string - Description string - Namespace string - Enabled bool - Chart string - Version string - ValuesFile string - Purge bool - Test bool - Protected bool - Wait bool - Priority int - Set map[string]string + Name string + Description string + Namespace string + Enabled bool + Chart string + Version string + ValuesFile string + Purge bool + Test bool + Protected bool + Wait bool + Priority int + Set map[string]string } // validateRelease validates if a release inside a desired state meets the specifications or not. // check the full specification @ https://github.com/Praqma/helmsman/docs/desired_state_spec.md func validateRelease(r *release, names map[string]bool, s state) (bool, string) { - _, err := os.Stat(r.ValuesFile) - if r.Name == "" || names[r.Name] { - return false, "release name can't be empty and must be unique." - } else if nsOverride == "" && r.Namespace == "" { - return false, "release targeted namespace can't be empty." - } else if nsOverride == "" && r.Namespace != "" && !checkNamespaceDefined(r.Namespace, s) { - return false, "release " + r.Name + " is using namespace [ " + r.Namespace + " ] which is not defined in the Namespaces section of your desired state file." + - " Release [ " + r.Name + " ] can't be installed in that Namespace until its defined." - } else if r.Chart == "" || !strings.ContainsAny(r.Chart, "/") { - return false, "chart can't be empty and must be of the format: repo/chart." - } else if r.Version == "" { - return false, "version can't be empty." - } else if r.ValuesFile != "" && (!isOfType(r.ValuesFile, ".yaml") || err != nil) { - return false, "valuesFile must be a valid file path for a yaml file, Or can be left empty." - } else if r.Priority != 0 && r.Priority > 0 { - return false, "priority can only be 0 or negative value, positive values are not allowed." - } + _, err := os.Stat(r.ValuesFile) + if r.Name == "" || names[r.Name] { + return false, "release name can't be empty and must be unique." + } else if nsOverride == "" && r.Namespace == "" { + return false, "release targeted namespace can't be empty." + } else if nsOverride == "" && r.Namespace != "" && !checkNamespaceDefined(r.Namespace, s) { + return false, "release " + r.Name + " is using namespace [ " + r.Namespace + " ] which is not defined in the Namespaces section of your desired state file." + + " Release [ " + r.Name + " ] can't be installed in that Namespace until its defined." + } else if r.Chart == "" || !strings.ContainsAny(r.Chart, "/") { + return false, "chart can't be empty and must be of the format: repo/chart." + } else if r.Version == "" { + return false, "version can't be empty." + } else if r.ValuesFile != "" && (!isOfType(r.ValuesFile, ".yaml") || err != nil) { + return false, "valuesFile must be a valid file path for a yaml file, Or can be left empty." + } else if len(r.Set) > 0 { + for k, v := range r.Set { + if !strings.HasPrefix(v, "$") { + return false, "the value for variable [ " + k + " ] must be an environment variable name and start with '$'." + } else if ok, _ := envVarExists(v); !ok { + return false, "env variable [ " + v + " ] is not found in the environment." + } + } + } else if r.Priority != 0 && r.Priority > 0 { + return false, "priority can only be 0 or negative value, positive values are not allowed." + } - names[r.Name] = true - return true, "" + names[r.Name] = true + return true, "" } // checkNamespaceDefined checks if a given namespace is defined in the namespaces section of the desired state file func checkNamespaceDefined(ns string, s state) bool { - _, ok := s.Namespaces[ns] - if !ok { - return false - } - return true + _, ok := s.Namespaces[ns] + if !ok { + return false + } + return true } // overrideNamespace overrides a release defined namespace with a new given one func overrideNamespace(r *release, newNs string) { - log.Println("INFO: overriding namespace for app: " + r.Name) - r.Namespace = newNs + log.Println("INFO: overriding namespace for app: " + r.Name) + r.Namespace = newNs } // print prints the details of the release func (r release) print() { - fmt.Println("") - fmt.Println("\tname : ", r.Name) - fmt.Println("\tdescription : ", r.Description) - fmt.Println("\tnamespace : ", r.Namespace) - fmt.Println("\tenabled : ", r.Enabled) - fmt.Println("\tchart : ", r.Chart) - fmt.Println("\tversion : ", r.Version) - fmt.Println("\tvaluesFile : ", r.ValuesFile) - fmt.Println("\tpurge : ", r.Purge) - fmt.Println("\ttest : ", r.Test) - fmt.Println("\tprotected : ", r.Protected) - fmt.Println("\twait : ", r.Wait) - fmt.Println("\tpriority : ", r.Priority) - fmt.Println("\tvalues to override from env:") - printMap(r.Set) - fmt.Println("------------------- ") + fmt.Println("") + fmt.Println("\tname : ", r.Name) + fmt.Println("\tdescription : ", r.Description) + fmt.Println("\tnamespace : ", r.Namespace) + fmt.Println("\tenabled : ", r.Enabled) + fmt.Println("\tchart : ", r.Chart) + fmt.Println("\tversion : ", r.Version) + fmt.Println("\tvaluesFile : ", r.ValuesFile) + fmt.Println("\tpurge : ", r.Purge) + fmt.Println("\ttest : ", r.Test) + fmt.Println("\tprotected : ", r.Protected) + fmt.Println("\twait : ", r.Wait) + fmt.Println("\tpriority : ", r.Priority) + fmt.Println("\tvalues to override from env:") + printMap(r.Set) + fmt.Println("------------------- ") } - diff --git a/state.go b/state.go index 404281aa..a9eb92b8 100644 --- a/state.go +++ b/state.go @@ -1,204 +1,213 @@ package main import ( - "fmt" - "log" - "net/url" - "os" - "strings" + "fmt" + "log" + "net/url" + "os" + "strings" ) // namespace type represents the fields of a namespace type namespace struct { - Protected bool - InstallTiller bool - TillerServiceAccount string - CaCert string - TillerCert string - TillerKey string - ClientCert string - ClientKey string + Protected bool + InstallTiller bool + TillerServiceAccount string + CaCert string + TillerCert string + TillerKey string + ClientCert string + ClientKey string } // state type represents the desired state of applications on a k8s cluster. type state struct { - Metadata map[string]string - Certificates map[string]string - Settings map[string]string - Namespaces map[string]namespace - HelmRepos map[string]string - Apps map[string]*release + Metadata map[string]string + Certificates map[string]string + Settings map[string]string + Namespaces map[string]namespace + HelmRepos map[string]string + Apps map[string]*release } // validate validates that the values specified in the desired state are valid according to the desired state spec. // check https://github.com/Praqma/Helmsman/docs/desired_state_spec.md for the detailed specification func (s state) validate() (bool, string) { - // settings - if s.Settings == nil || len(s.Settings) == 0 { - return false, "ERROR: settings validation failed -- no settings table provided in TOML." - } else if value, ok := s.Settings["kubeContext"]; !ok || value == "" { - return false, "ERROR: settings validation failed -- you have not provided a " + - "kubeContext to use. Can't work without it. Sorry!" - } else if value, ok = s.Settings["clusterURI"]; ok { - - s.Settings["clusterURI"] = os.ExpandEnv(value) - if _, err := url.ParseRequestURI(s.Settings["clusterURI"]); err != nil { - return false, "ERROR: settings validation failed -- clusterURI must have a valid URL set in an env varibale or passed directly. Either the env var is missing/empty or the URL is invalid." - } - - if _, ok = s.Settings["username"]; !ok { - return false, "ERROR: settings validation failed -- username must be provided if clusterURI is defined." - } - if value, ok = s.Settings["password"]; ok { - s.Settings["password"] = os.ExpandEnv(value) - } else { - return false, "ERROR: settings validation failed -- password must be provided if clusterURI is defined." - } - - if s.Settings["password"] == "" { - return false, "ERROR: settings validation failed -- password should be set as an env variable. It is currently missing or empty. " - } - } - - // certificates - if s.Certificates != nil && len(s.Certificates) != 0 { - _, ok1 := s.Settings["clusterURI"] - _, ok2 := s.Certificates["caCrt"] - _, ok3 := s.Certificates["caKey"] - if ok1 && (!ok2 || !ok3) { - return false, "ERROR: certifications validation failed -- You want me to connect to your cluster for you " + - "but have not given me the cert/key to do so. Please add [caCrt] and [caKey] under Certifications. You might also need to provide [clientCrt]." - } else if ok1 { - for key, value := range s.Certificates { - r, path := isValidCert(value) - if !r { - return false, "ERROR: certifications validation failed -- [ " + key + " ] must be a valid S3 or GCS bucket URL or a valid relative file path." - } - s.Certificates[key] = path - } - } else { - log.Println("INFO: certificates provided but not needed. Skipping certificates validation.") - } - - } else { - if _, ok := s.Settings["clusterURI"]; ok { - return false, "ERROR: certifications validation failed -- You want me to connect to your cluster for you " + - "but have not given me the cert/key to do so. Please add [caCrt] and [caKey] under Certifications. You might also need to provide [clientCrt]." - } - } - - // namespaces - if nsOverride == "" { - if s.Namespaces == nil || len(s.Namespaces) == 0 { - return false, "ERROR: namespaces validation failed -- I need at least one namespace " + - "to work with!" - } - - for k, v := range s.Namespaces { - if !v.InstallTiller && k != "kube-system" { - log.Println("INFO: naemspace validation -- Tiller is not desired to be deployed in namespace [ " + k + " ].") - } else { - if tillerTLSEnabled(k) { - // validating the TLS certs and keys for Tiller - // if they are valid, their values (if they are env vars) are substituted - var ok1, ok2, ok3, ok4, ok5 bool - ok1, v.CaCert = isValidCert(v.CaCert) - ok2, v.ClientCert = isValidCert(v.ClientCert) - ok3, v.ClientKey = isValidCert(v.ClientKey) - ok4, v.TillerCert = isValidCert(v.TillerCert) - ok5, v.TillerKey = isValidCert(v.TillerKey) - if !ok1 || !ok2 || !ok3 || !ok4 || !ok5 { - return false, "ERROR: namespaces validation failed -- some certs/keys are not valid for Tiller TLS in namespace [ " + k + " ]." - } - log.Println("INFO: namespace validation -- Tiller is desired to be deployed with TLS in namespace [ " + k + " ]. ") - } else { - log.Println("INFO: namespace validation -- Tiller is desired to be deployed WITHOUT TLS in namespace [ " + k + " ]. ") - } - } - } - } else { - log.Println("INFO: ns-override is used to override all namespaces with [ " + nsOverride + " ] Skipping defined namespaces validation.") - } - - // repos - if s.HelmRepos == nil || len(s.HelmRepos) == 0 { - return false, "ERROR: repos validation failed -- I need at least one helm repo " + - "to work with!" - } - for k, v := range s.HelmRepos { - _, err := url.ParseRequestURI(v) - if err != nil { - return false, "ERROR: repos validation failed -- repo [" + k + " ] " + - "must have a valid URL." - } - - continue - - } - - // apps - if s.Apps == nil { - log.Println("INFO: You have not specified any apps. I have nothing to do. ", - "Horraayyy!.") - os.Exit(0) - } - - names := make(map[string]bool) - for appLabel, r := range s.Apps { - result, errMsg := validateRelease(r, names, s) - if !result { - return false, "ERROR: apps validation failed -- for app [" + appLabel + " ]. " + errMsg - } - } - - return true, "" + // settings + if s.Settings == nil || len(s.Settings) == 0 { + return false, "ERROR: settings validation failed -- no settings table provided in TOML." + } else if value, ok := s.Settings["kubeContext"]; !ok || value == "" { + return false, "ERROR: settings validation failed -- you have not provided a " + + "kubeContext to use. Can't work without it. Sorry!" + } else if value, ok = s.Settings["clusterURI"]; ok { + + s.Settings["clusterURI"] = subsituteEnv(value) + if _, err := url.ParseRequestURI(s.Settings["clusterURI"]); err != nil { + return false, "ERROR: settings validation failed -- clusterURI must have a valid URL set in an env varibale or passed directly. Either the env var is missing/empty or the URL is invalid." + } + + if _, ok = s.Settings["username"]; !ok { + return false, "ERROR: settings validation failed -- username must be provided if clusterURI is defined." + } + if value, ok = s.Settings["password"]; ok { + s.Settings["password"] = subsituteEnv(value) + } else { + return false, "ERROR: settings validation failed -- password must be provided if clusterURI is defined." + } + + if s.Settings["password"] == "" { + return false, "ERROR: settings validation failed -- password should be set as an env variable. It is currently missing or empty. " + } + } + + // certificates + if s.Certificates != nil && len(s.Certificates) != 0 { + _, ok1 := s.Settings["clusterURI"] + _, ok2 := s.Certificates["caCrt"] + _, ok3 := s.Certificates["caKey"] + if ok1 && (!ok2 || !ok3) { + return false, "ERROR: certifications validation failed -- You want me to connect to your cluster for you " + + "but have not given me the cert/key to do so. Please add [caCrt] and [caKey] under Certifications. You might also need to provide [clientCrt]." + } else if ok1 { + for key, value := range s.Certificates { + r, path := isValidCert(value) + if !r { + return false, "ERROR: certifications validation failed -- [ " + key + " ] must be a valid S3 or GCS bucket URL or a valid relative file path." + } + s.Certificates[key] = path + } + } else { + log.Println("INFO: certificates provided but not needed. Skipping certificates validation.") + } + + } else { + if _, ok := s.Settings["clusterURI"]; ok { + return false, "ERROR: certifications validation failed -- You want me to connect to your cluster for you " + + "but have not given me the cert/key to do so. Please add [caCrt] and [caKey] under Certifications. You might also need to provide [clientCrt]." + } + } + + // namespaces + if nsOverride == "" { + if s.Namespaces == nil || len(s.Namespaces) == 0 { + return false, "ERROR: namespaces validation failed -- I need at least one namespace " + + "to work with!" + } + + for k, v := range s.Namespaces { + if !v.InstallTiller && k != "kube-system" { + log.Println("INFO: naemspace validation -- Tiller is not desired to be deployed in namespace [ " + k + " ].") + } else { + if tillerTLSEnabled(k) { + // validating the TLS certs and keys for Tiller + // if they are valid, their values (if they are env vars) are substituted + var ok1, ok2, ok3, ok4, ok5 bool + ok1, v.CaCert = isValidCert(v.CaCert) + ok2, v.ClientCert = isValidCert(v.ClientCert) + ok3, v.ClientKey = isValidCert(v.ClientKey) + ok4, v.TillerCert = isValidCert(v.TillerCert) + ok5, v.TillerKey = isValidCert(v.TillerKey) + if !ok1 || !ok2 || !ok3 || !ok4 || !ok5 { + return false, "ERROR: namespaces validation failed -- some certs/keys are not valid for Tiller TLS in namespace [ " + k + " ]." + } + log.Println("INFO: namespace validation -- Tiller is desired to be deployed with TLS in namespace [ " + k + " ]. ") + } else { + log.Println("INFO: namespace validation -- Tiller is desired to be deployed WITHOUT TLS in namespace [ " + k + " ]. ") + } + } + } + } else { + log.Println("INFO: ns-override is used to override all namespaces with [ " + nsOverride + " ] Skipping defined namespaces validation.") + } + + // repos + if s.HelmRepos == nil || len(s.HelmRepos) == 0 { + return false, "ERROR: repos validation failed -- I need at least one helm repo " + + "to work with!" + } + for k, v := range s.HelmRepos { + _, err := url.ParseRequestURI(v) + if err != nil { + return false, "ERROR: repos validation failed -- repo [" + k + " ] " + + "must have a valid URL." + } + + continue + + } + + // apps + if s.Apps == nil { + log.Println("INFO: You have not specified any apps. I have nothing to do. ", + "Horraayyy!.") + os.Exit(0) + } + + names := make(map[string]bool) + for appLabel, r := range s.Apps { + result, errMsg := validateRelease(r, names, s) + if !result { + return false, "ERROR: apps validation failed -- for app [" + appLabel + " ]. " + errMsg + } + } + + return true, "" +} + +// substitueEnv checks if a string is an env variable (starts with '$'), then it returns its value +// if the env variable is empty or unset, an empty string is returned +// if the string does not start with '$', it is returned as is. +func subsituteEnv(name string) string { + if strings.Contains(name, "$") { + return os.ExpandEnv(name) + } + return name } // isValidCert checks if a certificate/key path/URI is valid func isValidCert(value string) (bool, string) { - tmp := os.ExpandEnv(value) - _, err1 := url.ParseRequestURI(tmp) - _, err2 := os.Stat(tmp) - if err2 != nil && (err1 != nil || (!strings.HasPrefix(tmp, "s3://") && !strings.HasPrefix(tmp, "gs://"))) { - return false, "" - } - return true, tmp + tmp := subsituteEnv(value) + _, err1 := url.ParseRequestURI(tmp) + _, err2 := os.Stat(tmp) + if err2 != nil && (err1 != nil || (!strings.HasPrefix(tmp, "s3://") && !strings.HasPrefix(tmp, "gs://"))) { + return false, "" + } + return true, tmp } // tillerTLSEnabled checks if Tiller is desired to be deployed with TLS enabled for a given namespace // TLS is considered desired ONLY if all certs and keys for both Tiller and the Helm client are defined. func tillerTLSEnabled(namespace string) bool { - ns := s.Namespaces[namespace] - if ns.CaCert != "" && ns.TillerCert != "" && ns.TillerKey != "" && ns.ClientCert != "" && ns.ClientKey != "" { - return true - } - return false + ns := s.Namespaces[namespace] + if ns.CaCert != "" && ns.TillerCert != "" && ns.TillerKey != "" && ns.ClientCert != "" && ns.ClientKey != "" { + return true + } + return false } // print prints the desired state func (s state) print() { - fmt.Println("\nMetadata: ") - fmt.Println("--------- ") - printMap(s.Metadata) - fmt.Println("\nCertificates: ") - fmt.Println("--------- ") - printMap(s.Certificates) - fmt.Println("\nSettings: ") - fmt.Println("--------- ") - printMap(s.Settings) - fmt.Println("\nNamespaces: ") - fmt.Println("------------- ") - printNamespacesMap(s.Namespaces) - fmt.Println("\nRepositories: ") - fmt.Println("------------- ") - printMap(s.HelmRepos) - fmt.Println("\nApplications: ") - fmt.Println("--------------- ") - for _, r := range s.Apps { - r.print() - } + fmt.Println("\nMetadata: ") + fmt.Println("--------- ") + printMap(s.Metadata) + fmt.Println("\nCertificates: ") + fmt.Println("--------- ") + printMap(s.Certificates) + fmt.Println("\nSettings: ") + fmt.Println("--------- ") + printMap(s.Settings) + fmt.Println("\nNamespaces: ") + fmt.Println("------------- ") + printNamespacesMap(s.Namespaces) + fmt.Println("\nRepositories: ") + fmt.Println("------------- ") + printMap(s.HelmRepos) + fmt.Println("\nApplications: ") + fmt.Println("--------------- ") + for _, r := range s.Apps { + r.print() + } } - diff --git a/utils.go b/utils.go index f1037a7a..1ad40fb9 100644 --- a/utils.go +++ b/utils.go @@ -1,208 +1,220 @@ package main import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "log" - "os" - "path/filepath" - "strconv" - "strings" - - "github.com/BurntSushi/toml" - "github.com/Praqma/helmsman/aws" - "github.com/Praqma/helmsman/gcs" + "bytes" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/BurntSushi/toml" + "github.com/Praqma/helmsman/aws" + "github.com/Praqma/helmsman/gcs" ) // printMap prints to the console any map of string keys and values. func printMap(m map[string]string) { - for key, value := range m { - fmt.Println(key, " : ", value) - } + for key, value := range m { + fmt.Println(key, " : ", value) + } } // printObjectMap prints to the console any map of string keys and object values. func printNamespacesMap(m map[string]namespace) { - for key, value := range m { - fmt.Println(key, " : protected = ", value) - } + for key, value := range m { + fmt.Println(key, " : protected = ", value) + } } // fromTOML reads a toml file and decodes it to a state type. // It uses the BurntSuchi TOML parser which throws an error if the TOML file is not valid. func fromTOML(file string, s *state) (bool, string) { - if _, err := toml.DecodeFile(file, s); err != nil { - return false, err.Error() - } - return true, "INFO: Parsed [[ " + file + " ]] successfully and found [ " + strconv.Itoa(len(s.Apps)) + " ] apps." + if _, err := toml.DecodeFile(file, s); err != nil { + return false, err.Error() + } + return true, "INFO: Parsed [[ " + file + " ]] successfully and found [ " + strconv.Itoa(len(s.Apps)) + " ] apps." } // toTOML encodes a state type into a TOML file. // It uses the BurntSuchi TOML parser. func toTOML(file string, s *state) { - log.Println("printing generated toml ... ") - var buff bytes.Buffer - var ( - newFile *os.File - err error - ) - - if err := toml.NewEncoder(&buff).Encode(s); err != nil { - log.Fatal(err) - os.Exit(1) - } - newFile, err = os.Create(file) - if err != nil { - log.Fatal(err) - } - bytesWritten, err := newFile.Write(buff.Bytes()) - if err != nil { - log.Fatal(err) - } - log.Printf("Wrote %d bytes.\n", bytesWritten) - newFile.Close() + log.Println("printing generated toml ... ") + var buff bytes.Buffer + var ( + newFile *os.File + err error + ) + + if err := toml.NewEncoder(&buff).Encode(s); err != nil { + log.Fatal(err) + os.Exit(1) + } + newFile, err = os.Create(file) + if err != nil { + log.Fatal(err) + } + bytesWritten, err := newFile.Write(buff.Bytes()) + if err != nil { + log.Fatal(err) + } + log.Printf("Wrote %d bytes.\n", bytesWritten) + newFile.Close() } // isOfType checks if the file extension of a filename/path is the same as "filetype". // isisOfType is case insensitive. filetype should contain the "." e.g. ".yaml" func isOfType(filename string, filetype string) bool { - return filepath.Ext(strings.ToLower(filename)) == strings.ToLower(filetype) + return filepath.Ext(strings.ToLower(filename)) == strings.ToLower(filetype) } // readFile returns the content of a file as a string. // takes a file path as input. It throws an error and breaks the program execution if it failes to read the file. func readFile(filepath string) string { - data, err := ioutil.ReadFile(filepath) - if err != nil { - log.Fatal("ERROR: failed to read [ " + filepath + " ] file content: " + err.Error()) - } - return string(data) + data, err := ioutil.ReadFile(filepath) + if err != nil { + log.Fatal("ERROR: failed to read [ " + filepath + " ] file content: " + err.Error()) + } + return string(data) } // printHelp prints Helmsman commands func printHelp() { - fmt.Println("Helmsman version: " + version) - fmt.Println("Helmsman is a Helm Charts as Code tool which allows you to automate the deployment/management of your Helm charts.") - fmt.Println("Usage: helmsman [options]") - fmt.Println() - fmt.Println("Options:") - fmt.Println("--f specifies the desired state TOML file.") - fmt.Println("--debug prints basic logs during execution.") - fmt.Println("--apply generates and applies an action plan.") - fmt.Println("--verbose prints more verbose logs during execution.") - fmt.Println("--ns-override override defined namespaces with a provided one.") - fmt.Println("--skip-validation generates and applies an action plan.") - fmt.Println("--help prints Helmsman help.") - fmt.Println("--v prints Helmsman version.") + fmt.Println("Helmsman version: " + version) + fmt.Println("Helmsman is a Helm Charts as Code tool which allows you to automate the deployment/management of your Helm charts.") + fmt.Println("Usage: helmsman [options]") + fmt.Println() + fmt.Println("Options:") + fmt.Println("--f specifies the desired state TOML file.") + fmt.Println("--debug prints basic logs during execution.") + fmt.Println("--apply generates and applies an action plan.") + fmt.Println("--verbose prints more verbose logs during execution.") + fmt.Println("--ns-override override defined namespaces with a provided one.") + fmt.Println("--skip-validation generates and applies an action plan.") + fmt.Println("--help prints Helmsman help.") + fmt.Println("--v prints Helmsman version.") } // logVersions prints the versions of kubectl and helm to the logs func logVersions() { - cmd := command{ - Cmd: "bash", - Args: []string{"-c", "kubectl version"}, - Description: "Kubectl version: ", - } - - exitCode, result := cmd.exec(debug, false) - if exitCode != 0 { - log.Fatal("ERROR: while checking kubectl version: " + result) - } - - log.Println("VERBOSE: kubectl version: \n " + result + "\n") - - cmd = command{ - Cmd: "bash", - Args: []string{"-c", "helm version"}, - Description: "Helm version: ", - } - - exitCode, result = cmd.exec(debug, false) - if exitCode != 0 { - log.Fatal("ERROR: while checking helm version: " + result) - } - log.Println("VERBOSE: helm version: \n" + result + "\n") + cmd := command{ + Cmd: "bash", + Args: []string{"-c", "kubectl version"}, + Description: "Kubectl version: ", + } + + exitCode, result := cmd.exec(debug, false) + if exitCode != 0 { + log.Fatal("ERROR: while checking kubectl version: " + result) + } + + log.Println("VERBOSE: kubectl version: \n " + result + "\n") + + cmd = command{ + Cmd: "bash", + Args: []string{"-c", "helm version"}, + Description: "Helm version: ", + } + + exitCode, result = cmd.exec(debug, false) + if exitCode != 0 { + log.Fatal("ERROR: while checking helm version: " + result) + } + log.Println("VERBOSE: helm version: \n" + result + "\n") +} + +// envVarExists checks if an environment variable is set or not and returns it. +// empty string is returned for unset env vars +// it accepts env var with/without '$' at the beginning +func envVarExists(v string) (bool, string) { + + if strings.HasPrefix(v, "$") { + v = strings.SplitAfter(v, "$")[1] + } + + value, ok := os.LookupEnv(v) + return ok, value } // sliceContains checks if a string slice contains a given string func sliceContains(slice []string, s string) bool { - for _, a := range slice { - if strings.TrimSpace(a) == s { - return true - } - } - return false + for _, a := range slice { + if strings.TrimSpace(a) == s { + return true + } + } + return false } // validateServiceAccount checks if k8s service account exists in a given namespace func validateServiceAccount(sa string, namespace string) (bool, string) { - if namespace == "" { - namespace = "default" - } - ns := " -n " + namespace - - cmd := command{ - Cmd: "bash", - Args: []string{"-c", "kubectl get serviceaccount " + sa + ns}, - Description: "validating that serviceaccount [ " + sa + " ] exists in namespace [ " + namespace + " ].", - } - - if exitCode, err := cmd.exec(debug, verbose); exitCode != 0 { - return false, err - } - return true, "" + if namespace == "" { + namespace = "default" + } + ns := " -n " + namespace + + cmd := command{ + Cmd: "bash", + Args: []string{"-c", "kubectl get serviceaccount " + sa + ns}, + Description: "validating that serviceaccount [ " + sa + " ] exists in namespace [ " + namespace + " ].", + } + + if exitCode, err := cmd.exec(debug, verbose); exitCode != 0 { + return false, err + } + return true, "" } // downloadFile downloads a file from GCS or AWS buckets and name it with a given outfile // if downloaded, returns the outfile name. If the file path is local file system path, it is returned as is. func downloadFile(path string, outfile string) string { - if strings.HasPrefix(path, "s3") { + if strings.HasPrefix(path, "s3") { - tmp := getBucketElements(path) - aws.ReadFile(tmp["bucketName"], tmp["filePath"], outfile) + tmp := getBucketElements(path) + aws.ReadFile(tmp["bucketName"], tmp["filePath"], outfile) - } else if strings.HasPrefix(path, "gs") { + } else if strings.HasPrefix(path, "gs") { - tmp := getBucketElements(path) - gcs.ReadFile(tmp["bucketName"], tmp["filePath"], outfile) + tmp := getBucketElements(path) + gcs.ReadFile(tmp["bucketName"], tmp["filePath"], outfile) - } else { + } else { - log.Println("INFO: " + outfile + " will be used from local file system.") - copyFile(path, outfile) - } - return outfile + log.Println("INFO: " + outfile + " will be used from local file system.") + copyFile(path, outfile) + } + return outfile } // copyFile copies a file from source to destination func copyFile(source string, destination string) { - from, err := os.Open(source) - if err != nil { - log.Fatal("ERROR: while copying " + source + " to " + destination + " : " + err.Error()) - } - defer from.Close() - - to, err := os.OpenFile(destination, os.O_RDWR|os.O_CREATE, 0666) - if err != nil { - log.Fatal("ERROR: while copying " + source + " to " + destination + " : " + err.Error()) - } - defer to.Close() - - _, err = io.Copy(to, from) - if err != nil { - log.Fatal("ERROR: while copying " + source + " to " + destination + " : " + err.Error()) - } + from, err := os.Open(source) + if err != nil { + log.Fatal("ERROR: while copying " + source + " to " + destination + " : " + err.Error()) + } + defer from.Close() + + to, err := os.OpenFile(destination, os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + log.Fatal("ERROR: while copying " + source + " to " + destination + " : " + err.Error()) + } + defer to.Close() + + _, err = io.Copy(to, from) + if err != nil { + log.Fatal("ERROR: while copying " + source + " to " + destination + " : " + err.Error()) + } } // deleteFile deletes a file func deleteFile(path string) { - log.Println("INFO: cleaning up ... deleting " + path) - if err := os.Remove(path); err != nil { - log.Fatal("ERROR: could not delete file: " + path) - } + log.Println("INFO: cleaning up ... deleting " + path) + if err := os.Remove(path); err != nil { + log.Fatal("ERROR: could not delete file: " + path) + } } -