diff --git a/decision_maker.go b/decision_maker.go index 7ff9d2d3..8105dcde 100644 --- a/decision_maker.go +++ b/decision_maker.go @@ -26,6 +26,12 @@ func makePlan(s *state) *plan { // 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) { + if destroy { + if ok, rs := helmReleaseExists(r, ""); ok { + deleteRelease(r, rs) + return + } + } // check for deletion if !r.Enabled { @@ -102,7 +108,7 @@ func installRelease(r *release) { cmd := command{ Cmd: "bash", - Args: []string{"-c", "helm install " + r.Chart + " -n " + r.Name + " --namespace " + r.Namespace + getValuesFiles(r) + " --version " + r.Version + getSetValues(r) + getWait(r) + getDesiredTillerNamespaceFlag(r) + getTLSFlags(r) + getTimeout(r) + getNoHooks(r) + getDryRunFlags()}, + Args: []string{"-c", "helm install " + r.Chart + " -n " + r.Name + " --namespace " + r.Namespace + getValuesFiles(r) + " --version " + r.Version + getSetValues(r) + getSetStringValues(r) + getWait(r) + getDesiredTillerNamespaceFlag(r) + getTLSFlags(r) + getTimeout(r) + getNoHooks(r) + getDryRunFlags()}, Description: "installing release [ " + r.Name + " ] in namespace [[ " + r.Namespace + " ]] using Tiller in [ " + getDesiredTillerNamespace(r) + " ]", } outcome.addCommand(cmd, r.Priority, r) @@ -219,7 +225,7 @@ func diffRelease(r *release) string { cmd := command{ Cmd: "bash", - Args: []string{"-c", "helm diff " + colorFlag + "upgrade " + r.Name + " " + r.Chart + getValuesFiles(r) + " --version " + r.Version + " " + getSetValues(r) + getWait(r) + getDesiredTillerNamespaceFlag(r) + getTLSFlags(r) + getTimeout(r) + getNoHooks(r)}, + Args: []string{"-c", "helm diff " + colorFlag + "upgrade " + r.Name + " " + r.Chart + getValuesFiles(r) + " --version " + r.Version + " " + getSetValues(r) + getSetStringValues(r) + getWait(r) + getDesiredTillerNamespaceFlag(r) + getTLSFlags(r) + getTimeout(r) + getNoHooks(r)}, Description: "upgrading release [ " + r.Name + " ] using Tiller in [ " + getDesiredTillerNamespace(r) + " ]", } @@ -236,7 +242,7 @@ func diffRelease(r *release) string { func upgradeRelease(r *release) { cmd := command{ Cmd: "bash", - Args: []string{"-c", "helm upgrade " + r.Name + " " + r.Chart + getValuesFiles(r) + " --version " + r.Version + " --force " + getSetValues(r) + getWait(r) + getDesiredTillerNamespaceFlag(r) + getTLSFlags(r) + getTimeout(r) + getNoHooks(r) + getDryRunFlags()}, + Args: []string{"-c", "helm upgrade " + r.Name + " " + r.Chart + getValuesFiles(r) + " --version " + r.Version + " --force " + getSetValues(r) + getSetStringValues(r) + getWait(r) + getDesiredTillerNamespaceFlag(r) + getTLSFlags(r) + getTimeout(r) + getNoHooks(r) + getDryRunFlags()}, Description: "upgrading release [ " + r.Name + " ] using Tiller in [ " + getDesiredTillerNamespace(r) + " ]", } @@ -256,7 +262,7 @@ func reInstallRelease(r *release, rs releaseState) { installCmd := command{ Cmd: "bash", - Args: []string{"-c", "helm install " + r.Chart + " --version " + r.Version + " -n " + r.Name + " --namespace " + r.Namespace + getValuesFiles(r) + getSetValues(r) + getWait(r) + getDesiredTillerNamespaceFlag(r) + getTLSFlags(r) + getTimeout(r) + getNoHooks(r) + getDryRunFlags()}, + Args: []string{"-c", "helm install " + r.Chart + " --version " + r.Version + " -n " + r.Name + " --namespace " + r.Namespace + getValuesFiles(r) + getSetValues(r) + getSetStringValues(r) + getWait(r) + getDesiredTillerNamespaceFlag(r) + getTLSFlags(r) + getTimeout(r) + getNoHooks(r) + getDryRunFlags()}, Description: "installing release [ " + r.Name + " ] in namespace [[ " + r.Namespace + " ]] using Tiller in [ " + getDesiredTillerNamespace(r) + " ]", } outcome.addCommand(installCmd, r.Priority, r) @@ -345,6 +351,16 @@ func getSetValues(r *release) string { return result } +// getSetStringValues returns --set-string params to be used with helm install/upgrade commands +func getSetStringValues(r *release) string { + result := "" + for k, v := range r.SetString { + value := substituteEnv(v) + result = result + " --set-string " + 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 { diff --git a/docs/desired_state_specification.md b/docs/desired_state_specification.md index 3b0bbc57..1d415000 100644 --- a/docs/desired_state_specification.md +++ b/docs/desired_state_specification.md @@ -1,5 +1,5 @@ --- -version: v1.5.0 +version: v1.6.2 --- # Helmsman desired state specification @@ -43,9 +43,9 @@ Optional : Yes, only needed if you want Helmsman to connect kubectl to your clus Synopsis: defines where to find the certificates needed for connecting kubectl to a k8s cluster. If connection settings (username/password/clusterAPI) are provided in the Settings section below, then you need AT LEAST to provide caCrt and caKey. You can optionally provide a client certificate (caClient) depending on your cluster connection setup. Options: -- caCrt : a valid S3/GCS bucket or local relative file path to a certificate file. -- caKey : a valid S3/GCS bucket or local relative file path to a client key file. -- caClient: a valid S3/GCS bucket or local relative file path to a client certificate file. +- **caCrt** : a valid S3/GCS bucket or local relative file path to a certificate file. +- **caKey** : a valid S3/GCS bucket or local relative file path to a client key file. +- **caClient**: a valid S3/GCS bucket or local relative file path to a client certificate file. > You can use environment variables to pass the values of the options above. The environment variable name should start with $ @@ -80,13 +80,13 @@ Options: The following options can be skipped if your kubectl context is already created and you don't want Helmsman to connect kubectl to your cluster for you. When using Helmsman in CI pipeline, these details are required to connect to your cluster every time the pipeline is executed. -- username : the username to be used for kubectl credentials. -- password : an environment variable name (starting with `$`) where your password is stored. Get the password from your k8s admin or consult k8s docs on how to get/set it. -- clusterURI : the URI for your cluster API or the name of an environment variable (starting with `$`) containing the URI. -- serviceAccount: the name of the service account to use to initiate helm. This should have enough permissions to allow Helm to work and should exist already in the cluster. More details can be found in [helm's RBAC guide](https://github.com/kubernetes/helm/blob/master/docs/rbac.md) -- storageBackend : by default Helm stores release information in configMaps, using secrets is for storage is recommended for security. Setting this flag to `secret` will deploy/upgrade Tiller with the `--storage=secret`. Other values will be skipped and configMaps will be used. -- slackWebhook : a [Slack](slack.com) Webhook URL to receive Helmsman notifications. This can be passed directly or in an environment variable. -- reverseDelete : if set to `true` it will reverse the priority order whilst deleting. +- **username** : the username to be used for kubectl credentials. +- **password** : an environment variable name (starting with `$`) where your password is stored. Get the password from your k8s admin or consult k8s docs on how to get/set it. +- **clusterURI** : the URI for your cluster API or the name of an environment variable (starting with `$`) containing the URI. +- **serviceAccount**: the name of the service account to use to initiate helm. This should have enough permissions to allow Helm to work and should exist already in the cluster. More details can be found in [helm's RBAC guide](https://github.com/kubernetes/helm/blob/master/docs/rbac.md) +- **storageBackend** : by default Helm stores release information in configMaps, using secrets is for storage is recommended for security. Setting this flag to `secret` will deploy/upgrade Tiller with the `--storage=secret`. Other values will be skipped and configMaps will be used. +- **slackWebhook** : a [Slack](slack.com) Webhook URL to receive Helmsman notifications. This can be passed directly or in an environment variable. +- **reverseDelete** : if set to `true` it will reverse the priority order whilst deleting. > If you use `storageBackend` with a Tiller that has been previously deployed with configMaps as storage backend, you need to migrate your release information from the configMap to the new secret on your own. Helm does not support this yet. @@ -126,12 +126,13 @@ Synopsis: defines the namespaces to be used/created in your k8s cluster and whet If a namespace does not already exist, Helmsman will create it. Options: -- protected : defines if a namespace is protected (true or false). Default false. +- **protected** : defines if a namespace is protected (true or false). Default false. > For the definition of what a protected namespace means, check the [protection guide](how_to/protect_namespaces_and_releases.md) -- installTiller: defines if Tiller should be deployed in this namespace or not. Default is false. Any chart desired to be deployed into a namespace with a Tiller deployed, will be deployed using that Tiller and not the one in kube-system unless you use the `TillerNamespace` option (see the [Apps](#apps) section below) to use another Tiller. +- **installTiller**: defines if Tiller should be deployed in this namespace or not. Default is false. Any chart desired to be deployed into a namespace with a Tiller deployed, will be deployed using that Tiller and not the one in kube-system unless you use the `TillerNamespace` option (see the [Apps](#apps) section below) to use another Tiller. > By default Tiller will be deployed into `kube-system` even if you don't define kube-system in the namespaces section. To prevent deploying Tiller into `kube-system, add kube-system in your namespaces section and set its installTiller to false. +-**useTiller**: defines that you would like to use an existing Tiller from that namespace. Can't be set together with `installTiller` -- tillerServiceAccount: defines what service account to use when deploying Tiller. If this is not set, the following options are considered: +- **tillerServiceAccount**: defines what service account to use when deploying Tiller. If this is not set, the following options are considered: 1. If the `serviceAccount` defined in the `settings` section exists in the namespace you want to deploy Tiller in, it will be used, else 2. Helmsman creates the service account in that namespace and binds it to a role. If the namespace is kube-system, the service account is bound to `cluster-admin` clusterrole. Otherwise, a new role called `helmsman-tiller` is created in that namespace and only gives access to that namespace. @@ -139,11 +140,11 @@ Options: > If `installTiller` is not defined or set to false, this flag is ignored. - The following options are `ALL` needed for deploying Tiller with TLS enabled. If they are not all defined, they will be ignored and Tiller will be deployed without TLS. All of these options can be provided as either: a valid local file path, a valid GCS or S3 bucket URI or an environment variable containing a file path or bucket URI. - - caCert: the CA certificate. - - tillerCert: the SSL certificate for Tiller. - - tillerKey: the SSL certificate private key for Tiller. - - clientCert: the SSL certificate for the Helm client. - - clientKey: the SSL certificate private key for the Helm client. + - **caCert**: the CA certificate. + - **tillerCert**: the SSL certificate for Tiller. + - **tillerKey**: the SSL certificate private key for Tiller. + - **clientCert**: the SSL certificate for the Helm client. + - **clientKey**: the SSL certificate private key for the Helm client. Example: @@ -154,6 +155,7 @@ Example: # installTiller = false # this line can be omitted since installTiller defaults to false [namespaces.staging] [namespaces.dev] +useTiller = true # use a Tiller which has been deployed in dev namespace protected = false [namespaces.production] protected = true @@ -174,6 +176,7 @@ namespaces: staging: dev: protected: false + useTiller: true # use a Tiller which has been deployed in dev namespace production: protected: true installTiller: true @@ -226,39 +229,40 @@ Optional : Yes. Synopsis: defines the releases (instances of Helm charts) you would like to manage in your k8s cluster. -Releases must have unique names which are defined under `apps`. Example: in `[apps.jenkins]`, the release name will be `jenkins` and it should be unique in your cluster. +Releases must have unique names which are defined under `apps`. Example: in `[apps.jenkins]`, the release name will be `jenkins` and it should be unique within the Tiller which manages it . Options: **Required** -- namespace : the namespace where the release should be deployed. The namespace should map to one of the ones defined in [namespaces](#namespaces). -- enabled : describes the required state of the release (true for enabled, false for disabled). Once a release is deployed, you can change it to false if you want to delete this release [default is false]. -- chart : the chart name. It should contain the repo name as well. Example: repoName/chartName. Changing the chart name means delete and reinstall this release using the new Chart. -- version : the chart version. +- **namespace** : the namespace where the release should be deployed. The namespace should map to one of the ones defined in [namespaces](#namespaces). +- **enabled** : describes the required state of the release (true for enabled, false for disabled). Once a release is deployed, you can change it to false if you want to delete this release [default is false]. +- **chart** : the chart name. It should contain the repo name as well. Example: repoName/chartName. Changing the chart name means delete and reinstall this release using the new Chart. +- **version** : the chart version. **Optional** -- tillerNamespace : which Tiller to use for deploying this release. This is available starting from v1.4.0-rc The decision on which Tiller to use for deploying a release follows the following criteria: +- **tillerNamespace** : which Tiller to use for deploying this release. This is available starting from v1.4.0-rc The decision on which Tiller to use for deploying a release follows the following criteria: 1. If `tillerNamespace`is explicitly defined, it is used. 2. If `tillerNamespace`is not defined and the namespace in which the release will be deployed has a Tiller installed by Helmsman (i.e. has `installTiller set to true` in the [Namespaces](#namespaces) section), Tiller in that namespace is used. 3. If none of the above, the shared Tiller in `kube-system` is used. -- name : the Helm release name. Releases must have unique names within a Helm Tiller. If not set, the release name will be taken from the app identifier in your desired state file. e.g, for ` apps.jenkins ` the name release name will be `jenkins`. -- description : a release metadata for human readers. -- valuesFile : a valid path to custom Helm values.yaml file. File extension must be `yaml`. Cannot be used with valuesFiles together. Leaving it empty uses the default chart values. -- valuesFiles : array of valid paths to custom Helm values.yaml file. File extension must be `yaml`. Cannot be used with valuesFile together. Leaving it empty uses the default chart values. +- **name** : the Helm release name. Releases must have unique names within a Helm Tiller. If not set, the release name will be taken from the app identifier in your desired state file. e.g, for ` apps.jenkins ` the release name will be `jenkins`. +- **description** : a release metadata for human readers. +- **valuesFile** : a valid path to custom Helm values.yaml file. File extension must be `yaml`. Cannot be used with valuesFiles together. Leaving it empty uses the default chart values. +- **valuesFiles** : array of valid paths to custom Helm values.yaml file. File extension must be `yaml`. Cannot be used with valuesFile together. Leaving it empty uses the default chart values. > The values file(s) path is relative from the location of the (first) desired state file you pass in your Helmsman command. -- secretsFile : a valid path to custom Helm secrets.yaml file. File extension must be `yaml`. Cannot be used with secretsFiles together. Leaving it empty uses the default chart secrets. -- secretsFiles : array of valid paths to custom Helm secrets.yaml file. File extension must be `yaml`. Cannot be used with secretsFile together. Leaving it empty uses the default chart secrets. +- **secretsFile** : a valid path to custom Helm secrets.yaml file. File extension must be `yaml`. Cannot be used with secretsFiles together. Leaving it empty uses the default chart secrets. +- **secretsFiles** : array of valid paths to custom Helm secrets.yaml file. File extension must be `yaml`. Cannot be used with secretsFile together. Leaving it empty uses the default chart secrets. > The secrets file(s) path is relative from the location of the (first) desired state file you pass in your Helmsman command. > To use the secrets files you must have the helm-secrets plugin -- purge : defines whether to use the Helm purge flag when deleting the release. (true/false) -- test : defines whether to run the chart tests whenever the release is installed. -- protected : defines if the release should be protected against changes. Namespace-level protection has higher priority than this flag. Check the [protection guide](how_to/protect_namespaces_and_releases.md) for more details. -- wait : defines whether Helmsman should block execution until all k8s resources are in a ready state. Default is false. -- timeout : helm timeout in seconds. Default 300 seconds. -- noHooks : helm noHooks option. If true, it will disable pre/post upgrade hooks. Default is false. -- priority : defines the priority of applying operations on this release. Only negative values allowed and the lower the value, the higher the priority. Default priority is 0. Apps with equal priorities will be applied in the order they were added in your state file (DSF). -- [apps..set] : is used to override certain values from values.yaml with values from environment variables (or ,starting from v1.3.0-rc, directly provided in the Desired State File). This is particularly useful for passing secrets to charts. If the an environment variable with the same name as the provided value exists, the environment variable value will be used, otherwise, the provided value will be used as is. +- **purge** : defines whether to use the Helm purge flag when deleting the release. (true/false) +- **test** : defines whether to run the chart tests whenever the release is installed. +- **protected** : defines if the release should be protected against changes. Namespace-level protection has higher priority than this flag. Check the [protection guide](how_to/protect_namespaces_and_releases.md) for more details. +- **wait** : defines whether Helmsman should block execution until all k8s resources are in a ready state. Default is false. +- **timeout** : helm timeout in seconds. Default 300 seconds. +- **noHooks** : helm noHooks option. If true, it will disable pre/post upgrade hooks. Default is false. +- **priority** : defines the priority of applying operations on this release. Only negative values allowed and the lower the value, the higher the priority. Default priority is 0. Apps with equal priorities will be applied in the order they were added in your state file (DSF). +- **set** : is used to override certain values from values.yaml with values from environment variables (or ,starting from v1.3.0-rc, directly provided in the Desired State File). This is particularly useful for passing secrets to charts. If the an environment variable with the same name as the provided value exists, the environment variable value will be used, otherwise, the provided value will be used as is. The TOML stanza for this is `[apps..set]` +- **setString** : is used to override String values from values.yaml or chart's defaults. This uses the `--set-string` flag in helm which is available only in helm >v2.9.0. This option is useful for image tags and the like. The TOML stanza for this is `[apps..setString]` Example: @@ -283,6 +287,8 @@ Example: [apps.jenkins.set] secret1="$SECRET_ENV_VAR1" secret2="SECRET_ENV_VAR2" # works with/without $ at the beginning + [apps.jenkins.setString] + longInt = "1234567890" ``` ```yaml @@ -303,4 +309,6 @@ apps: set: secret1: "$SECRET_ENV_VAR1" secret2: "$SECRET_ENV_VAR2" + setString: + longInt: "1234567890" ``` diff --git a/example.toml b/example.toml index a9648028..2055bc5d 100644 --- a/example.toml +++ b/example.toml @@ -1,4 +1,4 @@ -# version: v1.5.0 +# version: v1.6.2 # metadata -- add as many key/value pairs as you want [metadata] org = "example.com" @@ -74,8 +74,10 @@ protected = true priority= -3 wait = true - [apps.jenkins.set] # values to override values from values.yaml with values from env vars or directly entered-- useful for passing secrets to charts + [apps.jenkins.setString] # values to override values from values.yaml with values from env vars or directly entered-- useful for passing secrets to charts AdminPassword="$JENKINS_PASSWORD" # $JENKINS_PASSWORD must exist in the environment + MyLongIntVar="1234567890" + [apps.jenkins.set] AdminUser="admin" diff --git a/example.yaml b/example.yaml index 360ed1a8..afe85013 100644 --- a/example.yaml +++ b/example.yaml @@ -73,6 +73,9 @@ apps: set: # values to override values from values.yaml with values from env vars-- useful for passing secrets to charts AdminPassword: "$JENKINS_PASSWORD" # $JENKINS_PASSWORD must exist in the environment AdminUser: "admin" + setString: + MyLongIntVar: "1234567890" + # artifactory will be deployed using the Tiller in the kube-system namespace artifactory: diff --git a/init.go b/init.go index b48cd63c..d697ed22 100644 --- a/init.go +++ b/init.go @@ -43,6 +43,7 @@ func init() { flag.BoolVar(&apply, "apply", false, "apply the plan directly") flag.BoolVar(&debug, "debug", false, "show the execution logs") flag.BoolVar(&dryRun, "dry-run", false, "apply the dry-run option for helm commands.") + flag.BoolVar(&destroy, "destroy", false, "delete all deployed releases. Purge delete is used if the purge option is set to true for the releases.") flag.BoolVar(&v, "v", false, "show the version") flag.BoolVar(&verbose, "verbose", false, "show verbose execution logs") flag.BoolVar(&noBanner, "no-banner", false, "don't show the banner") @@ -71,6 +72,10 @@ func init() { logError("ERROR: --apply and --dry-run can't be used together.") } + if destroy && apply { + logError("ERROR: --destroy and --apply can't be used together.") + } + helmVersion = strings.TrimSpace(strings.SplitN(getHelmClientVersion(), ": ", 2)[1]) kubectlVersion = strings.TrimSpace(strings.SplitN(getKubectlClientVersion(), ": ", 2)[1]) @@ -128,11 +133,6 @@ func init() { } } - // print all env variables - // for _, pair := range os.Environ() { - // fmt.Println(pair) - // } - // read the TOML/YAML desired state file var fileState state for _, f := range files { diff --git a/main.go b/main.go index 043694ac..a53eacc5 100644 --- a/main.go +++ b/main.go @@ -32,12 +32,13 @@ var checkCleanup bool var skipValidation bool var applyLabels bool var keepUntrackedReleases bool -var appVersion = "v1.6.1" +var appVersion = "v1.6.2" var helmVersion string var kubectlVersion string var pwd string var relativeDir string var dryRun bool +var destroy bool func main() { // set the kubecontext to be used Or create it if it does not exist @@ -81,6 +82,9 @@ func main() { } log.Println("INFO: checking what I need to do for your charts ... ") + if destroy { + log.Println("WARN: --destroy is enabled. Your releases will be deleted!") + } p := makePlan(&s) if !keepUntrackedReleases { @@ -91,7 +95,7 @@ func main() { p.printPlan() p.sendPlanToSlack() - if apply || dryRun { + if apply || dryRun || destroy { p.execPlan() } diff --git a/release.go b/release.go index 6e7c3ef6..f8803226 100644 --- a/release.go +++ b/release.go @@ -5,28 +5,31 @@ import ( "log" "os" "strings" + + version "github.com/hashicorp/go-version" ) // 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 + Name string `yaml:"name"` + Description string `yaml:"description"` + Namespace string `yaml:"namespace"` + Enabled bool `yaml:"enabled"` + Chart string `yaml:"chart"` + Version string `yaml:"version"` ValuesFile string `yaml:"valuesFile"` ValuesFiles []string `yaml:"valuesFiles"` SecretFile string `yaml:"secretFile"` SecretFiles []string `yaml:"secretFiles"` - Purge bool - Test bool - Protected bool - Wait bool - Priority int - TillerNamespace string + Purge bool `yaml:"purge"` + Test bool `yaml:"test"` + Protected bool `yaml:"protected"` + Wait bool `yaml:"wait"` + Priority int `yaml:"priority"` + TillerNamespace string `yaml:"tillerNamespace"` Set map[string]string - NoHooks bool + SetString map[string]string `yaml:"setString"` + NoHooks bool `yaml:"noHooks"` Timeout int } @@ -105,6 +108,14 @@ func validateRelease(appLabel string, r *release, names map[string]map[string]bo names[r.Name]["kube-system"] = true } + if len(r.SetString) > 0 { + v1, _ := version.NewVersion(helmVersion) + setStringConstraint, _ := version.NewConstraint(">=2.9.0") + if !setStringConstraint.Check(v1) { + return false, "you are using setString in your desired state, but your helm client does not support it. You need helm v2.9.0 or above for this feature." + } + } + return true, "" } diff --git a/test_files/dockerfile b/test_files/dockerfile index f7f41e03..4b6a0216 100644 --- a/test_files/dockerfile +++ b/test_files/dockerfile @@ -3,7 +3,7 @@ FROM golang:1.10-alpine3.7 as builder ENV KUBE_VERSION v1.8.2 -ENV HELM_VERSION v2.7.0 +ENV HELM_VERSION v2.10.0 RUN apk add --update --no-cache ca-certificates git \ && apk add --update -t deps curl tar gzip make bash \