├── tests ├── values.xml ├── values.yaml ├── values2.yaml ├── chart-test │ └── Chart.yaml ├── post-renderer.sh ├── Invalid_example_spec.yaml ├── overlay.sample.yaml ├── invalid_example.toml ├── invalid_example.yaml ├── secrets │ └── valid_eyaml_secrets.yaml └── keys │ ├── public_key.pkcs7.pem │ └── private_key.pkcs7.pem ├── examples ├── appsTemplates │ ├── apps │ │ ├── tomcat │ │ │ ├── testing-values.yaml │ │ │ ├── development-values.yaml │ │ │ └── common-values.yaml │ │ └── puppetserver │ │ │ ├── testing-values.yaml │ │ │ ├── development-values.yaml │ │ │ └── common-values.yaml │ ├── README.md │ └── config │ │ └── helmsman.yaml ├── composition │ ├── spec.yaml │ ├── README.md │ ├── main.yaml │ ├── kyverno.yaml │ ├── artifactory.yaml │ └── argo.yaml ├── example-spec.yaml ├── job.yaml ├── minimal-example-overwrite.yaml ├── minimal-example.yaml ├── minimal-example.toml ├── example.yaml └── example.toml ├── .dockerignore ├── docs ├── images │ ├── helmsman.png │ ├── multi-DSF.png │ ├── CI-pipeline-helmsman.jpg │ ├── CI-pipeline-after-helm.jpg │ └── CI-pipeline-before-helm.jpg ├── how_to │ ├── misc │ │ ├── use-dry-code.md │ │ ├── send_slack_notifications_from_helmsman.md │ │ ├── send_ms_teams_notifications_from_helmsman.md │ │ ├── multiple_desired_state_files_specification.md │ │ ├── auth_to_storage_providers.md │ │ ├── limit-deployment-to-specific-apps.md │ │ ├── limit-deployment-to-specific-group-of-apps.md │ │ ├── exclude-apps-or-groups-from-deployment.md │ │ ├── migrate_to_3.md │ │ ├── protect_namespaces_and_releases.md │ │ └── merge_desired_state_files.md │ ├── helm_repos │ │ ├── local.md │ │ ├── README.md │ │ ├── s3.md │ │ ├── pre_configured.md │ │ ├── basic_auth.md │ │ ├── oci.md │ │ ├── gcs.md │ │ └── default.md │ ├── namespaces │ │ ├── create.md │ │ ├── protection.md │ │ ├── labels_and_annotations.md │ │ ├── quotas.md │ │ └── limits.md │ ├── apps │ │ ├── destroy.md │ │ ├── override_context_from_cmd.md │ │ ├── protection.md │ │ ├── helm_tests.md │ │ ├── migrate_contexts.md │ │ ├── environment_vars.md │ │ ├── multiple_values_files.md │ │ ├── secrets.md │ │ ├── order.md │ │ ├── override_namespaces.md │ │ ├── basic.md │ │ ├── moving_across_namespaces.md │ │ └── lifecycle_hooks.md │ ├── settings │ │ ├── current_kube_context.md │ │ ├── existing_kube_context.md │ │ ├── use-hiera-eyaml-as-secrets-encryption.md │ │ ├── creating_kube_context_with_token.md │ │ └── creating_kube_context_with_certs.md │ ├── deployments │ │ ├── ci.md │ │ └── inside_k8s.md │ └── README.md ├── best_practice.md ├── why_helmsman.md ├── cmd_reference.md └── deployment_strategies.md ├── .commitlintrc.yaml ├── .github ├── dependabot.yml └── workflows │ ├── stale.yml │ ├── test.yml │ └── build.yml ├── CODEOWNERS ├── cmd └── helmsman │ └── main.go ├── .gitignore ├── schema.go ├── internal ├── app │ ├── spec_state.go │ ├── spec_state_test.go │ ├── helm_time.go │ ├── logging.go │ ├── hooks.go │ ├── custom_types.go │ ├── helm_helpers_test.go │ ├── namespace.go │ ├── cli_test.go │ ├── main.go │ ├── release_files.go │ ├── helm_release.go │ ├── custom_types_test.go │ ├── plan_test.go │ └── command.go ├── aws │ ├── aws_test.go │ └── aws.go ├── gcs │ └── gcs.go └── azure │ └── azblob.go ├── LICENSE ├── .goreleaser.yaml ├── CONTRIBUTION.md ├── go.mod ├── Makefile └── README.md /tests/values.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/values.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/values2.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/appsTemplates/apps/tomcat/testing-values.yaml: -------------------------------------------------------------------------------- 1 | hostPort: 8010 2 | -------------------------------------------------------------------------------- /examples/appsTemplates/apps/tomcat/development-values.yaml: -------------------------------------------------------------------------------- 1 | hostPort: 8009 2 | -------------------------------------------------------------------------------- /tests/chart-test/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | name: chart-test 3 | version: 1.0.0 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .circleci 3 | .github 4 | keys 5 | vendor 6 | .gitignore 7 | .idea 8 | -------------------------------------------------------------------------------- /docs/images/helmsman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkubaczyk/helmsman/HEAD/docs/images/helmsman.png -------------------------------------------------------------------------------- /docs/images/multi-DSF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkubaczyk/helmsman/HEAD/docs/images/multi-DSF.png -------------------------------------------------------------------------------- /tests/post-renderer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ytt --ignore-unknown-comments -f - -f $(dirname $0)/overlay.sample.yaml 4 | -------------------------------------------------------------------------------- /docs/images/CI-pipeline-helmsman.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkubaczyk/helmsman/HEAD/docs/images/CI-pipeline-helmsman.jpg -------------------------------------------------------------------------------- /docs/images/CI-pipeline-after-helm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkubaczyk/helmsman/HEAD/docs/images/CI-pipeline-after-helm.jpg -------------------------------------------------------------------------------- /docs/images/CI-pipeline-before-helm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkubaczyk/helmsman/HEAD/docs/images/CI-pipeline-before-helm.jpg -------------------------------------------------------------------------------- /examples/appsTemplates/apps/puppetserver/testing-values.yaml: -------------------------------------------------------------------------------- 1 | r10k: 2 | code: 3 | cronJob: 4 | schedule: "*/1 * * * *" 5 | -------------------------------------------------------------------------------- /tests/Invalid_example_spec.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | stateFiles: 3 | name1: invalid/example.yaml 4 | name2: invalid/minimal-example.yaml 5 | -------------------------------------------------------------------------------- /examples/appsTemplates/apps/puppetserver/development-values.yaml: -------------------------------------------------------------------------------- 1 | r10k: 2 | code: 3 | cronJob: 4 | schedule: "*/2 * * * *" 5 | -------------------------------------------------------------------------------- /examples/appsTemplates/apps/puppetserver/common-values.yaml: -------------------------------------------------------------------------------- 1 | puppetserver: 2 | puppeturl: "https://github.com/puppetlabs/control-repo.git" 3 | -------------------------------------------------------------------------------- /.commitlintrc.yaml: -------------------------------------------------------------------------------- 1 | extends: 2 | - '@commitlint/config-conventional' 3 | rules: 4 | type-enum: 5 | - 2 6 | - always 7 | - [feat, fix, chore] 8 | -------------------------------------------------------------------------------- /examples/composition/spec.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | stateFiles: 3 | - path: main.yaml 4 | - path: argo.yaml 5 | - path: artifactory.yaml 6 | - path: kyverno.yaml 7 | -------------------------------------------------------------------------------- /examples/appsTemplates/apps/tomcat/common-values.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | limits: 3 | cpu: 100m 4 | memory: 256Mi 5 | requests: 6 | cpu: 100m 7 | memory: 256Mi 8 | -------------------------------------------------------------------------------- /examples/example-spec.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | stateFiles: 3 | - path: examples/example.yaml 4 | - path: examples/minimal-example.yaml 5 | - path: examples/minimal-example.toml 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "sunday" 8 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Workflow files require owner approval 2 | /.github/workflows/ @mkubaczyk 3 | 4 | # Docker and release configuration 5 | /Dockerfile @mkubaczyk 6 | /.goreleaser.yaml @mkubaczyk 7 | -------------------------------------------------------------------------------- /cmd/helmsman/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/mkubaczyk/helmsman/internal/app" 7 | ) 8 | 9 | func main() { 10 | exitCode := app.Main() 11 | os.Exit(exitCode) 12 | } 13 | -------------------------------------------------------------------------------- /docs/how_to/misc/use-dry-code.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.0.0 3 | --- 4 | 5 | # Use DRY-ed code in YAML 6 | 7 | If you want to use as a baseline or deploy the DRY-ed example, please refer to the provided [app templates](../../../examples/appsTemplates) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.passwd 2 | *.key 3 | *.crt 4 | /dist 5 | /vendor/ 6 | *.world 7 | *.world1 8 | helmsman 9 | helmsman.exe 10 | keys/* 11 | !tests/secrets/keys 12 | !cmd/**/* 13 | .helmsman-tmp 14 | .idea 15 | # Added by goreleaser init: 16 | dist/ 17 | -------------------------------------------------------------------------------- /examples/composition/README.md: -------------------------------------------------------------------------------- 1 | # Composition 2 | 3 | Desired state configuration can be split into multiplle files and applied with: 4 | 5 | ```sh 6 | helmsman --apply -f main.yaml -f argo.yaml -f artifactory.yaml 7 | ``` 8 | 9 | or using a spec file: 10 | 11 | ```sh 12 | helmsman --apply --spec spec.yaml 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/job.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: pi 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: pi 10 | image: perl 11 | command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] 12 | restartPolicy: Never 13 | backoffLimit: 4 14 | -------------------------------------------------------------------------------- /examples/minimal-example-overwrite.yaml: -------------------------------------------------------------------------------- 1 | ## This is a minimal example. 2 | ## It will use your current kube context and will deploy Tiller without RBAC service account. 3 | ## For the full config spec and options, check https://github.com/mkubaczyk/helmsman/blob/master/docs/desired_state_specification.md 4 | 5 | apps: 6 | jenkins: 7 | enabled: false 8 | -------------------------------------------------------------------------------- /examples/appsTemplates/README.md: -------------------------------------------------------------------------------- 1 | # DRY-ed Example 2 | 3 | ## Execution Plan 4 | 5 | To deploy the DRY-ed example, the app templates placed in [config](config) and [apps](apps) need to be used. To do so - please run: 6 | 7 | ```bash 8 | helmsman -apply -f config/helmsman.yaml 9 | ``` 10 | 11 | > **Tip**: That kind of DRY-ed code can be achieved only using YAML desired state files. 12 | -------------------------------------------------------------------------------- /tests/overlay.sample.yaml: -------------------------------------------------------------------------------- 1 | #@ load("@ytt:overlay", "overlay") 2 | #@overlay/remove 3 | #@overlay/match by=overlay.subset({"kind":"ServiceAccount"}),expects="1+" 4 | --- 5 | #@overlay/merge 6 | #@overlay/match by=overlay.subset({"kind":"Deployment"}),expects="1+" 7 | --- 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | spec: 11 | template: 12 | spec: 13 | serviceAccountName: default 14 | -------------------------------------------------------------------------------- /examples/composition/main.yaml: -------------------------------------------------------------------------------- 1 | context: test-infra 2 | 3 | metadata: 4 | org: "example.com/my_org/" 5 | maintainer: "k8s-admin (me@example.com)" 6 | description: "example Desired State File for demo purposes." 7 | 8 | settings: 9 | kubeContext: "minikube" 10 | globalMaxHistory: 5 11 | 12 | helmRepos: 13 | argo: "https://argoproj.github.io/argo-helm" 14 | jfrog: "https://charts.jfrog.io" 15 | -------------------------------------------------------------------------------- /tests/invalid_example.toml: -------------------------------------------------------------------------------- 1 | # THIS IS AN INVALID TOML FILE USED FOR TESTING PURPOSES ONLY. 2 | [metadata] 3 | org = orgX 4 | maintainer = "k8s-admin" 5 | 6 | [certificates] 7 | 8 | [settings] 9 | kubeContext = 10 | 11 | [namespaces] 12 | staging = "staging" 13 | production = "default" 14 | 15 | [helmRepos] 16 | jenkins = "https://charts.jenkins.io" 17 | jfrog = "https://charts.jfrog.io" 18 | 19 | [apps] 20 | -------------------------------------------------------------------------------- /tests/invalid_example.yaml: -------------------------------------------------------------------------------- 1 | # THIS IS AN INVALID YAML FILE USED FOR TESTING PURPOSES ONLY. 2 | metadata: 3 | org: orgX 4 | maintainer: "k8s-admin" 5 | 6 | certificates: 7 | 8 | settings: 9 | kubeContext: 10 | 11 | namespaces: 12 | staging: "staging" 13 | production: "default" 14 | 15 | helmRepos: 16 | jenkins: "https://charts.jenkins.io" 17 | jfrog: "https://charts.jfrog.io" 18 | 19 | apps: 20 | -------------------------------------------------------------------------------- /docs/how_to/helm_repos/local.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v1.3.0-rc 3 | --- 4 | 5 | # Use local helm charts 6 | 7 | You can use your locally developed charts. 8 | 9 | ## From file system 10 | 11 | If you use a file path (relative to the DSF, or absolute) for the ```chart``` attribute 12 | helmsman will try to resolve that chart from the local file system. The chart on the 13 | local file system must have a version matching the version specified in the DSF. 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/how_to/helm_repos/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Defining Helm Repositories 3 | 4 | Following list contains guides on how to define helm repositories 5 | 6 | - [Using default helm repos](default.md) 7 | - [Using private repos in Google GCS](gcs.md) 8 | - [Using private repos in AWS S3](s3.md) 9 | - [Using private repos with basic auth](basic_auth.md) 10 | - [Using pre-configured repos](pre_configured.md) 11 | - [Using local charts](local.md) 12 | - [Using OCI registries](oci.md) 13 | -------------------------------------------------------------------------------- /docs/how_to/namespaces/create.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v1.8.0 3 | --- 4 | 5 | # Create namespaces 6 | 7 | You can define namespaces to be used in your cluster. If they don't exist, Helmsman will create them for you. 8 | 9 | ```toml 10 | #... 11 | 12 | [namespaces] 13 | [namespaces.staging] 14 | [namespaces.production] 15 | 16 | #... 17 | ``` 18 | 19 | ```yaml 20 | 21 | namespaces: 22 | staging: 23 | production: 24 | 25 | ``` 26 | 27 | The example above will create two namespaces; staging and production. -------------------------------------------------------------------------------- /docs/how_to/apps/destroy.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.0.0-beta5 3 | --- 4 | 5 | # Delete all deployed releases 6 | 7 | Helmsman allows you to delete all the helm releases that were deployed by Helmsman from a given desired state. 8 | 9 | The `--destroy` flag will remove all deployed releases from a given desired state file (DSF). Note that this does not currently delete the namespaces nor the Kubernetes contexts created. 10 | 11 | This was originally requested in issue [#88](https://github.com/mkubaczyk/helmsman/issues/88). 12 | 13 | -------------------------------------------------------------------------------- /tests/secrets/valid_eyaml_secrets.yaml: -------------------------------------------------------------------------------- 1 | value: ENC[PKCS7,MIIBeQYJKoZIhvcNAQcDoIIBajCCAWYCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAkEDncOHeDmZ19t7YpI6cgnuwszv4Hg7N3/h0RN2rKm+TrUag4bMc4ePypgFeGYutLcIkcxT6jmbGxVOWHY86jLojlnrcoYaD3bk9QVAxcrOPkXZRA7jTRkloUKGyXSIb6AMjs/1oGmrAmT2o/DkzrJxEXN6kiXF/W0Jf+StC7wBg/daVCioqUU7YT8NV6Pp7jyNxgFCCf8OTRFv7147/B+1QzBMa/i59O6+s2ERoVut2AT2BcAPA6efRtB5K+lZRi65CIrSAfM5svNHaYAFfTV5yWoZQDDWXbdSA5zzWHPFeiQkiu5QwaI9elgT9BpgWytXdWSBU46vv9EN3ri8FCjA8BgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBCk92BXC05sXpZeBYsUi5OwgBC4BbiUBeDabYS9h0cU1psK] 2 | -------------------------------------------------------------------------------- /schema.go: -------------------------------------------------------------------------------- 1 | // go:build exclude 2 | 3 | package main 4 | 5 | import ( 6 | "encoding/json" 7 | "os" 8 | 9 | "github.com/invopop/jsonschema" 10 | "github.com/mkubaczyk/helmsman/internal/app" 11 | ) 12 | 13 | func main() { 14 | r := new(jsonschema.Reflector) 15 | r.AllowAdditionalProperties = true 16 | if err := r.AddGoComments("github.com/mkubaczyk/helmsman", "./internal/app"); err != nil { 17 | panic(err) 18 | } 19 | s := r.Reflect(&app.State{}) 20 | data, _ := json.MarshalIndent(s, "", " ") 21 | os.WriteFile("schema.json", data, 0o644) 22 | } 23 | -------------------------------------------------------------------------------- /examples/composition/kyverno.yaml: -------------------------------------------------------------------------------- 1 | helmRepos: 2 | kyverno: https://kyverno.github.io/kyverno/ 3 | 4 | namespaces: 5 | kyverno: 6 | protected: false 7 | 8 | apps: 9 | kyverno: 10 | namespace: kyverno 11 | enabled: true 12 | chart: kyverno/kyverno 13 | version: 2.4.1 14 | kyverno-policies: 15 | namespace: kyverno 16 | enabled: true 17 | chart: kyverno/kyverno-policies 18 | version: 2.4.0 19 | kyverno-reporter: 20 | namespace: kyverno 21 | enabled: true 22 | chart: kyverno/kyverno-reporter 23 | version: 2.9.0 24 | -------------------------------------------------------------------------------- /docs/how_to/settings/current_kube_context.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v1.8.0 3 | --- 4 | 5 | # Cluster connection -- Using the current kube context 6 | 7 | Helmsman can use the current configured kube context. In this case, the `kubeContext` field in the `settings` stanza needs to be left empty. If no other `settings` fields are needed, you can delete the whole `settings` stanza. 8 | 9 | 10 | If you want Helmsman to create the kube context for you, see [this guide](creating_kube_context_with_certs.md) for more details on creating a context with certs or [here](creating_kube_context_with_token.md) for details on creating context with bearer token. -------------------------------------------------------------------------------- /examples/composition/artifactory.yaml: -------------------------------------------------------------------------------- 1 | namespaces: 2 | production: 3 | protected: true 4 | limits: 5 | - type: Container 6 | default: 7 | cpu: "300m" 8 | memory: "200Mi" 9 | defaultRequest: 10 | cpu: "200m" 11 | memory: "100Mi" 12 | - type: Pod 13 | max: 14 | memory: "300Mi" 15 | 16 | apps: 17 | artifactory: 18 | namespace: "production" 19 | enabled: true 20 | chart: "jfrog/artifactory" 21 | version: "8.3.2" 22 | valuesFile: "" 23 | test: false 24 | priority: -2 25 | noHooks: false 26 | timeout: 300 27 | maxHistory: 4 28 | helmFlags: 29 | - "--devel" 30 | -------------------------------------------------------------------------------- /examples/minimal-example.yaml: -------------------------------------------------------------------------------- 1 | ## This is a minimal example. 2 | ## It will use your current kube context and will deploy Tiller without RBAC service account. 3 | ## For the full config spec and options, check https://github.com/mkubaczyk/helmsman/blob/master/docs/desired_state_specification.md 4 | helmRepos: 5 | jenkins: https://charts.jenkins.io 6 | jfrog: https://charts.jfrog.io 7 | 8 | namespaces: 9 | staging: 10 | 11 | apps: 12 | jenkins: 13 | namespace: staging 14 | enabled: true 15 | chart: jenkins/jenkins 16 | version: 2.15.1 17 | 18 | artifactory: 19 | namespace: staging 20 | enabled: true 21 | chart: jfrog/artifactory 22 | version: 11.4.2 23 | -------------------------------------------------------------------------------- /docs/how_to/helm_repos/s3.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v1.8.0 3 | --- 4 | 5 | # Using private helm repos in S3 6 | 7 | Helmsman allows you to use private charts from private repos. Currently only repos hosted in S3 or GCS buckets are supported for private repos. 8 | 9 | You need to provide one of the following env variables: 10 | 11 | - `AWS_ACCESS_KEY_ID` 12 | - `AWS_SECRET_ACCESS_KEY` 13 | - `AWS_DEFAULT_REGION` 14 | 15 | Helmsman uses the [helm s3](https://github.com/hypnoglow/helm-s3) plugin to work with S3 helm repos. 16 | 17 | ```toml 18 | [helmRepos] 19 | myPrivateRepo = "s3://this-is-a-private-repo/charts" 20 | ``` 21 | 22 | ```yaml 23 | helmRepos: 24 | myPrivateRepo: "s3://this-is-a-private-repo/charts" 25 | ``` 26 | -------------------------------------------------------------------------------- /internal/app/spec_state.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "sigs.k8s.io/yaml" 7 | ) 8 | 9 | type StatePath struct { 10 | Path string `json:"path"` 11 | } 12 | 13 | type StateFiles struct { 14 | StateFiles []StatePath `json:"stateFiles"` 15 | } 16 | 17 | // specFromYAML reads a yaml file and decodes it to a state type. 18 | // parser which throws an error if the YAML file is not valid. 19 | func (pc *StateFiles) specFromYAML(file string) error { 20 | rawYamlFile, err := ioutil.ReadFile(file) 21 | if err != nil { 22 | log.Errorf("specFromYaml %v %v", file, err) 23 | return err 24 | } 25 | 26 | yamlFile := string(rawYamlFile) 27 | 28 | return yaml.Unmarshal([]byte(yamlFile), pc) 29 | } 30 | -------------------------------------------------------------------------------- /docs/how_to/helm_repos/pre_configured.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v1.8.0 3 | --- 4 | 5 | # Using pre-configured helm repos 6 | 7 | The primary use-case is if you have some helm repositories that require HTTP basic authentication and you don't want to store the password in the desired state file or as an environment variable. In this case you can execute the following sequence to have those repositories configured: 8 | 9 | Set up the helmsman configuration: 10 | 11 | ```toml 12 | preconfiguredHelmRepos = [ "myrepo1", "myrepo2" ] 13 | ``` 14 | 15 | ```yaml 16 | preconfiguredHelmRepos: 17 | - myrepo1 18 | - myrepo2 19 | ``` 20 | 21 | > In this case you will manually need to execute `helm repo add myrepo1 --username= --password=` 22 | -------------------------------------------------------------------------------- /examples/composition/argo.yaml: -------------------------------------------------------------------------------- 1 | namespaces: 2 | staging: 3 | protected: false 4 | labels: 5 | env: "staging" 6 | quotas: 7 | limits.cpu: "10" 8 | limits.memory: "20Gi" 9 | pods: 25 10 | requests.cpu: "10" 11 | requests.memory: "30Gi" 12 | customQuotas: 13 | - name: "requests.nvidia.com/gpu" 14 | value: "2" 15 | 16 | helmRepos: 17 | argo: "https://argoproj.github.io/argo-helm" 18 | 19 | apps: 20 | argo: 21 | namespace: "staging" 22 | enabled: true 23 | chart: "argo/argo" 24 | version: "0.8.5" 25 | valuesFile: "" 26 | test: false 27 | protected: true 28 | priority: -3 29 | wait: true 30 | set: 31 | "images.tag": latest 32 | -------------------------------------------------------------------------------- /examples/minimal-example.toml: -------------------------------------------------------------------------------- 1 | ## This is a minimal example. 2 | ## It will use your current kube context and will deploy Tiller without RBAC service account. 3 | ## For the full config spec and options, check https://github.com/mkubaczyk/helmsman/blob/master/docs/desired_state_specification.md 4 | 5 | [helmRepos] 6 | jenkins = "https://charts.jenkins.io" 7 | jfrog = "https://charts.jfrog.io" 8 | 9 | [namespaces] 10 | [namespaces.staging] 11 | 12 | [apps] 13 | [apps.jenkins] 14 | namespace = "staging" 15 | enabled = true 16 | chart = "jenkins/jenkins" 17 | version = "2.15.1" 18 | 19 | [apps.artifactory] 20 | namespace = "staging" 21 | enabled = true 22 | chart = "jfrog/artifactory" 23 | version = "11.4.2" 24 | -------------------------------------------------------------------------------- /docs/how_to/helm_repos/basic_auth.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v1.8.0 3 | --- 4 | 5 | # Using private helm repos with basic auth 6 | 7 | Helmsman allows you to use any private helm repo hosting which supports basic auth (e.g. Artifactory). 8 | 9 | For such repos, you need to add the basic auth information in the repo URL as in the example below: 10 | 11 | > Be aware that some special characters in the username or password can make the URL invalid. 12 | 13 | ```toml 14 | [helmRepos] 15 | # PASS is an env var containing the password 16 | myPrivateRepo = "https://user:$PASS@myprivaterepo.org" 17 | ``` 18 | 19 | ```yaml 20 | helmRepos: 21 | # PASS is an env var containing the password 22 | myPrivateRepo: "https://user:$PASS@myprivaterepo.org" 23 | ``` 24 | 25 | -------------------------------------------------------------------------------- /docs/how_to/misc/send_slack_notifications_from_helmsman.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v1.5.0 3 | --- 4 | 5 | # Slack notifications from Helmsman 6 | 7 | Starting from v1.4.0-rc, Helmsman can send slack notifications to a channel of your choice. To enable the notifications, simply add a `slack webhook` in the `settings` section of your desired state file. The webhook URL can be passed directly or from an environment variable. 8 | 9 | ```toml 10 | [settings] 11 | ... 12 | slackWebhook = $MY_SLACK_WEBHOOK 13 | ``` 14 | 15 | ```yaml 16 | settings: 17 | # ... 18 | slackWebhook : "$MY_SLACK_WEBHOOK" 19 | # ... 20 | ``` 21 | 22 | ## Getting a Slack Webhook URL 23 | 24 | Follow the [slack guide](https://api.slack.com/incoming-webhooks) for generating a webhook URL. 25 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: '0 5 * * 1' 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | permissions: 12 | issues: write 13 | pull-requests: write 14 | 15 | steps: 16 | - uses: actions/stale@v9 17 | with: 18 | repo-token: ${{ secrets.GITHUB_TOKEN }} 19 | days-before-stale: 60 20 | days-before-close: 30 21 | stale-issue-message: 'This issue has been marked stale due to an inactivity.' 22 | stale-pr-message: 'This PR has been marked stale due to an inactivity.' 23 | stale-issue-label: 'stale' 24 | stale-pr-label: 'stale' 25 | ascending: true 26 | operations-per-run: '60' 27 | -------------------------------------------------------------------------------- /docs/how_to/misc/send_ms_teams_notifications_from_helmsman.md: -------------------------------------------------------------------------------- 1 | # Microsoft Teams notifications from Helmsman 2 | 3 | Helmsman can send MS Teams notifications to a channel of your choice. To enable the notifications, simply add a `msTeamsWebhook webhook` in the `settings` section of your desired state file. The webhook URL can be passed directly or from an environment variable. 4 | 5 | ```toml 6 | [settings] 7 | ... 8 | msTeamsWebhook = $MY_MS_TEAMS_WEBHOOK 9 | ``` 10 | 11 | ```yaml 12 | settings: 13 | # ... 14 | msTeamsWebhook : "$MY_MS_TEAMS_WEBHOOK" 15 | # ... 16 | ``` 17 | 18 | ## Getting a MS Teams Webhook URL 19 | 20 | Follow the [Microsoft Teams Guide](https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook) for generating a webhook URL. 21 | -------------------------------------------------------------------------------- /docs/how_to/helm_repos/oci.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.7.1 3 | --- 4 | 5 | # Using OCI registries for helm charts 6 | 7 | Helmsman allows you to use charts stored in OCI registries. 8 | 9 | You need to export the following env variables: 10 | 11 | - `HELM_EXPERIMENTAL_OCI=1` 12 | 13 | if the registry requires authentication, you must login before running Helmsman 14 | 15 | ```sh 16 | helm registry login -u myuser my-registry.local 17 | ``` 18 | 19 | ```toml 20 | [apps] 21 | [apps.my-app] 22 | chart = "oci://my-registry.local/my-chart" 23 | version = "1.0.0" 24 | ``` 25 | 26 | ```yaml 27 | #... 28 | apps: 29 | my-app: 30 | chart: oci://my-registry.local/my-chart 31 | version: 1.0.0 32 | ``` 33 | 34 | For more information, read the [helm registries documentation](https://helm.sh/docs/topics/registries/). 35 | -------------------------------------------------------------------------------- /docs/how_to/settings/existing_kube_context.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v1.8.0 3 | --- 4 | 5 | # Cluster connection -- Using an existing kube context 6 | 7 | Helmsman can use any predefined kube context in the environment. All you need to do is set the context name in the `settings` stanza. 8 | 9 | ```toml 10 | [settings] 11 | kubeContext = "minikube" 12 | ``` 13 | 14 | ```yaml 15 | settings: 16 | kubeContext: "minikube" 17 | ``` 18 | 19 | In the examples above, Helmsman tries to set the kube context to `minikube`. If that fails, it will attempt to create that kube context. Creating kube context requires more information provided. See [this guide](creating_kube_context_with_certs.md) for more details on creating a context with certs or [here](creating_kube_context_with_token.md) for details on creating context with bearer token. 20 | -------------------------------------------------------------------------------- /docs/how_to/apps/override_context_from_cmd.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.2.0 3 | --- 4 | 5 | # Override Helmsman context name from CMD flags using `--context-override` 6 | 7 | There are two main use cases for this flag: 8 | 9 | 1. To speed up Helmsman's execution when you have too many release (see [issue #418](https://github.com/mkubaczyk/helmsman/issues/418)) 10 | This flag works by skipping the search for the context information which Helmsman adds in the form of labels to the helm release state (secrets/configmaps). 11 | 12 | > Use this option with caution. You must be sure that this won't cause conflicts. 13 | 14 | 2. [Not recommended] If ,for whatever reason, you want to temporarily override the context defined on the release state (in labels on secrets/configmaps) with something. **Use [`--migrate-context`](migrate_contexts.md) instead to permanently rename your context** -------------------------------------------------------------------------------- /docs/how_to/apps/protection.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v1.8.0 3 | --- 4 | 5 | # Protecting apps (releases) 6 | 7 | You can define apps to be protected using the `protected` field. Please check [this doc](../misc/protect_namespaces_and_releases.md) for details about what protection means and the difference between namespace-level and release-level protection. 8 | 9 | Here is an example of a protected app: 10 | 11 | ```toml 12 | [apps] 13 | 14 | [apps.jenkins] 15 | namespace = "staging" 16 | enabled = true 17 | chart = "jenkins/jenkins" 18 | version = "2.15.1" 19 | protected = true # defining this release to be protected. 20 | ``` 21 | 22 | ```yaml 23 | apps: 24 | 25 | jenkins: 26 | namespace: "staging" 27 | enabled: true 28 | chart: "jenkins/jenkins" 29 | version: "2.15.1" 30 | protected: true # defining this release to be protected. 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/how_to/namespaces/protection.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v1.8.0 3 | --- 4 | 5 | # Protecting namespaces 6 | 7 | You can define namespaces to be used in your cluster. If they don't exist, Helmsman will create them for you. 8 | 9 | You can also define certain namespaces to be protected using the `protected` field. Please check [this doc](../misc/protect_namespaces_and_releases.md) for details about what protection means and the difference between namespace-level and release-level protection. 10 | 11 | 12 | ```toml 13 | #... 14 | 15 | [namespaces] 16 | [namespaces.staging] 17 | [namespaces.production] 18 | protected = true 19 | 20 | #... 21 | ``` 22 | 23 | ```yaml 24 | 25 | namespaces: 26 | staging: 27 | production: 28 | protected: true 29 | 30 | ``` 31 | 32 | The example above will create two namespaces; staging and production. Where Helmsman sees the production namespace as a protected namespace. 33 | -------------------------------------------------------------------------------- /docs/how_to/helm_repos/gcs.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v1.8.0 3 | --- 4 | 5 | # Using private helm repos in GCS 6 | 7 | Helmsman allows you to use private charts from private repos. Currently only repos hosted in S3 or GCS buckets are supported for private repos. 8 | 9 | You need to provide one of the following env variables: 10 | 11 | - `GOOGLE_APPLICATION_CREDENTIALS` environment variable to contain the absolute path to your Google cloud credentials.json file. 12 | - Or, `GCLOUD_CREDENTIALS` environment variable to contain the content of the credentials.json file. 13 | 14 | If running inside GCP helmsman can use metadata server to use Service Account permissions. 15 | 16 | Helmsman uses the [helm GCS](https://github.com/nouney/helm-gcs) plugin to work with GCS helm repos. 17 | 18 | ```toml 19 | [helmRepos] 20 | gcsRepo = "gs://myrepobucket/charts" 21 | ``` 22 | 23 | ```yaml 24 | helmRepos: 25 | gcsRepo: "gs://myrepobucket/charts" 26 | ``` 27 | 28 | -------------------------------------------------------------------------------- /docs/how_to/namespaces/labels_and_annotations.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v1.8.0 3 | --- 4 | 5 | # Label & annotate namespaces 6 | 7 | You can define namespaces to be used in your cluster. If they don't exist, Helmsman will create them for you. You can also set some labels to apply for those namespaces. 8 | 9 | ```toml 10 | #... 11 | 12 | [namespaces] 13 | [namespaces.staging] 14 | [namespaces.staging.labels] 15 | env = "staging" 16 | [namespaces.production] 17 | [namespaces.production.annotations] 18 | "iam.amazonaws.com/role" = "dynamodb-reader" 19 | 20 | 21 | #... 22 | ``` 23 | 24 | ```yaml 25 | 26 | namespaces: 27 | staging: 28 | labels: 29 | env: "staging" 30 | production: 31 | annotations: 32 | iam.amazonaws.com/role: "dynamodb-reader" 33 | 34 | ``` 35 | 36 | The above examples create two namespaces; staging and production. The staging namespace has one label `env`= `staging` while the production namespace has one annotation `iam.amazonaws.com/role`=`dynamodb-reader`. 37 | -------------------------------------------------------------------------------- /docs/how_to/deployments/ci.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.0.0-beta5 3 | --- 4 | 5 | # Run Helmsman in CI 6 | 7 | You can run Helmsman as a job in your CI system using the [helmsman docker image](https://hub.docker.com/r/praqma/helmsman/). 8 | The following example is a `config.yml` file for CircleCI but can be replicated for other CI systems. 9 | 10 | ```yaml 11 | version: 2 12 | jobs: 13 | 14 | deploy-apps: 15 | docker: 16 | - image: praqma/helmsman:v3.0.0-beta5 17 | steps: 18 | - checkout 19 | - run: 20 | name: Deploy Helm Packages using helmsman 21 | command: helmsman --apply -f helmsman-deployments.toml 22 | 23 | 24 | workflows: 25 | version: 2 26 | build: 27 | jobs: 28 | - deploy-apps 29 | ``` 30 | 31 | > IMPORTANT: If your CI build logs are publicly readable, don't use the `--verbose` together with `--debug` flags as logs any secrets being passed from env vars to the helm charts. 32 | 33 | The `helmsman-deployments.toml` is your desired state file which will version controlled in your git repo. 34 | -------------------------------------------------------------------------------- /docs/how_to/misc/multiple_desired_state_files_specification.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.8.1 3 | --- 4 | 5 | # Specification file 6 | 7 | Starting from v3.8.0, Helmsman allows you to use Specification file passed with `--spec ` flag 8 | in order to define multiple Desired State Files to be merged together. 9 | 10 | An example Specification file `spec.yaml`: 11 | 12 | ```yaml 13 | --- 14 | stateFiles: 15 | - path: examples/example.yaml 16 | - path: examples/minimal-example.yaml 17 | - path: examples/minimal-example.toml 18 | 19 | ``` 20 | 21 | This file can be then run with: 22 | 23 | ```shell 24 | helmsman --spec spec.yaml ... 25 | ``` 26 | 27 | It takes the files from `stateFiles` list in the same order they are defined. 28 | Then Helmsman will read each file one by one and merge the previous states with the current file it goes through. 29 | 30 | One can take advantage of that and define the state of the environment starting with more general definitions and then reaching more specific cases in the end, 31 | which would overwrite or extend things from previous files. 32 | -------------------------------------------------------------------------------- /docs/how_to/misc/auth_to_storage_providers.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v1.8.0 3 | --- 4 | 5 | # Authenticating to cloud storage providers 6 | 7 | Helmsman can read files like certificates for connecting to the cluster or TLS certificates for communicating with Tiller from some cloud storage providers; namely: GCS, S3 and Azure blob storage. Below is the authentication requirement for each provider: 8 | 9 | ## AWS S3 10 | 11 | You need to provide ALL the following AWS env variables: 12 | 13 | - `AWS_ACCESS_KEY_ID` 14 | - `AWS_SECRET_ACCESS_KEY` 15 | - `AWS_DEFAULT_REGION` 16 | 17 | ## Google GCS 18 | 19 | You need to provide ONE of the following env variables: 20 | 21 | - `GOOGLE_APPLICATION_CREDENTIALS` the absolute path to your Google cloud credentials.json file. 22 | - Or, `GCLOUD_CREDENTIALS` the content of the credentials.json file. 23 | 24 | If running inside GCP helmsman can use metadata server to use Service Account permissions. 25 | 26 | ## Microsoft Azure 27 | 28 | You need to provide ALL of the following env variables: 29 | 30 | - `AZURE_STORAGE_ACCOUNT` 31 | - `AZURE_STORAGE_ACCESS_KEY` -------------------------------------------------------------------------------- /docs/how_to/apps/helm_tests.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.0.0-beta5 3 | --- 4 | 5 | # Test charts 6 | 7 | Helm allows running [chart tests](https://github.com/helm/helm/blob/master/docs/chart_tests.md). 8 | 9 | You can specify that you would like a chart to be tested whenever it is installed for the first time using the `test` key as follows: 10 | 11 | ```toml 12 | ... 13 | [apps] 14 | 15 | [apps.jenkins] 16 | description = "jenkins" 17 | namespace = "staging" 18 | enabled = true 19 | chart = "jenkins/jenkins" 20 | version = "2.15.1" 21 | valuesFile = "" 22 | test = true # setting this to true, means you want the charts tests to be run on this release when it is installed. 23 | 24 | ... 25 | 26 | ``` 27 | 28 | ```yaml 29 | # ... 30 | apps: 31 | 32 | jenkins: 33 | description: "jenkins" 34 | namespace: "staging" 35 | enabled: true 36 | chart: "jenkins/jenkins" 37 | version: "2.15.1" 38 | valuesFile: "" 39 | test: true # setting this to true, means you want the charts tests to be run on this release when it is installed. 40 | 41 | #... 42 | 43 | ``` 44 | -------------------------------------------------------------------------------- /tests/keys/public_key.pkcs7.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC2TCCAcGgAwIBAgIBATANBgkqhkiG9w0BAQsFADAAMCAXDTE5MTAyNTA5MTg0 3 | NVoYDzIwNjkxMDEyMDkxODQ1WjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 4 | CgKCAQEAmB+jCif3+GPmlPrmYgpAcbYh6Ny1iX8V+9hRbfW6A6lVDaAX2n4uzRup 5 | OSBj98hBeakdj/TDI1VJwgbgaHfR+IWNNFOp+pr3uLU54vZHJSeSVm7+OhIKumAT 6 | LEUyMDs2sYCmhc0qKeolI8qOp2jlapJ4b2s/Y/TMRfW3vdMEpukvhMs5RB/Aky5g 7 | GSnW7AZZyF0tTv7So+GJKmula6rsp7Vx+kr00ccNrvbqLSYmjkDYdMeyAoZ5Slb0 8 | 4zub9kP21Jm8Qw2CeiHZ1h/Jkc41V13CAnM8zSB/tS/9uRXEEuQMiUBe3I9SfukL 9 | CKbsx0oryCdjnt8Gblz8kpq2O4EadwIDAQABo1wwWjAPBgNVHRMBAf8EBTADAQH/ 10 | MB0GA1UdDgQWBBTCU7egHRTyB/PuwdcYkGT8N/Lk1TAoBgNVHSMEITAfgBTCU7eg 11 | HRTyB/PuwdcYkGT8N/Lk1aEEpAIwAIIBATANBgkqhkiG9w0BAQsFAAOCAQEAJb6+ 12 | dXiNPUAV5aqM/mEN2lOxpY0RtsN0QE5ZCSgFT1wBVvqI5yAx9kvyYmfkRUbuFbq7 13 | YkBNNLirljz/QTOqoLvRdnPj3v3Gbs6q7hxS3Mezxrc/6RKOvNn/HfWYmIDx8Oot 14 | 5y5h2gElkml/AVrN6lUQi9cl0Za3nm9KHnAElDhHPL1kV3apHMGRGMafJD1+e44I 15 | CjHGzRkj9tZ/11VOnQaKHLlSOhAxenFf0qSylX9ZVKyUyC/rw81d3SrCduSYfxMX 16 | 1JwJlb4HAGtjEDY8FoOzCqA506EdZqqS/PhfxovWZ42VBiF+yPh8S1tJ/t0qfAgN 17 | TWRW5Kv+GwkPe9lhRQ== 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Praqma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/how_to/settings/use-hiera-eyaml-as-secrets-encryption.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v1.13.0 3 | --- 4 | 5 | # Using hiera-eyaml as backend for secrets' encryption 6 | 7 | Helmsman uses helm-secrets as a default solution for secrets' encryption. 8 | And while it is a good off-the-shelve solution it may quickly start causing problems when few developers start working on the secrets files simultaneously. 9 | SOPS-based secrets can not be easily merged or rebased in case of conflicts etc. 10 | That is why another solution for secrets organised in YAMLs was proposed in [hiera-eyaml](https://github.com/voxpupuli/hiera-eyaml). 11 | 12 | ## Example 13 | 14 | Having environment defined with: 15 | 16 | * example.yaml: 17 | ```yaml 18 | settings: 19 | eyamlEnabled: true 20 | ``` 21 | 22 | Helmsman will use hiera-eyaml gem to decrypt secrets files defined for applications. 23 | They public and private keys should be placed in `keys` directory with names of `public_key.pkcs7.pem` and `private_key.pkcs7.pem`. 24 | The keys' path can be overwritten with 25 | 26 | ```yaml 27 | settings: 28 | eyamlEnabled: true 29 | eyamlPrivateKeyPath: ../keys/custom.pem 30 | eyamlPublicKeyPath: ../keys/custom.pub 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/how_to/apps/migrate_contexts.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.2.0 3 | --- 4 | 5 | # Migrating releases from Helmsman context to another 6 | 7 | The `context` stanza has been introduced in v3.0.0 to allow you to [distinguish releases managed by different Helmsman's files](misc/merge_desired_state_files.md#distinguishing-releases-deployed-from-different-desired-state-files). However, once a context is defined, it couldn't be modified. 8 | 9 | From v.3.2.0, you can migrate releases in a DSF to another context (or rename the context) using the `--migrate-context`. This option can be combined with any other Helmsman flags. Behind the scenes, it will just update the Helmsman labels that contain the context name on the release secrets/configmaps before proceeding with the regular execution. 10 | 11 | # Remember 12 | - It is safe to run the `--migrate-context` flag multiple times. 13 | - It can be used in conjunction with other cmd flags. 14 | - It will respect `--target` & `--group` flags if specified (i.e. context migration will only be applied to the selected releases). 15 | - The flag introduces an extra operation done before any other operations are done. So to reduce execution time, don't use it when it's not needed. -------------------------------------------------------------------------------- /docs/how_to/namespaces/quotas.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: 3.3.0 3 | --- 4 | 5 | # Define resource quotas for namespaces 6 | 7 | You can define namespaces to be used in your cluster. If they don't exist, Helmsman will create them for you. You can also define how much resource limits to set for each namespace. 8 | 9 | You can read more about the `Quotas` specification [here](https://kubernetes.io/docs/tasks/administer-cluster/manage-resources/quota-memory-cpu-namespace/#create-a-resourcequota). 10 | 11 | ```toml 12 | #... 13 | [namespaces] 14 | 15 | [namespaces.helmsman1] 16 | 17 | [namespaces.helmsman1.quotas] 18 | "limits.cpu" = "10" 19 | "limits.memory" = "30Gi" 20 | pods = "25" 21 | "requests.cpu" = "10" 22 | "requests.memory" = "30Gi" 23 | 24 | [[namespaces.helmsman1.quotas.customQuotas]] 25 | name = "requests.nvidia.com/gpu" 26 | value = "2" 27 | #... 28 | ``` 29 | 30 | ```yaml 31 | namespaces: 32 | helmsman1: 33 | quotas: 34 | limits.cpu: '10' 35 | limits.memory: '30Gi' 36 | pods: '25' 37 | requests.cpu: '10' 38 | requests.memory: '30Gi' 39 | customQuotas: 40 | - name: 'requests.nvidia.com/gpu' 41 | value: '2' 42 | ``` 43 | 44 | The example above will create one namespace - helmsman1 - with resource quotas defined for the helmsman1 namespace. -------------------------------------------------------------------------------- /docs/how_to/settings/creating_kube_context_with_token.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v1.8.0 3 | --- 4 | 5 | # Cluster connection -- creating the kube context with bearer tokens 6 | 7 | Helmsman can create the kube context for you (i.e. establish connection to your cluster). This guide describe how its done with bearer tokens. If you want to use certificates, check [this guide](creating_kube_context_with_certs.md). 8 | 9 | All you need to do is set `bearerToken` to true and set the `clusterURI` to point to your cluster API endpoint in the `settings` stanza. 10 | 11 | > Note: Helmsman and therefore helm will only be able to do what the kubernetes service account (from which the token is taken) allows. 12 | 13 | By default, Helmsman will look for a token in `/var/run/secrets/kubernetes.io/serviceaccount/token`. If you have the token else where, you can specify its path with `bearerTokenPath`. 14 | 15 | ```toml 16 | [settings] 17 | kubeContext = "test" # the name of the context to be created 18 | bearerToken = true 19 | clusterURI = "https://kubernetes.default" 20 | # bearerTokenPath = "/path/to/custom/bearer/token/file" 21 | ``` 22 | 23 | ```yaml 24 | settings: 25 | kubeContext: "test" # the name of the context to be created 26 | bearerToken: true 27 | clusterURI: "https://kubernetes.default" 28 | # bearerTokenPath: "/path/to/custom/bearer/token/file" 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/how_to/helm_repos/default.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.0.0-beta5 3 | --- 4 | 5 | # Default helm repos 6 | 7 | Helm v3 no longer adds the `stable` and `incubator` repos by default. Up to Helmsman v3.0.0-beta5, Helmsman adds these two repos by default. And you can disable the automatic addition of these two repos, use the `--no-default-repos` flag. 8 | 9 | Starting from `v3.0.0-beta6`, Helmsman complies with the Helm v3 behavior and DOES NOT add `stable` nor `incubator` by default. The `--no-default-repos` is also deprecated. 10 | 11 | This example would have only the `custom` repo defined explicitly: 12 | 13 | ```toml 14 | [helmRepos] 15 | custom = "https://mycustomrepo.org" 16 | ``` 17 | 18 | ```yaml 19 | helmRepos: 20 | custom: "https://mycustomrepo.org" 21 | ``` 22 | 23 | This example would have `stable` defined with a custom repo: 24 | 25 | ```toml 26 | #... 27 | [helmRepos] 28 | stable = "https://mycustomstablerepo.com" 29 | #... 30 | ``` 31 | 32 | ```yaml 33 | # ... 34 | helmRepos: 35 | stable: "https://mycustomstablerepo.com" 36 | # ... 37 | ``` 38 | 39 | This example would have `stable` defined with a Google deprecated stable repo: 40 | 41 | ```toml 42 | #... 43 | [helmRepos] 44 | stable = "https://kubernetes-charts.storage.googleapis.com" 45 | #... 46 | ``` 47 | 48 | ```yaml 49 | # ... 50 | helmRepos: 51 | stable: "https://kubernetes-charts.storage.googleapis.com" 52 | # ... 53 | ``` 54 | -------------------------------------------------------------------------------- /internal/app/spec_state_test.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_specFromYAML(t *testing.T) { 8 | type args struct { 9 | file string 10 | s *StateFiles 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | want bool 16 | }{ 17 | { 18 | name: "test case 1 -- Valid YAML", 19 | args: args{ 20 | file: "../../examples/example-spec.yaml", 21 | s: new(StateFiles), 22 | }, 23 | want: true, 24 | }, { 25 | name: "test case 2 -- Invalid Yaml", 26 | args: args{ 27 | file: "../../tests/Invalid_example_spec.yaml", 28 | s: new(StateFiles), 29 | }, 30 | want: false, 31 | }, { 32 | name: "test case 3 -- Commposition example", 33 | args: args{ 34 | file: "../../examples/composition/spec.yaml", 35 | s: new(StateFiles), 36 | }, 37 | want: true, 38 | }, 39 | } 40 | 41 | teardownTestCase, err := setupStateFileTestCase(t) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | defer teardownTestCase(t) 46 | for _, tt := range tests { 47 | // os.Args = append(os.Args, "-f ../../examples/example.yaml") 48 | t.Run(tt.name, func(t *testing.T) { 49 | err := tt.args.s.specFromYAML(tt.args.file) 50 | if err != nil { 51 | t.Log(err) 52 | } 53 | 54 | got := err == nil 55 | if got != tt.want { 56 | t.Errorf("specFromYaml() = %v, want %v", got, tt.want) 57 | } 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /internal/app/helm_time.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | const ( 10 | ctLayout = "2006-01-02 15:04:05.000000000 -0700 MST" 11 | ctLayout2 = "2006-01-02 15:04:05.000000000 -0700 -0700" 12 | ) 13 | 14 | var nilTime = (time.Time{}).UnixNano() 15 | 16 | type HelmTime struct { 17 | time.Time 18 | } 19 | 20 | func (ht *HelmTime) UnmarshalJSON(b []byte) (err error) { 21 | s := strings.Trim(string(b), "\"") 22 | if s == "null" { 23 | ht.Time = time.Time{} 24 | return 25 | } 26 | // we need to split the time into parts and make sure milliseconds len = 6, it happens to skip trailing zeros 27 | updatedFields := strings.Fields(s) 28 | updatedHour := strings.Split(updatedFields[1], ".") 29 | milliseconds := updatedHour[1] 30 | for i := len(milliseconds); i < 9; i++ { 31 | milliseconds = fmt.Sprintf("%s0", milliseconds) 32 | } 33 | s = fmt.Sprintf("%s %s.%s %s %s", updatedFields[0], updatedHour[0], milliseconds, updatedFields[2], updatedFields[3]) 34 | ht.Time, err = time.Parse(ctLayout, s) 35 | if err != nil { 36 | ht.Time, err = time.Parse(ctLayout2, s) 37 | } 38 | return 39 | } 40 | 41 | func (ht *HelmTime) MarshalJSON() ([]byte, error) { 42 | if ht.Time.UnixNano() == nilTime { 43 | return []byte("null"), nil 44 | } 45 | return []byte(fmt.Sprintf("\"%s\"", ht.Time.Format(ctLayout))), nil 46 | } 47 | 48 | func (ht *HelmTime) IsSet() bool { 49 | return ht.UnixNano() != nilTime 50 | } 51 | -------------------------------------------------------------------------------- /internal/app/logging.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "net/url" 5 | "os" 6 | 7 | "github.com/apsdehal/go-logger" 8 | ) 9 | 10 | type Logger struct { 11 | *logger.Logger 12 | SlackWebhook string 13 | MSTeamsWebhook string 14 | } 15 | 16 | func (l *Logger) Debug(message string) { 17 | if flags.debug { 18 | l.Logger.Debug(message) 19 | } 20 | } 21 | 22 | func (l *Logger) Verbose(message string) { 23 | if flags.verbose { 24 | l.Logger.Info(message) 25 | } 26 | } 27 | 28 | func (l *Logger) Error(message string) { 29 | l.notifyAboutFailureUsingWebhooks(message) 30 | l.Logger.Error(message) 31 | } 32 | 33 | func (l *Logger) Fatal(message string) { 34 | l.notifyAboutFailureUsingWebhooks(message) 35 | l.Logger.Fatal(message) 36 | } 37 | 38 | func (l *Logger) notifyAboutFailureUsingWebhooks(message string) { 39 | if _, err := url.ParseRequestURI(l.SlackWebhook); err == nil { 40 | notifySlack(message, l.SlackWebhook, true, flags.apply) 41 | } 42 | if _, err := url.ParseRequestURI(l.MSTeamsWebhook); err == nil { 43 | notifyMSTeams(message, l.MSTeamsWebhook, true, flags.apply) 44 | } 45 | } 46 | 47 | func initLogs(verbose bool, noColors bool) { 48 | logger.SetDefaultFormat("%{time:2006-01-02 15:04:05} %{level}: %{message}") 49 | logLevel := logger.InfoLevel 50 | if verbose { 51 | logLevel = logger.DebugLevel 52 | } 53 | colors := 1 54 | if noColors { 55 | colors = 0 56 | } 57 | log.Logger, _ = logger.New("logger", colors, os.Stdout, logLevel) 58 | } 59 | -------------------------------------------------------------------------------- /docs/how_to/namespaces/limits.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v1.8.0 3 | --- 4 | 5 | # Define resource limits for namespaces 6 | 7 | You can define namespaces to be used in your cluster. If they don't exist, Helmsman will create them for you. You can also define how much resource limits to set for each namespace. 8 | 9 | You can read more about the `LimitRange` specification [here](https://docs.openshift.com/container-platform/3.11/dev_guide/compute_resources.html#dev-limit-ranges). 10 | 11 | ```toml 12 | #... 13 | 14 | [namespaces] 15 | [namespaces.staging] 16 | [[namespaces.staging.limits]] 17 | type = "Container" 18 | [namespaces.staging.limits.default] 19 | cpu = "300m" 20 | memory = "200Mi" 21 | [namespaces.staging.limits.defaultRequest] 22 | cpu = "200m" 23 | memory = "100Mi" 24 | [[namespaces.staging.limits]] 25 | type = "Pod" 26 | [namespaces.staging.limits.max] 27 | memory = "300Mi" 28 | [namespaces.production] 29 | 30 | #... 31 | ``` 32 | 33 | ```yaml 34 | 35 | namespaces: 36 | staging: 37 | limits: 38 | - type: Container 39 | default: 40 | cpu: "300m" 41 | memory: "200Mi" 42 | defaultRequest: 43 | cpu: "200m" 44 | memory: "100Mi" 45 | - type: Pod 46 | max: 47 | memory: "300Mi" 48 | production: 49 | 50 | ``` 51 | 52 | The example above will create two namespaces - staging and production - with resource limits defined for the staging namespace. 53 | -------------------------------------------------------------------------------- /docs/how_to/deployments/inside_k8s.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v1.8.0 3 | --- 4 | 5 | # Running Helmsman inside your k8s cluster 6 | 7 | Helmsman can be deployed inside your k8s cluster and can talk to the k8s API using a `bearer token`. 8 | 9 | See [connecting to your cluster with bearer token](../settings/creating_kube_context_with_token.md) for more details. 10 | 11 | Your desired state will look like: 12 | 13 | ```toml 14 | [settings] 15 | kubeContext = "test" # the name of the context to be created 16 | bearerToken = true 17 | clusterURI = "https://kubernetes.default" 18 | ``` 19 | 20 | ```yaml 21 | settings: 22 | kubeContext: "test" # the name of the context to be created 23 | bearerToken: true 24 | clusterURI: "https://kubernetes.default" 25 | ``` 26 | 27 | To deploy Helmsman into a k8s cluster, few steps are needed: 28 | 29 | > The steps below assume default namespace 30 | 31 | 1. Create a k8s service account 32 | 33 | ```shell 34 | kubectl create sa helmsman 35 | ``` 36 | 37 | 2. Create a clusterrolebinding 38 | 39 | ```shell 40 | kubectl create clusterrolebinding helmsman-cluster-admin --clusterrole=cluster-admin --serviceaccount=default:helmsman 41 | ``` 42 | 43 | 3. Deploy helmsman 44 | 45 | This command gives an interactive session: 46 | 47 | ```shell 48 | kubectl run helmsman --restart Never --image praqma/helmsman --serviceaccount=helmsman -- helmsman -f -- sleep 3600 49 | ``` 50 | 51 | But you can also create a proper kubernetes deployment and mount a volume to it containing your desired state file(s). 52 | -------------------------------------------------------------------------------- /internal/app/hooks.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | preInstall = "preInstall" 10 | postInstall = "postInstall" 11 | preUpgrade = "preUpgrade" 12 | postUpgrade = "postUpgrade" 13 | preDelete = "preDelete" 14 | postDelete = "postDelete" 15 | test = "test" 16 | ) 17 | 18 | var ( 19 | validManifestFiles = []string{".yaml", ".yml", ".json"} 20 | validHookFiles = []string{".yaml", ".yml", ".json", ".sh", ".py", ".rb"} 21 | ) 22 | 23 | // TODO: Create different types for Command and Manifest hooks 24 | // with methods for getting their commands for the plan 25 | type hookCmd struct { 26 | Command 27 | Type string 28 | } 29 | 30 | func (h *hookCmd) getAnnotationKey() (string, error) { 31 | if h.Type == "" { 32 | return "", fmt.Errorf("no type specified") 33 | } 34 | return "helmsman/" + h.Type, nil 35 | } 36 | 37 | // validateHooks validates that hook files exist and are of correct type 38 | func validateHooks(hooks map[string]interface{}) error { 39 | for key, value := range hooks { 40 | switch key { 41 | case preInstall, postInstall, preUpgrade, postUpgrade, preDelete, postDelete: 42 | hook := value.(string) 43 | if !isOfType(hook, validManifestFiles) && ToolExists(strings.Fields(hook)[0]) { 44 | return nil 45 | } 46 | if err := isValidFile(hook, validManifestFiles); err != nil { 47 | return fmt.Errorf("invalid hook manifest: %w", err) 48 | } 49 | case "successCondition", "successTimeout", "deleteOnSuccess": 50 | continue 51 | default: 52 | return fmt.Errorf("%s is an Invalid hook type", key) 53 | } 54 | } 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | helm3-version: 7 | required: true 8 | type: string 9 | helm4-version: 10 | required: true 11 | type: string 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | helm-version: ['${{ inputs.helm3-version }}', '${{ inputs.helm4-version }}'] 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v6 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@v6 26 | with: 27 | go-version: stable 28 | 29 | - name: Install Helm ${{ matrix.helm-version }} 30 | run: | 31 | curl -fsSL https://get.helm.sh/helm-${{ matrix.helm-version }}-linux-amd64.tar.gz | tar xz 32 | sudo mv linux-amd64/helm /usr/local/bin/helm 33 | rm -rf linux-amd64 34 | helm version --short 35 | 36 | - name: Install kubectl 37 | run: | 38 | curl -LO "https://dl.k8s.io/release/v1.34.2/bin/linux/amd64/kubectl" 39 | chmod +x kubectl 40 | sudo mv kubectl /usr/local/bin/ 41 | kubectl version --client 42 | 43 | - name: Install Helm plugins 44 | run: | 45 | VERIFY_FLAG="" 46 | [[ "${{ matrix.helm-version }}" == v4* ]] && VERIFY_FLAG="--verify=false" 47 | helm plugin install $VERIFY_FLAG https://github.com/databus23/helm-diff --version v3.12.4 48 | 49 | - name: Install eyaml 50 | run: sudo gem install hiera-eyaml --no-doc 51 | 52 | - name: Run tests 53 | run: make test 54 | -------------------------------------------------------------------------------- /tests/keys/private_key.pkcs7.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAmB+jCif3+GPmlPrmYgpAcbYh6Ny1iX8V+9hRbfW6A6lVDaAX 3 | 2n4uzRupOSBj98hBeakdj/TDI1VJwgbgaHfR+IWNNFOp+pr3uLU54vZHJSeSVm7+ 4 | OhIKumATLEUyMDs2sYCmhc0qKeolI8qOp2jlapJ4b2s/Y/TMRfW3vdMEpukvhMs5 5 | RB/Aky5gGSnW7AZZyF0tTv7So+GJKmula6rsp7Vx+kr00ccNrvbqLSYmjkDYdMey 6 | AoZ5Slb04zub9kP21Jm8Qw2CeiHZ1h/Jkc41V13CAnM8zSB/tS/9uRXEEuQMiUBe 7 | 3I9SfukLCKbsx0oryCdjnt8Gblz8kpq2O4EadwIDAQABAoIBAGhQaXCxb6z4dEl8 8 | szZPaVmQVzhjAGlEqEKGV3BbrC6OkzBAs5q0JEupyCTQPTzQKXXPreHlKVq1RVqz 9 | dHauk2Ej02wqYsjiMzSJsSQdVTP5KrPycIpJjOm4r+0PlhbUw/B8E7R0t5D+anFc 10 | mO3bVFX8EnH0zQcx+lGO6WxVoz8AYU4EV2oxveR77in/8f7Aj8urrzRdSRnbUgp/ 11 | q27FA2Ct9S9KWn+V+zByTULHN2FjViWqaWqIA+/BbOoe+a7VP6579yK5bRxoCS01 12 | u1Xv4fEu3d382C3nbqNS0fszce3u7r9rSwat/su42bkjmNC0Gmtk5TVTGuvtv5Nq 13 | IkndF+ECgYEAyVo+Qb7stssiLq39VH0P415rTGv3JrscuwfkOCe6smJO9/KFeQjb 14 | waNjR7KEkgTTqB3s6m8n4GX2AvVMiNbfoLscp6j031Uuq/cWX/DDHTcv9ZN3igcJ 15 | hNWSXIM9Ru5d4GXZGcYiLtpDfp2ekHEtMlIWlceOv1cy+Va5bn7RrsUCgYEAwWkF 16 | MzpRdPoBvfOJImXsyWvz9VksGQ8L9133wB+6n3QsFqdd43MmbvwqN5tZW/qLEFeU 17 | oH4Od8M9fH5IjmJdqyNhNDx9CHaEXLZHaPhdDlJZUzryjIAjn4JMjMhZ4jxr8SHR 18 | zZBqmgGdhQYssXCtJWU1PD/DNWQtQEVTkJVtuAsCgYEAsZ/wh+M7w02TjAZlEqF4 19 | 4KUslrAvyXULNVsS0w8JPdBHxaemY02TP1E5hchP9thXN1me5HjGfsizq4xlxdl4 20 | Ubx+3NDJpDLrBzzj+iLUnPNQVZ2PuK3YkdwuT3pfFjG1kv2F9Zy6Dwbwv8OgW9/b 21 | dSbBUcRHgzgTea4tyvIJW9kCgYBhvI5yKsBLGqOSt+TOyy7zQmhPzbYpG59ya7vt 22 | DJukRHKbKAycCe6cGzXCT/DCOEPaCEgFKm5pOvJxXOeRfEfVWdWfLgoJIssUhtBj 23 | TU7JE/grxRgYxBA8ZP4GDqDNYLczbWG2PYqBNNvDAzHGoSf+Q7y5K4ecDXmIhwAJ 24 | ilmdrQKBgCnvFxO8mFLCsINfhKTWi8JQUNY+QuQbM9/NztDo6mPj4R8bw3jBEgq9 25 | fLPV0x7CmuGs5x6D+ZHKKqAaWpPO1e7YiYom2jKv40iIgvdEckYLlpQbCFRfJu/b 26 | /wSppmgeZlUAyftw+0c1Yl+Nqv13KuI4TII+/0fqxRG1T3B2s9jq 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /docs/best_practice.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.0.0-beta5 3 | --- 4 | 5 | # Best Practice 6 | 7 | When using Helmsman, we recommend the following best practices: 8 | 9 | - Add useful metadata in your desired state files (DSFs) so that others (who have access to them) can understand what your DSF is for. We recommend the following metadata: organization, maintainer (name and email), and description/purpose. 10 | 11 | - Define `context` (see [the DSF spec](desired_state_specification.md#context)) for each DSF. This helps prevent different DSFs from operating on each other's releases. 12 | 13 | - Store your DSFs in git (or any other VCS) so that you have an audit trail of your deployments. You can also rollback to a previous state by going back to previous commits. 14 | > Rollback can be more complex regarding application data. 15 | 16 | - Do not store secrets in your DSFs! Use one of [the supported ways to pass secrets to your releases](how_to/apps/secrets.md). 17 | 18 | - To protect against accidental operations, define certain namespaces (e.g, production) as protected namespaces (supported in v1.0.0+) and deploy your production-ready releases there. 19 | 20 | - If you use multiple desired state files (DSFs) with the same cluster, make sure your namespace protection definitions are identical across all DSFs. 21 | 22 | - When using multiple DSFs, make sure that apps managed in the same namespace are in one DSF. This avoids the need for defining the same namespace (with its settings) across multiple DSFs 23 | 24 | - Don't maintain the same release in multiple DSFs. 25 | 26 | - While the decision on how many DSFs to use and what each can contain is up to you and depends on your case, we recommend coming up with your own rules for how to split them. For example, you can have one for infra (3rd party tools), one for staging, and one for production apps. 27 | 28 | -------------------------------------------------------------------------------- /docs/how_to/misc/limit-deployment-to-specific-apps.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v1.9.0 3 | --- 4 | 5 | # Limit execution to explicitly defined apps 6 | 7 | Starting from v1.9.0, Helmsman allows you to pass the `--target` flag multiple times to specify multiple apps 8 | that limits apps considered by Helmsman during this specific execution. 9 | Thanks to this one can deploy specific applications among all defined for an environment. 10 | 11 | ## Example 12 | 13 | Having environment defined with such apps: 14 | 15 | example.yaml: 16 | 17 | ```yaml 18 | # ... 19 | apps: 20 | jenkins: 21 | namespace: "staging" # maps to the namespace as defined in namespaces above 22 | enabled: true # change to false if you want to delete this app release empty: false: 23 | chart: "jenkins/jenkins" # changing the chart name means delete and recreate this chart 24 | version: "2.15.1" # chart version 25 | 26 | artifactory: 27 | namespace: "production" # maps to the namespace as defined in namespaces above 28 | enabled: true # change to false if you want to delete this app release empty: false: 29 | chart: "jfrog/artifactory" # changing the chart name means delete and recreate this chart 30 | version: "11.4.2" # chart version 31 | # ... 32 | ``` 33 | 34 | running Helmsman with `-f example.yaml` would result in checking state and invoking deployment for both jenkins and artifactory application. 35 | 36 | With `--target` flag in command like 37 | 38 | ```shell 39 | helmsman -f example.yaml --target artifactory ... 40 | ``` 41 | 42 | one can execute Helmsman's environment defined with example.yaml limited to only one `artifactory` app. Others are ignored until the flag is defined. 43 | 44 | Multiple applications can be set with `--target`, like 45 | 46 | ```shell 47 | helmsman -f example.yaml --target artifactory --target jenkins ... 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/how_to/misc/limit-deployment-to-specific-group-of-apps.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v1.13.0 3 | --- 4 | 5 | # Limit execution to explicitly defined group of apps 6 | 7 | Starting from v1.13.0, Helmsman allows you to pass the `--group` flag to specify group of apps 8 | the execution of Helmsman deployment will be limited to. 9 | Thanks to this one can deploy specific applications among all defined for an environment. 10 | 11 | ## Example 12 | 13 | Having environment defined with such apps: 14 | 15 | example.yaml: 16 | 17 | ```yaml 18 | # ... 19 | apps: 20 | jenkins: 21 | namespace: "staging" # maps to the namespace as defined in namespaces above 22 | group: "critical" # group name 23 | enabled: true # change to false if you want to delete this app release empty: false: 24 | chart: "jenkins/jenkins" # changing the chart name means delete and recreate this chart 25 | version: "2.15.1" # chart version 26 | 27 | artifactory: 28 | namespace: "production" # maps to the namespace as defined in namespaces above 29 | group: "sidecar" # group name 30 | enabled: true # change to false if you want to delete this app release empty: false: 31 | chart: "jfrog/artifactory" # changing the chart name means delete and recreate this chart 32 | version: "11.4.2" # chart version 33 | # ... 34 | ``` 35 | 36 | running Helmsman with `-f example.yaml` would result in checking state and invoking deployment for both jenkins and artifactory application. 37 | 38 | With `--group` flag in command like 39 | 40 | ```shell 41 | helmsman -f example.yaml --group critical ... 42 | ``` 43 | 44 | one can execute Helmsman's environment defined with example.yaml limited to only one `jenkins` app, since its group is `critical`. 45 | Others are ignored until the flag is defined. 46 | 47 | Multiple applications can be set with `--group`, like 48 | 49 | ```shell 50 | helmsman -f example.yaml --group critical --group sidecar ... 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/how_to/misc/exclude-apps-or-groups-from-deployment.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.10.0 3 | --- 4 | 5 | # Exclude specific apps or groups from execution 6 | 7 | Starting from v3.10.0, Helmsman allows you to pass the `--exclude-target` or `--exclude-group` flag multiple times 8 | to specify which apps or groups should be excluded from execution. 9 | Thanks to this one can exclude specific applications among all defined for an environment. 10 | 11 | ## Example 12 | 13 | Having environment defined with such apps: 14 | 15 | example.yaml: 16 | 17 | ```yaml 18 | # ... 19 | apps: 20 | jenkins: 21 | namespace: "staging" # maps to the namespace as defined in namespaces above 22 | enabled: true # change to false if you want to delete this app release empty: false: 23 | chart: "jenkins/jenkins" # changing the chart name means delete and recreate this chart 24 | version: "2.15.1" # chart version 25 | 26 | artifactory: 27 | namespace: "production" # maps to the namespace as defined in namespaces above 28 | enabled: true # change to false if you want to delete this app release empty: false: 29 | chart: "jfrog/artifactory" # changing the chart name means delete and recreate this chart 30 | version: "11.4.2" # chart version 31 | # ... 32 | ``` 33 | 34 | running Helmsman with `-f example.yaml` would result in checking state and invoking deployment for both jenkins and artifactory application. 35 | 36 | With `--exclude-target` flag in command like 37 | 38 | ```shell 39 | helmsman -f example.yaml --exclude-target artifactory ... 40 | ``` 41 | 42 | one can execute Helmsman's environment defined with example.yaml limited to only one `jenkins` app by excluding second one - `artifactory` from the execution. 43 | 44 | Multiple applications can be excluded with `--exclude-target`, like 45 | 46 | ```shell 47 | helmsman -f example.yaml --exclude-target artifactory --exclude-target jenkins ... 48 | ``` 49 | 50 | Same rules apply for `--exclude-groups`. 51 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | 4 | # The lines below are called `modelines`. See `:help modeline` 5 | # Feel free to remove those if you don't want/need to use them. 6 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 7 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 8 | 9 | version: 2 10 | 11 | builds: 12 | - binary: helmsman 13 | ldflags: -s -w -X github.com/mkubaczyk/helmsman/internal/app.appVersion={{.Version}} -extldflags "-static" 14 | flags: 15 | - -trimpath 16 | env: 17 | - CGO_ENABLED=0 18 | goos: 19 | - darwin 20 | - linux 21 | - windows 22 | goarch: 23 | - amd64 24 | - arm64 25 | main: ./cmd/helmsman/main.go 26 | 27 | release: 28 | footer: | 29 | ## Docker Images 30 | 31 | ### Helm 4 (Recommended) 32 | Default build with Helm v{{ .Env.HELM4_VERSION }}: 33 | ``` 34 | docker pull ghcr.io/mkubaczyk/helmsman:latest 35 | docker pull ghcr.io/mkubaczyk/helmsman:{{ .Tag }} 36 | docker pull ghcr.io/mkubaczyk/helmsman:{{ .Tag }}-helm4 37 | docker pull ghcr.io/mkubaczyk/helmsman:{{ .Tag }}-helm{{ .Env.HELM4_VERSION }} 38 | ``` 39 | 40 | ### Helm 3 41 | Build with Helm v{{ .Env.HELM3_VERSION }}: 42 | ``` 43 | docker pull ghcr.io/mkubaczyk/helmsman:{{ .Tag }}-helm3 44 | docker pull ghcr.io/mkubaczyk/helmsman:{{ .Tag }}-helm{{ .Env.HELM3_VERSION }} 45 | ``` 46 | 47 | changelog: 48 | use: github 49 | sort: asc 50 | groups: 51 | - title: Breaking Changes 52 | regexp: '^.*?(\w+)(\(.+\))?!:.*$' 53 | order: 0 54 | - title: Features 55 | regexp: '^.*?feat(\(.+\))?:.*$' 56 | order: 1 57 | - title: Fixes 58 | regexp: '^.*?fix(\(.+\))?:.*$' 59 | order: 2 60 | - title: Chore 61 | regexp: '^.*?(chore|docs)(\(.+\))?:.*$' 62 | order: 3 63 | - title: Others 64 | order: 4 65 | -------------------------------------------------------------------------------- /docs/how_to/apps/environment_vars.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.4.0 3 | --- 4 | 5 | # Using Environment Variables in Helmsman DSF and Helm values files 6 | 7 | You can use environment variables in any Helmsman desired state file or helm values files or [lifecycle hooks](lifecycle_hooks.md) files (K8S manifests). Both formats `${MY_VAR}` and `$MY_VAR` are accepted. 8 | 9 | > To expand environment variables in helm values files and lifecycle hooks files, you have to enable the `--subst-env-values`. 10 | 11 | ## How does it work? 12 | 13 | Helmsman will expand those variables at run time. For helm values files and Helmsman lifecycle hooks files, the variables are expanded into temporary files which are used during runtime and removed at the end of execution. 14 | 15 | ## Validating against unset env variables 16 | 17 | By default, Helmsman will validate that your environment variables are set before using them. If they are unset, an error will be produced. 18 | The validation will parse Helmsman DSF files and other files (values files, lifecycle hooks files) line-by-line. This maybe become slow if you have very large files. 19 | 20 | ## Skipping env variables validation 21 | 22 | Validation of environment variables being set is skipped in the following cases: 23 | - If `--skip-validation` flag is used, no env variables validation is performed on any file. 24 | - If `--no-env-subst` flag is used, no env variables validation is performed on Helmsman desired state files. 25 | - If `--subst-env-values` flag is NOT used, no env variables validation is performed on helm values files and lifecycle hooks files. 26 | 27 | ## Escaping the `$` sign 28 | 29 | ### In Helmsman desired state files 30 | 31 | If you want to pass the `$` as is, you can escape it like so: `$$` 32 | 33 | ### In Helm values files and lifecycle hooks files 34 | 35 | If you don't enable `--subst-env-values`, the `$` is passed as is without the need to escape it. However, if you enable `--subst-env-values` and want to pass the `$` as is, you have to escape it like so `$$` -------------------------------------------------------------------------------- /docs/how_to/apps/multiple_values_files.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.3.0-beta1 3 | --- 4 | 5 | # Multiple value files 6 | 7 | You can include multiple yaml value files to separate configuration for different environments. 8 | 9 | > file paths can be a URL (e.g. to a public git repo) , cloud bucket, local absolute/relative file path. 10 | 11 | ```toml 12 | ... 13 | [apps] 14 | 15 | [apps.jenkins-prod] 16 | description = "production jenkins" 17 | namespace = "production" 18 | enabled = true 19 | chart = "jenkins/jenkins" 20 | version = "2.15.1" # chart version 21 | valuesFiles = [ 22 | "../my-jenkins-common-values.yaml", 23 | "../my-jenkins-production-values.yaml" 24 | ] 25 | 26 | # the jenkins release below is being tested in the staging namespace 27 | [apps.jenkins-test] 28 | description = "test release of jenkins, testing xyz feature" 29 | namespace = "staging" 30 | enabled = true 31 | chart = "jenkins/jenkins" 32 | version = "2.15.1" # chart version 33 | valuesFiles = [ 34 | "../my-jenkins-common-values.yaml", 35 | "../my-jenkins-testing-values.yaml" 36 | ] 37 | 38 | #... 39 | ``` 40 | 41 | ```yaml 42 | # ... 43 | apps: 44 | 45 | jenkins-prod: 46 | description: "production jenkins" 47 | namespace: "production" 48 | enabled: true 49 | chart: "jenkins/jenkins" 50 | version: "2.15.1" # chart version 51 | valuesFiles: 52 | - "../my-jenkins-common-values.yaml" 53 | - "../my-jenkins-production-values.yaml" 54 | 55 | # the jenkins release below is being tested in the staging namespace 56 | jenkins-test: 57 | name: "jenkins-test" # should be unique across all apps 58 | description: "test release of jenkins, testing xyz feature" 59 | namespace: "staging" 60 | enabled: true 61 | chart: "jenkins/jenkins" 62 | version: "2.15.1" # chart version 63 | valuesFiles: 64 | - "../my-jenkins-common-values.yaml" 65 | - "../my-jenkins-testing-values.yaml" 66 | # ... 67 | ``` 68 | -------------------------------------------------------------------------------- /internal/app/custom_types.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "strconv" 7 | 8 | "github.com/invopop/jsonschema" 9 | ) 10 | 11 | // truthy and falsy NullBool values 12 | var ( 13 | True = NullBool{HasValue: true, Value: true} 14 | False = NullBool{HasValue: true, Value: false} 15 | ) 16 | 17 | // NullBool represents a bool that may be null. 18 | type NullBool struct { 19 | Value bool 20 | HasValue bool // true if bool is not null 21 | } 22 | 23 | func (b NullBool) MarshalJSON() ([]byte, error) { 24 | value := b.HasValue && b.Value 25 | return json.Marshal(value) 26 | } 27 | 28 | func (b *NullBool) UnmarshalJSON(data []byte) error { 29 | var unmarshalledJson bool 30 | 31 | err := json.Unmarshal(data, &unmarshalledJson) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | b.Value = unmarshalledJson 37 | b.HasValue = true 38 | 39 | return nil 40 | } 41 | 42 | func (b *NullBool) UnmarshalText(text []byte) error { 43 | str := string(text) 44 | if len(str) < 1 { 45 | return nil 46 | } 47 | 48 | value, err := strconv.ParseBool(str) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | b.HasValue = true 54 | b.Value = value 55 | 56 | return nil 57 | } 58 | 59 | // JSONSchema instructs the jsonschema generator to represent NullBool type as boolean 60 | func (NullBool) JSONSchema() *jsonschema.Schema { 61 | return &jsonschema.Schema{ 62 | Type: "boolean", 63 | } 64 | } 65 | 66 | type MergoTransformer func(typ reflect.Type) func(dst, src reflect.Value) error 67 | 68 | func (m MergoTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error { 69 | return m(typ) 70 | } 71 | 72 | // NullBoolTransformer is a custom imdario/mergo transformer for the NullBool type 73 | func NullBoolTransformer(typ reflect.Type) func(dst, src reflect.Value) error { 74 | if typ != reflect.TypeOf(NullBool{}) { 75 | return nil 76 | } 77 | 78 | return func(dst, src reflect.Value) error { 79 | if src.FieldByName("HasValue").Bool() { 80 | dst.Set(src) 81 | } 82 | return nil 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /internal/aws/aws_test.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/aws/credentials" 11 | "github.com/aws/aws-sdk-go/aws/session" 12 | "github.com/aws/aws-sdk-go/service/s3" 13 | "github.com/aws/aws-sdk-go/service/s3/s3manager" 14 | ) 15 | 16 | var _ = flag.String("f", "", "") // Accept -f flag from Makefile 17 | 18 | // Verify AWS SDK returns an error when no valid credentials are found. 19 | func TestS3DownloadFailsWithoutCredentials(t *testing.T) { 20 | // Clear AWS-related env vars and restore them later 21 | envVars := []string{ 22 | "AWS_ACCESS_KEY_ID", 23 | "AWS_SECRET_ACCESS_KEY", 24 | "AWS_SESSION_TOKEN", 25 | "AWS_PROFILE", 26 | "AWS_SHARED_CREDENTIALS_FILE", 27 | "AWS_CONFIG_FILE", 28 | } 29 | saved := make(map[string]string) 30 | for _, v := range envVars { 31 | saved[v] = os.Getenv(v) 32 | os.Unsetenv(v) 33 | } 34 | defer func() { 35 | for k, v := range saved { 36 | if v != "" { 37 | os.Setenv(k, v) 38 | } 39 | } 40 | }() 41 | 42 | sess, err := session.NewSession(&aws.Config{ 43 | Region: aws.String("us-east-1"), 44 | Credentials: credentials.NewCredentials(&credentials.ChainProvider{ 45 | Providers: []credentials.Provider{ 46 | &credentials.EnvProvider{}, 47 | &credentials.SharedCredentialsProvider{Filename: "/nonexistent", Profile: "nonexistent"}, 48 | }, 49 | VerboseErrors: true, 50 | }), 51 | }) 52 | if err != nil { 53 | t.Fatalf("session creation failed: %v", err) 54 | } 55 | 56 | downloader := s3manager.NewDownloader(sess) 57 | _, err = downloader.Download(&fakeWriterAt{}, &s3.GetObjectInput{ 58 | Bucket: aws.String("test-bucket"), 59 | Key: aws.String("test-key"), 60 | }) 61 | 62 | if err == nil { 63 | t.Fatal("expected error when no credentials available, got nil") 64 | } 65 | 66 | errMsg := err.Error() 67 | if !strings.Contains(errMsg, "NoCredentialProviders") { 68 | t.Errorf("expected NoCredentialProviders error, got: %v", err) 69 | } 70 | } 71 | 72 | type fakeWriterAt struct{} 73 | 74 | func (f *fakeWriterAt) WriteAt(p []byte, off int64) (n int, err error) { 75 | return len(p), nil 76 | } 77 | -------------------------------------------------------------------------------- /docs/how_to/settings/creating_kube_context_with_certs.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v1.8.0 3 | --- 4 | 5 | # Cluster connection -- creating the kube context with certificates 6 | 7 | Helmsman can create the kube context for you (i.e. establish connection to your cluster). This guide describe how its done with certificates. If you want to use bearer tokens, check [this guide](creating_kube_context_with_token.md). 8 | 9 | Creating the context with certs, requires both the `settings` and `certificates` stanzas. 10 | 11 | > If you use GCS, S3, or Azure blob storage for your certificates, you will need to provide means to authenticate to the respective cloud provider in the environment. See [authenticating to cloud storage providers](../misc/auth_to_storage_providers.md) for details. 12 | 13 | ```toml 14 | [settings] 15 | kubeContext = "mycontext" # the name of the context to be created 16 | username = "admin" # the cluster user name 17 | password = "$K8S_PASSWORD" # the name of an environment variable containing the k8s password 18 | clusterURI = "${CLUSTER_URI}" # the name of an environment variable containing the cluster API endpoint 19 | #clusterURI = "https://192.168.99.100:8443" # equivalent to the above 20 | 21 | [certificates] 22 | caClient = "gs://mybucket/client.crt" # GCS bucket path 23 | caCrt = "s3://mybucket/ca.crt" # S3 bucket path 24 | # caCrt = "az://myblobcontainer/ca.crt" # Azure blob object 25 | caKey = "../ca.key" # valid local file relative path to the DSF file 26 | ``` 27 | 28 | ```yaml 29 | settings: 30 | kubeContext: "mycontext" # the name of the context to be created 31 | username: "admin" # the cluster user name 32 | password: "$K8S_PASSWORD" # the name of an environment variable containing the k8s password 33 | clusterURI: "${CLUSTER_URI}" # the name of an environment variable containing the cluster API endpoint 34 | #clusterURI: "https://192.168.99.100:8443" # equivalent to the above 35 | 36 | certificates: 37 | caClient: "gs://mybucket/client.crt" # GCS bucket path 38 | caCrt: "s3://mybucket/ca.crt" # S3 bucket path 39 | #caCrt: "az://myblobcontainer/ca.crt" # Azure blob object 40 | caKey: "../ca.key" # valid local file relative path to the DSF file 41 | 42 | ``` 43 | -------------------------------------------------------------------------------- /internal/app/helm_helpers_test.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func Test_getChartInfo(t *testing.T) { 9 | // version string = the first semver-valid string after the last hypen in the chart string. 10 | type args struct { 11 | r *Release 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | want *ChartInfo 17 | }{ 18 | { 19 | name: "getChartInfo - local chart should return given release info", 20 | args: args{ 21 | r: &Release{ 22 | Name: "release1", 23 | Namespace: "namespace", 24 | Version: "1.0.0", 25 | Chart: "./../../tests/chart-test", 26 | Enabled: True, 27 | }, 28 | }, 29 | want: &ChartInfo{Name: "chart-test", Version: "1.0.0"}, 30 | }, 31 | { 32 | name: "getChartInfo - local chart semver should return latest matching release", 33 | args: args{ 34 | r: &Release{ 35 | Name: "release1", 36 | Namespace: "namespace", 37 | Version: "1.0.*", 38 | Chart: "./../../tests/chart-test", 39 | Enabled: True, 40 | }, 41 | }, 42 | want: &ChartInfo{Name: "chart-test", Version: "1.0.0"}, 43 | }, 44 | { 45 | name: "getChartInfo - unknown chart should error", 46 | args: args{ 47 | r: &Release{ 48 | Name: "release1", 49 | Namespace: "namespace", 50 | Version: "1.0.0", 51 | Chart: "random-chart-name-1f8147", 52 | Enabled: True, 53 | }, 54 | }, 55 | want: nil, 56 | }, 57 | { 58 | name: "getChartInfo - wrong local version should error", 59 | args: args{ 60 | r: &Release{ 61 | Name: "release1", 62 | Namespace: "namespace", 63 | Version: "0.9.0", 64 | Chart: "./../../tests/chart-test", 65 | Enabled: True, 66 | }, 67 | }, 68 | want: nil, 69 | }, 70 | } 71 | for _, tt := range tests { 72 | t.Run(tt.name, func(t *testing.T) { 73 | got, err := getChartInfo(tt.args.r.Chart, tt.args.r.Version) 74 | if err != nil && tt.want != nil { 75 | t.Errorf("getChartInfo() = Unexpected error: %v", err) 76 | } 77 | if !reflect.DeepEqual(got, tt.want) { 78 | t.Errorf("getChartInfo() = %v, want %v", got, tt.want) 79 | } 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /internal/aws/aws.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/aws/session" 9 | "github.com/aws/aws-sdk-go/service/s3" 10 | "github.com/aws/aws-sdk-go/service/s3/s3manager" 11 | "github.com/aws/aws-sdk-go/service/ssm" 12 | "github.com/logrusorgru/aurora" 13 | ) 14 | 15 | // colorizer 16 | var style aurora.Aurora 17 | 18 | // ReadFile reads a file from S3 bucket and saves it in a desired location. 19 | func ReadFile(bucketName string, filename string, outFile string, noColors bool) { 20 | style = aurora.NewAurora(!noColors) 21 | 22 | // Create Session -- use config (credentials + region) from env vars or aws profile 23 | sess, err := session.NewSession() 24 | if err != nil { 25 | log.Fatal(style.Bold(style.Red("ERROR: Can't create AWS session: " + err.Error()))) 26 | } 27 | // create S3 download manger 28 | downloader := s3manager.NewDownloader(sess) 29 | 30 | file, err := os.Create(outFile) 31 | if err != nil { 32 | log.Fatal(style.Bold(style.Red("ERROR: Failed to open file " + outFile + ": " + err.Error()))) 33 | } 34 | 35 | defer file.Close() 36 | 37 | _, err = downloader.Download(file, 38 | &s3.GetObjectInput{ 39 | Bucket: aws.String(bucketName), 40 | Key: aws.String(filename), 41 | }) 42 | if err != nil { 43 | log.Fatal(style.Bold(style.Red("ERROR: Failed to download file " + filename + " from S3: " + err.Error()))) 44 | } 45 | 46 | log.Println("Successfully downloaded " + filename + " from S3 as " + outFile) 47 | } 48 | 49 | // ReadSSMParam reads a value from an SSM Parameter 50 | func ReadSSMParam(keyname string, withDecryption bool, noColors bool) string { 51 | style = aurora.NewAurora(!noColors) 52 | 53 | // Create Session -- use config (credentials + region) from env vars or aws profile 54 | sess, err := session.NewSession() 55 | if err != nil { 56 | log.Fatal(style.Bold(style.Red("ERROR: Can't create AWS session: " + err.Error()))) 57 | } 58 | 59 | ssmsvc := ssm.New(sess, aws.NewConfig()) 60 | param, err := ssmsvc.GetParameter(&ssm.GetParameterInput{ 61 | Name: &keyname, 62 | WithDecryption: &withDecryption, 63 | }) 64 | if err != nil { 65 | log.Fatal(style.Bold(style.Red("ERROR: Can't find the SSM Parameter " + keyname + " : " + err.Error()))) 66 | } 67 | 68 | value := *param.Parameter.Value 69 | return value 70 | } 71 | -------------------------------------------------------------------------------- /docs/how_to/apps/secrets.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.0.0-beta5 3 | --- 4 | 5 | # Secrets 6 | 7 | ## Passing secrets from env variables 8 | 9 | Starting from v0.1.3, Helmsman allows you to pass secrets and other user input to helm charts from environment variables as follows: 10 | 11 | ```toml 12 | # ... 13 | [apps] 14 | 15 | [apps.jira] 16 | description = "jira" 17 | namespace = "staging" 18 | enabled = true 19 | chart = "myrepo/jira" 20 | version = "0.1.5" 21 | valuesFile = "applications/jira-values.yaml" 22 | test = true 23 | [apps.jira.set] # the format is [apps.<>.set] 24 | db_username= "$JIRA_DB_USERNAME" # pass any number of key/value pairs where the key is the input expected by the helm charts and the value is an env variable name starting with $ 25 | db_password= "$JIRA_DB_PASSWORD" 26 | # ... 27 | ``` 28 | 29 | ```yaml 30 | # ... 31 | apps: 32 | 33 | jira: 34 | description: "jira" 35 | namespace: "staging" 36 | enabled: true 37 | chart: "myrepo/jira" 38 | version: "0.1.5" 39 | valuesFile: "applications/jira-values.yaml" 40 | test: true 41 | set: 42 | db_username: "$JIRA_DB_USERNAME" # pass any number of key/value pairs where the key is the input expected by the helm charts and the value is an env variable name starting with $ 43 | db_password: "$JIRA_DB_PASSWORD" 44 | # ... 45 | 46 | ``` 47 | 48 | These input variables will be passed to the chart when it is deployed/upgraded using helm's `--set <>=<>` 49 | 50 | ## Passing secrets from env files 51 | 52 | You can also keep these environment variables in files, by default Helmsman will load variables from a `.env` file but you can also specify files by using the `-e` option: 53 | 54 | ```shell 55 | helmsman -e myVars 56 | ``` 57 | 58 | Below are some examples of valid env files 59 | 60 | ```shell 61 | # I am a comment and that is OK 62 | SOME_VAR=someval 63 | FOO=BAR # comments at line end are OK too 64 | export BAR=BAZ 65 | ``` 66 | 67 | Or you can do YAML(ish) style 68 | 69 | ```yaml 70 | FOO: bar 71 | BAR: baz 72 | ``` 73 | 74 | ## Passing secrets using helm secrets plugin 75 | 76 | You can also use the [helm secrets plugin](https://github.com/jkroepke/helm-secrets) to pass your secrets. 77 | 78 | ## Passing secrets using hiera eyaml 79 | 80 | An alternative method is to use heira eyaml as described in [this guide](../settings/use-hiera-eyaml-as-secrets-encryption.md). 81 | 82 | -------------------------------------------------------------------------------- /examples/appsTemplates/config/helmsman.yaml: -------------------------------------------------------------------------------- 1 | metadata: 2 | scope: "K8s Cluster kind-1" 3 | maintainer: "devops" 4 | 5 | settings: 6 | kubeContext: "kind-kind-1" # the name of the context to be created 7 | slackWebhook: "$MY_SLACK_WEBHOOK" 8 | 9 | namespaces: 10 | testing: 11 | labels: 12 | env: "testing" 13 | limits: 14 | - type: Container 15 | default: 16 | cpu: "200m" 17 | memory: "250Mi" 18 | defaultRequest: 19 | cpu: "100m" 20 | memory: "150Mi" 21 | - type: Pod 22 | max: 23 | memory: "300Mi" 24 | development: 25 | labels: 26 | env: "development" 27 | limits: 28 | - type: Container 29 | default: 30 | cpu: "300m" 31 | memory: "300Mi" 32 | defaultRequest: 33 | cpu: "200m" 34 | memory: "200Mi" 35 | - type: Pod 36 | max: 37 | memory: "400Mi" 38 | 39 | helmRepos: 40 | jenkins: "https://charts.jenkins.io" 41 | jfrog: "https://charts.jfrog.io" 42 | bitnami: "https://charts.bitnami.com/bitnami" 43 | puppet: "https://puppetlabs.github.io/puppetserver-helm-chart" 44 | 45 | appsTemplates: 46 | common: &common 47 | test: true 48 | 49 | testing: &testing 50 | namespace: "testing" 51 | protected: false # defining all "testing" releases to be protected. 52 | wait: true 53 | 54 | development: &development 55 | namespace: "development" 56 | protected: true # defining all "development" releases to be protected. 57 | wait: false 58 | 59 | puppetserver: &puppetserver 60 | enabled: true 61 | priority: -1 62 | chart: "puppet/puppetserver-helm-chart" 63 | version: "3.0.2" # chart version 64 | valuesFiles: ["../apps/puppetserver/common-values.yaml"] 65 | 66 | tomcat: &tomcat 67 | enabled: true 68 | priority: -2 69 | chart: "bitnami/tomcat" 70 | version: "6.5.3" # chart version 71 | valuesFiles: ["../apps/tomcat/common-values.yaml"] 72 | 73 | apps: 74 | testing-puppetserver: 75 | <<: *common 76 | <<: *testing 77 | <<: *puppetserver 78 | valuesFiles: ["../apps/puppetserver/testing-values.yaml"] 79 | 80 | testing-tomcat: 81 | <<: *common 82 | <<: *testing 83 | <<: *tomcat 84 | valuesFiles: ["../apps/tomcat/testing-values.yaml"] 85 | 86 | development-puppetserver: 87 | <<: *common 88 | <<: *development 89 | <<: *puppetserver 90 | valuesFiles: ["../apps/puppetserver/development-values.yaml"] 91 | 92 | development-tomcat: 93 | <<: *common 94 | <<: *development 95 | <<: *tomcat 96 | valuesFiles: ["../apps/tomcat/development-values.yaml"] 97 | -------------------------------------------------------------------------------- /docs/how_to/misc/migrate_to_3.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.2.0 3 | --- 4 | 5 | # Migrate from Helm2 (Helmsman v1.x) to Helm3 (Helmsman v3.x) 6 | 7 | This guide describes the process of migrating your Helmsman managed releases from Helm v2 to v3. 8 | Helmsman v3.x is Helm v3-compatible, while Helmsman v1.x is Helm v2-compatible. 9 | 10 | The migration process can go as follows: 11 | 12 | ## Migrate Helm v2 release state to Helm v3 13 | - Go through the [Helm's v2 to v3 migration guide](https://helm.sh/docs/topics/v2_v3_migration/) 14 | - Manually migrate your releases state/history with the [helm3 2to3 plugin](https://helm.sh/blog/migrate-from-helm-v2-to-helm-v3/) (e.g. usage helm3 2to3 convert ). 15 | 16 | > At this stage, Helm v3 can see and operate on your releases, but Helmsman can't. This is because Helmsman defined labels haven't been migrated to the Helm v3 releases state. 17 | 18 | ## Migrate to Helmsman v3.x 19 | - Download the latest Helmsman v3.x release from [Github releases](https://github.com/mkubaczyk/helmsman/releases) 20 | - Modify your Helmsman's TOML/YAML desired state files (DSFs) to be Helmsman v3.x compatible. You can check [v3.0.0 release notes](https://github.com/mkubaczyk/helmsman/blob/v3.0.0/release-notes.md) for what's changed and verify from the [Desired State Spec](https://github.com/mkubaczyk/helmsman/blob/master/docs/desired_state_specification.md) that your DSF files are compatible. 21 | 22 | > Everything related to Tiller will be removed from your DSFs at this stage. 23 | 24 | - Helmsman v3.x introduces the [`context` stanza](../../desired_state_specification.md#context) to logically group different groups of applications managed by Helmsman. It is highly recommended that you define a unique `context` for each of your DSFs at this stage. 25 | 26 | - In order for Helmsman to recognize the Helm v3 releases state, you need to use the `--migrate-context` flag on your first Helmsman v3.x run. This flag will recreate the Helmsman labels needed to recognize the Helm v3 releases state. 27 | - Make sure that `helm` binary points to Helm v3 in your environment before you run this command. 28 | - The `--migrate-context` flag is only available in Helmsman v.3.2.0 and above. If you are using Helmsman v3.0.x or v3.1.x, you can recreate the labels manually or with a script, but we highly recommend using Helmsman v3.2.0 or above. 29 | - You only need to use `--migrate-context` once. However, the flag is safe to use multiple times. 30 | 31 | ```bash 32 | $ helmsman --debug --migrate-context -f .yaml 33 | ``` 34 | 35 | At this stage, you should have your release migrate to Helm v3 and Helmsman v3.x is able to see and manage those releases as usual. The next step would be to clean up your Helm v2 state and remove Tiller deployment which you can do with the [Helm v3 2to3 plugin](https://helm.sh/blog/migrate-from-helm-v2-to-helm-v3/). -------------------------------------------------------------------------------- /internal/app/namespace.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Resources type 8 | type Resources struct { 9 | // CPU is the number of CPU cores 10 | CPU string `json:"cpu,omitempty"` 11 | // Memory is the amount of memory 12 | Memory string `json:"memory,omitempty"` 13 | } 14 | 15 | // custom resource type 16 | type CustomResource struct { 17 | // Name of the custom resource 18 | Name string `json:"name,omitempty"` 19 | // Value of the custom resource 20 | Value string `json:"value,omitempty"` 21 | } 22 | 23 | // Limit represents a resource limit 24 | type Limit struct { 25 | // Max defines the resource limits 26 | Max Resources `json:"max,omitempty"` 27 | // Min defines the resource request 28 | Min Resources `json:"min,omitempty"` 29 | // Default stes resource limits to pods without defined resource limits 30 | Default Resources `json:"default,omitempty"` 31 | // DefaultRequest sets the resource requests for pods without defined resource requests 32 | DefaultRequest Resources `json:"defaultRequest,omitempty"` 33 | // MaxLimitRequestRatio set the max limit request ratio 34 | MaxLimitRequestRatio Resources `json:"maxLimitRequestRatio,omitempty"` 35 | Type string `json:"type"` 36 | } 37 | 38 | // Limits type 39 | type Limits []Limit 40 | 41 | // quota type 42 | type Quotas struct { 43 | // Pods is the pods quota 44 | Pods string `json:"pods,omitempty"` 45 | // CPULimits is the CPU quota 46 | CPULimits string `json:"limits.cpu,omitempty"` 47 | // CPURequests is the CPU requests quota 48 | CPURequests string `json:"requests.cpu,omitempty"` 49 | // MemoryLimits is the memory quota 50 | MemoryLimits string `json:"limits.memory,omitempty"` 51 | // MemoryRequests is the memory requests quota 52 | MemoryRequests string `json:"requests.memory,omitempty"` 53 | // CustomResource is a list of custom resource quotas 54 | CustomQuotas []CustomResource `json:"customQuotas,omitempty"` 55 | } 56 | 57 | // Namespace type represents the fields of a Namespace 58 | type Namespace struct { 59 | // Protected if set to true no changes can be applied to the namespace 60 | Protected bool `json:"protected"` 61 | // Limits to set on the namespace 62 | Limits Limits `json:"limits,omitempty"` 63 | // Labels to set to the namespace 64 | Labels map[string]string `json:"labels,omitempty"` 65 | // Annotations to set on the namespace 66 | Annotations map[string]string `json:"annotations,omitempty"` 67 | // Quotas to set on the namespace 68 | Quotas *Quotas `json:"quotas,omitempty"` 69 | disabled bool 70 | } 71 | 72 | func (n *Namespace) Disable() { 73 | n.disabled = true 74 | } 75 | 76 | // print prints the namespace 77 | func (n *Namespace) print() { 78 | fmt.Println("\tprotected: ", n.Protected) 79 | fmt.Println("\tdisabled: ", n.disabled) 80 | fmt.Println("\tlabels:") 81 | printMap(n.Labels, 2) 82 | fmt.Println("\tannotations:") 83 | printMap(n.Annotations, 2) 84 | fmt.Println("-------------------") 85 | } 86 | -------------------------------------------------------------------------------- /internal/gcs/gcs.go: -------------------------------------------------------------------------------- 1 | package gcs 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | 10 | // Imports the Google Cloud Storage client package. 11 | "cloud.google.com/go/storage" 12 | "github.com/logrusorgru/aurora" 13 | netContext "golang.org/x/net/context" 14 | ) 15 | 16 | // colorizer 17 | var style aurora.Aurora 18 | 19 | func IsRunningInGCP() bool { 20 | resp, err := http.Get("http://metadata.google.internal") 21 | 22 | if resp != nil && resp.Body != nil { 23 | defer resp.Body.Close() 24 | } 25 | 26 | return err == nil 27 | } 28 | 29 | // Auth checks for GCLOUD_CREDENTIALS in the environment 30 | // returns true if they exist and creates a json credentials file and sets the GOOGLE_APPLICATION_CREDENTIALS env var 31 | // returns true if GCP metadata server is present 32 | // returns false if credentials are not found 33 | func Auth() (string, error) { 34 | if os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") != "" { 35 | return "GOOGLE_APPLICATION_CREDENTIALS is already set in the environment", nil 36 | } 37 | 38 | if os.Getenv("GCLOUD_CREDENTIALS") != "" { 39 | credFile := "/tmp/gcloud_credentials.json" 40 | // write the credentials content into a json file 41 | d := []byte(os.Getenv("GCLOUD_CREDENTIALS")) 42 | err := ioutil.WriteFile(credFile, d, 0o644) 43 | if err != nil { 44 | return fmt.Sprintf("Cannot create credentials file: %s", err), err 45 | } 46 | 47 | os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", credFile) 48 | return "ok", nil 49 | } 50 | 51 | if IsRunningInGCP() { 52 | return "Metadata server present, running in GCP", nil 53 | } 54 | 55 | return "can't authenticate", fmt.Errorf("can't authenticate") 56 | } 57 | 58 | // ReadFile reads a file from storage bucket and saves it in a desired location. 59 | func ReadFile(bucketName string, filename string, outFile string, noColors bool) (string, error) { 60 | style = aurora.NewAurora(!noColors) 61 | if msg, err := Auth(); err != nil { 62 | return msg, nil 63 | } 64 | 65 | ctx := netContext.Background() 66 | client, err := storage.NewClient(ctx) 67 | if err != nil { 68 | return "Failed to configure Storage bucket: ", err 69 | } 70 | storageBucket := client.Bucket(bucketName) 71 | 72 | // Creates an Object handler for our file 73 | obj := storageBucket.Object(filename) 74 | 75 | // Read the object. 76 | r, err := obj.NewReader(ctx) 77 | if err != nil { 78 | return fmt.Sprintf("Failed to create object reader: %s", err), err 79 | } 80 | defer r.Close() 81 | 82 | // create output file and write to it 83 | var writers []io.Writer 84 | file, err := os.Create(outFile) 85 | if err != nil { 86 | return fmt.Sprintf("Failed to create an output file: %s", err), err 87 | } 88 | writers = append(writers, file) 89 | defer file.Close() 90 | 91 | dest := io.MultiWriter(writers...) 92 | if _, err := io.Copy(dest, r); err != nil { 93 | return fmt.Sprintf("Failed to read object content: %s", err), err 94 | } 95 | return "Successfully downloaded " + filename + " from GCS as " + outFile, nil 96 | } 97 | -------------------------------------------------------------------------------- /CONTRIBUTION.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | Pull requests, feeback/feature requests are all welcome. This guide will be updated overtime. 4 | 5 | ## Build helmsman from source 6 | 7 | To build helmsman from source, you need go:1.17+. Follow the steps below: 8 | 9 | ```sh 10 | git clone https://github.com/mkubaczyk/helmsman.git 11 | make tools # installs few tools for testing, building, releasing 12 | make build 13 | make test 14 | ``` 15 | 16 | ## The branches and tags 17 | 18 | `master` is where Helmsman latest code lives. 19 | `1.x` this is where Helmsman versions 1.x lives. 20 | 21 | > Helmsman v1.x supports helm v2.x only and will no longer be supported except for bug fixes and minor changes. 22 | 23 | ## Commit messages 24 | 25 | This project uses [Conventional Commits](https://www.conventionalcommits.org/) to auto-generate release notes. 26 | 27 | | Type | Purpose | Example | 28 | |------|---------|---------| 29 | | `feat!:` | Breaking change | `feat!: remove deprecated API` | 30 | | `feat:` | New feature | `feat: add parallel release execution` | 31 | | `fix:` | Bug fix | `fix: helm 3.x compatibility in Dockerfile` | 32 | | `chore:` | Build, CI, deps, docs, infra | `chore: update goreleaser config` | 33 | 34 | Commits not matching these patterns appear under "Others" in release notes. 35 | 36 | ## Submitting pull requests 37 | 38 | - If your PR is for Helmsman v1.x, it should target the `1.x` branch. 39 | - Please make sure you state the purpose of the pull request and that the code you submit is documented. If in doubt, [this guide](https://blog.github.com/2015-01-21-how-to-write-the-perfect-pull-request/) offers some good tips on writing a PR. 40 | - Please make sure you update the documentation with new features or the changes your PR adds. The following places are required. 41 | - Update existing [how_to](docs/how_to/) guides or create new ones. 42 | - If necessary, Update the [Desired State File spec](docs/desired_state_specification.md) 43 | - If adding new flags, Update the [cmd reference](docs/cmd_reference.md) 44 | - Please add tests wherever possible to test your new changes. 45 | 46 | ## Contribution to documentation 47 | 48 | Contribution to the documentation can be done via pull requests or by opening an issue. 49 | 50 | ## Reporting issues/feature requests 51 | 52 | Please provide details of the issue, versions of helmsman, helm and kubernetes and all possible logs. 53 | 54 | ## Releasing Helmsman 55 | 56 | Release is automated via GitHub Actions based on Git tags. [Goreleaser](https://goreleaser.com) builds and publishes binaries to GitHub Releases, while the Docker workflow builds and pushes images to GHCR. Release notes are auto-generated from commit messages. 57 | 58 | To cut a release: 59 | 60 | 1. Ensure commits on master follow the commit message convention. 61 | 2. Create and push the tag: 62 | ```bash 63 | git checkout master && git pull 64 | git tag -a vX.Y.Z -m "vX.Y.Z" 65 | git push --tags 66 | ``` 67 | 68 | The tag triggers the build pipeline which runs tests, creates the GitHub release with binaries and auto-generated changelog, and pushes the Docker image. 69 | -------------------------------------------------------------------------------- /docs/how_to/misc/protect_namespaces_and_releases.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v1.3.0-rc 3 | --- 4 | 5 | # Namespace and Release Protection 6 | 7 | Since helmsman is used with version controlled code and is often configured to be triggered as part of a CI pipeline, accidental mistakes could happen by the user (e.g, disabling a production application and taking out of service as a result of a mistaken change in the desired state file). 8 | 9 | As of version v1.0.0, helmsman provides a fine-grained mechanism to protect releases/namespaces from accidental desired state file changes. 10 | 11 | ## Protection definition 12 | 13 | - When a release (application) is protected, it CANNOT: 14 | - deleted 15 | - upgraded 16 | - moved to another namespace 17 | 18 | - A release CAN be moved into protection from a non-protected state. 19 | - If a protected release need to be updated/changed or even deleted, this is possible, but the protection has to be removed first (i.e. remove the namespace/release from the protected state). This explained further below. 20 | 21 | > A release is an instance (installation) of an application which has been packaged as a helm chart. 22 | 23 | ## Protection mechanism 24 | Protection is supported in two forms: 25 | 26 | - **Namespace-level Protection**: is defined at the namespace level. A namespace can be declaratively defined to be protected in the desired state file as in the example below: 27 | 28 | ```toml 29 | [namespaces] 30 | [namespaces.staging] 31 | protected = false 32 | [namespaces.production] 33 | protected = true 34 | 35 | ``` 36 | 37 | - **Release-level Protection** is defined at the release level as in the example below: 38 | 39 | ```toml 40 | [apps] 41 | 42 | [apps.jenkins] 43 | namespace = "staging" 44 | enabled = true 45 | chart = "jenkins/jenkins" 46 | version = "2.15.1" 47 | protected = true # defining this release to be protected. 48 | ``` 49 | 50 | ```yaml 51 | apps: 52 | 53 | jenkins: 54 | namespace: "staging" 55 | enabled: true 56 | chart: "jenkins/jenkins" 57 | version: "2.15.1" 58 | protected: true # defining this release to be protected. 59 | ``` 60 | 61 | > All releases in a protected namespace are automatically protected. Namespace protection has higher priority than the release-level protection. 62 | 63 | ## Important Notes 64 | 65 | - You can combine both types of protection in your desired state file. The namespace-level protection always has a higher priority. 66 | - Removing the protection from a namespace means all releases deployed in that namespace are no longer protected. 67 | - We recommend using namespace-level protection for production namespace(s) and release-level protection for releases deployed in other namespaces. 68 | - Release/namespace protection is only applied on single desired state files. It is your responsibility to make sure that multiple desired state files (if used) do not conflict with each other (e.g, one defines a particular namespace as protected and another defines it unprotected.) If you use multiple desired state files with the same cluster, please refer to [deployment strategies](../../deployment_strategies.md) and [best practice](../../best_practice.md) documentation. 69 | -------------------------------------------------------------------------------- /internal/azure/azblob.go: -------------------------------------------------------------------------------- 1 | package azure 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/url" 10 | "os" 11 | 12 | "github.com/Azure/azure-pipeline-go/pipeline" 13 | "github.com/Azure/azure-storage-blob-go/azblob" 14 | "github.com/logrusorgru/aurora" 15 | ) 16 | 17 | // colorizer 18 | var ( 19 | style aurora.Aurora 20 | accountName string 21 | accountKey string 22 | p pipeline.Pipeline 23 | ) 24 | 25 | // auth checks for AZURE_STORAGE_ACCOUNT and AZURE_STORAGE_ACCESS_KEY in the environment 26 | // if env vars are set, it will authenticate and create an azblob request pipeline 27 | // returns false and error message if credentials are not set or are invalid 28 | func auth() error { 29 | accountName, accountKey = os.Getenv("AZURE_STORAGE_ACCOUNT"), os.Getenv("AZURE_STORAGE_ACCESS_KEY") 30 | if len(accountName) != 0 && len(accountKey) != 0 { 31 | log.Println("AZURE_STORAGE_ACCOUNT and AZURE_STORAGE_ACCESS_KEY are set in the environment. They will be used to connect to Azure storage.") 32 | // Create a default request pipeline 33 | credential, err := azblob.NewSharedKeyCredential(accountName, accountKey) 34 | if err == nil { 35 | p = azblob.NewPipeline(credential, azblob.PipelineOptions{}) 36 | return nil 37 | } 38 | return err 39 | 40 | } 41 | return fmt.Errorf("either the AZURE_STORAGE_ACCOUNT or AZURE_STORAGE_ACCESS_KEY environment variable is not set") 42 | } 43 | 44 | // ReadFile reads a file from storage container and saves it in a desired location. 45 | func ReadFile(containerName string, filename string, outFile string, noColors bool) { 46 | style = aurora.NewAurora(!noColors) 47 | if err := auth(); err != nil { 48 | log.Fatal(style.Bold(style.Red("ERROR: " + err.Error()))) 49 | } 50 | 51 | URL, _ := url.Parse( 52 | fmt.Sprintf("https://%s.blob.core.windows.net/%s", accountName, containerName)) 53 | 54 | containerURL := azblob.NewContainerURL(*URL, p) 55 | 56 | ctx := context.Background() 57 | 58 | blobURL := containerURL.NewBlockBlobURL(filename) 59 | downloadResponse, err := blobURL.Download(ctx, 0, azblob.CountToEnd, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{}) 60 | if err != nil { 61 | log.Fatal(style.Bold(style.Red("ERROR: failed to download file " + filename + " with error: " + err.Error()))) 62 | } 63 | bodyStream := downloadResponse.Body(azblob.RetryReaderOptions{MaxRetryRequests: 20}) 64 | 65 | // read the body into a buffer 66 | downloadedData := bytes.Buffer{} 67 | if _, err = downloadedData.ReadFrom(bodyStream); err != nil { 68 | log.Fatal(style.Bold(style.Red("ERROR: failed to download file " + filename + " with error: " + err.Error()))) 69 | } 70 | 71 | // create output file and write to it 72 | var writers []io.Writer 73 | file, err := os.Create(outFile) 74 | if err != nil { 75 | log.Fatal(style.Bold(style.Red("ERROR: Failed to create an output file: " + err.Error()))) 76 | } 77 | writers = append(writers, file) 78 | defer file.Close() 79 | 80 | dest := io.MultiWriter(writers...) 81 | if _, err := downloadedData.WriteTo(dest); err != nil { 82 | log.Fatal(style.Bold(style.Red("ERROR: Failed to read object content: " + err.Error()))) 83 | } 84 | log.Println("INFO: Successfully downloaded " + filename + " from Azure storage as " + outFile) 85 | } 86 | -------------------------------------------------------------------------------- /docs/how_to/apps/order.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.0.0-beta5 3 | --- 4 | 5 | # Using the priority key for Apps 6 | 7 | The `priority` flag in Apps definition allows you to define the order at which apps operations will be applied. This is useful if you have dependencies between your apps/services. 8 | 9 | Priority is an optional flag and has a default value of 0 (zero). If set, it can only use a negative value. The lower the value, the higher the priority. 10 | 11 | If you want your apps to be deleted in the reverse order as they where created, you can also use the optional `Settings` flag `reverseDelete`, to achieve this, set it to `true` 12 | 13 | ## Example 14 | 15 | ```toml 16 | [metadata] 17 | org = "example.com" 18 | description = "example Desired State File for demo purposes." 19 | 20 | [settings] 21 | kubeContext = "minikube" 22 | reverseDelete = false # Optional flag to reverse the priorities when deleting 23 | 24 | [namespaces] 25 | [namespaces.staging] 26 | protected = false 27 | [namespaces.production] 28 | protected = true 29 | 30 | [helmRepos] 31 | jenkins = https://charts.jenkins.io 32 | center = https://repo.chartcenter.io 33 | 34 | [apps] 35 | [apps.jenkins] 36 | description = "jenkins" 37 | namespace = "staging" # maps to the namespace as defined in environments above 38 | enabled = true # change to false if you want to delete this app release [empty = false] 39 | chart = "jenkins/jenkins" # changing the chart name means delete and recreate this chart 40 | version = "2.15.1" # chart version 41 | valuesFile = "" # leaving it empty uses the default chart values 42 | priority= -2 43 | 44 | [apps.jenkins1] 45 | description = "jenkins" 46 | namespace = "staging" # maps to the namespace as defined in environments above 47 | enabled = true # change to false if you want to delete this app release [empty = false] 48 | chart = "jenkins/jenkins" # changing the chart name means delete and recreate this chart 49 | version = "2.15.1" # chart version 50 | valuesFile = "" # leaving it empty uses the default chart values 51 | 52 | [apps.jenkins2] 53 | description = "jenkins" 54 | namespace = "production" # maps to the namespace as defined in environments above 55 | enabled = true # change to false if you want to delete this app release [empty = false] 56 | chart = "jenkins/jenkins" # changing the chart name means delete and recreate this chart 57 | version = "2.15.1" # chart version 58 | valuesFile = "" # leaving it empty uses the default chart values 59 | priority= -3 60 | 61 | [apps.artifactory] 62 | description = "artifactory" 63 | namespace = "staging" # maps to the namespace as defined in environments above 64 | enabled = true # change to false if you want to delete this app release [empty = false] 65 | chart = "jfrog/artifactory" # changing the chart name means delete and recreate this chart 66 | version = "11.4.2" # chart version 67 | valuesFile = "" # leaving it empty uses the default chart values 68 | priority= -2 69 | ``` 70 | 71 | The above example will generate the following plan: 72 | 73 | ```console 74 | DECISION: release [ jenkins2 ] is not present in the current k8s context. Will install it in namespace [[ production ]] -- priority: -3 75 | DECISION: release [ jenkins ] is not present in the current k8s context. Will install it in namespace [[ staging ]] -- priority: -2 76 | DECISION: release [ artifactory ] is not present in the current k8s context. Will install it in namespace [[ staging ]] -- priority: -2 77 | DECISION: release [ jenkins1 ] is not present in the current k8s context. Will install it in namespace [[ staging ]] -- priority: 0 78 | ``` 79 | -------------------------------------------------------------------------------- /docs/why_helmsman.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v0.1.2 3 | --- 4 | 5 | # Why Helmsman? 6 | 7 | This document describes the reasoning and need behind the inception of Helmsman. 8 | 9 | ## Before Helm 10 | 11 | Helmsman was created with continuous deployment in mind. 12 | When we started using Kubernetes (k8s), we deployed applications on our cluster directly from k8s manifest files. Initially, we had a custom shell script added to our CI system to deploy the k8s resources on the cluster. 13 | 14 | ![CI-pipeline-before-helm](images/CI-pipeline-before-helm.jpg) 15 | 16 | That script could only create the k8s resources from the manifest files. Soon we needed to have a more flexible way to dynamically create/delete those resources. We structured our git repo and used custom file names (adding enabled or disabled into file names) and updated the shell script accordingly. It did not take long before we realized that this does not scale and is difficult to maintain. 17 | 18 | ## Helm to the rescue? 19 | 20 | While looking for solutions for managing the growing number of k8s manifest files from a CI pipeline, we came to know about Helm and quickly realized its potential. 21 | By creating Helm charts, we packaged related k8s manifests together into a single entity: "a chart". 22 | 23 | ![CI-pipeline-after-helm](images/CI-pipeline-after-helm.jpg) 24 | 25 | This reduced the amount of files the CI script has to deal with. However, all the CI shell script could do is package a chart and install/upgrade it in our k8s cluster whenever a new commit is done into the chart's files in git. 26 | 27 | But there were a few issues: 28 | 1. Helm has more to it than package and install. Operations such as rollback, running chart tests, etc. are only doable from Helm's CLI client. 29 | 2. You have to keep updating your CI script every time you add a chart to k8s. 30 | 3. What if you want to do the same on another cluster? you will have to replicate your CI pipeline and possibly change your CI script accordingly. 31 | 32 | Helm chart development is split from the git repositories where they are used. This is simply to let us develop the charts independently from the projects where we used them and to allow us to reuse them in different projects. 33 | 34 | With all this in mind, we needed a flexible and dynamic solution that can let us deploy and manage Helm charts into multiple k8s clusters independently and with minimum human intervention. Such a solution should be generic enough to be reusable for many different projects/cluster. And this is where Helmsman was born! 35 | 36 | ## The Helmsman way 37 | 38 | In English, a [Helmsman](https://www.merriam-webster.com/dictionary/helmsman) is the person at the helm (on a ship). In k8s and Helm context, Helmsman holds the Helm and maintains your Helm charts' lifecycle in your k8s cluster(s). Helmsman gets its directions to navigate from a [declarative file](desired_state_specification.md) maintained by the user (k8s admin). 39 | 40 | > Although knowledge about Helm and K8S is highly beneficial, such knowledge is NOT required to use Helmsman. 41 | 42 | As the diagram below shows, we recommend having a Helmsman _desired state file_ for each k8s cluster you are managing. 43 | 44 | ![CI-pipeline-helmsman](images/CI-pipeline-helmsman.jpg) 45 | 46 | Along with that file, you would need to have any custom [values yaml files](https://docs.helm.sh/chart_template_guide/#values-files) for the Helm charts you deploy on your k8s. Then you could configure your CI pipeline to use Helmsman docker images to process your desired state file whenever a commit is made to it. 47 | 48 | 49 | > Helmsman can also be used manually as a binary tool on a machine which has Helm and Kubectl installed. 50 | -------------------------------------------------------------------------------- /internal/app/cli_test.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import "testing" 4 | 5 | var _ = func() bool { 6 | testing.Init() 7 | return true 8 | }() 9 | 10 | func init() { 11 | flags.parse() 12 | } 13 | 14 | func Test_readState(t *testing.T) { 15 | type result struct { 16 | numApps int 17 | numNSs int 18 | numEnabledApps int 19 | numEnabledNSs int 20 | } 21 | tests := []struct { 22 | name string 23 | flags cli 24 | want result 25 | }{ 26 | { 27 | name: "yaml minimal example; no validation", 28 | flags: cli{ 29 | files: fileOptionArray([]fileOption{{"../../examples/minimal-example.yaml", 0}}), 30 | skipValidation: true, 31 | }, 32 | want: result{ 33 | numApps: 2, 34 | numNSs: 1, 35 | numEnabledApps: 2, 36 | numEnabledNSs: 1, 37 | }, 38 | }, 39 | { 40 | name: "toml minimal example; no validation", 41 | flags: cli{ 42 | files: fileOptionArray([]fileOption{{"../../examples/minimal-example.toml", 0}}), 43 | skipValidation: true, 44 | }, 45 | want: result{ 46 | numApps: 2, 47 | numNSs: 1, 48 | numEnabledApps: 2, 49 | numEnabledNSs: 1, 50 | }, 51 | }, 52 | { 53 | name: "yaml minimal example; no validation with bad target", 54 | flags: cli{ 55 | target: stringArray([]string{"foo"}), 56 | files: fileOptionArray([]fileOption{{"../../examples/minimal-example.yaml", 0}}), 57 | skipValidation: true, 58 | }, 59 | want: result{ 60 | numApps: 2, 61 | numNSs: 1, 62 | numEnabledApps: 0, 63 | numEnabledNSs: 0, 64 | }, 65 | }, 66 | { 67 | name: "yaml minimal example; no validation; target jenkins", 68 | flags: cli{ 69 | target: stringArray([]string{"jenkins"}), 70 | files: fileOptionArray([]fileOption{{"../../examples/minimal-example.yaml", 0}}), 71 | skipValidation: true, 72 | }, 73 | want: result{ 74 | numApps: 2, 75 | numNSs: 1, 76 | numEnabledApps: 1, 77 | numEnabledNSs: 1, 78 | }, 79 | }, 80 | { 81 | name: "yaml and toml minimal examples merged; no validation", 82 | flags: cli{ 83 | files: fileOptionArray([]fileOption{{"../../examples/minimal-example.yaml", 0}, {"../../examples/minimal-example.toml", 0}}), 84 | skipValidation: true, 85 | }, 86 | want: result{ 87 | numApps: 2, 88 | numNSs: 1, 89 | numEnabledApps: 2, 90 | numEnabledNSs: 1, 91 | }, 92 | }, 93 | } 94 | for _, tt := range tests { 95 | t.Run(tt.name, func(t *testing.T) { 96 | s := State{} 97 | if err := tt.flags.readState(&s); err != nil { 98 | t.Errorf("readState() = Unexpected error: %v", err) 99 | } 100 | if len(s.Apps) != tt.want.numApps { 101 | t.Errorf("readState() = app count mismatch: want: %d, got: %d", tt.want.numApps, len(s.Apps)) 102 | } 103 | if len(s.Namespaces) != tt.want.numNSs { 104 | t.Errorf("readState() = NS count mismatch: want: %d, got: %d", tt.want.numNSs, len(s.Namespaces)) 105 | } 106 | 107 | var enabledApps, enabledNSs int 108 | for _, a := range s.Apps { 109 | if !a.disabled { 110 | enabledApps++ 111 | } 112 | } 113 | if enabledApps != tt.want.numEnabledApps { 114 | t.Errorf("readState() = app count mismatch: want: %d, got: %d", tt.want.numEnabledApps, enabledApps) 115 | } 116 | for _, n := range s.Namespaces { 117 | if !n.disabled { 118 | enabledNSs++ 119 | } 120 | } 121 | if enabledNSs != tt.want.numEnabledNSs { 122 | t.Errorf("readState() = app count mismatch: want: %d, got: %d", tt.want.numEnabledNSs, enabledNSs) 123 | } 124 | }) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /internal/app/main.go: -------------------------------------------------------------------------------- 1 | // Package app contains the main logic for the application. 2 | package app 3 | 4 | import ( 5 | "os" 6 | ) 7 | 8 | const ( 9 | helmBin = "helm" 10 | kubectlBin = "kubectl" 11 | tempFilesDir = ".helmsman-tmp" 12 | defaultContextName = "default" 13 | resourcePool = 10 14 | ) 15 | 16 | var appVersion = "dev" 17 | 18 | const ( 19 | exitCodeSucceed = 0 20 | exitCodeSucceedWithChanges = 2 21 | ) 22 | 23 | var ( 24 | flags cli 25 | settings *Config 26 | curContext string 27 | log = &Logger{} 28 | ) 29 | 30 | func init() { 31 | // Parse cli flags and read config files 32 | flags.setup() 33 | } 34 | 35 | // Main is the app main function 36 | func Main() int { 37 | var s State 38 | 39 | flags.parse() 40 | 41 | // delete temp files with substituted env vars when the program terminates 42 | defer os.RemoveAll(tempFilesDir) 43 | if !flags.noCleanup { 44 | defer s.cleanup() 45 | } 46 | 47 | if err := flags.readState(&s); err != nil { 48 | log.Fatal(err.Error()) 49 | } 50 | 51 | if len(flags.target) > 0 && len(s.targetMap) == 0 { 52 | log.Info("No apps defined with -target flag were found, exiting") 53 | os.Exit(0) 54 | } 55 | 56 | if len(flags.group) > 0 && len(s.targetMap) == 0 { 57 | log.Info("No apps defined with -group flag were found, exiting") 58 | os.Exit(0) 59 | } 60 | 61 | log.SlackWebhook = s.Settings.SlackWebhook 62 | log.MSTeamsWebhook = s.Settings.MSTeamsWebhook 63 | 64 | settings = &s.Settings 65 | curContext = s.Context 66 | 67 | // set the kubecontext to be used Or create it if it does not exist 68 | log.Info("Setting up kubectl") 69 | if !setKubeContext(s.Settings.KubeContext) { 70 | if err := createContext(&s); err != nil { 71 | log.Fatal(err.Error()) 72 | } 73 | } 74 | 75 | // add repos -- fails if they are not valid 76 | log.Info("Setting up helm") 77 | if err := addHelmRepos(s.HelmRepos); err != nil && !flags.destroy { 78 | log.Fatal(err.Error()) 79 | } 80 | 81 | if flags.apply || flags.dryRun || flags.destroy { 82 | // add/validate namespaces 83 | if !flags.noNs { 84 | log.Info("Setting up namespaces") 85 | if flags.nsOverride == "" { 86 | addNamespaces(&s) 87 | } else { 88 | createNamespace(flags.nsOverride, nil, nil) 89 | s.overrideAppsNamespace(flags.nsOverride) 90 | } 91 | } 92 | } 93 | 94 | log.Info("Getting chart information") 95 | 96 | err := s.getReleaseChartsInfo() 97 | if flags.skipValidation { 98 | log.Info("Skipping charts' validation.") 99 | } else if err != nil { 100 | log.Fatal(err.Error()) 101 | } else { 102 | log.Info("Charts validated.") 103 | } 104 | 105 | if flags.destroy { 106 | log.Warning("Destroy flag is enabled. Your releases will be deleted!") 107 | } 108 | 109 | if flags.migrateContext { 110 | log.Warning("migrate-context flag is enabled. Context will be changed to [ " + s.Context + " ] and Helmsman labels will be applied.") 111 | s.updateContextLabels() 112 | } 113 | 114 | if flags.checkForChartUpdates { 115 | for _, r := range s.Apps { 116 | r.checkChartForUpdates() 117 | } 118 | } 119 | 120 | log.Info("Preparing plan") 121 | cs := s.getCurrentState() 122 | p := cs.makePlan(&s) 123 | if !flags.keepUntrackedReleases { 124 | cs.cleanUntrackedReleases(&s, p) 125 | } 126 | 127 | p.sort() 128 | p.print() 129 | if flags.debug { 130 | p.printCmds() 131 | } 132 | p.sendToSlack() 133 | p.sendToMSTeams() 134 | 135 | if flags.apply || flags.dryRun || flags.destroy { 136 | p.exec() 137 | } 138 | 139 | exitCode := exitCodeSucceed 140 | 141 | if flags.detailedExitCode && len(p.Commands) > 0 { 142 | exitCode = exitCodeSucceedWithChanges 143 | } 144 | 145 | return exitCode 146 | } 147 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mkubaczyk/helmsman 2 | 3 | go 1.24.1 4 | 5 | require ( 6 | cloud.google.com/go/storage v1.58.0 7 | dario.cat/mergo v1.0.2 8 | github.com/Azure/azure-pipeline-go v0.2.3 9 | github.com/Azure/azure-storage-blob-go v0.15.0 10 | github.com/BurntSushi/toml v1.6.0 11 | github.com/Masterminds/semver v1.5.0 12 | github.com/apsdehal/go-logger v0.0.0-20190515212710-b0d6ccfee0e6 13 | github.com/aws/aws-sdk-go v1.55.8 14 | github.com/invopop/jsonschema v0.13.0 15 | github.com/logrusorgru/aurora v2.0.3+incompatible 16 | github.com/subosito/gotenv v1.6.0 17 | golang.org/x/net v0.48.0 18 | sigs.k8s.io/yaml v1.6.0 19 | ) 20 | 21 | require ( 22 | cel.dev/expr v0.24.0 // indirect 23 | cloud.google.com/go v0.123.0 // indirect 24 | cloud.google.com/go/auth v0.17.0 // indirect 25 | cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect 26 | cloud.google.com/go/compute/metadata v0.9.0 // indirect 27 | cloud.google.com/go/iam v1.5.3 // indirect 28 | cloud.google.com/go/monitoring v1.24.2 // indirect 29 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect 30 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect 31 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect 32 | github.com/bahlo/generic-list-go v0.2.0 // indirect 33 | github.com/buger/jsonparser v1.1.1 // indirect 34 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 35 | github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect 36 | github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect 37 | github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect 38 | github.com/felixge/httpsnoop v1.0.4 // indirect 39 | github.com/go-jose/go-jose/v4 v4.1.2 // indirect 40 | github.com/go-logr/logr v1.4.3 // indirect 41 | github.com/go-logr/stdr v1.2.2 // indirect 42 | github.com/google/s2a-go v0.1.9 // indirect 43 | github.com/google/uuid v1.6.0 // indirect 44 | github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect 45 | github.com/googleapis/gax-go/v2 v2.15.0 // indirect 46 | github.com/jmespath/go-jmespath v0.4.0 // indirect 47 | github.com/mailru/easyjson v0.7.7 // indirect 48 | github.com/mattn/go-ieproxy v0.0.1 // indirect 49 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect 50 | github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect 51 | github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect 52 | github.com/zeebo/errs v1.4.0 // indirect 53 | go.opentelemetry.io/auto/sdk v1.2.1 // indirect 54 | go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect 55 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect 56 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect 57 | go.opentelemetry.io/otel v1.38.0 // indirect 58 | go.opentelemetry.io/otel/metric v1.38.0 // indirect 59 | go.opentelemetry.io/otel/sdk v1.38.0 // indirect 60 | go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect 61 | go.opentelemetry.io/otel/trace v1.38.0 // indirect 62 | go.yaml.in/yaml/v2 v2.4.2 // indirect 63 | golang.org/x/crypto v0.46.0 // indirect 64 | golang.org/x/oauth2 v0.33.0 // indirect 65 | golang.org/x/sync v0.19.0 // indirect 66 | golang.org/x/sys v0.39.0 // indirect 67 | golang.org/x/text v0.32.0 // indirect 68 | golang.org/x/time v0.14.0 // indirect 69 | google.golang.org/api v0.256.0 // indirect 70 | google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 // indirect 71 | google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect 72 | google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect 73 | google.golang.org/grpc v1.76.0 // indirect 74 | google.golang.org/protobuf v1.36.10 // indirect 75 | gopkg.in/yaml.v3 v3.0.1 // indirect 76 | ) 77 | -------------------------------------------------------------------------------- /docs/how_to/README.md: -------------------------------------------------------------------------------- 1 | 2 | # How To Guides 3 | 4 | This page contains a list of guides on how to use Helmsman. 5 | 6 | It is recommended that you also check the [DSF spec](../desired_state_specification.md), [cmd reference](../cmd_reference.md), and the [best practice guide](../best_practice.md). 7 | 8 | - [Migrating from Helm 2 (Helmsman v1.x) to Helm 3 (Helmsman v3.x)](misc/migrate_to_3.md) 9 | 10 | - Connecting to Kubernetes clusters 11 | - [Using an existing kube context](settings/existing_kube_context.md) 12 | - [Using the current kube context](settings/current_kube_context.md) 13 | - [Connecting with certificates](settings/creating_kube_context_with_certs.md) 14 | - [Connecting with bearer token](settings/creating_kube_context_with_token.md) 15 | - Defining Namespaces 16 | - [Create namespaces](namespaces/create.md) 17 | - [Label namespaces](namespaces/labels_and_annotations.md) 18 | - [Set resource limits for namespaces](namespaces/limits.md) 19 | - [Protecting namespaces](namespaces/protection.md) 20 | - [Namespace resource quotas](namespaces/quotas.md) 21 | - Defining Helm repositories 22 | - [Using default helm repos](helm_repos/default.md) 23 | - [Using private repos in Google GCS](helm_repos/gcs.md) 24 | - [Using private repos in AWS S3](helm_repos/s3.md) 25 | - [Using private repos with basic auth](helm_repos/basic_auth.md) 26 | - [Using pre-configured repos](helm_repos/pre_configured.md) 27 | - [Using local charts](helm_repos/local.md) 28 | - Manipulating Apps 29 | - [Basic operations](apps/basic.md) 30 | - [Passing secrets to releases](apps/secrets.md) 31 | - [Using environment variables in helmsman file and helm values files](apps/environment_vars.md) 32 | - [Apply K8S manifest before/after Helmsman operations](apps/lifecycle_hooks.md) 33 | - [Use multiple values files for apps](apps/multiple_values_files.md) 34 | - [Protect releases (apps)](apps/protection.md) 35 | - [Moving releases (apps) across namespaces](apps/moving_across_namespaces.md) 36 | - [Override defined namespaces](apps/override_namespaces.md) 37 | - [Run helm tests for deployed releases (apps)](apps/helm_tests.md) 38 | - [Define the order of apps operations](apps/order.md) 39 | - [Delete all releases (apps)](apps/destroy.md) 40 | - [Distinguish releases deployed from different DSF files using Helmsman's contexts](misc/merge_desired_state_files.md#distinguishing-releases-deployed-from-different-desired-state-files) 41 | - [Migrating releases from Helmsman context to another](apps/migrate_contexts.md) 42 | - [Rename Helmsman's contexts](apps/migrate_contexts.md) 43 | - [Speed up Helmsman execution by skipping context fetching](apps/override_context_from_cmd.md) 44 | - [Override context from cmd flags](apps/override_context_from_cmd.md) 45 | - Running Helmsman in different environments 46 | - [Running Helmsman in CI](deployments/ci.md) 47 | - [Running Helmsman inside your k8s cluster](deployments/inside_k8s.md) 48 | - Misc 49 | - [Authenticating to cloud storage providers](misc/auth_to_storage_providers.md) 50 | - [Protecting namespaces and releases](misc/protect_namespaces_and_releases.md) 51 | - [Send slack notifications from Helmsman](misc/send_slack_notifications_from_helmsman.md) 52 | - [Send MS Teams notifications from Helmsman](misc/send_ms_teams_notifications_from_helmsman.md) 53 | - [Use multiple desired state files with Specification file (--spec flag)](misc/multiple_desired_state_files_specification.md) 54 | - [Merge multiple desired state files](misc/merge_desired_state_files.md) 55 | - [Limit Helmsman deployment to specific apps](misc/limit-deployment-to-specific-apps.md) 56 | - [Limit Helmsman deployment to specific group of apps](misc/limit-deployment-to-specific-group-of-apps.md) 57 | - [Exclude apps or groups from Helmsman deployment](misc/exclude-apps-or-groups-from-deployment.md) 58 | - [Use hiera-eyaml as secrets encryption backend](settings/use-hiera-eyaml-as-secrets-encryption.md) 59 | - [Use DRY-ed code](misc/use-dry-code.md) 60 | -------------------------------------------------------------------------------- /docs/how_to/apps/override_namespaces.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.0.0-beta5 3 | --- 4 | 5 | # Override defined namespaces from command line 6 | 7 | If you use different release branches for your releasing/managing your applications in your k8s clusters, then you might want to use the same desired state but with different namespaces on each branch. Instead of duplicating the DSF in multiple branches and adjusting it, you can use the `--ns-override` command line flag when running helmsman. 8 | 9 | This flag overrides all namespaces defined in your DSF with the single one you pass from command line. 10 | 11 | ## Example 12 | 13 | `dsf.toml`: 14 | 15 | ```toml 16 | [metadata] 17 | org = "example.com" 18 | description = "example Desired State File for demo purposes." 19 | 20 | 21 | [settings] 22 | kubeContext = "minikube" 23 | 24 | [namespaces] 25 | [namespaces.staging] 26 | protected = false 27 | [namespaces.production] 28 | prtoected = true 29 | 30 | [helmRepos] 31 | jenkins = https://charts.jenkins.io 32 | center = https://repo.chartcenter.io 33 | 34 | [apps] 35 | 36 | [apps.jenkins] 37 | description = "jenkins" 38 | namespace = "production" # maps to the namespace as defined in environments above 39 | enabled = true # change to false if you want to delete this app release [empty = false] 40 | chart = "jenkins/jenkins" # changing the chart name means delete and recreate this chart 41 | version = "2.15.1" # chart version 42 | valuesFile = "" # leaving it empty uses the default chart values 43 | 44 | [apps.artifactory] 45 | description = "artifactory" 46 | namespace = "staging" # maps to the namespace as defined in environments above 47 | enabled = true # change to false if you want to delete this app release [empty = false] 48 | chart = "jfrog/artifactory" # changing the chart name means delete and recreate this chart 49 | version = "11.4.2" # chart version 50 | valuesFile = "" # leaving it empty uses the default chart values 51 | ``` 52 | 53 | `dsf.yaml`: 54 | 55 | ```yaml 56 | metadata: 57 | org: "example.com" 58 | description: "example Desired State File for demo purposes." 59 | 60 | 61 | settings: 62 | kubeContext: "minikube" 63 | 64 | namespaces: 65 | staging: 66 | protected: false 67 | production: 68 | protected: true 69 | 70 | helmRepos: 71 | jenkins: https://charts.jenkins.io 72 | jfrog: https://charts.jfrog.io 73 | 74 | apps: 75 | 76 | jenkins: 77 | description: "jenkins" 78 | namespace: "production" # maps to the namespace as defined in environments above 79 | enabled: true # change to false if you want to delete this app release [empty: false] 80 | chart: "jenkins/jenkins" # changing the chart name means delete and recreate this chart 81 | version: "2.15.1" # chart version 82 | valuesFile: "" # leaving it empty uses the default chart values 83 | 84 | artifactory: 85 | description: "artifactory" 86 | namespace: "staging" # maps to the namespace as defined in environments above 87 | enabled: true # change to false if you want to delete this app release [empty: false] 88 | chart: "jfrog/artifactory" # changing the chart name means delete and recreate this chart 89 | version: "11.4.2" # chart version 90 | valuesFile: "" # leaving it empty uses the default chart values 91 | ``` 92 | 93 | In command line, we run : 94 | 95 | ```shell 96 | helmsman -f dsf.toml --debug --ns-override testing 97 | ``` 98 | 99 | This will override the `staging` and `production` namespaces defined in `dsf.toml` : 100 | 101 | ```console 102 | 2018/03/31 17:38:12 INFO: Plan generated at: Sat Mar 31 2018 17:37:57 103 | DECISION: release [ jenkins ] is not present in the current k8s context. Will install it in namespace [[ testing ]] -- priority: 0 104 | DECISION: release [ artifactory ] is not present in the current k8s context. Will install it in namespace [[ testing ]] -- priority: 0 105 | ``` 106 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | PKGS := $(shell go list ./... | grep -v /vendor/) 4 | TAG := $(shell git describe --always --tags --abbrev=0 HEAD) 5 | LAST := $(shell git describe --always --tags --abbrev=0 HEAD^) 6 | BODY := "`git log ${LAST}..HEAD --oneline --decorate` `printf '\n\#\#\# [Build Info](${BUILD_URL})'`" 7 | DATE := $(shell date +'%d%m%y') 8 | PRJNAME := $(shell basename "$(PWD)") 9 | 10 | # Ensure we have an unambiguous GOPATH. 11 | GOPATH := $(shell go env GOPATH) 12 | 13 | ifneq ($(strip $(CIRCLE_WORKING_DIRECTORY)),) 14 | GOPATH := $(subst /src/$(PRJNAME),,$(CIRCLE_WORKING_DIRECTORY)) 15 | $(info "Using CIRCLE_WORKING_DIRECTORY for GOPATH") 16 | endif 17 | 18 | ifneq ($(OS),Windows_NT) 19 | # Before we start test that we have the mandatory executables available 20 | EXECUTABLES = go 21 | OK := $(foreach exec,$(EXECUTABLES),\ 22 | $(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH, please install $(exec)"))) 23 | endif 24 | 25 | export CGO_ENABLED=0 26 | export GO111MODULE=on 27 | export GOFLAGS=-mod=vendor 28 | 29 | help: 30 | @echo "Available options:" 31 | @grep -E '^[/1-9a-zA-Z._%-]+:.*?## .*$$' $(MAKEFILE_LIST) \ 32 | | sort \ 33 | | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-45s\033[0m %s\n", $$1, $$2}' 34 | .PHONY: help 35 | 36 | clean: ## Remove build artifacts 37 | @git clean -fdX 38 | .PHONY: clean 39 | 40 | fmt: ## Reformat package sources 41 | @go fmt ./... 42 | .PHONY: fmt 43 | 44 | vet: fmt 45 | @go vet ./... 46 | .PHONY: vet 47 | 48 | imports: ## Ensure imports are present and formatted 49 | @goimports -w $(shell find . -type f -name '*.go' -not -path './vendor/*') 50 | .PHONY: goimports 51 | 52 | deps: ## Install depdendencies. Runs `go get` internally. 53 | @GOFLAGS="" go get -t -v ./... 54 | @GOFLAGS="" go mod tidy 55 | @GOFLAGS="" go mod vendor 56 | 57 | 58 | update-deps: ## Update depdendencies. Runs `go get -u` internally. 59 | @GOFLAGS="" go get -t -u ./... 60 | @GOFLAGS="" go mod tidy 61 | @GOFLAGS="" go mod vendor 62 | 63 | build: deps vet schema ## Build the package 64 | @go build -o helmsman -ldflags '-X github.com/mkubaczyk/helmsman/internal/app.appVersion="${TAG}-${DATE}" -extldflags "-static"' cmd/helmsman/main.go 65 | 66 | generate: 67 | @go generate #${PKGS} 68 | .PHONY: generate 69 | 70 | schema: ## Generate the schema.json file 71 | @go run schema.go 72 | 73 | repo: 74 | @helm repo list | grep -q "^prometheus-community " || helm repo add prometheus-community https://prometheus-community.github.io/helm-charts 75 | @helm repo update 76 | .PHONY: repo 77 | 78 | test: deps vet schema repo ## Run unit tests 79 | @go test -v -cover -p=1 ./... -args -f ../../examples/example.toml 80 | .PHONY: test 81 | 82 | test-%: deps vet repo ## Run a specific unit test 83 | @go test -v -cover -p=1 -run $(*) ./... -args -f ../../examples/example.toml 84 | 85 | cross: deps ## Create binaries for all OSs 86 | @gox -os 'windows linux darwin' -arch 'arm64 amd64' -output "dist/{{.Dir}}_{{.OS}}_{{.Arch}}" -ldflags '-X github.com/mkubaczyk/helmsman/internal/app.appVersion=${TAG}-${DATE}' ./... 87 | .PHONY: cross 88 | 89 | release: ## Generate a new release 90 | @goreleaser --release-notes release-notes.md --rm-dist 91 | 92 | tools: ## Get extra tools used by this makefile 93 | @go get -d -u github.com/golang/dep/cmd/dep 94 | @go get -d -u github.com/mitchellh/gox 95 | @go get -d -u github.com/goreleaser/goreleaser 96 | @gem install hiera-eyaml 97 | .PHONY: tools 98 | 99 | helmPlugins: ## Install helm plugins used by Helmsman 100 | @mkdir -p ~/.helm/plugins 101 | @helm plugin install --verify=false https://github.com/hypnoglow/helm-s3.git 102 | @helm plugin install --verify=false https://github.com/nouney/helm-gcs 103 | @helm plugin install --verify=false https://github.com/databus23/helm-diff 104 | @helm plugin install --verify=false https://github.com/jkroepke/helm-secrets 105 | .PHONY: helmPlugins 106 | -------------------------------------------------------------------------------- /docs/how_to/misc/merge_desired_state_files.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.0.0-beta5 3 | --- 4 | 5 | # Supply multiple desired state files 6 | 7 | Starting from v1.5.0, Helmsman allows you to pass the `-f` flag multiple times to specify multiple desired state files 8 | that should be merged. This allows us to do things like specify our non-environment-specific config in a `common.toml` file 9 | and environment specific info in a `nonprod.toml` or `prod.toml` file. This process uses [this library](https://github.com/imdario/mergo) 10 | to do the merging, and is subject to the limitations described there. 11 | 12 | For example: 13 | 14 | `common.toml`: 15 | 16 | ```toml 17 | [metadata] 18 | org = "Organization Name" 19 | maintainer = "project-owners@example.com" 20 | description = "Project charts" 21 | 22 | [settings] 23 | serviceAccount = "tiller" 24 | storageBackend = "secret" 25 | ... 26 | ``` 27 | 28 | `nonprod.toml`: 29 | 30 | ```toml 31 | [settings] 32 | kubeContext = "cluster-nonprod" 33 | 34 | [apps] 35 | [apps.external-dns] 36 | valuesFiles = ["./external-dns/values.yaml", "./external-dns/nonprod.yaml"] 37 | 38 | [apps.cert-issuer] 39 | valuesFile = "./cert-issuer/nonprod.yaml" 40 | ... 41 | ``` 42 | 43 | One can then run the following to use the merged config of the above files, with later files override values of earlier ones: 44 | 45 | ```shell 46 | helmsman -f common.toml -f nonprod.toml ... 47 | ``` 48 | 49 | ## Distinguishing releases deployed from different Desired State Files 50 | 51 | When using multiple DSFs -and since Helmsman doesn't maintain any external state-, it has been possible for operations from one DSF to cause problems to releases deployed by other DSFs. A typical example is that releases deployed by other DSFs are considered `untracked` and get scheduled for deleting. Workarounds existed (e.g. using the `--keep-untracked-releases`, `--target` and `--group` flags). 52 | 53 | Starting from Helmsman v3.0.0-beta5, `context` is introduced to define the context in which a DSF is used. This context is used as the ID of that specific DSF and must be unique across the used DSFs. The context is then used to label the different releases to link them to the DSF they were first deployed from. These labels are then checked by Helmsman on each run to make sure operations are limited to releases from a specific context. 54 | 55 | Here is how it is used: 56 | 57 | `infra.yaml`: 58 | 59 | ```yaml 60 | context: infra-apps 61 | settings: 62 | kubeContext: "cluster" 63 | storageBackend: "secret" 64 | 65 | namespaces: 66 | infra: 67 | protected: true 68 | 69 | apps: 70 | external-dns: 71 | namespace: infra 72 | valuesFile: "./external-dns/values.yaml" 73 | ... 74 | 75 | cert-issuer: 76 | namespace: infra 77 | valuesFile: "./cert-issuer/nonprod.yaml" 78 | ... 79 | ... 80 | ``` 81 | 82 | `prod.yaml`: 83 | 84 | ```yaml 85 | context: prod-apps 86 | settings: 87 | kubeContext: "cluster" 88 | storageBackend: "secret" 89 | 90 | namespaces: 91 | prod: 92 | protected: true 93 | 94 | apps: 95 | my-prod-app: 96 | namespace: prod 97 | valuesFile: "./my-prod-app/values.yaml" 98 | ... 99 | ... 100 | ``` 101 | 102 | > If you need to migrate releases from one Helmsman's context to another, check this [guide](../apps/migrate_contexts.md). 103 | 104 | ### Limitations 105 | 106 | * If no context is provided in DSF (or merged DSFs), `default` is applied as a default context. This means any set of DSFs that don't define custom contexts can still operate on each other's releases (same behavior as in Helmsman 1.x). 107 | 108 | * When merging multiple DSFs, context from the firs DSF in the list gets overridden by the context in the last DSF. 109 | 110 | * If multiple DSFs use the same context name, they will mess up each other's releases. You can use `--keep-untracked-releases` to avoid that. However, it is recommended to avoid having multiple DSFs using the same context name. 111 | -------------------------------------------------------------------------------- /internal/app/release_files.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // substituteVarsInStaticFiles loops through the values/secrets files and substitutes variables into them. 8 | func (r *Release) substituteVarsInStaticFiles() { 9 | if r.ValuesFile != "" { 10 | r.ValuesFile = substituteVarsInYaml(r.ValuesFile) 11 | } 12 | if r.SecretsFile != "" { 13 | r.SecretsFile = substituteVarsInYaml(r.SecretsFile) 14 | } 15 | 16 | for i := range r.ValuesFiles { 17 | r.ValuesFiles[i] = substituteVarsInYaml(r.ValuesFiles[i]) 18 | } 19 | for i := range r.SecretsFiles { 20 | r.SecretsFiles[i] = substituteVarsInYaml(r.SecretsFiles[i]) 21 | } 22 | 23 | for key, val := range r.Hooks { 24 | if key != "deleteOnSuccess" && key != "successTimeout" && key != "successCondition" { 25 | hook := val.(string) 26 | if isOfType(hook, []string{".yaml", ".yml"}) { 27 | r.Hooks[key] = substituteVarsInYaml(hook) 28 | } 29 | } 30 | } 31 | } 32 | 33 | // resolvePaths resolves relative paths of certs/keys/chart/value file/secret files/etc and replace them with a absolute paths 34 | func (r *Release) resolvePaths(dir, downloadDest string) { 35 | if r.ValuesFile != "" { 36 | r.ValuesFile, _ = resolveOnePath(r.ValuesFile, dir, downloadDest) 37 | } 38 | if r.SecretsFile != "" { 39 | r.SecretsFile, _ = resolveOnePath(r.SecretsFile, dir, downloadDest) 40 | } 41 | 42 | for i, file := range r.ValuesFiles { 43 | r.ValuesFiles[i], _ = resolveOnePath(file, dir, downloadDest) 44 | } 45 | for i, file := range r.SecretsFiles { 46 | r.SecretsFiles[i], _ = resolveOnePath(file, dir, downloadDest) 47 | } 48 | 49 | for key, val := range r.Hooks { 50 | if key != "deleteOnSuccess" && key != "successTimeout" && key != "successCondition" { 51 | hook := strings.Fields(val.(string)) 52 | if isOfType(hook[0], validHookFiles) && !ToolExists(hook[0]) { 53 | hook[0], _ = resolveOnePath(hook[0], dir, downloadDest) 54 | r.Hooks[key] = strings.Join(hook, " ") 55 | } 56 | } 57 | } 58 | } 59 | 60 | // getValuesFiles return partial install/upgrade release command to substitute the -f flag in Helm. 61 | func (r *Release) getValuesFiles() []string { 62 | var fileList []string 63 | 64 | if r.ValuesFile != "" { 65 | fileList = append(fileList, r.ValuesFile) 66 | } else if len(r.ValuesFiles) > 0 { 67 | fileList = append(fileList, r.ValuesFiles...) 68 | } 69 | 70 | if r.SecretsFile != "" || len(r.SecretsFiles) > 0 { 71 | if settings.EyamlEnabled { 72 | if !ToolExists("eyaml") { 73 | log.Fatal("hiera-eyaml is not installed/configured correctly. Aborting!") 74 | } 75 | } else if settings.VaultEnabled { 76 | if !helmPluginExists("vault") { 77 | log.Fatal("helm vault plugin is not installed/configured correctly. Aborting!") 78 | } 79 | } else { 80 | if !helmPluginExists("secrets") { 81 | log.Fatal("helm secrets plugin is not installed/configured correctly. Aborting!") 82 | } 83 | } 84 | } 85 | if r.SecretsFile != "" { 86 | if !isOfType(r.SecretsFile, []string{".dec"}) { 87 | if err := decryptSecret(r.SecretsFile); err != nil { 88 | log.Fatal(err.Error()) 89 | } 90 | r.SecretsFile += ".dec" 91 | } 92 | fileList = append(fileList, r.SecretsFile) 93 | } else if len(r.SecretsFiles) > 0 { 94 | for i := 0; i < len(r.SecretsFiles); i++ { 95 | if isOfType(r.SecretsFiles[i], []string{".dec"}) { 96 | // if .dec extension is added before to the secret filename, don't add it again. 97 | // This happens at upgrade time (where diff and upgrade both call this function) 98 | // and we don't need to decrypt the file again 99 | continue 100 | } 101 | 102 | if err := decryptSecret(r.SecretsFiles[i]); err != nil { 103 | log.Fatal(err.Error()) 104 | } 105 | r.SecretsFiles[i] += ".dec" 106 | } 107 | fileList = append(fileList, r.SecretsFiles...) 108 | } 109 | 110 | fileListArgs := []string{} 111 | for _, file := range fileList { 112 | fileListArgs = append(fileListArgs, "-f", file) 113 | } 114 | return fileListArgs 115 | } 116 | -------------------------------------------------------------------------------- /docs/cmd_reference.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.0.0 3 | --- 4 | 5 | # CMD reference 6 | 7 | This lists available CMD options in Helmsman: 8 | 9 | > you can find the CMD options for the version you are using by typing: `helmsman -h` or `helmsman --help` 10 | 11 | `--always-upgrade` 12 | upgrade release even if no changes are found. 13 | 14 | `--apply` 15 | apply the plan directly. 16 | 17 | `--context-override string` 18 | override releases context defined in release state with this one. 19 | 20 | `--debug` 21 | show the debug execution logs and actual helm/kubectl commands. This can log secrets and should only be used for debugging purposes. 22 | 23 | `--verbose` 24 | show verbose execution logs. 25 | 26 | `--destroy` 27 | delete all deployed releases. 28 | 29 | `-detailed-exit-code` 30 | returns a detailed exit code (0 - no changes, 1 - error, 2 - changes present) 31 | 32 | `--diff-context num` 33 | number of lines of context to show around changes in helm diff output. 34 | 35 | `-p` 36 | max number of concurrent helm releases to run 37 | 38 | `--dry-run` 39 | apply the dry-run (do not update) option for helm commands. 40 | 41 | `-e value` 42 | additional file(s) to load environment variables from, may be supplied more than once, it extends default .env file lookup, every next file takes precedence over previous ones in case of having the same environment variables defined. 43 | If a `.env` file exists, it will be loaded by default, if additional env files are specified using the `-e` flag, the environment file will be loaded in order where the last file will take precedence. 44 | 45 | `-f value` 46 | desired state file name(s), may be supplied more than once to merge state files. 47 | 48 | `--force-upgrades` 49 | use --force when upgrading helm releases. May cause resources to be recreated. 50 | 51 | `--keep-untracked-releases` 52 | keep releases that are managed by Helmsman from the used DSFs in the command, and are no longer tracked in your desired state. 53 | 54 | `--kubeconfig` 55 | path to the kubeconfig file to use for CLI requests. Defaults to false if the helm diff plugin is installed. 56 | 57 | `--kubectl-diff` 58 | Use kubectl diff instead of helm diff 59 | 60 | `--migrate-context` 61 | Updates the context name for all apps defined in the DSF and applies Helmsman labels. Using this flag is required if you want to change context name after it has been set. 62 | 63 | `--no-banner` 64 | don't show the banner. 65 | 66 | `--no-color` 67 | don't use colors. 68 | 69 | `--no-env-subst` 70 | turn off environment substitution globally. 71 | 72 | `--no-recursive-env-expand` 73 | disable recursive environment variables expansion. 74 | 75 | `--subst-env-values` 76 | turn on environment substitution in values files. 77 | 78 | `--no-fancy` 79 | don't display the banner and don't use colors. 80 | 81 | `--no-ns` 82 | don't create namespaces. 83 | 84 | `--no-ssm-subst` 85 | turn off SSM parameter substitution globally. 86 | 87 | `--replace-on-rename` 88 | uninstall the existing release when a chart with a different name is used. 89 | 90 | `--spec string` 91 | specification file name, contains locations of desired state files to be merged 92 | 93 | `--subst-ssm-values` 94 | turn on SSM parameter substitution in values files. 95 | 96 | `--ns-override string` 97 | override defined namespaces with this one. 98 | 99 | `--show-diff` 100 | show helm diff results. Can expose sensitive information. 101 | 102 | `--skip-validation` 103 | skip desired state validation. 104 | 105 | `--target` 106 | limit execution to specific app. 107 | 108 | `--exclude-target` 109 | exclude specific app from execution. 110 | 111 | `--group` 112 | limit execution to specific group of apps. 113 | 114 | `--exclude-group` 115 | exclude specific group of apps from execution. 116 | 117 | `--update-deps` 118 | run 'helm dep up' for local chart 119 | 120 | `--check-for-chart-updates` 121 | compares the chart versions in the state file to the latest versions in the chart repositories and shows available updates 122 | 123 | `--verify` 124 | verify if charts are signed and valid before using them 125 | 126 | `--v` show the version. 127 | -------------------------------------------------------------------------------- /internal/app/helm_release.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | ) 11 | 12 | // TODO: can we import these from helm? 13 | const ( 14 | helmStatusDeployed = "deployed" 15 | helmStatusUninstalled = "uninstalled" 16 | helmStatusFailed = "failed" 17 | helmStatusPendingUpgrade = "pending-upgrade" 18 | helmStatusPendingInstall = "pending-install" 19 | helmStatusPendingRollback = "pending-rollback" 20 | helmStatusUninstalling = "uninstalling" 21 | helmStatusMissing = "missing" 22 | ) 23 | 24 | // helmRelease represents the current state of a release 25 | type helmRelease struct { 26 | Name string `json:"Name"` 27 | Namespace string `json:"Namespace"` 28 | Revision int `json:"Revision,string"` 29 | Updated HelmTime `json:"Updated"` 30 | Status string `json:"Status"` 31 | Chart string `json:"Chart"` 32 | AppVersion string `json:"AppVersion,omitempty"` 33 | HelmsmanContext string 34 | } 35 | 36 | // getHelmReleases fetches a list of all releases in a k8s cluster 37 | func getHelmReleases(s *State) []helmRelease { 38 | var ( 39 | allReleases []helmRelease 40 | wg sync.WaitGroup 41 | mutex = &sync.Mutex{} 42 | ) 43 | for ns, cfg := range s.Namespaces { 44 | if cfg.disabled { 45 | continue 46 | } 47 | wg.Add(1) 48 | go func(ns string) { 49 | var releases []helmRelease 50 | var targetReleases []helmRelease 51 | defer wg.Done() 52 | args := []string{"list", "--max", "0", "--output", "json", "-n", ns} 53 | if checkHelmVersion("<4.0.0") { 54 | args = append(args, "--all") 55 | } 56 | cmd := helmCmd(args, "Listing all existing releases in [ "+ns+" ] namespace") 57 | res, err := cmd.RetryExec(3) 58 | if err != nil { 59 | log.Fatal(err.Error()) 60 | } 61 | if err := json.Unmarshal([]byte(res.output), &releases); err != nil { 62 | log.Fatal(fmt.Sprintf("failed to unmarshal Helm CLI output: %s", err)) 63 | } 64 | if len(s.targetMap) > 0 { 65 | for _, r := range releases { 66 | if use, ok := s.targetMap[r.Name]; ok && use { 67 | targetReleases = append(targetReleases, r) 68 | } 69 | } 70 | } else { 71 | targetReleases = releases 72 | } 73 | mutex.Lock() 74 | allReleases = append(allReleases, targetReleases...) 75 | mutex.Unlock() 76 | }(ns) 77 | } 78 | wg.Wait() 79 | return allReleases 80 | } 81 | 82 | func (r *helmRelease) key() string { 83 | return fmt.Sprintf("%s-%s", r.Name, r.Namespace) 84 | } 85 | 86 | // uninstall creates the helm command to uninstall an untracked release 87 | func (r *helmRelease) uninstall(p *plan) { 88 | cmd := helmCmd(concat([]string{"uninstall", r.Name, "--namespace", r.Namespace}, flags.getRunFlags()), "Delete untracked release [ "+r.Name+" ] in namespace [ "+r.Namespace+" ]") 89 | 90 | p.addCommand(cmd, -800, nil, []hookCmd{}, []hookCmd{}) 91 | } 92 | 93 | // getRevision returns the revision number for an existing helm release 94 | func (r *helmRelease) getRevision() string { 95 | return strconv.Itoa(r.Revision) 96 | } 97 | 98 | // getChartName extracts and returns the Helm chart name from the chart info in a release state. 99 | // example: chart in release state is "jenkins-0.9.0" and this function will extract "jenkins" from it. 100 | func (r *helmRelease) getChartName() string { 101 | chart := r.Chart 102 | runes := []rune(chart) 103 | return string(runes[0:strings.LastIndexByte(chart[0:strings.IndexByte(chart, '.')], '-')]) 104 | } 105 | 106 | // getChartVersion extracts and returns the Helm chart version from the chart info in a release state. 107 | // example: chart in release state is returns "jenkins-0.9.0" and this functions will extract "0.9.0" from it. 108 | // It should also handle semver-valid pre-release/meta information, example: in: jenkins-0.9.0-1, out: 0.9.0-1 109 | // in the event of an error, an empty string is returned. 110 | func (r *helmRelease) getChartVersion() string { 111 | chart := r.Chart 112 | re := regexp.MustCompile(`-(v?[0-9]+\.[0-9]+\.[0-9]+.*)`) 113 | matches := re.FindStringSubmatch(chart) 114 | if len(matches) > 1 { 115 | return matches[1] 116 | } 117 | return "" 118 | } 119 | 120 | // getCurrentNamespaceProtection returns the protection state for the namespace where a release is currently installed. 121 | // It returns true if a namespace is defined as protected in the desired state file, false otherwise. 122 | func (r *helmRelease) getCurrentNamespaceProtection(s *State) bool { 123 | return s.Namespaces[r.Namespace].Protected 124 | } 125 | -------------------------------------------------------------------------------- /internal/app/custom_types_test.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestNullBool_MarshalJSON(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | value NullBool 14 | want []byte 15 | wantErr bool 16 | }{ 17 | { 18 | name: "should be false", 19 | want: []byte(`false`), 20 | wantErr: false, 21 | }, 22 | { 23 | name: "should be true", 24 | want: []byte(`true`), 25 | value: NullBool{HasValue: true, Value: true}, 26 | wantErr: false, 27 | }, 28 | { 29 | name: "should be false when HasValue is false", 30 | want: []byte(`false`), 31 | value: NullBool{HasValue: false, Value: true}, 32 | wantErr: false, 33 | }, 34 | } 35 | for _, tt := range tests { 36 | t.Run(tt.name, func(t *testing.T) { 37 | got, err := tt.value.MarshalJSON() 38 | if (err != nil) != tt.wantErr { 39 | t.Errorf("MarshalJSON() error = %v, wantErr %v", err, tt.wantErr) 40 | return 41 | } 42 | if !reflect.DeepEqual(got, tt.want) { 43 | t.Errorf("MarshalJSON() got = %v, want %v", got, tt.want) 44 | } 45 | }) 46 | } 47 | } 48 | 49 | func TestNullBool_UnmarshalJSON(t *testing.T) { 50 | type output struct { 51 | Value NullBool `json:"value"` 52 | } 53 | tests := []struct { 54 | name string 55 | data []byte 56 | want output 57 | wantErr bool 58 | }{ 59 | { 60 | name: "should have value set to false", 61 | data: []byte(`{"value": false}`), 62 | want: output{NullBool{HasValue: true, Value: false}}, 63 | }, 64 | { 65 | name: "should have value set to true", 66 | data: []byte(`{"value": true}`), 67 | want: output{NullBool{HasValue: true, Value: true}}, 68 | }, 69 | { 70 | name: "should have value unset", 71 | data: []byte("{}"), 72 | want: output{NullBool{HasValue: false, Value: false}}, 73 | }, 74 | } 75 | for _, tt := range tests { 76 | t.Run(tt.name, func(t *testing.T) { 77 | var got output 78 | if err := json.NewDecoder(bytes.NewReader(tt.data)).Decode(&got); (err != nil) != tt.wantErr { 79 | t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) 80 | } 81 | if !reflect.DeepEqual(got, tt.want) { 82 | t.Errorf("UnmarshalJSON() got = %v, want %v", got, tt.want) 83 | } 84 | }) 85 | } 86 | } 87 | 88 | func TestNullBool_UnmarshalText(t *testing.T) { 89 | tests := []struct { 90 | name string 91 | text []byte 92 | want NullBool 93 | wantErr bool 94 | }{ 95 | { 96 | name: "should have the value set to false", 97 | text: []byte("false"), 98 | want: NullBool{HasValue: true, Value: false}, 99 | }, 100 | { 101 | name: "should have the value set to true", 102 | text: []byte("false"), 103 | want: NullBool{HasValue: true, Value: false}, 104 | }, 105 | { 106 | name: "should have the value unset", 107 | text: []byte(""), 108 | want: NullBool{HasValue: false, Value: false}, 109 | }, 110 | { 111 | name: "should return an error on wrong input", 112 | text: []byte("wrong_input"), 113 | wantErr: true, 114 | want: NullBool{HasValue: false, Value: false}, 115 | }, 116 | } 117 | for _, tt := range tests { 118 | t.Run(tt.name, func(t *testing.T) { 119 | var got NullBool 120 | if err := got.UnmarshalText(tt.text); (err != nil) != tt.wantErr { 121 | t.Errorf("UnmarshalText() error = %v, wantErr %v", err, tt.wantErr) 122 | } 123 | if !reflect.DeepEqual(got, tt.want) { 124 | t.Errorf("UnmarshalText() got = %v, want %v", got, tt.want) 125 | } 126 | }) 127 | } 128 | } 129 | 130 | func TestNullBoolTransformer(t *testing.T) { 131 | type args struct { 132 | dst NullBool 133 | src NullBool 134 | } 135 | tests := []struct { 136 | name string 137 | args args 138 | want NullBool 139 | }{ 140 | { 141 | name: "should overwrite true to false when the dst has the value", 142 | args: args{ 143 | dst: NullBool{HasValue: true, Value: true}, 144 | src: NullBool{HasValue: true, Value: false}, 145 | }, 146 | want: NullBool{HasValue: true, Value: false}, 147 | }, 148 | { 149 | name: "shouldn't overwrite when the value is unset", 150 | args: args{ 151 | dst: NullBool{HasValue: true, Value: true}, 152 | src: NullBool{HasValue: false, Value: false}, 153 | }, 154 | want: NullBool{HasValue: true, Value: true}, 155 | }, 156 | { 157 | name: "shouldn overwrite when the value is set and equal true", 158 | args: args{ 159 | dst: NullBool{HasValue: true, Value: false}, 160 | src: NullBool{HasValue: true, Value: true}, 161 | }, 162 | want: NullBool{HasValue: true, Value: true}, 163 | }, 164 | } 165 | for _, tt := range tests { 166 | t.Run(tt.name, func(t *testing.T) { 167 | dst := tt.args.dst 168 | src := tt.args.src 169 | 170 | transformer := NullBoolTransformer(reflect.TypeOf(NullBool{})) 171 | 172 | transformer(reflect.ValueOf(&dst).Elem(), reflect.ValueOf(src)) 173 | 174 | if !reflect.DeepEqual(dst, tt.want) { 175 | t.Errorf("NullBoolTransformer() = %v, want %v", dst, tt.want) 176 | } 177 | }) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /docs/how_to/apps/basic.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.0.0-beta5 3 | --- 4 | 5 | # Basics 6 | 7 | ## Install releases 8 | 9 | You can run helmsman with the [example.toml](https://github.com/mkubaczyk/helmsman/blob/master/examples/example.toml) or [example.yaml](https://github.com/mkubaczyk/helmsman/blob/master/examples/example.yaml) file. 10 | 11 | ```shell 12 | $ helmsman --apply -f example.toml 13 | 2017/11/19 18:17:57 Parsed [[ example.toml ]] successfully and found [ 2 ] apps. 14 | 2017/11/19 18:17:59 WARN: I could not create namespace [staging ]. It already exists. I am skipping this. 15 | 2017/11/19 18:17:59 WARN: I could not create namespace [default ]. It already exists. I am skipping this. 16 | 2017/11/19 18:18:02 INFO: Executing the following plan ... 17 | --------------- 18 | Ok, I have generated a plan for you at: 2017-11-19 18:17:59.347859706 +0100 CET m=+2.255430021 19 | DECISION: release [ jenkins ] is not present in the current k8s context. Will install it in namespace [[ staging ]] 20 | DECISION: release [ artifactory ] is not present in the current k8s context. Will install it in namespace [[ staging ]] 21 | 2017/11/19 18:18:02 INFO: attempting: -- installing release [ jenkins ] in namespace [[ staging ]] 22 | 2017/11/19 18:18:05 INFO: attempting: -- installing release [ artifactory ] in namespace [[ staging ]] 23 | ``` 24 | 25 | ```shell 26 | $ helm list --namespace staging 27 | NAME REVISION UPDATED STATUS CHART NAMESPACE 28 | artifactory 1 Sun Nov 19 18:18:06 2017 DEPLOYED artifactory-6.2.0 staging 29 | jenkins 1 Sun Nov 19 18:18:03 2017 DEPLOYED jenkins-0.9.1 staging 30 | ``` 31 | 32 | ## Delete releases 33 | 34 | You can then change your desire, for example to disable the Jenkins release that was created above by setting `enabled = false` : 35 | 36 | Then run Helmsman again and it will detect that you want to delete Jenkins: 37 | 38 | > Note: As of v1.4.0-rc, deleting the jenkins app entry in the desired state file WILL result in deleting the jenkins release. To prevent this, use the `--keep-untracked-releases` flag with your Helmsman command. 39 | 40 | ```shell 41 | $ helmsman --apply -f example.toml 42 | 2017/11/19 18:28:27 Parsed [[ example.toml ]] successfully and found [ 2 ] apps. 43 | 2017/11/19 18:28:29 WARN: I could not create namespace [staging ]. It already exists. I am skipping this. 44 | 2017/11/19 18:28:29 WARN: I could not create namespace [default ]. It already exists. I am skipping this. 45 | 2017/11/19 18:29:01 INFO: Executing the following plan ... 46 | --------------- 47 | Ok, I have generated a plan for you at: 2017-11-19 18:28:29.437061909 +0100 CET m=+1.987623555 48 | DECISION: release [ jenkins ] is desired to be deleted . Planning this for you! 49 | DECISION: release [ artifactory ] is desired to be upgraded. Planning this for you! 50 | 2017/11/19 18:29:01 INFO: attempting: -- deleting release [ jenkins ] 51 | 2017/11/19 18:29:11 INFO: attempting: -- upgrading release [ artifactory ] 52 | ``` 53 | 54 | ```shell 55 | $ helm list --namespace staging 56 | NAME REVISION UPDATED STATUS CHART NAMESPACE 57 | artifactory 2 Sun Nov 19 18:29:11 2017 DEPLOYED artifactory-6.2.0 staging 58 | ``` 59 | 60 | ```yaml 61 | # ... 62 | apps: 63 | jenkins: 64 | description: "jenkins" 65 | namespace: "staging" 66 | enabled: false # this tells helmsman to delete it 67 | chart: "jenkins/jenkins" 68 | version: "2.15.1" 69 | valuesFile: "" 70 | test: false 71 | 72 | # ... 73 | ``` 74 | 75 | # Rollback releases 76 | 77 | > Rollbacks in helm versions 2.8.2 and higher may not work due to a [bug](https://github.com/helm/helm/issues/3722). 78 | Similarly, if you change `enabled` back to `true`, it will figure out that you would like to roll it back. 79 | 80 | ```shell 81 | $ helmsman --apply -f example.toml 82 | 2017/11/19 18:30:41 Parsed [[ example.toml ]] successfully and found [ 2 ] apps. 83 | 2017/11/19 18:30:42 WARN: I could not create namespace [staging ]. It already exists. I am skipping this. 84 | 2017/11/19 18:30:43 WARN: I could not create namespace [default ]. It already exists. I am skipping this. 85 | 2017/11/19 18:30:49 INFO: Executing the following plan ... 86 | --------------- 87 | Ok, I have generated a plan for you at: 2017-11-19 18:30:43.108693039 +0100 CET m=+1.978435517 88 | DECISION: release [ jenkins ] is currently deleted and is desired to be rolledback to namespace [[ staging ]] . No problem! 89 | DECISION: release [ artifactory ] is desired to be upgraded. Planning this for you! 90 | 2017/11/19 18:30:49 INFO: attempting: -- rolling back release [ jenkins ] 91 | 2017/11/19 18:30:50 INFO: attempting: -- upgrading release [ artifactory ] 92 | ``` 93 | 94 | ## Upgrade releases 95 | 96 | Every time you run Helmsman, (unless the release is [protected or deployed in a protected namespace](../misc/protect_namespaces_and_releases.md)) it will check if upgrade is necessary (using the helm-diff plugin) and only upgrade if there are changes. 97 | 98 | If you change the chart, the existing release will be deleted and a new one with the same name will be created using the new chart. 99 | 100 | -------------------------------------------------------------------------------- /examples/example.yaml: -------------------------------------------------------------------------------- 1 | # version: v3.4.0 2 | 3 | # context defines the context of this Desired State File. 4 | # It is used to allow Helmsman identify which releases are managed by which DSF. 5 | # Therefore, it is important that each DSF uses a unique context. 6 | context: test-infra # defaults to "default" if not provided 7 | 8 | # metadata -- add as many key/value pairs as you want 9 | metadata: 10 | org: "example.com/$ORG_PATH/" 11 | maintainer: "k8s-admin (me@example.com)" 12 | description: "example Desired State File for demo purposes." 13 | key: ${VALUE} 14 | 15 | # paths to the certificate for connecting to the cluster 16 | # You can skip this if you use Helmsman on a machine with kubectl already connected to your k8s cluster. 17 | # you have to use exact key names here : 'caCrt' for certificate and 'caKey' for the key and caClient for the client certificate 18 | # certificates: 19 | #caClient: "gs://mybucket/client.crt" # GCS bucket path 20 | #caCrt: "s3://mybucket/ca.crt" # S3 bucket path 21 | #caKey: "../ca.key" # valid local file relative path 22 | 23 | settings: 24 | kubeContext: "minikube" # will try connect to this context first, if it does not exist, it will be created using the details below 25 | #username: "admin" 26 | #password: "$K8S_PASSWORD" # the name of an environment variable containing the k8s password 27 | #clusterURI: "$SET_URI" # the name of an environment variable containing the cluster API 28 | #clusterURI: "https://192.168.99.100:8443" # equivalent to the above 29 | #storageBackend: "secret" 30 | #slackWebhook: "$slack" # or your slack webhook url 31 | #reverseDelete: false # reverse the priorities on delete 32 | #### to use bearer token: 33 | # bearerToken: true 34 | # clusterURI: "https://kubernetes.default" 35 | # globalHooks: 36 | # successCondition: "Initialized" 37 | # deleteOnSuccess: true 38 | # postInstall: "job.yaml" 39 | globalMaxHistory: 5 40 | 41 | # define your environments and their k8s namespaces 42 | namespaces: 43 | production: 44 | protected: true 45 | limits: 46 | - type: Container 47 | default: 48 | cpu: "300m" 49 | memory: "200Mi" 50 | defaultRequest: 51 | cpu: "200m" 52 | memory: "100Mi" 53 | - type: Pod 54 | max: 55 | memory: "300Mi" 56 | staging: 57 | protected: false 58 | labels: 59 | env: "staging" 60 | quotas: 61 | limits.cpu: "10" 62 | limits.memory: "20Gi" 63 | pods: 25 64 | requests.cpu: "10" 65 | requests.memory: "30Gi" 66 | customQuotas: 67 | - name: "requests.nvidia.com/gpu" 68 | value: "2" 69 | 70 | # define any private/public helm charts repos you would like to get charts from 71 | # syntax: repo_name: "repo_url" 72 | # only private repos hosted in s3 buckets are now supported 73 | helmRepos: 74 | argo: "https://argoproj.github.io/argo-helm" 75 | jfrog: "https://charts.jfrog.io" 76 | #myS3repo: "s3://my-S3-private-repo/charts" 77 | #myGCSrepo: "gs://my-GCS-private-repo/charts" 78 | #custom: "https://$user:$pass@mycustomrepo.org" 79 | 80 | # define the desired state of your applications helm charts 81 | # each contains the following: 82 | 83 | apps: 84 | argo: 85 | namespace: "staging" # maps to the namespace as defined in namespaces above 86 | enabled: true # change to false if you want to delete this app release empty: false: 87 | chart: "argo/argo" # changing the chart name means delete and recreate this chart 88 | version: "0.8.5" # chart version 89 | ### Optional values below 90 | valuesFile: "" # leaving it empty uses the default chart values 91 | test: false 92 | protected: true 93 | priority: -3 94 | wait: true 95 | hooks: 96 | successCondition: "Complete" 97 | successTimeout: "90s" 98 | deleteOnSuccess: true 99 | preInstall: "job.yaml" 100 | # preInstall: "https://github.com/jetstack/cert-manager/releases/download/v0.14.0/cert-manager.crds.yaml" 101 | # postInstall: "https://raw.githubusercontent.com/jetstack/cert-manager/release-0.14/deploy/manifests/00-crds.yaml" 102 | # postInstall: "job.yaml" 103 | # preUpgrade: "job.yaml" 104 | # postUpgrade: "job.yaml" 105 | # preDelete: "job.yaml" 106 | # postDelete: "job.yaml" 107 | set: 108 | "images.tag": $$TAG # $$ is escaped and $TAG is passed literally to images.tag (no env variable expansion) 109 | 110 | artifactory: 111 | namespace: "production" # maps to the namespace as defined in namespaces above 112 | enabled: true # change to false if you want to delete this app release empty: false: 113 | chart: "jfrog/artifactory" # changing the chart name means delete and recreate this chart 114 | version: "8.3.2" # chart version 115 | ### Optional values below 116 | valuesFile: "" 117 | test: false 118 | priority: -2 119 | noHooks: false 120 | timeout: 300 121 | maxHistory: 4 122 | # additional helm flags for this release 123 | helmFlags: 124 | - "--devel" 125 | # See https://github.com/mkubaczyk/helmsman/blob/master/docs/desired_state_specification.md#apps for more apps options 126 | -------------------------------------------------------------------------------- /internal/app/plan_test.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func Test_createPlan(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | want *plan 13 | }{ 14 | { 15 | name: "test creating a plan", 16 | want: &plan{}, 17 | }, 18 | } 19 | for _, tt := range tests { 20 | t.Run(tt.name, func(t *testing.T) { 21 | if got := createPlan(); reflect.DeepEqual(got, tt.want) { 22 | t.Errorf("createPlan() = %v, want %v", got, tt.want) 23 | } 24 | }) 25 | } 26 | } 27 | 28 | func Test_plan_addCommand(t *testing.T) { 29 | type fields struct { 30 | Commands []orderedCommand 31 | Decisions []orderedDecision 32 | Created time.Time 33 | } 34 | type args struct { 35 | c Command 36 | } 37 | tests := []struct { 38 | name string 39 | fields fields 40 | args args 41 | }{ 42 | { 43 | name: "testing command 1", 44 | fields: fields{ 45 | Commands: []orderedCommand{}, 46 | Decisions: []orderedDecision{}, 47 | Created: time.Now(), 48 | }, 49 | args: args{ 50 | c: Command{ 51 | Cmd: "bash", 52 | Args: []string{"-c", "echo this is fun"}, 53 | Description: "A bash command execution test with echo.", 54 | }, 55 | }, 56 | }, 57 | } 58 | for _, tt := range tests { 59 | t.Run(tt.name, func(t *testing.T) { 60 | p := &plan{ 61 | Commands: tt.fields.Commands, 62 | Decisions: tt.fields.Decisions, 63 | Created: tt.fields.Created, 64 | } 65 | r := &Release{} 66 | p.addCommand(tt.args.c, 0, r, []hookCmd{}, []hookCmd{}) 67 | if got := len(p.Commands); got != 1 { 68 | t.Errorf("addCommand(): got %v, want 1", got) 69 | } 70 | }) 71 | } 72 | } 73 | 74 | func Test_plan_addDecision(t *testing.T) { 75 | type fields struct { 76 | Commands []orderedCommand 77 | Decisions []orderedDecision 78 | Created time.Time 79 | } 80 | type args struct { 81 | decision string 82 | } 83 | tests := []struct { 84 | name string 85 | fields fields 86 | args args 87 | }{ 88 | { 89 | name: "testing decision adding", 90 | fields: fields{ 91 | Commands: []orderedCommand{}, 92 | Decisions: []orderedDecision{}, 93 | Created: time.Now(), 94 | }, 95 | args: args{ 96 | decision: "This is a test decision.", 97 | }, 98 | }, 99 | } 100 | for _, tt := range tests { 101 | t.Run(tt.name, func(t *testing.T) { 102 | p := &plan{ 103 | Commands: tt.fields.Commands, 104 | Decisions: tt.fields.Decisions, 105 | Created: tt.fields.Created, 106 | } 107 | p.addDecision(tt.args.decision, 0, noop) 108 | if got := len(p.Decisions); got != 1 { 109 | t.Errorf("addDecision(): got %v, want 1", got) 110 | } 111 | }) 112 | } 113 | } 114 | 115 | // func Test_plan_execPlan(t *testing.T) { 116 | // type fields struct { 117 | // Commands []command 118 | // Decisions []string 119 | // Created time.Time 120 | // } 121 | // tests := []struct { 122 | // name string 123 | // fields fields 124 | // }{ 125 | // { 126 | // name: "testing executing a plan", 127 | // fields: fields{ 128 | // Commands: []command{ 129 | // { 130 | // Cmd: "bash", 131 | // Args: []string{"-c", "touch hello.world"}, 132 | // Description: "Creating hello.world file.", 133 | // }, { 134 | // Cmd: "bash", 135 | // Args: []string{"-c", "touch hello.world1"}, 136 | // Description: "Creating hello.world1 file.", 137 | // }, 138 | // }, 139 | // Decisions: []string{"Create hello.world.", "Create hello.world1."}, 140 | // Created: time.Now(), 141 | // }, 142 | // }, 143 | // } 144 | // for _, tt := range tests { 145 | // t.Run(tt.name, func(t *testing.T) { 146 | // p := plan{ 147 | // Commands: tt.fields.Commands, 148 | // Decisions: tt.fields.Decisions, 149 | // Created: tt.fields.Created, 150 | // } 151 | // p.execPlan() 152 | // c := command{ 153 | // Cmd: "bash", 154 | // Args: []string{"-c", "ls | grep hello.world | wc -l"}, 155 | // Description: "", 156 | // } 157 | // if _, got := c.exec(); strings.TrimSpace(got) != "2" { 158 | // t.Errorf("execPlan(): got %v, want hello world, again!", got) 159 | // } 160 | // }) 161 | // } 162 | // } 163 | 164 | // func Test_plan_printPlanCmds(t *testing.T) { 165 | // type fields struct { 166 | // Commands []command 167 | // Decisions []string 168 | // Created time.Time 169 | // } 170 | // tests := []struct { 171 | // name string 172 | // fields fields 173 | // }{ 174 | // // TODO: Add test cases. 175 | // } 176 | // for _, tt := range tests { 177 | // t.Run(tt.name, func(t *testing.T) { 178 | // p := plan{ 179 | // Commands: tt.fields.Commands, 180 | // Decisions: tt.fields.Decisions, 181 | // Created: tt.fields.Created, 182 | // } 183 | // p.printPlanCmds() 184 | // }) 185 | // } 186 | // } 187 | 188 | // func Test_plan_printPlan(t *testing.T) { 189 | // type fields struct { 190 | // Commands []command 191 | // Decisions []string 192 | // Created time.Time 193 | // } 194 | // tests := []struct { 195 | // name string 196 | // fields fields 197 | // }{ 198 | // // TODO: Add test cases. 199 | // } 200 | // for _, tt := range tests { 201 | // t.Run(tt.name, func(t *testing.T) { 202 | // p := plan{ 203 | // Commands: tt.fields.Commands, 204 | // Decisions: tt.fields.Decisions, 205 | // Created: tt.fields.Created, 206 | // } 207 | // p.printPlan() 208 | // }) 209 | // } 210 | // } 211 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | tags: 7 | - v* 8 | pull_request: 9 | branches: [master] 10 | 11 | jobs: 12 | load-helm-versions: 13 | runs-on: ubuntu-latest 14 | outputs: 15 | helm3: ${{ steps.versions.outputs.helm3 }} 16 | helm4: ${{ steps.versions.outputs.helm4 }} 17 | steps: 18 | - uses: actions/checkout@v6 19 | 20 | - name: Load and export versions 21 | id: versions 22 | run: | 23 | source build/helm-versions.env 24 | echo "helm3=$HELM3_VERSION" >> $GITHUB_OUTPUT 25 | echo "helm4=$HELM4_VERSION" >> $GITHUB_OUTPUT 26 | 27 | commitlint: 28 | if: github.event_name == 'pull_request' 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v6 32 | with: 33 | fetch-depth: 0 34 | 35 | - uses: wagoid/commitlint-github-action@v6 36 | 37 | test: 38 | needs: load-helm-versions 39 | uses: ./.github/workflows/test.yml 40 | with: 41 | helm3-version: v${{ needs.load-helm-versions.outputs.helm3 }} 42 | helm4-version: v${{ needs.load-helm-versions.outputs.helm4 }} 43 | 44 | goreleaser: 45 | needs: [test, load-helm-versions] 46 | if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/') 47 | runs-on: ubuntu-latest 48 | permissions: 49 | contents: write 50 | steps: 51 | - name: Build mode summary 52 | run: | 53 | if [[ "${{ github.ref_type }}" == "tag" ]]; then 54 | echo "### Release: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY 55 | echo "GoReleaser will **publish** binaries to GitHub Releases." >> $GITHUB_STEP_SUMMARY 56 | else 57 | echo "### Validation build (no release)" >> $GITHUB_STEP_SUMMARY 58 | echo "GoReleaser will build artifacts but **skip publishing**." >> $GITHUB_STEP_SUMMARY 59 | echo "" >> $GITHUB_STEP_SUMMARY 60 | echo "Tag a version to publish: \`git tag v1.2.3 && git push --tags\`" >> $GITHUB_STEP_SUMMARY 61 | fi 62 | 63 | - name: Checkout 64 | uses: actions/checkout@v6 65 | with: 66 | fetch-depth: 0 67 | 68 | - name: Set up Go 69 | uses: actions/setup-go@v6 70 | with: 71 | go-version: stable 72 | 73 | - name: Run GoReleaser 74 | uses: goreleaser/goreleaser-action@v6 75 | with: 76 | distribution: goreleaser 77 | version: '~> v2' 78 | args: release ${{ github.ref_type != 'tag' && '--skip=publish,validate' || '' }} 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | HELM3_VERSION: ${{ needs.load-helm-versions.outputs.helm3 }} 82 | HELM4_VERSION: ${{ needs.load-helm-versions.outputs.helm4 }} 83 | 84 | docker: 85 | needs: [test, load-helm-versions] 86 | runs-on: ubuntu-latest 87 | strategy: 88 | matrix: 89 | include: 90 | - helm-version: ${{ needs.load-helm-versions.outputs.helm3 }} 91 | helm-major: 'helm3' 92 | default: false 93 | - helm-version: ${{ needs.load-helm-versions.outputs.helm4 }} 94 | helm-major: 'helm4' 95 | default: true 96 | permissions: 97 | contents: read 98 | packages: write 99 | attestations: write 100 | id-token: write 101 | steps: 102 | - name: Build mode summary 103 | run: | 104 | if [[ "${{ github.ref_type }}" == "tag" ]]; then 105 | echo "### Docker: ${{ matrix.helm-major }} (${{ matrix.helm-version }})" >> $GITHUB_STEP_SUMMARY 106 | echo "Image will be **pushed** to ghcr.io/${{ github.repository }}." >> $GITHUB_STEP_SUMMARY 107 | else 108 | echo "### Docker: ${{ matrix.helm-major }} (${{ matrix.helm-version }}) - validation only" >> $GITHUB_STEP_SUMMARY 109 | echo "Image will be built but **not pushed**." >> $GITHUB_STEP_SUMMARY 110 | fi 111 | 112 | - name: Checkout repository 113 | uses: actions/checkout@v6 114 | with: 115 | fetch-depth: 0 116 | 117 | - name: Log in to the Container registry 118 | if: github.event_name != 'pull_request' 119 | uses: docker/login-action@v3 120 | with: 121 | registry: ghcr.io 122 | username: ${{ github.actor }} 123 | password: ${{ secrets.GITHUB_TOKEN }} 124 | 125 | - name: Extract metadata (tags, labels) for Docker 126 | id: meta 127 | uses: docker/metadata-action@v5 128 | with: 129 | images: ghcr.io/${{ github.repository }} 130 | flavor: latest=${{ matrix.default }} 131 | tags: | 132 | type=semver,pattern=v{{version}},enable=${{ matrix.default }} 133 | type=semver,pattern=v{{version}},suffix=-${{ matrix.helm-major }} 134 | type=semver,pattern=v{{version}},suffix=-helm${{ matrix.helm-version }} 135 | type=ref,event=pr,suffix=-${{ matrix.helm-major }} 136 | 137 | - name: Build and push Docker image 138 | uses: docker/build-push-action@v6 139 | with: 140 | context: . 141 | file: build/Dockerfile 142 | push: ${{ startsWith(github.ref, 'refs/tags/') }} 143 | tags: ${{ steps.meta.outputs.tags }} 144 | labels: ${{ steps.meta.outputs.labels }} 145 | build-args: | 146 | VERSION=${{ github.ref_type == 'tag' && github.ref_name || 'latest' }} 147 | GLOBAL_HELM_VERSION=v${{ matrix.helm-version }} 148 | -------------------------------------------------------------------------------- /docs/how_to/apps/moving_across_namespaces.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.0.0-beta5 3 | --- 4 | 5 | # Move charts across namespaces 6 | 7 | If you have a workflow for testing a release first in the `staging` namespace then move it to the `production` namespace, Helmsman can help you. 8 | 9 | > NOTE: If your chart uses a persistent volume, then you have to read the note on PVs below first. 10 | 11 | ```toml 12 | #... 13 | [namespaces] 14 | [namespaces.staging] 15 | [namespaces.production] 16 | 17 | [apps] 18 | [apps.jenkins] 19 | description = "jenkins" 20 | namespace = "staging" # this is where it is deployed 21 | enabled = true 22 | chart = "jenkins/jenkins" 23 | version = "2.15.1" 24 | valuesFile = "" 25 | test = true 26 | 27 | #... 28 | ``` 29 | 30 | ```yaml 31 | # ... 32 | namespaces: 33 | staging: 34 | production: 35 | 36 | apps: 37 | jenkins: 38 | description: "jenkins" 39 | namespace: "staging" # this is where it is deployed 40 | enabled: true 41 | chart: "jenkins/jenkins" 42 | version: "2.15.1" 43 | valuesFile: "" 44 | test: true 45 | 46 | # ... 47 | ``` 48 | 49 | Then if you change the namespace key for jenkins: 50 | 51 | ```toml 52 | #... 53 | [namespaces] 54 | [namespaces.staging] 55 | [namespaces.production] 56 | 57 | [apps] 58 | [apps.jenkins] 59 | description = "jenkins" 60 | namespace = "production" # we want to move it to production 61 | enabled = true 62 | chart = "jenkins/jenkins" 63 | version = "2.15.1" 64 | valuesFile = "" 65 | test = true 66 | 67 | #... 68 | ``` 69 | 70 | ```yaml 71 | # ... 72 | namespaces: 73 | staging: 74 | production: 75 | 76 | apps: 77 | jenkins: 78 | description: "jenkins" 79 | namespace: "production" # we want to move it to production 80 | enabled: true 81 | chart: "jenkins/jenkins" 82 | version: "2.15.1" 83 | valuesFile: "" 84 | test: true 85 | 86 | # ... 87 | ``` 88 | 89 | Helmsman will delete the jenkins release from the `staging` namespace and install it in the `production` namespace (default in the above setup). 90 | 91 | ## Note on Persistent Volumes 92 | 93 | Helmsman does not automatically move PVCs across namespaces. You have to follow the steps below to retain your data when moving an app to a different namespace. 94 | 95 | Persistent Volumes (PV) are accessed through Persistent Volume Claims (PVC). But **PVCs are namespaced objects** which means moving an application from one namespace to another will result in a new PVC created in the new namespace. The old PV -which possibly contains your application data- will still be mounted to the old PVC (the one in the old namespace) even if you have deleted your application helm release. 96 | 97 | Now, the newly created PVC (in the new namespace) will not be able to mount to the old PV and instead it will mount to any other available one or (in the case of dynamic provisioning) will provision a new PV. This means the application in the new namespace does not have the old data. Don't panic, the old PV is still there and contains your old data. 98 | 99 | ### Mounting the old PV to the new PVC (in the new namespace) 100 | 101 | 1. You have to make sure the _Reclaim Policy_ of the old PV is set to **Retain**. In dynamic provisioned PVs, the default is Delete. 102 | To change it: 103 | 104 | ```shell 105 | kubectl patch pv -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}' 106 | ``` 107 | 108 | 2. Once your old helm release is deleted, the old PVC and PV are still there. Go ahead and delete the PVC 109 | 110 | ```shell 111 | kubectl delete pvc --namespace 112 | ``` 113 | 114 | Since, we changed the Reclaim Policy to Retain, the PV will stay around (with all your data). 115 | 116 | 3. The PV is now in the **Released** state but not yet available for mounting. 117 | 118 | ```shell 119 | kubectl get pv 120 | NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM STORAGECLASS REASON AGE 121 | ... 122 | pvc-f791ef92-01ab-11e8-8a7e-02412acf5adc 20Gi RWO Retain Released staging/myapp-persistent-storage-test-old-0 gp2 5m 123 | ``` 124 | 125 | Now, you need to make it Available, for that we need to remove the `PV.Spec.ClaimRef` from the PV spec: 126 | 127 | ```shell 128 | kubectl edit pv 129 | # edit the file and save it 130 | ``` 131 | 132 | Now, the PV should become in the **Available** state: 133 | 134 | ```shell 135 | kubectl get pv 136 | NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM STORAGECLASS REASON AGE 137 | ... 138 | pvc-f791ef92-01ab-11e8-8a7e-02412acf5adc 20Gi RWO Retain Available gp2 7m 139 | ``` 140 | 141 | 4. Delete the new PVC (and its mounted PV if necessary), then delete your application pod(s) in the new namespace. Assuming you have a deployment/replication controller in place, the pod will be recreated in the new namespace and this time will mount to the old volume and your data will be once again available to your application. 142 | 143 | > NOTE: if there are multiple PVs in the Available state and they match capacity and read access for your application, then your application (in the new namespace) might mount to any of them. In this case, either ensure only the right PV is in the available state or make the PV available to a specific PVC - pre-fill `PV.Spec.ClaimRef` with a pointer to a PVC. Leave the `PV.Spec.ClaimRef,UID` empty, as the PVC does not need to exist at this point and you don't know PVC's UID. This PV can be bound only to the specified PVC 144 | 145 | Further details: 146 | 147 | * https://github.com/kubernetes/kubernetes/issues/48609 148 | * https://kubernetes.io/docs/tasks/administer-cluster/change-pv-reclaim-policy/ 149 | 150 | -------------------------------------------------------------------------------- /examples/example.toml: -------------------------------------------------------------------------------- 1 | # version: v3.0.0 2 | 3 | # context defines the context of this Desired State File. 4 | # It is used to allow Helmsman identify which releases are managed by which DSF. 5 | # Therefore, it is important that each DSF uses a unique context. 6 | context= "test-infra" # defaults to "default" if not provided 7 | 8 | # metadata -- add as many key/value pairs as you want 9 | [metadata] 10 | org = "example.com/${ORG_PATH}/" 11 | maintainer = "k8s-admin (me@example.com)" 12 | description = "example Desired State File for demo purposes." 13 | key= "${VALUE}" 14 | 15 | 16 | # paths to the certificate for connecting to the cluster 17 | # You can skip this if you use Helmsman on a machine with kubectl already connected to your k8s cluster. 18 | # you have to use exact key names here : 'caCrt' for certificate and 'caKey' for the key and caClient for the client certificate 19 | [certificates] 20 | # caClient = "gs://mybucket/client.crt" # GCS bucket path 21 | # caCrt = "s3://mybucket/ca.crt" # S3 bucket path 22 | # caKey = "../ca.key" # valid local file relative path 23 | 24 | [settings] 25 | kubeContext = "minikube" # will try connect to this context first, if it does not exist, it will be created using the details below 26 | # username = "admin" 27 | # password = "$K8S_PASSWORD" # the name of an environment variable containing the k8s password 28 | # clusterURI = "${SET_URI}" # the name of an environment variable containing the cluster API 29 | # #clusterURI = "https://192.168.99.100:8443" # equivalent to the above 30 | # storageBackend = "secret" # default is secret 31 | # slackWebhook = "$slack" # or "your slack webhook url" 32 | # reverseDelete = false # reverse the priorities on delete 33 | #### to use bearer token: 34 | # bearerToken = true 35 | # clusterURI = "https://kubernetes.default" 36 | # [settings.globalHooks] 37 | # successCondition= "Initialized" 38 | # deleteOnSuccess= true 39 | # postInstall= "job.yaml" 40 | globalMaxHistory= 5 41 | 42 | 43 | 44 | 45 | # define your environments and their k8s namespaces 46 | # syntax: 47 | # [namespaces.] -- whitespace before this entry does not matter, use whatever indentation style you like 48 | # protected = -- default to false 49 | [namespaces] 50 | [namespaces.production] 51 | protected = true 52 | [[namespaces.production.limits]] 53 | type = "Container" 54 | [namespaces.production.limits.default] 55 | cpu = "300m" 56 | memory = "200Mi" 57 | [namespaces.production.limits.defaultRequest] 58 | cpu = "200m" 59 | memory = "100Mi" 60 | [[namespaces.production.limits]] 61 | type = "Pod" 62 | [namespaces.production.limits.max] 63 | memory = "300Mi" 64 | [namespaces.staging] 65 | protected = false 66 | [namespaces.staging.labels] 67 | env = "staging" 68 | [namespaces.staging.quotas] 69 | "limits.cpu" = "10" 70 | "limits.memory" = "30Gi" 71 | pods = "25" 72 | "requests.cpu" = "10" 73 | "requests.memory" = "30Gi" 74 | [[namespaces.staging.quotas.customQuotas]] 75 | name = "requests.nvidia.com/gpu" 76 | value = "2" 77 | 78 | 79 | # define any private/public helm charts repos you would like to get charts from 80 | # syntax: repo_name = "repo_url" 81 | # only private repos hosted in s3 buckets are now supported 82 | [helmRepos] 83 | argo = "https://argoproj.github.io/argo-helm" 84 | jfrog = "https://charts.jfrog.io" 85 | # myS3repo = "s3://my-S3-private-repo/charts" 86 | # myGCSrepo = "gs://my-GCS-private-repo/charts" 87 | # custom = "https://$user:$pass@mycustomrepo.org" 88 | 89 | 90 | # define the desired state of your applications helm charts 91 | # each contains the following: 92 | [apps] 93 | 94 | [apps.argo] 95 | namespace = "production" # maps to the namespace as defined in namespaces above 96 | enabled = true # change to false if you want to delete this app release [default = false] 97 | chart = "argo/argo" # changing the chart name means delete and recreate this release 98 | version = "0.8.5" # chart version 99 | ### Optional values below 100 | valuesFile = "" # leaving it empty uses the default chart values 101 | test = false # run the tests when this release is installed for the first time only 102 | protected = true 103 | priority= -3 104 | wait = true 105 | [apps.argo.hooks] 106 | successCondition= "Complete" 107 | successTimeout= "90s" 108 | deleteOnSuccess= true 109 | preInstall="job.yaml" 110 | # preInstall="https://github.com/jetstack/cert-manager/releases/download/v0.14.0/cert-manager.crds.yaml" 111 | # postInstall="https://raw.githubusercontent.com/jetstack/cert-manager/release-0.14/deploy/manifests/00-crds.yaml" 112 | # preUpgrade="job.yaml" 113 | # postUpgrade="job.yaml" 114 | # preDelete="job.yaml" 115 | # postDelete="job.yaml" 116 | # [apps.argo.setString] # values to override values from values.yaml with values from env vars or directly entered-- useful for passing secrets to charts 117 | # AdminPassword="$SOME_PASSWORD" # $SOME_PASSWORD must exist in the environment 118 | # MyLongIntVar="1234567890" 119 | [apps.argo.set] 120 | "images.tag"="$$TAG" # $$ is escaped and $TAG is passed literally to images.tag (no env variable expansion) 121 | 122 | 123 | [apps.artifactory] 124 | namespace = "production" # maps to the namespace as defined in namespaces above 125 | enabled = true # change to false if you want to delete this app release [default = false] 126 | chart = "jfrog/artifactory" # changing the chart name means delete and recreate this release 127 | version = "8.3.2" # chart version 128 | ### Optional values below 129 | valuesFile = "" # leaving it empty uses the default chart values 130 | test = false # run the tests when this release is installed for the first time only 131 | priority= -2 132 | noHooks= false 133 | timeout= 300 134 | maxHistory = 4 135 | # additional helm flags for this release 136 | helmFlags= [ 137 | "--devel", 138 | ] 139 | 140 | # See https://github.com/mkubaczyk/helmsman/blob/master/docs/desired_state_specification.md#apps for more apps options 141 | -------------------------------------------------------------------------------- /docs/how_to/apps/lifecycle_hooks.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.5.2 3 | --- 4 | 5 | # Helmsman Lifecycle hooks 6 | 7 | With lifecycle hooks, you can declaratively define certain operations to perform before and/or after helmsman operations. 8 | These operations can be running installing dependencies (e.g. CRDs), executing certain tests, sending custom notifications, etc. 9 | Another useful use-case is if you are using a 3rd party chart which does not define native helm lifecycle hooks that you wish to have. 10 | 11 | ## Prerequisites 12 | 13 | - Hook operations can be defined in a Kubernetes manifest. They can be any kubernetes resource(s) (jobs, cron jobs, deployments, pods, etc). 14 | - You can only define one manifest file for each lifecycle hook. So make sure all your needed resources are in this manifest. 15 | - Hook operations can also be a script or a command. 16 | - Script or manifest paths must be either absolute or relative to the DSF. 17 | - Hook k8s manifests can also be defined as an URL. 18 | 19 | ## Supported lifecycle stages 20 | 21 | > hook types are case sensitive. Also, note the camleCase. 22 | 23 | - `preInstall` : before installing a release. 24 | - `postInstall`: after installing a release. 25 | - `preUpgrade`: before upgrading a release. 26 | - `postUpgrade`: after upgrading a release. 27 | - `preDelete`: before uninstalling a release. 28 | - `postDelete`: after uninstalling a release. 29 | 30 | ## Hooks stanza details 31 | 32 | The following items can be defined in the hooks stanza: 33 | 34 | **pre/postInstall, pre/postUpgrade, pre/postDelete**: 35 | 36 | A valid path (URL, cloud bucket, local file path) to your hook's k8s manifest or a valid path to a script or a shell command. 37 | 38 | The following options only apply to kubernetes manifest type of hooks. 39 | 40 | **successCondition**: 41 | 42 | The Kubernetes status condition that indicates that your resources have finished their job successfully. You can find out what the status conditions are for different k8s resources with a kubectl command similar to: `kubectl get job -o=jsonpath='{range .items[*]}{.status.conditions[0].type}{"\n"}{end}'` 43 | 44 | - For jobs, it is `Complete` 45 | - For pods, it is `Initialized` 46 | - For deployments, it is `Available` 47 | 48 | **successTimeout**: (default 30s) 49 | 50 | How much time to wait for the `successCondition` 51 | 52 | **deleteOnSuccess**: (true/false) 53 | 54 | Indicates if you wish to delete the hook's manifest after the hook succeeds. This is only used if you define `successCondition` 55 | 56 | > Note: successCondition, deleteOnSuccess and successTimeout are ignored when the `--dry-run` flag is used. 57 | 58 | ## Global vs App-specific hooks 59 | 60 | You can define two types of hooks in your desired state file: 61 | 62 | **Global** hooks: 63 | 64 | Are defined in the `settings` stanza and are inherited by all releases in the DSF if they haven't defined their own. 65 | 66 | These are defined as follows: 67 | 68 | ```toml 69 | [settings] 70 | #... 71 | [settings.globalHooks] 72 | successCondition= "Initialized" 73 | deleteOnSuccess= true 74 | postInstall= "job.yaml" 75 | ``` 76 | 77 | ```yaml 78 | settings: 79 | #... 80 | globalHooks: 81 | successCondition: "Initialized" 82 | deleteOnSuccess: true 83 | postInstall: "job.yaml" 84 | #... 85 | ``` 86 | 87 | **App-specific** hooks: 88 | 89 | Each app (release) can define its own hooks which **override any global ones**. 90 | 91 | These are defined as follows: 92 | 93 | ```toml 94 | [apps] 95 | [apps.argo] 96 | namespace = "production" # maps to the namespace as defined in namespaces above 97 | enabled = true # change to false if you want to delete this app release [default = false] 98 | chart = "argo/argo" # changing the chart name means delete and recreate this release 99 | version = "0.6.4" # chart version 100 | [apps.argo.hooks] 101 | successCondition= "Complete" 102 | successTimeout= "90s" 103 | deleteOnSuccess= true 104 | preInstall="job.yaml" 105 | preInstall="https://github.com/jetstack/cert-manager/releases/download/v0.14.0/cert-manager.crds.yaml" 106 | postInstall="https://raw.githubusercontent.com/jetstack/cert-manager/release-0.14/deploy/manifests/00-crds.yaml" 107 | preUpgrade="job.yaml" 108 | postUpgrade="job.yaml" 109 | preDelete="job.yaml" 110 | postDelete="job.yaml" 111 | ``` 112 | 113 | ```yaml 114 | apps: 115 | argo: 116 | namespace: "staging" # maps to the namespace as defined in namespaces above 117 | enabled: true # change to false if you want to delete this app release empty: false: 118 | chart: "argo/argo" # changing the chart name means delete and recreate this chart 119 | version: "0.6.5" # chart version 120 | hooks: 121 | successCondition: "Complete" 122 | successTimeout: "90s" 123 | deleteOnSuccess: true 124 | preInstall: "job.yaml" 125 | preInstall: "https://github.com/jetstack/cert-manager/releases/download/v0.14.0/cert-manager.crds.yaml" 126 | postInstall: "https://raw.githubusercontent.com/jetstack/cert-manager/release-0.14/deploy/manifests/00-crds.yaml" 127 | postInstall: "job.yaml" 128 | preUpgrade: "job.yaml" 129 | postUpgrade: "job.yaml" 130 | preDelete: "job.yaml" 131 | postDelete: "job.yaml" 132 | ``` 133 | 134 | ## Enforcing hook manifests deletion on all apps 135 | 136 | You can do that by setting `deleteOnSuccess` to true in the `globalHooks` stanza under `settings`. If you need to make an exception for some app, you can set it to `false` in the `hooks` stanza of this app. This overrides the global hooks. 137 | 138 | ## Expanding variables in hook manifests 139 | 140 | You can expand variables/parameters in the hook manifests at run time in one of the following ways: 141 | 142 | - use env variables (defined as `$MY_VAR` in your manifests) and run helmsman with `--subst-env-values`. Environment variables can be read from the environment or you can [load them from an env file](https://github.com/mkubaczyk/helmsman/blob/master/docs/how_to/apps/secrets.md#passing-secrets-from-env-files) 143 | 144 | - use [AWS SSM parameters](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html) (defined as `{{ssm: MY_PARAM }}` in your manifests) and run helmsman with `--subst-ssm-values`. 145 | 146 | - Pass encrypted values with [hiera-eyaml](https://github.com/mkubaczyk/helmsman/blob/master/docs/how_to/settings/use-hiera-eyaml-as-secrets-encryption.md) 147 | 148 | ## Limitations 149 | 150 | - You can only have one manifest file per lifecycle. 151 | - If you have multiple k8s resources in your hook manifest file, `successCondition` may not work. 152 | - pre/postDelete hooks are not respected before/after deleting untracked releases (releases which are no longer defined in your desired state file). 153 | -------------------------------------------------------------------------------- /internal/app/command.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "os/exec" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | // Command type representing all executable commands Helmsman needs 13 | // to execute in order to inspect the environment|releases|charts etc. 14 | type Command struct { 15 | Cmd string 16 | Args []string 17 | Description string 18 | } 19 | 20 | // CmdPipe is a os/exec.Commnad wrapper for UNIX pipe 21 | type CmdPipe []Command 22 | 23 | type ExitStatus struct { 24 | code int 25 | errors string 26 | output string 27 | } 28 | 29 | func (e ExitStatus) String() string { 30 | str := strings.TrimSpace(e.output) 31 | if errs := strings.TrimSpace(e.errors); errs != "" { 32 | str = fmt.Sprintf("%s\n--- stderr ---\n%s", str, errs) 33 | } 34 | return str 35 | } 36 | 37 | func (c *Command) String() string { 38 | var sb strings.Builder 39 | sb.WriteString(c.Cmd) 40 | for i := 0; i < len(c.Args); i++ { 41 | arg := c.Args[i] 42 | sb.WriteRune(' ') 43 | if strings.HasPrefix(arg, "--token=") { 44 | sb.WriteString("--token=******") 45 | continue 46 | } 47 | if strings.HasPrefix(arg, "--password=") { 48 | sb.WriteString("--password=******") 49 | continue 50 | } 51 | if arg == "--token" { 52 | sb.WriteString(arg) 53 | sb.WriteString("=******") 54 | i++ 55 | continue 56 | } 57 | if arg == "--password" { 58 | sb.WriteString(arg) 59 | sb.WriteString("=******") 60 | i++ 61 | continue 62 | } 63 | sb.WriteString(arg) 64 | } 65 | return sb.String() 66 | } 67 | 68 | // RetryExec runs exec command with retry 69 | func (c *Command) RetryExec(attempts int) (ExitStatus, error) { 70 | return c.RetryExecWithThreshold(attempts, 0) 71 | } 72 | 73 | // RetryExecWithThreshold runs exec command with retry and allows specifying the threshold for the exit code to be considered erroneous 74 | func (c *Command) RetryExecWithThreshold(attempts, exitCodeThreshold int) (ExitStatus, error) { 75 | var ( 76 | result ExitStatus 77 | err error 78 | ) 79 | 80 | for i := 0; i < attempts; i++ { 81 | result, err = c.Exec() 82 | if err == nil || (result.code >= 0 && result.code <= exitCodeThreshold) { 83 | return result, nil 84 | } 85 | if i < (attempts - 1) { 86 | time.Sleep(time.Duration(math.Pow(2, float64(2+i))) * time.Second) 87 | log.Infof("Retrying %s due to error: %v", c.Description, err) 88 | } 89 | } 90 | 91 | return result, fmt.Errorf("%s, failed after %d attempts with: %w", c.Description, attempts, err) 92 | } 93 | 94 | func (c *Command) command() *exec.Cmd { 95 | // Only use non-empty string args 96 | var args []string 97 | 98 | for _, str := range c.Args { 99 | if str != "" { 100 | args = append(args, str) 101 | } 102 | } 103 | 104 | log.Verbose(c.Description) 105 | log.Debug(c.String()) 106 | 107 | return exec.Command(c.Cmd, args...) 108 | } 109 | 110 | // Exec executes the executable command and returns the exit code and execution result 111 | func (c *Command) Exec() (ExitStatus, error) { 112 | var stdout, stderr bytes.Buffer 113 | cmd := c.command() 114 | cmd.Stdout = &stdout 115 | cmd.Stderr = &stderr 116 | 117 | if err := cmd.Start(); err != nil { 118 | log.Info("cmd.Start: " + err.Error()) 119 | return ExitStatus{ 120 | code: 1, 121 | errors: err.Error(), 122 | }, err 123 | } 124 | 125 | err := cmd.Wait() 126 | res := ExitStatus{ 127 | output: strings.TrimSpace(stdout.String()), 128 | errors: strings.TrimSpace(stderr.String()), 129 | } 130 | if err != nil { 131 | res.code = 126 132 | if exiterr, ok := err.(*exec.ExitError); ok { 133 | res.code = exiterr.ExitCode() 134 | } 135 | err = newExitError(c.Description, res.code, res.output, res.errors, err) 136 | } 137 | return res, err 138 | } 139 | 140 | // Exec pipes the executable commands and returns the exit code and execution result 141 | func (p CmdPipe) Exec() (ExitStatus, error) { 142 | var ( 143 | stdout, stderr bytes.Buffer 144 | stack []*exec.Cmd 145 | ) 146 | 147 | l := len(p) - 1 148 | if l < 0 { 149 | // nonthing to do here 150 | return ExitStatus{}, nil 151 | } 152 | if l == 0 { 153 | // it's just one command we can just run it 154 | return p[0].Exec() 155 | } 156 | 157 | for i, c := range p { 158 | stack = append(stack, c.command()) 159 | stack[i].Stderr = &stderr 160 | if i > 0 { 161 | stack[i].Stdin, _ = stack[i-1].StdoutPipe() 162 | } 163 | } 164 | stack[l].Stdout = &stdout 165 | 166 | err := call(stack) 167 | res := ExitStatus{ 168 | output: strings.TrimSpace(stdout.String()), 169 | errors: strings.TrimSpace(stderr.String()), 170 | } 171 | if err != nil { 172 | res.code = 126 173 | if exiterr, ok := err.(*exec.ExitError); ok { 174 | res.code = exiterr.ExitCode() 175 | } 176 | err = newExitError(p[l].Description, res.code, res.output, res.errors, err) 177 | } 178 | return res, err 179 | } 180 | 181 | // RetryExec runs piped commands with retry 182 | func (p CmdPipe) RetryExec(attempts int) (ExitStatus, error) { 183 | return p.RetryExecWithThreshold(attempts, 0) 184 | } 185 | 186 | // RetryExecWithThreshold runs piped commands with retry and allows specifying the threshold for the exit code to be considered erroneous 187 | func (p CmdPipe) RetryExecWithThreshold(attempts, exitCodeThreshold int) (ExitStatus, error) { 188 | var ( 189 | result ExitStatus 190 | err error 191 | ) 192 | 193 | l := len(p) - 1 194 | for i := 0; i < attempts; i++ { 195 | result, err = p.Exec() 196 | if err == nil || (result.code >= 0 && result.code <= exitCodeThreshold) { 197 | return result, nil 198 | } 199 | if i < (attempts - 1) { 200 | time.Sleep(time.Duration(math.Pow(2, float64(2+i))) * time.Second) 201 | log.Infof("Retrying %s due to error: %v", p[l].Description, err) 202 | } 203 | } 204 | 205 | return result, fmt.Errorf("%s, failed after %d attempts with: %w", p[l].Description, attempts, err) 206 | } 207 | 208 | func call(stack []*exec.Cmd) (err error) { 209 | if stack[0].Process == nil { 210 | if err = stack[0].Start(); err != nil { 211 | return err 212 | } 213 | } 214 | if len(stack) > 1 { 215 | if err = stack[1].Start(); err != nil { 216 | return err 217 | } 218 | defer func() { 219 | if err == nil { 220 | err = call(stack[1:]) 221 | } else { 222 | err = stack[1].Wait() 223 | } 224 | if err != nil { 225 | log.Infof("call: %v", err) 226 | } 227 | }() 228 | } 229 | return stack[0].Wait() 230 | } 231 | 232 | func newExitError(cmd string, code int, stdout, stderr string, cause error) error { 233 | return fmt.Errorf( 234 | "%s failed with non-zero exit code %d: %w\noutput: %s", 235 | cmd, code, cause, 236 | fmt.Sprintf( 237 | "\n--- stdout ---\n%s\n--- stderr ---\n%s", 238 | strings.TrimSpace(stdout), 239 | strings.TrimSpace(stderr), 240 | ), 241 | ) 242 | } 243 | 244 | // ToolExists returns true if the tool is present in the environment and false otherwise. 245 | // It takes as input the tool's command to check if it is recognizable or not. e.g. helm or kubectl 246 | func ToolExists(tool string) bool { 247 | _, err := exec.LookPath(tool) 248 | return err == nil 249 | } 250 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub release](https://img.shields.io/github/v/release/mkubaczyk/helmsman)](https://github.com/mkubaczyk/helmsman/releases) 2 | 3 | ![helmsman-logo](docs/images/helmsman.png) 4 | 5 | > Helmsman v4.x supports Helm 3.x and Helm 4.x. For Helm 2.x, use Helmsman v1.x 6 | 7 | # What is Helmsman? 8 | 9 | Helmsman is a Helm Charts (k8s applications) as Code tool which allows you to automate the deployment/management of your Helm charts from version controlled code. 10 | 11 | # Why has this repository changed the owner? 12 | 13 | The previous owner (Praqma company, later Eficode company) of this repository decided to transfer the repository into the hands of current maintainers 14 | to make sure the project can be developed further with no interruptions and unnecessary dependencies. 15 | We'll do our best to get it up and running as soon as possible. 16 | Thank you for your patience and trusting Helmsman with your tasks! 17 | 18 | # How does it work? 19 | 20 | Helmsman uses a simple declarative [TOML](https://github.com/toml-lang/toml) file to allow you to describe a desired state for your k8s applications as in the [example toml file](https://github.com/mkubaczyk/helmsman/blob/master/examples/example.toml). 21 | Alternatively YAML declaration is also acceptable [example yaml file](https://github.com/mkubaczyk/helmsman/blob/master/examples/example.yaml). 22 | 23 | The desired state file (DSF) follows the [desired state specification](https://github.com/mkubaczyk/helmsman/blob/master/docs/desired_state_specification.md). 24 | 25 | Helmsman sees what you desire, validates that your desire makes sense (e.g. that the charts you desire are available in the repos you defined), compares it with the current state of Helm and figures out what to do to make your desire come true. 26 | 27 | To plan without executing: 28 | 29 | ```sh 30 | helmsman -f example.toml 31 | ``` 32 | 33 | To plan and execute the plan: 34 | 35 | ```sh 36 | helmsman --apply -f example.toml 37 | ``` 38 | 39 | To show debugging details: 40 | 41 | ```sh 42 | helmsman --debug --apply -f example.toml 43 | ``` 44 | 45 | To run a dry-run: 46 | 47 | ```sh 48 | helmsman --debug --dry-run -f example.toml 49 | ``` 50 | 51 | To limit execution to specific application: 52 | 53 | ```sh 54 | helmsman --debug --dry-run --target artifactory -f example.toml 55 | ``` 56 | 57 | # Features 58 | 59 | - **Built for CD**: Helmsman can be used as a docker image or a binary. 60 | - **Applications as code**: describe your desired applications and manage them from a single version-controlled declarative file. 61 | - **Suitable for Multitenant Clusters**: deploy Tiller in different namespaces with service accounts and TLS (versions 1.x). 62 | - **Easy to use**: deep knowledge of Helm CLI and Kubectl is NOT mandatory to use Helmsman. 63 | - **Plan, View, apply**: you can run Helmsman to generate and view a plan with/without executing it. 64 | - **Portable**: Helmsman can be used to manage charts deployments on any k8s cluster. 65 | - **Protect Namespaces/Releases**: you can define certain namespaces/releases to be protected against accidental human mistakes. 66 | - **Define the order of managing releases**: you can define the priorities at which releases are managed by helmsman (useful for dependencies). 67 | - **Parallelise**: Releases with the same priority can be executed in parallel. 68 | - **Idempotency**: As long your desired state file does not change, you can execute Helmsman several times and get the same result. 69 | - **Continue from failures**: In the case of partial deployment due to a specific chart deployment failure, fix your helm chart and execute Helmsman again without needing to rollback the partial successes first. 70 | 71 | # Install 72 | 73 | ## From binary 74 | 75 | Please make sure the following are installed prior to using `helmsman` as a binary (the docker image contains all of them): 76 | 77 | - [kubectl](https://github.com/kubernetes/kubectl) 78 | - [helm](https://github.com/helm/helm) (helm v3.x or v4.x for `helmsman` v4.x) 79 | - [helm-diff](https://github.com/databus23/helm-diff) (`helmsman` >= 1.6.0) 80 | 81 | If you use private helm repos, you will need either `helm-gcs` or `helm-s3` plugin or you can use basic auth to authenticate to your repos. See the [docs](https://github.com/mkubaczyk/helmsman/blob/master/docs/how_to/helm_repos) for details. 82 | 83 | Check the [releases page](https://github.com/mkubaczyk/helmsman/releases) for the different versions. 84 | 85 | ```sh 86 | # on Linux 87 | curl -L https://github.com/mkubaczyk/helmsman/releases/download/v4.0.3/helmsman_4.0.3_linux_amd64.tar.gz | tar zx 88 | # on MacOS 89 | curl -L https://github.com/mkubaczyk/helmsman/releases/download/v4.0.3/helmsman_4.0.3_darwin_amd64.tar.gz | tar zx 90 | 91 | mv helmsman /usr/local/bin/helmsman 92 | ``` 93 | 94 | ## As a docker image 95 | 96 | Docker images are published to `ghcr.io/mkubaczyk/helmsman` with variants for Helm 3 and Helm 4: 97 | 98 | | Tag | Description | 99 | |-----|-------------| 100 | | `latest` | Latest release with Helm 4 | 101 | | `vX.Y.Z` | Specific release with Helm 4 (default) | 102 | | `vX.Y.Z-helm3` | Specific release with Helm 3 | 103 | | `vX.Y.Z-helm4` | Specific release with Helm 4 | 104 | | `vX.Y.Z-v3.19.4` | Specific release with exact Helm version | 105 | | `vX.Y.Z-v4.0.4` | Specific release with exact Helm version | 106 | 107 | ```sh 108 | # Latest with Helm 4 (default) 109 | docker pull ghcr.io/mkubaczyk/helmsman:latest 110 | 111 | # Specific release with Helm 4 112 | docker pull ghcr.io/mkubaczyk/helmsman:v4.0.3 113 | 114 | # Specific release with Helm 3 115 | docker pull ghcr.io/mkubaczyk/helmsman:v4.0.3-helm3 116 | ``` 117 | 118 | ## As a package 119 | 120 | Helmsman has been packaged in Archlinux under `helmsman-bin` for the latest binary release, and `helmsman-git` for master. 121 | 122 | You can also install Helmsman using [Homebrew](https://brew.sh) 123 | 124 | ```sh 125 | brew install helmsman 126 | ``` 127 | 128 | ## As an [asdf-vm](https://asdf-vm.com/) plugin 129 | 130 | ```sh 131 | asdf plugin-add helmsman 132 | asdf install helmsman latest 133 | ``` 134 | 135 | # Documentation 136 | 137 | > Documentation for Helmsman v1.x can be found at: [docs v1.x](https://github.com/mkubaczyk/helmsman/tree/1.x/docs) 138 | 139 | - [How-Tos](https://github.com/mkubaczyk/helmsman/blob/master/docs/how_to/). 140 | - [Desired state specification](https://github.com/mkubaczyk/helmsman/blob/master/docs/desired_state_specification.md). 141 | - [CMD reference](https://github.com/mkubaczyk/helmsman/blob/master/docs/cmd_reference.md) 142 | 143 | ## Usage 144 | 145 | Helmsman can be used in three different settings: 146 | 147 | - [As a binary with a hosted cluster](https://github.com/mkubaczyk/helmsman/blob/master/docs/how_to/settings). 148 | - [As a docker image in a CI system or local machine](https://github.com/mkubaczyk/helmsman/blob/master/docs/how_to/deployments/ci.md) Always use a tagged docker image from [GHCR](https://github.com/mkubaczyk/helmsman/pkgs/container/helmsman). 149 | - [As a docker image inside a k8s cluster](https://github.com/mkubaczyk/helmsman/blob/master/docs/how_to/deployments/inside_k8s.md) 150 | 151 | # Contributing 152 | 153 | Pull requests, feedback/feature requests are welcome. Please check our [contribution guide](CONTRIBUTION.md). 154 | -------------------------------------------------------------------------------- /docs/deployment_strategies.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: v3.0.0-beta5 3 | --- 4 | 5 | # Deployment Strategies 6 | 7 | This document describes the different strategies to use Helmsman for maintaining your helm charts deployment to k8s clusters. 8 | 9 | ## Deploying 3rd party charts (apps) in a production cluster 10 | 11 | Suppose you are deploying 3rd party charts (e.g. Jenkins, Jira ... etc.) in your cluster. These applications can be deployed with Helmsman using a single desired state file. The desired state tells helmsman to deploy these apps into certain namespaces in a production cluster. 12 | 13 | You can test 3rd party charts in designated namespaces (e.g, staging) within the same production cluster. This also can be defined in the same desired state file. Below is an example of a desired state file for deploying 3rd party apps in production and staging namespaces: 14 | 15 | ```toml 16 | [metadata] 17 | org = "example" 18 | 19 | # using a minikube cluster 20 | [settings] 21 | kubeContext = "minikube" 22 | 23 | [namespaces] 24 | [namespaces.staging] 25 | protected = false 26 | [namespaces.production] 27 | protected = true 28 | 29 | [helmRepos] 30 | jenkins = https://charts.jenkins.io 31 | center = https://repo.chartcenter.io 32 | 33 | [apps] 34 | 35 | [apps.jenkins] 36 | name = "jenkins-prod" # should be unique across all apps 37 | description = "production jenkins" 38 | namespace = "production" 39 | enabled = true 40 | chart = "jenkins/jenkins" 41 | version = "2.15.1" # chart version 42 | valuesFiles = [ "../my-jenkins-common-values.yaml", "../my-jenkins-production-values.yaml" ] 43 | 44 | 45 | [apps.artifactory] 46 | name = "artifactory-prod" # should be unique across all apps 47 | description = "production artifactory" 48 | namespace = "production" 49 | enabled = true 50 | chart = "jfrog/artifactory" 51 | version = "11.4.2" # chart version 52 | valuesFile = "../my-artificatory-production-values.yaml" 53 | 54 | 55 | # the jenkins release below is being tested in the staging namespace 56 | [apps.jenkins-test] 57 | name = "jenkins-test" # should be unique across all apps 58 | description = "test release of jenkins, testing xyz feature" 59 | namespace = "staging" 60 | enabled = true 61 | chart = "jenkins/jenkins" 62 | version = "2.15.1" # chart version 63 | valuesFiles = [ "../my-jenkins-common-values.yaml", "../my-jenkins-testing-values.yaml" ] 64 | ``` 65 | 66 | ```yaml 67 | metadata: 68 | org: "example" 69 | 70 | # using a minikube cluster 71 | settings: 72 | kubeContext: "minikube" 73 | 74 | namespaces: 75 | staging: 76 | protected: false 77 | production: 78 | protected: true 79 | 80 | helmRepos: 81 | jenkins: https://charts.jenkins.io 82 | jfrog: https://charts.jfrog.io 83 | 84 | apps: 85 | jenkins: 86 | name: "jenkins-prod" # should be unique across all apps 87 | description: "production jenkins" 88 | namespace: "production" 89 | enabled: true 90 | chart: "jenkins/jenkins" 91 | version: "2.15.1" # chart version 92 | valuesFile: "../my-jenkins-production-values.yaml" 93 | 94 | artifactory: 95 | name: "artifactory-prod" # should be unique across all apps 96 | description: "production artifactory" 97 | namespace: "production" 98 | enabled: true 99 | chart: "jfrog/artifactory" 100 | version: "11.4.2" # chart version 101 | valuesFile: "../my-artifactory-production-values.yaml" 102 | 103 | # the jenkins release below is being tested in the staging namespace 104 | jenkins-test: 105 | name: "jenkins-test" # should be unique across all apps 106 | description: "test release of jenkins, testing xyz feature" 107 | namespace: "staging" 108 | enabled: true 109 | chart: "jenkins/jenkins" 110 | version: "2.15.1" # chart version 111 | valuesFile: "../my-jenkins-testing-values.yaml" 112 | 113 | ``` 114 | 115 | You can split the desired state file into multiple files if your deployment pipelines requires that, but it is important to read the notes below on using multiple desired state files with one cluster. 116 | 117 | ## Working with multiple clusters 118 | 119 | If you use multiple clusters for multiple purposes, you need at least one Helmsman desired state file for each cluster. 120 | 121 | 122 | ## Deploying your dev charts 123 | 124 | If you are developing your own applications/services and packaging them in helm charts, it makes sense to automatically deploy these charts to a staging namespace or a dev cluster on every source code commit. 125 | 126 | Often, you would have multiple apps developed in separate source code repositories but you would like to test their deployment in the same cluster/namespace. In that case, Helmsman can be used [as part of your CI pipeline](how_to/deployments/ci.md) as described in the diagram below: 127 | 128 | > as of v1.1.0 , you can use the `ns-override`flag to force helmsman to deploy/move all apps into a given namespace. For example, you could use this flag in a CI job that gets triggered on commits to the dev branch to deploy all apps into the `staging` namespace. 129 | 130 | ![multi-DSF](images/multi-DSF.png) 131 | 132 | Each repository will have a Helmsman desired state file (DSF). But it is important to consider the notes below on using multiple desired state files with a single cluster. 133 | 134 | If you need supporting applications (charts) for your application (e.g, reverse proxies, DB, k8s dashboard, etc.), you can describe the desired state for these in a separate file which can live in another repository. Adding such a file in the pipeline where you create your cluster from code makes total "DevOps" sense. 135 | 136 | ## Notes on using multiple Helmsman desired state files for the same cluster 137 | 138 | Helmsman v3.0.0-beta5 introduces the `context` stanza. 139 | When having multiple DSFs operating on different releases, it is essential to use the `context` stanza in each DSF to define what context the DSF covers. The user-provided value for `context` is used by Helmsman to label and distinguish which DSF manages which deployed releases in the cluster. This way, each helmsman operation will only operate on releases within the context defined in the DSF. 140 | 141 | When having multiple DSFs be aware of the following: 142 | 143 | - If no context is provided in the DSF (or merged DSFs), `default` is applied as a default context. This means any set of DSFs that don't define custom contexts can still operate on each other's releases (same behavior as in Helmsman 1.x). 144 | 145 | - If you don't define context in your DSFs, you would need to use the `--keep-untracked-releases` flag to avoid different DSFs deleting each other's releases. 146 | 147 | - When merging multiple DSFs in one Helmsman operation, context from the firs DSF in the list gets overridden by the context in the last DSF. 148 | 149 | - If multiple DSFs use the same context name, they will mess up each other's releases. 150 | 151 | - If two releases from two different DSFs (each with its own context) have the same name and namespace, Helmsman will only allow the first one of them to be installed. The second will be blocked by Helmsman. 152 | 153 | - If you deploy releases from multiple DSF to one namespace (not recommended!), that namespace's protection config does not automatically cascade between DSFs. You will have to enable the protection in each of the DSFs. 154 | 155 | Also please refer to the [best practice](best_practice.md) document. 156 | --------------------------------------------------------------------------------