From c803151ab12aba1220987686f9bf1ca314415a24 Mon Sep 17 00:00:00 2001 From: Luis Davim Date: Tue, 24 Dec 2019 14:15:26 +0000 Subject: [PATCH] reorganisig the code using receiver methods --- CONTRIBUTION.md | 16 +- Gopkg.lock | 479 ------------------ Gopkg.toml | 67 --- Makefile | 72 +-- README.md | 8 +- docs/best_practice.md | 6 +- docs/cmd_reference.md | 16 +- docs/deployment_strategies.md | 10 +- docs/desired_state_specification.md | 8 +- docs/how_to/README.md | 2 +- docs/how_to/apps/basic.md | 2 +- docs/how_to/apps/destroy.md | 2 +- docs/how_to/apps/helm_tests.md | 4 +- docs/how_to/apps/moving_across_namespaces.md | 2 +- docs/how_to/apps/order.md | 2 +- docs/how_to/apps/override_namespaces.md | 2 +- docs/how_to/apps/secrets.md | 2 +- docs/how_to/deployments/ci.md | 4 +- docs/how_to/deployments/inside_k8s.md | 8 +- docs/how_to/helm_repos/basic_auth.md | 4 +- docs/how_to/helm_repos/default.md | 6 +- docs/how_to/helm_repos/gcs.md | 2 +- docs/how_to/misc/auth_to_storage_providers.md | 2 +- .../misc/limit-deployment-to-specific-apps.md | 2 +- ...it-deployment-to-specific-group-of-apps.md | 4 +- docs/how_to/misc/merge_desired_state_files.md | 12 +- .../namespaces/labels_and_annotations.md | 4 +- .../creating_kube_context_with_certs.md | 6 +- .../creating_kube_context_with_token.md | 4 +- .../use-hiera-eyaml-as-secrets-encryption.md | 4 +- docs/why_helmsman.md | 8 +- examples/example.toml | 4 +- examples/example.yaml | 6 +- examples/minimal-example.toml | 22 +- examples/minimal-example.yaml | 8 +- go.mod | 26 +- go.sum | 41 +- internal/app/cli.go | 10 +- internal/app/command.go | 6 +- internal/app/decision_maker.go | 443 +++++----------- internal/app/decision_maker_test.go | 23 +- internal/app/helm_helpers.go | 370 +------------- internal/app/helm_helpers_test.go | 475 ----------------- internal/app/helm_release.go | 84 +++ internal/app/helm_time.go | 44 ++ internal/app/kube_helpers.go | 40 +- internal/app/main.go | 20 +- internal/app/namespace.go | 5 +- internal/app/release.go | 375 +++++++++++++- internal/app/release_test.go | 381 +++++++++++++- internal/app/state.go | 2 +- internal/app/utils.go | 64 ++- internal/app/utils_test.go | 91 ++++ release-notes.md | 7 +- 54 files changed, 1361 insertions(+), 1956 deletions(-) delete mode 100644 Gopkg.lock delete mode 100644 Gopkg.toml delete mode 100644 internal/app/helm_helpers_test.go create mode 100644 internal/app/helm_release.go create mode 100644 internal/app/helm_time.go diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md index f7e3c107..8d132853 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -13,15 +13,15 @@ make build ## The branches and tags -`master` is where Helmsman latest code lives. -`1.x` this is where Helmsman versions 1.x lives. -> Helmsman v1.x supports helm v2.x only and will no longer be supported except for bug fixes and minor changes. +`master` is where Helmsman latest code lives. +`1.x` this is where Helmsman versions 1.x lives. +> Helmsman v1.x supports helm v2.x only and will no longer be supported except for bug fixes and minor changes. ## Submitting pull requests -- If your PR is for Helmsman v1.x, it should target the `1.x` branch. +- If your PR is for Helmsman v1.x, it should target the `1.x` branch. - 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. -- Please make sure you update the documentation with new features or the changes your PR adds. The following places are required. +- Please make sure you update the documentation with new features or the changes your PR adds. The following places are required. - Update existing [how_to](docs/how_to/) guides or create new ones. - If necessary, Update the [Desired State File spec](docs/desired_state_specification.md) - If adding new flags, Update the [cmd reference](docs/cmd_reference.md) @@ -43,8 +43,8 @@ The following steps are needed to cut a release (They assume that you are on mas 1. Change the version variable in [main.go](internal/app/main.go) and in [.version](.version) 2. Update the [release-notes.md](release-notes.md) file with new version and changelog. 3. (Optional), if new helm versions are required, update the [circleci config](.circleci/config.yml) and add more docker commands. -4. Commit your changes locally. +4. Commit your changes locally. 5. Create a git tag with the following command: `git tag -a -m "" ` 6. Push your commit and tag with `git push --follow-tags` -7. This should trigger the [pipeline on circleci](https://circleci.com/gh/Praqma/workflows/helmsman) which eventually releases to Github and dockerhub. - +7. This should trigger the [pipeline on circleci](https://circleci.com/gh/Praqma/workflows/helmsman) which eventually releases to Github and dockerhub. + diff --git a/Gopkg.lock b/Gopkg.lock deleted file mode 100644 index 7e4fad17..00000000 --- a/Gopkg.lock +++ /dev/null @@ -1,479 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[projects]] - digest = "1:9854327327a05877e8c528f6830dcadd05d149f5be7c6405ac499c0202e9d43b" - name = "cloud.google.com/go" - packages = [ - "compute/metadata", - "iam", - "internal", - "internal/optional", - "internal/trace", - "internal/version", - "storage", - ] - pruneopts = "UT" - revision = "97efc2c9ffd9fe8ef47f7f3203dc60bbca547374" - version = "v0.28.0" - -[[projects]] - digest = "1:279540310125d2b219920588d7e2edb2a85b3317b528839166e896ce6b6f211c" - name = "github.com/Azure/azure-pipeline-go" - packages = ["pipeline"] - pruneopts = "UT" - revision = "55fedc85a614dcd0e942a66f302ae3efb83d563c" - version = "v0.1.9" - -[[projects]] - digest = "1:c4a5edf3b0f38e709a78dcc945997678a364c2b5adfd48842a3dd349c352f833" - name = "github.com/Azure/azure-storage-blob-go" - packages = ["azblob"] - pruneopts = "UT" - revision = "5152f14ace1c6db66bd9cb57840703a8358fa7bc" - version = "0.3.0" - -[[projects]] - digest = "1:9f3b30d9f8e0d7040f729b82dcbc8f0dead820a133b3147ce355fc451f32d761" - name = "github.com/BurntSushi/toml" - packages = ["."] - pruneopts = "UT" - revision = "3012a1dbe2e4bd1391d42b32f0577cb7bbc7f005" - version = "v0.3.1" - -[[projects]] - digest = "1:efe5038e25001cc88547d8ecc9ed76bbb97c15826b9bb91b931ec30dacb4e3f1" - name = "github.com/apsdehal/go-logger" - packages = ["."] - pruneopts = "UT" - revision = "1abdf898e0242cf7027d832c3e95388315c25de6" - version = "1.3.0" - -[[projects]] - digest = "1:1742529f9132aa5c14ffadd6c2e87c211ef47fe9b6df1139de41489179f13da0" - name = "github.com/aws/aws-sdk-go" - packages = [ - "aws", - "aws/arn", - "aws/awserr", - "aws/awsutil", - "aws/client", - "aws/client/metadata", - "aws/corehandlers", - "aws/credentials", - "aws/credentials/ec2rolecreds", - "aws/credentials/endpointcreds", - "aws/credentials/processcreds", - "aws/credentials/stscreds", - "aws/csm", - "aws/defaults", - "aws/ec2metadata", - "aws/endpoints", - "aws/request", - "aws/session", - "aws/signer/v4", - "internal/ini", - "internal/s3err", - "internal/sdkio", - "internal/sdkmath", - "internal/sdkrand", - "internal/sdkuri", - "internal/shareddefaults", - "private/protocol", - "private/protocol/eventstream", - "private/protocol/eventstream/eventstreamapi", - "private/protocol/json/jsonutil", - "private/protocol/jsonrpc", - "private/protocol/query", - "private/protocol/query/queryutil", - "private/protocol/rest", - "private/protocol/restxml", - "private/protocol/xml/xmlutil", - "service/s3", - "service/s3/internal/arn", - "service/s3/s3iface", - "service/s3/s3manager", - "service/ssm", - "service/sts", - "service/sts/stsiface", - ] - pruneopts = "UT" - revision = "b4f52c45248fcab90c27f5509504b840ca2fcf10" - version = "v1.26.2" - -[[projects]] - branch = "master" - digest = "1:b7cb6054d3dff43b38ad2e92492f220f57ae6087ee797dca298139776749ace8" - name = "github.com/golang/groupcache" - packages = ["lru"] - pruneopts = "UT" - revision = "611e8accdfc92c4187d399e95ce826046d4c8d73" - -[[projects]] - digest = "1:15b834d22254d28e02f4a591ff89427a1045e597b3832660cb1776546e3523b3" - name = "github.com/golang/protobuf" - packages = [ - "proto", - "protoc-gen-go", - "protoc-gen-go/descriptor", - "protoc-gen-go/generator", - "protoc-gen-go/generator/internal/remap", - "protoc-gen-go/grpc", - "protoc-gen-go/plugin", - "ptypes", - "ptypes/any", - "ptypes/duration", - "ptypes/timestamp", - ] - pruneopts = "UT" - revision = "6c65a5562fc06764971b7c5d05c76c75e84bdbf7" - version = "v1.3.2" - -[[projects]] - digest = "1:4f880e8e7ff8803aab4bcf2c479eb766435f7decaa1edf080d8f2bf56668de1d" - name = "github.com/googleapis/gax-go" - packages = [ - ".", - "v2", - ] - pruneopts = "UT" - revision = "bd5b16380fd03dc758d11cef74ba2e3bc8b0e8c2" - version = "v2.0.5" - -[[projects]] - digest = "1:88e0b0baeb9072f0a4afbcf12dda615fc8be001d1802357538591155998da21b" - name = "github.com/hashicorp/go-version" - packages = ["."] - pruneopts = "UT" - revision = "ac23dc3fea5d1a983c43f6a0f6e2c13f0195d8bd" - version = "v1.2.0" - -[[projects]] - digest = "1:78d28d5b84a26159c67ea51996a230da4bc07cac648adaae1dfb5fc0ec8e40d3" - name = "github.com/imdario/mergo" - packages = ["."] - pruneopts = "UT" - revision = "1afb36080aec31e0d1528973ebe6721b191b0369" - version = "v0.3.8" - -[[projects]] - digest = "1:bb81097a5b62634f3e9fec1014657855610c82d19b9a40c17612e32651e35dca" - name = "github.com/jmespath/go-jmespath" - packages = ["."] - pruneopts = "UT" - revision = "c2b33e84" - -[[projects]] - digest = "1:ecd9aa82687cf31d1585d4ac61d0ba180e42e8a6182b85bd785fcca8dfeefc1b" - name = "github.com/joho/godotenv" - packages = ["."] - pruneopts = "UT" - revision = "23d116af351c84513e1946b527c88823e476be13" - version = "v1.3.0" - -[[projects]] - branch = "master" - digest = "1:82589c260d8f6281bbcdda5f99638e2667054d0760e3783c1ea44dac5df67394" - name = "github.com/logrusorgru/aurora" - packages = ["."] - pruneopts = "UT" - revision = "66b7ad493a23a2523bac50571522bbfe5b90a835" - -[[projects]] - digest = "1:7b6ca9454e8d16e34b251f181a4ae430a25805acca64ea46b0448583b44fcfe6" - name = "go.opencensus.io" - packages = [ - ".", - "internal", - "internal/tagencoding", - "metric/metricdata", - "metric/metricproducer", - "plugin/ochttp", - "plugin/ochttp/propagation/b3", - "resource", - "stats", - "stats/internal", - "stats/view", - "tag", - "trace", - "trace/internal", - "trace/propagation", - "trace/tracestate", - ] - pruneopts = "UT" - revision = "aad2c527c5defcf89b5afab7f37274304195a6b2" - version = "v0.22.2" - -[[projects]] - branch = "master" - digest = "1:b1444bc98b5838c3116ed23e231fee4fa8509f975abd96e5d9e67e572dd01604" - name = "golang.org/x/exp" - packages = [ - "apidiff", - "cmd/apidiff", - ] - pruneopts = "UT" - revision = "2f50522955873285d9bf17ec91e55aec8ae82edc" - -[[projects]] - branch = "master" - digest = "1:334b27eac455cb6567ea28cd424230b07b1a64334a2f861a8075ac26ce10af43" - name = "golang.org/x/lint" - packages = [ - ".", - "golint", - ] - pruneopts = "UT" - revision = "fdd1cda4f05fd1fd86124f0ef9ce31a0b72c8448" - -[[projects]] - branch = "master" - digest = "1:676f320d34ccfa88bfa6d04bdf388ed7062af175355c805ef57ccda1a3f13432" - name = "golang.org/x/net" - packages = [ - "context", - "context/ctxhttp", - "http/httpguts", - "http2", - "http2/hpack", - "idna", - "internal/timeseries", - "trace", - ] - pruneopts = "UT" - revision = "c0dbc17a35534bf2e581d7a942408dc936316da4" - -[[projects]] - branch = "master" - digest = "1:e9dec12f847de7d8cd7c1d3d0d89aa40a345a768fd20e10d378a260da9ad65aa" - name = "golang.org/x/oauth2" - packages = [ - ".", - "google", - "internal", - "jws", - "jwt", - ] - pruneopts = "UT" - revision = "858c2ad4c8b6c5d10852cb89079f6ca1c7309787" - -[[projects]] - branch = "master" - digest = "1:634414cd43ed62a728aaf9b3e61fde85f012880f9b32b7d00678b6fd887ff9c1" - name = "golang.org/x/sys" - packages = ["unix"] - pruneopts = "UT" - revision = "ac6580df4449443a05718fd7858c1f91ad5f8d20" - -[[projects]] - digest = "1:8d8faad6b12a3a4c819a3f9618cb6ee1fa1cfc33253abeeea8b55336721e3405" - name = "golang.org/x/text" - packages = [ - "collate", - "collate/build", - "internal/colltab", - "internal/gen", - "internal/language", - "internal/language/compact", - "internal/tag", - "internal/triegen", - "internal/ucd", - "language", - "secure/bidirule", - "transform", - "unicode/bidi", - "unicode/cldr", - "unicode/norm", - "unicode/rangetable", - ] - pruneopts = "UT" - revision = "342b2e1fbaa52c93f31447ad2c6abc048c63e475" - version = "v0.3.2" - -[[projects]] - branch = "master" - digest = "1:3ad1f2cdd02ce959277d4e6680d6c89b92f4caeaed3963b2467418a48b810421" - name = "golang.org/x/tools" - packages = [ - "cmd/goimports", - "go/analysis", - "go/analysis/passes/inspect", - "go/ast/astutil", - "go/ast/inspector", - "go/buildutil", - "go/gcexportdata", - "go/internal/gcimporter", - "go/internal/packagesdriver", - "go/packages", - "go/types/objectpath", - "go/types/typeutil", - "internal/fastwalk", - "internal/gopathwalk", - "internal/imports", - "internal/module", - "internal/semver", - ] - pruneopts = "UT" - revision = "04c2e8eff935f58e9a5568b6b73542f91e0491e6" - -[[projects]] - digest = "1:d1204867de197747eaeedf6eb705968289bce4796c9e9f48ef7c8ef27a227528" - name = "google.golang.org/api" - packages = [ - "googleapi", - "googleapi/transport", - "internal", - "internal/gensupport", - "internal/third_party/uritemplates", - "iterator", - "option", - "storage/v1", - "transport/http", - "transport/http/internal/propagation", - ] - pruneopts = "UT" - revision = "8a410c21381766a810817fd6200fce8838ecb277" - version = "v0.14.0" - -[[projects]] - digest = "1:3c03b58f57452764a4499c55c582346c0ee78c8a5033affe5bdfd9efd3da5bd1" - name = "google.golang.org/appengine" - packages = [ - ".", - "internal", - "internal/app_identity", - "internal/base", - "internal/datastore", - "internal/log", - "internal/modules", - "internal/remote_api", - "internal/urlfetch", - "urlfetch", - ] - pruneopts = "UT" - revision = "971852bfffca25b069c31162ae8f247a3dba083b" - version = "v1.6.5" - -[[projects]] - branch = "master" - digest = "1:c21fbca8f5af1be04e383c4c57be0b85abeea2fe370a54dc805b1b04e443c9e9" - name = "google.golang.org/genproto" - packages = [ - "googleapis/api/annotations", - "googleapis/iam/v1", - "googleapis/rpc/code", - "googleapis/rpc/status", - "googleapis/type/expr", - ] - pruneopts = "UT" - revision = "0243a4be9c8f1264d238fdc2895620b4d9baf9e1" - -[[projects]] - digest = "1:b59ce3ddb11daeeccccc9cb3183b58ebf8e9a779f1c853308cd91612e817a301" - name = "google.golang.org/grpc" - packages = [ - ".", - "backoff", - "balancer", - "balancer/base", - "balancer/roundrobin", - "binarylog/grpc_binarylog_v1", - "codes", - "connectivity", - "credentials", - "credentials/internal", - "encoding", - "encoding/proto", - "grpclog", - "internal", - "internal/backoff", - "internal/balancerload", - "internal/binarylog", - "internal/buffer", - "internal/channelz", - "internal/envconfig", - "internal/grpcrand", - "internal/grpcsync", - "internal/resolver/dns", - "internal/resolver/passthrough", - "internal/syscall", - "internal/transport", - "keepalive", - "metadata", - "naming", - "peer", - "resolver", - "serviceconfig", - "stats", - "status", - "tap", - ] - pruneopts = "UT" - revision = "1a3960e4bd028ac0cec0a2afd27d7d8e67c11514" - version = "v1.25.1" - -[[projects]] - digest = "1:b75b3deb2bce8bc079e16bb2aecfe01eb80098f5650f9e93e5643ca8b7b73737" - name = "gopkg.in/yaml.v2" - packages = ["."] - pruneopts = "UT" - revision = "1f64d6156d11335c3f22d9330b0ad14fc1e789ce" - version = "v2.2.7" - -[[projects]] - digest = "1:131158a88aad1f94854d0aa21a64af2802d0a470fb0f01cb33c04fafd2047111" - name = "honnef.co/go/tools" - packages = [ - "arg", - "cmd/staticcheck", - "config", - "deprecated", - "facts", - "functions", - "go/types/typeutil", - "internal/cache", - "internal/passes/buildssa", - "internal/renameio", - "internal/sharedcheck", - "lint", - "lint/lintdsl", - "lint/lintutil", - "lint/lintutil/format", - "loader", - "printf", - "simple", - "ssa", - "ssautil", - "staticcheck", - "staticcheck/vrp", - "stylecheck", - "unused", - "version", - ] - pruneopts = "UT" - revision = "afd67930eec2a9ed3e9b19f684d17a062285f16a" - version = "2019.2.3" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - input-imports = [ - "cloud.google.com/go/storage", - "github.com/Azure/azure-pipeline-go/pipeline", - "github.com/Azure/azure-storage-blob-go/azblob", - "github.com/BurntSushi/toml", - "github.com/apsdehal/go-logger", - "github.com/aws/aws-sdk-go/aws", - "github.com/aws/aws-sdk-go/aws/session", - "github.com/aws/aws-sdk-go/service/s3", - "github.com/aws/aws-sdk-go/service/s3/s3manager", - "github.com/aws/aws-sdk-go/service/ssm", - "github.com/hashicorp/go-version", - "github.com/imdario/mergo", - "github.com/joho/godotenv", - "github.com/logrusorgru/aurora", - "golang.org/x/net/context", - "gopkg.in/yaml.v2", - ] - solver-name = "gps-cdcl" - solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml deleted file mode 100644 index c11b6e78..00000000 --- a/Gopkg.toml +++ /dev/null @@ -1,67 +0,0 @@ -# Gopkg.toml example -# -# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html -# for detailed Gopkg.toml documentation. -# -# required = ["github.com/user/thing/cmd/thing"] -# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] -# -# [[constraint]] -# name = "github.com/user/project" -# version = "1.0.0" -# -# [[constraint]] -# name = "github.com/user/project2" -# branch = "dev" -# source = "github.com/myfork/project2" -# -# [[override]] -# name = "github.com/x/y" -# version = "2.4.0" -# -# [prune] -# non-go = false -# go-tests = true -# unused-packages = true - -ignored = ["github.com/Praqma/helmsman"] - -[[constraint]] - version = "0.3.0" - name = "github.com/Azure/azure-storage-blob-go" - -[[constraint]] - name = "cloud.google.com/go" - version = "0.28.0" - -[[constraint]] - name = "github.com/BurntSushi/toml" - version = "0.3.1" - -[[constraint]] - name = "github.com/aws/aws-sdk-go" - version = "1.15.43" - -[[constraint]] - name = "github.com/imdario/mergo" - version = "0.3.6" - -[[constraint]] - name = "github.com/joho/godotenv" - version = "1.3.0" - -[[constraint]] - branch = "master" - name = "github.com/logrusorgru/aurora" - -[[constraint]] - branch = "master" - name = "golang.org/x/net" - -[[constraint]] - name = "gopkg.in/yaml.v2" - version = "2.2.2" - -[prune] - go-tests = true - unused-packages = true diff --git a/Makefile b/Makefile index 73cdabd6..581839af 100644 --- a/Makefile +++ b/Makefile @@ -15,25 +15,6 @@ ifneq ($(strip $(CIRCLE_WORKING_DIRECTORY)),) $(info "Using CIRCLE_WORKING_DIRECTORY for GOPATH") endif -ifneq "$(or $(findstring :,$(GOPATH)),$(findstring ;,$(GOPATH)))" "" - GOPATH := $(lastword $(subst :, ,$(GOPATH))) - $(info GOPATHs with multiple entries are not supported, defaulting to the last path in GOPATH) -endif - -GOPATH := $(realpath $(GOPATH)) -ifeq ($(strip $(GOPATH)),) - $(error GOPATH is not set and could not be automatically determined) -endif - -SRCDIR := $(GOPATH)/src/ -PRJDIR := $(CURDIR) - -ifeq ($(filter $(GOPATH)%,$(CURDIR)),) - GOPATH := $(shell mktemp -d "/tmp/dep.XXXXXXXX") - SRCDIR := $(GOPATH)/src/ - PRJDIR := $(SRCDIR)$(PRJNAME) -endif - ifneq ($(OS),Windows_NT) # Before we start test that we have the mandatory executables available EXECUTABLES = go @@ -42,6 +23,8 @@ ifneq ($(OS),Windows_NT) endif export CGO_ENABLED=0 +export GO111MODULE=on +export GOFLAGS=-mod=vendor help: @echo "Available options:" @@ -58,60 +41,48 @@ fmt: ## Reformat package sources @go fmt ./... .PHONY: fmt -dependencies: ## Ensure all the necessary dependencies - @go get -t -d -v ./... -.PHONY: dependencies +vet: fmt + @go vet ./... +.PHONY: vet -$(SRCDIR): - @mkdir -p $(SRCDIR) - @ln -s $(CURDIR) $(SRCDIR) +deps: ## Install depdendencies. Runs `go get` internally. + @GOFLAGS="" go get -t -d -v ./... + @GOFLAGS="" go mod tidy + @GOFLAGS="" go mod vendor -dep: $(SRCDIR) ## Ensure vendors with dep - @cd $(PRJDIR) && \ - dep ensure -v -.PHONY: dep -dep-update: $(SRCDIR) ## Ensure vendors with dep - @cd $(PRJDIR) && \ - dep ensure -v --update -.PHONY: dep-update +update-deps: ## Update depdendencies. Runs `go get -u` internally. + @GOFLAGS="" go get -u + @GOFLAGS="" go mod tidy + @GOFLAGS="" go mod vendor -build: dep ## Build the package - @cd $(PRJDIR) && \ - go build -o helmsman -ldflags '-X main.version="${TAG}-${DATE}" -extldflags "-static"' cmd/helmsman/main.go +build: vet deps ## Build the package + @go build -o helmsman -ldflags '-X main.version="${TAG}-${DATE}" -extldflags "-static"' cmd/helmsman/main.go generate: @go generate #${PKGS} .PHONY: generate -check: $(SRCDIR) fmt - @cd $(PRJDIR) && \ - dep check && \ - go vet ./... -.PHONY: check - repo: - @cd $(PRJDIR) && \ - helm repo add stable https://kubernetes-charts.storage.googleapis.com + @helm repo add stable https://kubernetes-charts.storage.googleapis.com .PHONY: repo -test: dep check repo ## Run unit tests +test: deps vet repo ## Run unit tests @go test -v -cover -p=1 ./... -args -f ../../examples/example.toml .PHONY: test -cross: dep ## Create binaries for all OSs - @cd $(PRJDIR) && \ - gox -os '!freebsd !netbsd' -arch '!arm' -output "dist/{{.Dir}}_{{.OS}}_{{.Arch}}" -ldflags '-X main.Version=${TAG}-${DATE}' ./... +cross: deps ## Create binaries for all OSs + @gox -os '!freebsd !netbsd' -arch '!arm' -output "dist/{{.Dir}}_{{.OS}}_{{.Arch}}" -ldflags '-X main.Version=${TAG}-${DATE}' ./... .PHONY: cross release: ## Generate a new release - @cd $(PRJDIR) && \ - goreleaser --release-notes release-notes.md --rm-dist + @goreleaser --release-notes release-notes.md --rm-dist tools: ## Get extra tools used by this makefile @go get -u github.com/golang/dep/cmd/dep @go get -u github.com/mitchellh/gox @go get -u github.com/goreleaser/goreleaser + @gem install hiera-eyaml .PHONY: tools helmPlugins: ## Install helm plugins used by Helmsman @@ -120,5 +91,4 @@ helmPlugins: ## Install helm plugins used by Helmsman @helm plugin install https://github.com/nouney/helm-gcs @helm plugin install https://github.com/databus23/helm-diff @helm plugin install https://github.com/futuresimple/helm-secrets - @helm plugin install https://github.com/rimusz/helm-tiller .PHONY: helmPlugins diff --git a/README.md b/README.md index 4b7f132a..d9fe7114 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![GitHub version](https://d25lcipzij17d.cloudfront.net/badge.svg?id=gh&type=6&v=v3.0.0-beta1&x2=0)](https://github.com/Praqma/helmsman/releases) [![CircleCI](https://circleci.com/gh/Praqma/helmsman/tree/master.svg?style=svg)](https://circleci.com/gh/Praqma/helmsman/tree/master) +[![GitHub version](https://d25lcipzij17d.cloudfront.net/badge.svg?id=gh&type=6&v=v3.0.0-beta2&x2=0)](https://github.com/Praqma/helmsman/releases) [![CircleCI](https://circleci.com/gh/Praqma/helmsman/tree/master.svg?style=svg)](https://circleci.com/gh/Praqma/helmsman/tree/master) ![helmsman-logo](docs/images/helmsman.png) @@ -61,9 +61,9 @@ If you use private helm repos, you will need either `helm-gcs` or `helm-s3` plug Check the [releases page](https://github.com/Praqma/Helmsman/releases) for the different versions. ``` # on Linux -curl -L https://github.com/Praqma/helmsman/releases/download/v3.0.0-beta1/helmsman_3.0.0-beta1_linux_amd64.tar.gz | tar zx +curl -L https://github.com/Praqma/helmsman/releases/download/v3.0.0-beta2/helmsman_3.0.0-beta1_linux_amd64.tar.gz | tar zx # on MacOS -curl -L https://github.com/Praqma/helmsman/releases/download/v3.0.0-beta1/helmsman_3.0.0-beta1_darwin_amd64.tar.gz | tar zx +curl -L https://github.com/Praqma/helmsman/releases/download/v3.0.0-beta2/helmsman_3.0.0-beta1_darwin_amd64.tar.gz | tar zx mv helmsman /usr/local/bin/helmsman ``` @@ -91,7 +91,7 @@ Helmsman can be used in three different settings: - [As a binary with a hosted cluster](https://github.com/Praqma/helmsman/blob/master/docs/how_to/settings). - [As a docker image in a CI system or local machine](https://github.com/Praqma/helmsman/blob/master/docs/how_to/deployments/ci.md) Always use a tagged docker image from [dockerhub](https://hub.docker.com/r/praqma/helmsman/) as the `latest` image can (at times) be unstable. -- [As a docker image inside a k8s cluster](https://github.com/Praqma/helmsman/blob/master/docs/how_to/deployments/inside_k8s.md) +- [As a docker image inside a k8s cluster](https://github.com/Praqma/helmsman/blob/master/docs/how_to/deployments/inside_k8s.md) # Contributing diff --git a/docs/best_practice.md b/docs/best_practice.md index c72e4f9e..ec49e90f 100644 --- a/docs/best_practice.md +++ b/docs/best_practice.md @@ -1,5 +1,5 @@ --- -version: v3.0.0-beta1 +version: v3.0.0-beta2 --- # Best Practice @@ -10,7 +10,7 @@ When using Helmsman, we recommend the following best practices: - 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. -- 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. +- 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. > Rollback can be more complex regarding application data. - Do not store secrets in your DSFs! Use one of [the supported ways to pass secrets to your releases](how_to/apps/secrets.md). @@ -23,5 +23,5 @@ When using Helmsman, we recommend the following best practices: - Don't maintain the same release in multiple DSFs. -- 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. +- 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. diff --git a/docs/cmd_reference.md b/docs/cmd_reference.md index 08edc91b..87854d48 100644 --- a/docs/cmd_reference.md +++ b/docs/cmd_reference.md @@ -1,5 +1,5 @@ --- -version: v3.0.0-beta1 +version: v3.0.0-beta2 --- # CMD reference @@ -30,7 +30,7 @@ This lists available CMD options in Helmsman: desired state file name(s), may be supplied more than once to merge state files. `--force-upgrades` - use --force when upgrading helm releases. May cause resources to be recreated. + use --force when upgrading helm releases. May cause resources to be recreated. `--keep-untracked-releases` keep releases that are managed by Helmsman from the used DSFs in the command, and are no longer tracked in your desired state. @@ -46,12 +46,12 @@ This lists available CMD options in Helmsman: `--no-default-repos` don't set default Helm repos from Google for 'stable' and 'incubator'. - + `--no-env-subst` turn off environment substitution globally. `--no-env-values-subst` - turn off environment substitution in values files only. (default true). + turn off environment substitution in values files only. (default true). `--no-fancy` don't display the banner and don't use colors. @@ -61,9 +61,9 @@ This lists available CMD options in Helmsman: `-no-ssm-subst` turn off SSM parameter substitution globally. - + `-no-ssm-values-subst` - turn off SSM parameter substitution in values files only (default true). + turn off SSM parameter substitution in values files only (default true). `--ns-override string` override defined namespaces with this one. @@ -79,12 +79,12 @@ This lists available CMD options in Helmsman: `--target` limit execution to specific app. - + `--group` limit execution to specific group of apps. `--update-deps` - run 'helm dep up' for local chart + run 'helm dep up' for local chart `--v` show the version. diff --git a/docs/deployment_strategies.md b/docs/deployment_strategies.md index e5bbbfda..42d1c148 100644 --- a/docs/deployment_strategies.md +++ b/docs/deployment_strategies.md @@ -1,5 +1,5 @@ --- -version: v3.0.0-beta1 +version: v3.0.0-beta2 --- # Deployment Strategies @@ -135,12 +135,12 @@ If you need supporting applications (charts) for your application (e.g, reverse ## Notes on using multiple Helmsman desired state files for the same cluster -Helmsman v3.0.0-beta1 introduces the `context` stanza. -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. +Helmsman v3.0.0-beta2 introduces the `context` stanza. +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. When having multiple DSFs be aware of the following: -- 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). +- 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). - 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. @@ -150,6 +150,6 @@ When having multiple DSFs be aware of the following: - 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. -- 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. +- 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. Also please refer to the [best practice](best_practice.md) document. diff --git a/docs/desired_state_specification.md b/docs/desired_state_specification.md index 4d32fd10..dba344d3 100644 --- a/docs/desired_state_specification.md +++ b/docs/desired_state_specification.md @@ -1,5 +1,5 @@ --- -version: v3.0.0-beta1 +version: v3.0.0-beta2 --- # Helmsman desired state specification @@ -84,7 +84,7 @@ Synopsis: defines the context in which a DSF is used. This context is used as th ```yaml context: prod-apps ... -``` +``` ## Settings @@ -104,7 +104,7 @@ The following options can be skipped if your kubectl context is already created - **clusterURI** : the URI for your cluster API or the name of an environment variable (starting with `$`) containing the URI. - **bearerToken**: whether you want helmsman to connect to the cluster using a bearer token. Default is `false` - **bearerTokenPath**: optional. If bearer token is used, you can specify a custom location for the token file. -- **storageBackend** : by default Helm v3 stores release information in secrets, using secrets for storage is recommended for security. +- **storageBackend** : by default Helm v3 stores release information in secrets, using secrets for storage is recommended for security. - **slackWebhook** : a [Slack](http://slack.com) Webhook URL to receive Helmsman notifications. This can be passed directly or in an environment variable. - **reverseDelete** : if set to `true` it will reverse the priority order whilst deleting. - **eyamlEnabled** : if set to `true' it will use [hiera-eyaml](https://github.com/voxpupuli/hiera-eyaml) to decrypt secret files instead of using default helm-secrets based on sops @@ -233,7 +233,7 @@ Authenticating to private cloud helm repos: - set `GOOGLE_APPLICATION_CREDENTIALS` environment variable to contain the absolute path to your Google cloud credentials.json file. - Or, set `GCLOUD_CREDENTIALS` environment variable to contain the content of the credentials.json file. -> You can also provide basic auth to access private repos that support basic auth. See the example below. +> You can also provide basic auth to access private repos that support basic auth. See the example below. Options: - you can define any key/value pair where the key is the repo name and value is a valid URI for the repo. Basic auth info can be added in the repo URL as in the example below. diff --git a/docs/how_to/README.md b/docs/how_to/README.md index 2980fb13..f6409c02 100644 --- a/docs/how_to/README.md +++ b/docs/how_to/README.md @@ -1,7 +1,7 @@ # How To Guides -This page contains a list of guides on how to use Helmsman. +This page contains a list of guides on how to use Helmsman. 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). diff --git a/docs/how_to/apps/basic.md b/docs/how_to/apps/basic.md index 91187c72..677b6782 100644 --- a/docs/how_to/apps/basic.md +++ b/docs/how_to/apps/basic.md @@ -1,5 +1,5 @@ --- -version: v3.0.0-beta1 +version: v3.0.0-beta2 --- # Install releases diff --git a/docs/how_to/apps/destroy.md b/docs/how_to/apps/destroy.md index ef03a818..f6d2d94e 100644 --- a/docs/how_to/apps/destroy.md +++ b/docs/how_to/apps/destroy.md @@ -1,5 +1,5 @@ --- -version: v3.0.0-beta1 +version: v3.0.0-beta2 --- # Delete all deployed releases diff --git a/docs/how_to/apps/helm_tests.md b/docs/how_to/apps/helm_tests.md index bd9f3429..a15a77fa 100644 --- a/docs/how_to/apps/helm_tests.md +++ b/docs/how_to/apps/helm_tests.md @@ -1,10 +1,10 @@ --- -version: v3.0.0-beta1 +version: v3.0.0-beta2 --- # Test charts -Helm allows running [chart tests](https://github.com/helm/helm/blob/master/docs/chart_tests.md). +Helm allows running [chart tests](https://github.com/helm/helm/blob/master/docs/chart_tests.md). 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: diff --git a/docs/how_to/apps/moving_across_namespaces.md b/docs/how_to/apps/moving_across_namespaces.md index 47d975fd..2cae21e5 100644 --- a/docs/how_to/apps/moving_across_namespaces.md +++ b/docs/how_to/apps/moving_across_namespaces.md @@ -1,5 +1,5 @@ --- -version: v3.0.0-beta1 +version: v3.0.0-beta2 --- # Move charts across namespaces diff --git a/docs/how_to/apps/order.md b/docs/how_to/apps/order.md index a22473a5..c705bc9b 100644 --- a/docs/how_to/apps/order.md +++ b/docs/how_to/apps/order.md @@ -1,5 +1,5 @@ --- -version: v3.0.0-beta1 +version: v3.0.0-beta2 --- # Using the priority key for Apps diff --git a/docs/how_to/apps/override_namespaces.md b/docs/how_to/apps/override_namespaces.md index 3fbf3d74..8ee6a81e 100644 --- a/docs/how_to/apps/override_namespaces.md +++ b/docs/how_to/apps/override_namespaces.md @@ -1,5 +1,5 @@ --- -version: v3.0.0-beta1 +version: v3.0.0-beta2 --- # Override defined namespaces from command line diff --git a/docs/how_to/apps/secrets.md b/docs/how_to/apps/secrets.md index b46de4b9..d410f577 100644 --- a/docs/how_to/apps/secrets.md +++ b/docs/how_to/apps/secrets.md @@ -1,5 +1,5 @@ --- -version: v3.0.0-beta1 +version: v3.0.0-beta2 --- # Passing secrets from env variables: diff --git a/docs/how_to/deployments/ci.md b/docs/how_to/deployments/ci.md index ec7e4870..249f0389 100644 --- a/docs/how_to/deployments/ci.md +++ b/docs/how_to/deployments/ci.md @@ -1,5 +1,5 @@ --- -version: v3.0.0-beta1 +version: v3.0.0-beta2 --- # Run Helmsman in CI @@ -13,7 +13,7 @@ jobs: deploy-apps: docker: - - image: praqma/helmsman:v3.0.0-beta1 + - image: praqma/helmsman:v3.0.0-beta2 steps: - checkout - run: diff --git a/docs/how_to/deployments/inside_k8s.md b/docs/how_to/deployments/inside_k8s.md index d72f47d6..e46cc48c 100644 --- a/docs/how_to/deployments/inside_k8s.md +++ b/docs/how_to/deployments/inside_k8s.md @@ -9,7 +9,7 @@ Helmsman can be deployed inside your k8s cluster and can talk to the k8s API usi See [connecting to your cluster with bearer token](../settings/creating_kube_context_with_token.md) for more details. -Your desired state will look like: +Your desired state will look like: ```toml [settings] @@ -22,7 +22,7 @@ Your desired state will look like: settings: kubeContext: "test" # the name of the context to be created bearerToken: true - clusterURI: "https://kubernetes.default" + clusterURI: "https://kubernetes.default" ``` To deploy Helmsman into a k8s cluster, few steps are needed: @@ -32,10 +32,10 @@ To deploy Helmsman into a k8s cluster, few steps are needed: 1. Create a k8s service account ```shell -$ kubectl create sa helmsman +$ kubectl create sa helmsman ``` -2. Create a clusterrolebinding +2. Create a clusterrolebinding ```shell $ kubectl create clusterrolebinding helmsman-cluster-admin --clusterrole=cluster-admin --serviceaccount=default:helmsman diff --git a/docs/how_to/helm_repos/basic_auth.md b/docs/how_to/helm_repos/basic_auth.md index 284729e3..0ea2b544 100644 --- a/docs/how_to/helm_repos/basic_auth.md +++ b/docs/how_to/helm_repos/basic_auth.md @@ -4,11 +4,11 @@ version: v1.8.0 # Using private helm repos with basic auth -Helmsman allows you to use any private helm repo hosting which supports basic auth (e.g. Artifactory). +Helmsman allows you to use any private helm repo hosting which supports basic auth (e.g. Artifactory). For such repos, you need to add the basic auth information in the repo URL as in the example below: -> Be aware that some special characters in the username or password can make the URL invalid. +> Be aware that some special characters in the username or password can make the URL invalid. ```toml diff --git a/docs/how_to/helm_repos/default.md b/docs/how_to/helm_repos/default.md index b6af780e..04cc4571 100644 --- a/docs/how_to/helm_repos/default.md +++ b/docs/how_to/helm_repos/default.md @@ -1,10 +1,10 @@ --- -version: v3.0.0-beta1 +version: v3.0.0-beta2 --- # Default helm repos -Helm v3 no longer adds the `stable` and `incubator` repos by default. However, Helmsman v3.0.0-beta1 still adds these two repos by default. These two DO NOT need to be defined explicitly in your desired state file (DSF). However, if you would like to configure some repo with the name stable for example, you can override the default repo. +Helm v3 no longer adds the `stable` and `incubator` repos by default. However, Helmsman v3.0.0-beta2 still adds these two repos by default. These two DO NOT need to be defined explicitly in your desired state file (DSF). However, if you would like to configure some repo with the name stable for example, you can override the default repo. > You can disable the automatic addition of these two repos, use the `--no-default-repos` flag. @@ -12,7 +12,7 @@ This example would have `stable` and `incubator` added by default and another `c ```toml - + [helmRepos] custom = "https://mycustomrepo.org" diff --git a/docs/how_to/helm_repos/gcs.md b/docs/how_to/helm_repos/gcs.md index 0f6e6c88..f4a47826 100644 --- a/docs/how_to/helm_repos/gcs.md +++ b/docs/how_to/helm_repos/gcs.md @@ -15,7 +15,7 @@ Helmsman uses the [helm GCS](https://github.com/nouney/helm-gcs) plugin to work ```toml - + [helmRepos] gcsRepo = "gs://myrepobucket/charts" diff --git a/docs/how_to/misc/auth_to_storage_providers.md b/docs/how_to/misc/auth_to_storage_providers.md index 171c7d00..3d980771 100644 --- a/docs/how_to/misc/auth_to_storage_providers.md +++ b/docs/how_to/misc/auth_to_storage_providers.md @@ -26,4 +26,4 @@ You need to provide ONE of the following env variables: You need to provide ALL of the following env variables: - `AZURE_STORAGE_ACCOUNT` -- `AZURE_STORAGE_ACCESS_KEY` \ No newline at end of file +- `AZURE_STORAGE_ACCESS_KEY` \ No newline at end of file diff --git a/docs/how_to/misc/limit-deployment-to-specific-apps.md b/docs/how_to/misc/limit-deployment-to-specific-apps.md index 23da9e60..79939d48 100644 --- a/docs/how_to/misc/limit-deployment-to-specific-apps.md +++ b/docs/how_to/misc/limit-deployment-to-specific-apps.md @@ -5,7 +5,7 @@ version: v1.9.0 # Limit execution to explicitly defined apps Starting from v1.9.0, Helmsman allows you to pass the `--target` flag multiple times to specify multiple apps -that limits apps considered by Helmsman during this specific execution. +that limits apps considered by Helmsman during this specific execution. Thanks to this one can deploy specific applications among all defined for an environment. ## Example diff --git a/docs/how_to/misc/limit-deployment-to-specific-group-of-apps.md b/docs/how_to/misc/limit-deployment-to-specific-group-of-apps.md index 880265d5..62649542 100644 --- a/docs/how_to/misc/limit-deployment-to-specific-group-of-apps.md +++ b/docs/how_to/misc/limit-deployment-to-specific-group-of-apps.md @@ -5,7 +5,7 @@ version: v1.13.0 # Limit execution to explicitly defined group of apps Starting from v1.13.0, Helmsman allows you to pass the `--group` flag to specify group of apps -the execution of Helmsman deployment will be limited to. +the execution of Helmsman deployment will be limited to. Thanks to this one can deploy specific applications among all defined for an environment. ## Example @@ -40,7 +40,7 @@ With `--group` flag in command like $ helmsman -f example.yaml --group critical ... ``` -one can execute Helmsman's environment defined with example.yaml limited to only one `jenkins` app, since its group is `critical`. +one can execute Helmsman's environment defined with example.yaml limited to only one `jenkins` app, since its group is `critical`. Others are ignored until the flag is defined. Multiple applications can be set with `--group`, like diff --git a/docs/how_to/misc/merge_desired_state_files.md b/docs/how_to/misc/merge_desired_state_files.md index c096fdac..971b1b99 100644 --- a/docs/how_to/misc/merge_desired_state_files.md +++ b/docs/how_to/misc/merge_desired_state_files.md @@ -1,5 +1,5 @@ --- -version: v3.0.0-beta1 +version: v3.0.0-beta2 --- # Supply multiple desired state files @@ -45,11 +45,11 @@ $ helmsman -f common.toml -f nonprod.toml ... ## Distinguishing releases deployed from different Desired State Files -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). +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). -Starting from Helmsman v3.0.0-beta1, `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. +Starting from Helmsman v3.0.0-beta2, `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. -Here is how it is used: +Here is how it is used: * `infra.yaml`: ```yaml @@ -58,7 +58,7 @@ settings: kubeContext: "cluster" storageBackend: "secret" -namespaces: +namespaces: infra: protected: true @@ -82,7 +82,7 @@ settings: kubeContext: "cluster" storageBackend: "secret" -namespaces: +namespaces: prod: protected: true diff --git a/docs/how_to/namespaces/labels_and_annotations.md b/docs/how_to/namespaces/labels_and_annotations.md index 3ffc092c..1656bf43 100644 --- a/docs/how_to/namespaces/labels_and_annotations.md +++ b/docs/how_to/namespaces/labels_and_annotations.md @@ -16,7 +16,7 @@ You can define namespaces to be used in your cluster. If they don't exist, Helms [namespaces.production] [namespaces.production.annotations] "iam.amazonaws.com/role" = "dynamodb-reader" - + #... ``` @@ -30,7 +30,7 @@ namespaces: production: annotations: iam.amazonaws.com/role: "dynamodb-reader" - + ``` 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`. diff --git a/docs/how_to/settings/creating_kube_context_with_certs.md b/docs/how_to/settings/creating_kube_context_with_certs.md index 14fdcaf4..b7104daa 100644 --- a/docs/how_to/settings/creating_kube_context_with_certs.md +++ b/docs/how_to/settings/creating_kube_context_with_certs.md @@ -22,7 +22,7 @@ Creating the context with certs, requires both the `settings` and `certificates` caClient = "gs://mybucket/client.crt" # GCS bucket path caCrt = "s3://mybucket/ca.crt" # S3 bucket path # caCrt = "az://myblobcontainer/ca.crt" # Azure blob object - caKey = "../ca.key" # valid local file relative path to the DSF file + caKey = "../ca.key" # valid local file relative path to the DSF file ``` ```yaml @@ -33,10 +33,10 @@ settings: clusterURI: "${CLUSTER_URI}" # the name of an environment variable containing the cluster API endpoint #clusterURI: "https://192.168.99.100:8443" # equivalent to the above -certificates: +certificates: caClient: "gs://mybucket/client.crt" # GCS bucket path caCrt: "s3://mybucket/ca.crt" # S3 bucket path #caCrt: "az://myblobcontainer/ca.crt" # Azure blob object caKey: "../ca.key" # valid local file relative path to the DSF file - + ``` diff --git a/docs/how_to/settings/creating_kube_context_with_token.md b/docs/how_to/settings/creating_kube_context_with_token.md index 5ea965c3..bcabb4bb 100644 --- a/docs/how_to/settings/creating_kube_context_with_token.md +++ b/docs/how_to/settings/creating_kube_context_with_token.md @@ -6,7 +6,7 @@ version: v1.8.0 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). -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. +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. > Note: Helmsman and therefore helm will only be able to do what the kubernetes service account (from which the token is taken) allows. @@ -24,6 +24,6 @@ By default, Helmsman will look for a token in `/var/run/secrets/kubernetes.io/se settings: kubeContext: "test" # the name of the context to be created bearerToken: true - clusterURI: "https://kubernetes.default" + clusterURI: "https://kubernetes.default" # bearerTokenPath: "/path/to/custom/bearer/token/file" ``` diff --git a/docs/how_to/settings/use-hiera-eyaml-as-secrets-encryption.md b/docs/how_to/settings/use-hiera-eyaml-as-secrets-encryption.md index eb2b0c81..15de8a8d 100644 --- a/docs/how_to/settings/use-hiera-eyaml-as-secrets-encryption.md +++ b/docs/how_to/settings/use-hiera-eyaml-as-secrets-encryption.md @@ -4,7 +4,7 @@ version: v1.13.0 # Using hiera-eyaml as backend for secrets' encryption -Helmsman uses helm-secrets as a default solution for secrets' encryption. +Helmsman uses helm-secrets as a default solution for secrets' encryption. 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. SOPS-based secrets can not be easily merged or rebased in case of conflicts etc. That is why another solution for secrets organised in YAMLs was proposed in [hiera-eyaml](https://github.com/voxpupuli/hiera-eyaml). @@ -21,7 +21,7 @@ settings: Helmsman will use hiera-eyaml gem to decrypt secrets files defined for applications. They public and private keys should be placed in `keys` directory with names of `public_key.pkcs7.pem` and `private_key.pkcs7.pem`. -The keys' path can be overwritten with +The keys' path can be overwritten with ```yaml settings: diff --git a/docs/why_helmsman.md b/docs/why_helmsman.md index 4f8a870f..ae39ac2b 100644 --- a/docs/why_helmsman.md +++ b/docs/why_helmsman.md @@ -9,7 +9,7 @@ This document describes the reasoning and need behind the inception of Helmsman. ## Before Helm Helmsman was created with continuous deployment in mind. -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. +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. ![CI-pipeline-before-helm](images/CI-pipeline-before-helm.jpg) @@ -17,8 +17,8 @@ That script could only create the k8s resources from the manifest files. Soon we ## Helm to the rescue? -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. -By creating Helm charts, we packaged related k8s manifests together into a single entity: "a chart". +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. +By creating Helm charts, we packaged related k8s manifests together into a single entity: "a chart". ![CI-pipeline-after-helm](images/CI-pipeline-after-helm.jpg) @@ -39,7 +39,7 @@ In English, a [Helmsman](https://www.merriam-webster.com/dictionary/helmsman) is > Although knowledge about Helm and K8S is highly beneficial, such knowledge is NOT required to use Helmsman. -As the diagram below shows, we recommend having a Helmsman _desired state file_ for each k8s cluster you are managing. +As the diagram below shows, we recommend having a Helmsman _desired state file_ for each k8s cluster you are managing. ![CI-pipeline-helmsman](images/CI-pipeline-helmsman.jpg) diff --git a/examples/example.toml b/examples/example.toml index 0edc5792..3a94c332 100644 --- a/examples/example.toml +++ b/examples/example.toml @@ -1,6 +1,6 @@ -# version: v3.0.0-beta1 +# version: v3.0.0-beta2 -# context defines the context of this Desired State File. +# context defines the context of this Desired State File. # It is used to allow Helmsman identify which releases are managed by which DSF. # Therefore, it is important that each DSF uses a unique context. context= "test-infra" # defaults to "default" if not provided diff --git a/examples/example.yaml b/examples/example.yaml index 657b65b6..fed8fc7c 100644 --- a/examples/example.yaml +++ b/examples/example.yaml @@ -1,4 +1,4 @@ -# version: v3.0.0-beta1 +# version: v3.0.0-beta2 # metadata -- add as many key/value pairs as you want metadata: org: "example.com/$ORG_PATH/" @@ -13,7 +13,7 @@ metadata: #caCrt: "s3://mybucket/ca.crt" # S3 bucket path #caKey: "../ca.key" # valid local file relative path -# context defines the context of this Desired State File. +# context defines the context of this Desired State File. # It is used to allow Helmsman identify which releases are managed by which DSF. # Therefore, it is important that each DSF uses a unique context. context: test-infra # defaults to "default" if not provided @@ -24,7 +24,7 @@ settings: #password: "$K8S_PASSWORD" # the name of an environment variable containing the k8s password #clusterURI: "$SET_URI" # the name of an environment variable containing the cluster API #clusterURI: "https://192.168.99.100:8443" # equivalent to the above - storageBackend: "secret" + storageBackend: "secret" #slackWebhook: "$slack" # or your slack webhook url #reverseDelete: false # reverse the priorities on delete #### to use bearer token: diff --git a/examples/minimal-example.toml b/examples/minimal-example.toml index 79b3575d..1f55dcc0 100644 --- a/examples/minimal-example.toml +++ b/examples/minimal-example.toml @@ -1,6 +1,6 @@ -## This is a minimal example. +## This is a minimal example. ## It will use your current kube context and will deploy Tiller without RBAC service account. -## For the full config spec and options, check https://github.com/Praqma/helmsman/blob/master/docs/desired_state_specification.md +## For the full config spec and options, check https://github.com/Praqma/helmsman/blob/master/docs/desired_state_specification.md [namespaces] [namespaces.staging] @@ -8,13 +8,13 @@ [apps] [apps.jenkins] - namespace = "staging" - enabled = true - chart = "stable/jenkins" - version = "0.14.3" - + namespace = "staging" + enabled = true + chart = "stable/jenkins" + version = "0.14.3" + [apps.artifactory] - namespace = "staging" - enabled = true - chart = "stable/artifactory" - version = "7.0.6" \ No newline at end of file + namespace = "staging" + enabled = true + chart = "stable/artifactory" + version = "7.0.6" diff --git a/examples/minimal-example.yaml b/examples/minimal-example.yaml index 8d28308f..29208b88 100644 --- a/examples/minimal-example.yaml +++ b/examples/minimal-example.yaml @@ -1,6 +1,6 @@ -## This is a minimal example. +## This is a minimal example. ## It will use your current kube context and will deploy Tiller without RBAC service account. -## For the full config spec and options, check https://github.com/Praqma/helmsman/blob/master/docs/desired_state_specification.md +## For the full config spec and options, check https://github.com/Praqma/helmsman/blob/master/docs/desired_state_specification.md namespaces: staging: @@ -11,9 +11,9 @@ apps: enabled: true chart: stable/jenkins version: 0.14.3 - + artifactory: namespace: staging enabled: true chart: stable/artifactory - version: 7.0.6 \ No newline at end of file + version: 7.0.6 diff --git a/go.mod b/go.mod index c0a51ca6..daedaed8 100644 --- a/go.mod +++ b/go.mod @@ -9,25 +9,21 @@ require ( github.com/BurntSushi/toml v0.3.1 github.com/apsdehal/go-logger v0.0.0-20190515211354-1abdf898e024 github.com/aws/aws-sdk-go v1.26.2 - github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 - github.com/golang/protobuf v1.3.2 + github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 // indirect github.com/hashicorp/go-version v1.2.0 github.com/imdario/mergo v0.3.8 - github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af github.com/joho/godotenv v1.3.0 + github.com/kr/pretty v0.1.0 // indirect github.com/logrusorgru/aurora v0.0.0-20191116043053-66b7ad493a23 - go.opencensus.io v0.22.2 - golang.org/x/exp v0.0.0-20191129062945-2f5052295587 - golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f + github.com/pkg/errors v0.8.1 // indirect + go.opencensus.io v0.22.2 // indirect golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 - golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 - golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 - golang.org/x/text v0.3.2 - golang.org/x/tools v0.0.0-20191213221258-04c2e8eff935 - google.golang.org/api v0.14.0 - google.golang.org/appengine v1.6.5 - google.golang.org/genproto v0.0.0-20191206224255-0243a4be9c8f - google.golang.org/grpc v1.25.1 + golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 // indirect + golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect + google.golang.org/api v0.14.0 // indirect + google.golang.org/appengine v1.6.5 // indirect + google.golang.org/genproto v0.0.0-20191206224255-0243a4be9c8f // indirect + google.golang.org/grpc v1.25.1 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v2 v2.2.7 - honnef.co/go/tools v0.0.1-2019.2.3 ) diff --git a/go.sum b/go.sum index e90cb6ba..afb32cee 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.28.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/azure-pipeline-go v0.1.8/go.mod h1:XA1kFWRVhSK+KNFiOhfv83Fv8L9achrP7OxIzeTn1Yg= github.com/Azure/azure-pipeline-go v0.1.9 h1:u7JFb9fFTE6Y/j8ae2VK33ePrRqJqoCM/IWkQdAZ+rg= github.com/Azure/azure-pipeline-go v0.1.9/go.mod h1:XA1kFWRVhSK+KNFiOhfv83Fv8L9achrP7OxIzeTn1Yg= @@ -11,17 +9,17 @@ github.com/Azure/azure-storage-blob-go v0.0.0-20181022225951-5152f14ace1c h1:Y5u github.com/Azure/azure-storage-blob-go v0.0.0-20181022225951-5152f14ace1c/go.mod h1:oGfmITT1V6x//CswqY2gtAHND+xIP64/qL7a5QJix0Y= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/apsdehal/go-logger v0.0.0-20190515211354-1abdf898e024 h1:dfZ6RF0UxHqt7xPz0r7h00apsaa6rIrFhT6Xly55Exk= github.com/apsdehal/go-logger v0.0.0-20190515211354-1abdf898e024/go.mod h1:U3/8D6R9+bVpX0ORZjV+3mU9pQ86m7h1lESgJbXNvXA= github.com/aws/aws-sdk-go v1.26.2 h1:MzYLmCeny4bMQcAbYcucIduVZKp0sEf1eRLvHpKI5Is= github.com/aws/aws-sdk-go v1.26.2/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 h1:uHTyIjqVhYRhLbJ8nIiOJHkEZZ+5YoOsAbD3sk82NiE= @@ -34,10 +32,11 @@ github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -52,44 +51,36 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/logrusorgru/aurora v0.0.0-20191116043053-66b7ad493a23 h1:Wp7NjqGKGN9te9N/rvXYRhlVcrulGdxnz8zadXWs7fc= github.com/logrusorgru/aurora v0.0.0-20191116043053-66b7ad493a23/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -107,8 +98,6 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ= @@ -125,18 +114,13 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191213221258-04c2e8eff935/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.14.0 h1:uMf5uLi4eQMRrMKhCplNik4U4H8Z6C1br3zOtAa/aDE= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -153,8 +137,8 @@ google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -162,4 +146,3 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/internal/app/cli.go b/internal/app/cli.go index 78b82b9c..6fcebae8 100644 --- a/internal/app/cli.go +++ b/internal/app/cli.go @@ -23,7 +23,7 @@ const ( ) func printUsage() { - fmt.Printf(banner) + fmt.Print(banner) fmt.Printf("Helmsman version: " + appVersion + "\n") fmt.Printf("Helmsman is a Helm Charts as Code tool which allows you to automate the deployment/management of your Helm charts.") fmt.Printf("") @@ -239,3 +239,11 @@ func Cli() { } } } + +// getDryRunFlags returns dry-run flag +func getDryRunFlags() []string { + if dryRun { + return []string{"--dry-run", "--debug"} + } + return []string{} +} diff --git a/internal/app/command.go b/internal/app/command.go index e15cbe16..61d5acf2 100644 --- a/internal/app/command.go +++ b/internal/app/command.go @@ -64,9 +64,5 @@ func toolExists(tool string) bool { exitCode, _, _ := cmd.exec(debug, false) - if exitCode != 0 { - return false - } - - return true + return exitCode == 0 } diff --git a/internal/app/decision_maker.go b/internal/app/decision_maker.go index 9261e8c2..9127bd3e 100644 --- a/internal/app/decision_maker.go +++ b/internal/app/decision_maker.go @@ -3,25 +3,43 @@ package app import ( "fmt" "regexp" - "strconv" "strings" "sync" ) var outcome plan -var settings config + +type currentState map[string]helmRelease + +// logDecision adds the decisions made to the plan. +// Depending on the debug flag being set or not, it will either log the the decision to output or not. +func logDecision(decision string, priority int, decisionType decisionType) { + outcome.addDecision(decision, priority, decisionType) +} + +// buildState builds the currentState map containing information about all releases existing in a k8s cluster +func buildState() currentState { + log.Info("Acquiring current Helm state from cluster...") + + cs := make(map[string]helmRelease) + rel := getHelmReleases() + + for _, r := range rel { + r.HelmsmanContext = getReleaseContext(r.Name, r.Namespace) + cs[fmt.Sprintf("%s-%s", r.Name, r.Namespace)] = r + } + return cs +} // makePlan creates a plan of the actions needed to make the desired state come true. -func makePlan(s *state) *plan { - settings = s.Settings +func (cs *currentState) makePlan(s *state) *plan { outcome = createPlan() - buildState() wg := sync.WaitGroup{} for _, r := range s.Apps { - checkChartDepUpdate(r) + r.checkChartDepUpdate() wg.Add(1) - go decide(r, s, &wg) + go cs.decide(r, s, &wg) } wg.Wait() @@ -30,31 +48,31 @@ func makePlan(s *state) *plan { // decide makes a decision about what commands (actions) need to be executed // to make a release section of the desired state come true. -func decide(r *release, s *state, wg *sync.WaitGroup) { +func (cs *currentState) decide(r *release, s *state, wg *sync.WaitGroup) { defer wg.Done() // check for presence in defined targets or groups - if !r.isReleaseConsideredToRun() { + if !r.isConsideredToRun() { logDecision("Release [ "+r.Name+" ] ignored", r.Priority, ignored) return } if destroy { - if ok := isReleaseExisting(r, ""); ok { - deleteRelease(r) + if ok := cs.releaseExists(r, ""); ok { + r.uninstall() } return } if !r.Enabled { - if ok := isReleaseExisting(r, ""); ok { + if ok := cs.releaseExists(r, ""); ok { - if isProtected(r) { + if cs.isProtected(r) { logDecision("Release [ "+r.Name+" ] in namespace [ "+r.Namespace+" ] is PROTECTED. Operations are not allowed on this release until "+ "protection is removed.", r.Priority, noop) return } - deleteRelease(r) + r.uninstall() return } logDecision("Release [ "+r.Name+" ] disabled", r.Priority, noop) @@ -62,31 +80,31 @@ func decide(r *release, s *state, wg *sync.WaitGroup) { } - if ok := isReleaseExisting(r, "deployed"); ok { - if !isProtected(r) { - inspectUpgradeScenario(r) // upgrade or move + if ok := cs.releaseExists(r, "deployed"); ok { + if !cs.isProtected(r) { + cs.inspectUpgradeScenario(r) // upgrade or move } else { logDecision("Release [ "+r.Name+" ] in namespace [ "+r.Namespace+" ] is PROTECTED. Operations are not allowed on this release until "+ "you remove its protection.", r.Priority, noop) } - } else if ok := isReleaseExisting(r, "deleted"); ok { - if !isProtected(r) { + } else if ok := cs.releaseExists(r, "deleted"); ok { + if !cs.isProtected(r) { - rollbackRelease(r) // rollback + r.rollback(cs) // rollback } else { logDecision("Release [ "+r.Name+" ] in namespace [ "+r.Namespace+" ] is PROTECTED. Operations are not allowed on this release until "+ "you remove its protection.", r.Priority, noop) } - } else if ok := isReleaseExisting(r, "failed"); ok { + } else if ok := cs.releaseExists(r, "failed"); ok { - if !isProtected(r) { + if !cs.isProtected(r) { logDecision("Release [ "+r.Name+" ] in namespace [ "+r.Namespace+" ] is in FAILED state. Upgrade is scheduled!", r.Priority, change) - upgradeRelease(r) + r.upgrade() } else { logDecision("Release [ "+r.Name+" ] in namespace [ "+r.Namespace+" ] is PROTECTED. Operations are not allowed on this release until "+ @@ -94,89 +112,105 @@ func decide(r *release, s *state, wg *sync.WaitGroup) { } } else { // If there is no release in the cluster with this name and in this namespace, then install it! - if _, ok := currentState[fmt.Sprintf("%s-%s", r.Name, r.Namespace)]; !ok { - installRelease(r) + if _, ok := (*cs)[fmt.Sprintf("%s-%s", r.Name, r.Namespace)]; !ok { + r.install() } else { // A release with the same name and in the same namespace exists, but it has a different context label (managed by another DSF) log.Fatal("Release [ " + r.Name + " ] in namespace [ " + r.Namespace + " ] already exists but is not managed by the" + " current context: [ " + s.Context + " ]. Applying changes will likely cause conflicts. Change the release name or namespace.") } } - return - -} - -// testRelease creates a Helm command to test a particular release. -func testRelease(r *release) { - cmd := command{ - Cmd: helmBin, - Args: []string{"test", "--namespace", r.Namespace, r.Name}, - Description: "Running tests for release [ " + r.Name + " ] in namespace [ " + r.Namespace + " ]", - } - outcome.addCommand(cmd, r.Priority, r) - logDecision("Release [ "+r.Name+" ] in namespace [ "+r.Namespace+" ] is required to be tested during installation", r.Priority, noop) } -// installRelease creates a Helm command to install a particular release in a particular namespace using a particular Tiller. -func installRelease(r *release) { - cmd := command{ - Cmd: helmBin, - Args: concat([]string{"install", r.Name, r.Chart, "--namespace", r.Namespace}, getValuesFiles(r), []string{"--version", r.Version}, getSetValues(r), getSetStringValues(r), getWait(r), getHelmFlags(r)), - Description: "Installing release [ " + r.Name + " ] in namespace [ " + r.Namespace + " ]", +// releaseExists checks if a Helm release is/was deployed in a k8s cluster. +// It searches the Current State for releases. +// The key format for releases uniqueness is: +// If status is provided as an input [deployed, deleted, failed], then the search will verify the release status matches the search status. +func (cs *currentState) releaseExists(r *release, status string) bool { + v, ok := (*cs)[fmt.Sprintf("%s-%s", r.Name, r.Namespace)] + if !ok || v.HelmsmanContext != s.Context { + return false } - outcome.addCommand(cmd, r.Priority, r) - logDecision("Release [ "+r.Name+" ] will be installed in [ "+r.Namespace+" ] namespace", r.Priority, create) - if r.Test { - testRelease(r) + if status != "" { + return v.Status == status } + return true } -// rollbackRelease evaluates if a rollback action needs to be taken for a given release. -// if the release is already deleted but from a different namespace than the one specified in input, -// it purge deletes it and create it in the specified namespace. -func rollbackRelease(r *release) { - rs, ok := currentState[fmt.Sprintf("%s-%s", r.Name, r.Namespace)] - if !ok { - return - } - - if r.Namespace == rs.Namespace { +// getHelmsmanReleases returns a map of all releases that are labeled with "MANAGED-BY=HELMSMAN" +// The releases are categorized by the namespaces in which they are deployed +// The returned map format is: map[:map[:true]] +func (cs *currentState) getHelmsmanReleases() map[string]map[helmRelease]bool { + var lines []string + releases := make(map[string]map[helmRelease]bool) + storageBackend := s.Settings.StorageBackend + for ns := range s.Namespaces { cmd := command{ - Cmd: helmBin, - Args: concat([]string{"rollback", r.Name, getReleaseRevision(rs)}, getWait(r), getTimeout(r), getNoHooks(r), getDryRunFlags()), - Description: "Rolling back release [ " + r.Name + " ] in namespace [ " + r.Namespace + " ]", + Cmd: "kubectl", + Args: []string{"get", storageBackend, "-n", ns, "-l", "MANAGED-BY=HELMSMAN", "-l", "HELMSMAN_CONTEXT=" + s.Context, "-o", "name"}, + Description: "Getting Helmsman-managed releases in namespace [ " + ns + " ]", + } + + exitCode, output, _ := cmd.exec(debug, verbose) + + if exitCode != 0 { + log.Fatal(output) + } + if strings.EqualFold("No resources found.", strings.TrimSpace(output)) { + lines = strings.Split(output, "\n") } - outcome.addCommand(cmd, r.Priority, r) - upgradeRelease(r) // this is to reflect any changes in values file(s) - logDecision("Release [ "+r.Name+" ] was deleted and is desired to be rolled back to "+ - "namespace [ "+r.Namespace+" ]", r.Priority, create) - } else { - reInstallRelease(r, rs) - logDecision("Release [ "+r.Name+" ] is deleted BUT from namespace [ "+rs.Namespace+ - " ]. Will purge delete it from there and install it in namespace [ "+r.Namespace+" ]", r.Priority, create) - logDecision("WARNING: rolling back release [ "+r.Name+" ] from [ "+rs.Namespace+" ] to [ "+r.Namespace+ - " ] might not correctly connect to existing volumes. Check https://github.com/Praqma/helmsman/blob/master/docs/how_to/apps/moving_across_namespaces.md"+ - " for details if this release uses PV and PVC.", r.Priority, create) + for _, r := range lines { + if r == "" { + continue + } + if _, ok := releases[ns]; !ok { + releases[ns] = make(map[helmRelease]bool) + } + releaseName := strings.Split(strings.Split(r, "/")[1], ".")[4] + releases[ns][(*cs)[releaseName+"-"+ns]] = true + } } + return releases } -// deleteRelease deletes a release from a particular Tiller in a k8s cluster -func deleteRelease(r *release) { - priority := r.Priority - if settings.ReverseDelete == true { - priority = priority * -1 +// cleanUntrackedReleases checks for any releases that are managed by Helmsman and are no longer tracked by the desired state +// It compares the currently deployed releases labeled with "MANAGED-BY=HELMSMAN" with Apps defined in the desired state +// For all untracked releases found, a decision is made to uninstall them and is added to the Helmsman plan +// NOTE: Untracked releases don't benefit from either namespace or application protection. +// NOTE: Removing/Commenting out an app from the desired state makes it untracked. +func (cs *currentState) cleanUntrackedReleases() { + toDelete := make(map[string]map[helmRelease]bool) + log.Info("Checking if any Helmsman managed releases are no longer tracked by your desired state ...") + for ns, releases := range cs.getHelmsmanReleases() { + for r := range releases { + tracked := false + for _, app := range s.Apps { + if app.Name == r.Name && app.Namespace == r.Namespace { + tracked = true + } + } + if !tracked { + if _, ok := toDelete[ns]; !ok { + toDelete[ns] = make(map[helmRelease]bool) + } + toDelete[ns][r] = true + } + } } - cmd := command{ - Cmd: helmBin, - Args: concat([]string{"uninstall", "--namespace", r.Namespace, r.Name}, getDryRunFlags()), - Description: "Deleting release [ " + r.Name + " ] in namespace [ " + r.Namespace + " ]", + if len(toDelete) == 0 { + log.Info("No untracked releases found") + } else { + for _, releases := range toDelete { + for r := range releases { + logDecision("Untracked release [ "+r.Name+" ] found and it will be deleted", -800, delete) + r.uninstall() + } + } } - outcome.addCommand(cmd, priority, r) - logDecision(fmt.Sprintf("release [ %s ] is desired to be DELETED.", r.Name), r.Priority, delete) } // inspectUpgradeScenario evaluates if a release should be upgraded. @@ -186,43 +220,43 @@ func deleteRelease(r *release) { // it will be purge deleted and installed in the same namespace using the new chart. // - If the release is NOT in the same namespace specified in the input, // it will be purge deleted and installed in the new namespace. -func inspectUpgradeScenario(r *release) { +func (cs *currentState) inspectUpgradeScenario(r *release) { - rs, ok := currentState[fmt.Sprintf("%s-%s", r.Name, r.Namespace)] + rs, ok := (*cs)[fmt.Sprintf("%s-%s", r.Name, r.Namespace)] if !ok { return } if r.Namespace == rs.Namespace { - version, msg := getChartVersion(r) + version, msg := r.getChartVersion() if msg != "" { log.Fatal(msg) return } r.Version = version - if extractChartName(r.Chart) == getReleaseChartName(rs) && r.Version != getReleaseChartVersion(rs) { + if extractChartName(r.Chart) == rs.getChartName() && r.Version != rs.getChartVersion() { // upgrade - diffRelease(r) - upgradeRelease(r) + r.diff() + r.upgrade() logDecision("Release [ "+r.Name+" ] will be updated", r.Priority, change) - } else if extractChartName(r.Chart) != getReleaseChartName(rs) { - reInstallRelease(r, rs) + } else if extractChartName(r.Chart) != rs.getChartName() { + r.reInstall(rs) logDecision("Release [ "+r.Name+" ] is desired to use a new chart [ "+r.Chart+ " ]. Delete of the current release will be planned and new chart will be installed in namespace [ "+ r.Namespace+" ]", r.Priority, change) } else { - if diff := diffRelease(r); diff != "" { - upgradeRelease(r) + if diff := r.diff(); diff != "" { + r.upgrade() logDecision("Release [ "+r.Name+" ] will be updated", r.Priority, change) } else { logDecision("Release [ "+r.Name+" ] installed and up-to-date", r.Priority, noop) } } } else { - reInstallRelease(r, rs) + r.reInstall(rs) logDecision("Release [ "+r.Name+" ] is desired to be enabled in a new namespace [ "+r.Namespace+ " ]. Uninstall of the current release from namespace [ "+rs.Namespace+" ] will be performed "+ "and then installation in namespace [ "+r.Namespace+" ] will take place", r.Priority, change) @@ -232,203 +266,14 @@ func inspectUpgradeScenario(r *release) { } } -// diffRelease diffs an existing release with the specified values.yaml -func diffRelease(r *release) string { - exitCode := 0 - msg := "" - colorFlag := "" - diffContextFlag := []string{} - suppressDiffSecretsFlag := "" - if noColors { - colorFlag = "--no-color" - } - if diffContext != -1 { - diffContextFlag = []string{"--context", strconv.Itoa(diffContext)} - } - if suppressDiffSecrets { - suppressDiffSecretsFlag = "--suppress-secrets" - } - - cmd := command{ - Cmd: helmBin, - Args: concat([]string{"diff", colorFlag}, diffContextFlag, []string{suppressDiffSecretsFlag, "--namespace", r.Namespace, "upgrade", r.Name, r.Chart}, getValuesFiles(r), []string{"--version", r.Version}, getSetValues(r), getSetStringValues(r)), - Description: "Diffing release [ " + r.Name + " ] in namespace [ " + r.Namespace + " ]", - } - - if exitCode, msg, _ = cmd.exec(debug, verbose); exitCode != 0 { - log.Fatal(fmt.Sprintf("Command returned with exit code: %d. And error message: %s ", exitCode, msg)) - } else { - if (verbose || showDiff) && msg != "" { - fmt.Println(msg) - } - } - - return msg -} - -// upgradeRelease upgrades an existing release with the specified values.yaml -func upgradeRelease(r *release) { - var force string - if forceUpgrades { - force = "--force" - } - cmd := command{ - Cmd: helmBin, - Args: concat([]string{"upgrade", "--namespace", r.Namespace, r.Name, r.Chart}, getValuesFiles(r), []string{"--version", r.Version, force}, getSetValues(r), getSetStringValues(r), getWait(r), getHelmFlags(r)), - Description: "Upgrading release [ " + r.Name + " ] in namespace [ " + r.Namespace + " ]", - } - - outcome.addCommand(cmd, r.Priority, r) -} - -// reInstallRelease purge deletes a release and reinstalls it. -// This is used when moving a release to another namespace or when changing the chart used for it. -func reInstallRelease(r *release, rs releaseState) { - - delCmd := command{ - Cmd: helmBin, - Args: concat([]string{"delete", "--purge", r.Name}, getDryRunFlags()), - Description: "Deleting release [ " + r.Name + " ] in namespace [ " + r.Namespace + " ]", - } - outcome.addCommand(delCmd, r.Priority, r) - - installCmd := command{ - Cmd: helmBin, - Args: concat([]string{"install", r.Chart, "--version", r.Version, "-n", r.Name, "--namespace", r.Namespace}, getValuesFiles(r), getSetValues(r), getSetStringValues(r), getWait(r), getHelmFlags(r)), - Description: "Installing release [ " + r.Name + " ] in namespace [ " + r.Namespace + " ]", - } - outcome.addCommand(installCmd, r.Priority, r) -} - -// logDecision adds the decisions made to the plan. -// Depending on the debug flag being set or not, it will either log the the decision to output or not. -func logDecision(decision string, priority int, decisionType decisionType) { - - outcome.addDecision(decision, priority, decisionType) - -} - -// extractChartName extracts the Helm chart name from full chart name in the desired state. -// example: it extracts "chartY" from "repoX/chartY" and "chartZ" from "c:\charts\chartZ" -func extractChartName(releaseChart string) string { - - m := chartNameExtractor.FindStringSubmatch(releaseChart) - if len(m) == 2 { - return m[1] - } - - return "" -} - -var chartNameExtractor = regexp.MustCompile(`[\\/]([^\\/]+)$`) - -// getNoHooks returns the no-hooks flag for install/upgrade commands -func getNoHooks(r *release) []string { - if r.NoHooks { - return []string{"--no-hooks"} - } - return []string{} -} - -// getTimeout returns the timeout flag for install/upgrade commands -func getTimeout(r *release) []string { - if r.Timeout != 0 { - return []string{"--timeout", strconv.Itoa(r.Timeout) + "s"} - } - return []string{} -} - -// getValuesFiles return partial install/upgrade release command to substitute the -f flag in Helm. -func getValuesFiles(r *release) []string { - var fileList []string - - if r.ValuesFile != "" { - fileList = append(fileList, r.ValuesFile) - } else if len(r.ValuesFiles) > 0 { - fileList = append(fileList, r.ValuesFiles...) - } - - if r.SecretsFile != "" { - if !helmPluginExists("secrets") { - log.Fatal("helm secrets plugin is not installed/configured correctly. Aborting!") - } - if err := decryptSecret(r.SecretsFile); err != nil { - log.Fatal(err.Error()) - } - fileList = append(fileList, r.SecretsFile+".dec") - } else if len(r.SecretsFiles) > 0 { - if !helmPluginExists("secrets") { - log.Fatal("helm secrets plugin is not installed/configured correctly. Aborting!") - } - for i := 0; i < len(r.SecretsFiles); i++ { - if err := decryptSecret(r.SecretsFiles[i]); err != nil { - log.Fatal(err.Error()) - } - // if .dec extension is added before to the secret filename, don't add it again. - // This happens at upgrade time (where diff and upgrade both call this function) - if !isOfType(r.SecretsFiles[i], []string{".dec"}) { - r.SecretsFiles[i] = r.SecretsFiles[i] + ".dec" - } - } - fileList = append(fileList, r.SecretsFiles...) - } - - fileListArgs := []string{} - for _, file := range fileList { - fileListArgs = append(fileListArgs, "-f", file) - } - return fileListArgs -} - -// getSetValues returns --set params to be used with helm install/upgrade commands -func getSetValues(r *release) []string { - result := []string{} - for k, v := range r.Set { - result = append(result, "--set", k+"="+strings.Replace(v, ",", "\\,", -1)+"") - } - return result -} - -// getSetStringValues returns --set-string params to be used with helm install/upgrade commands -func getSetStringValues(r *release) []string { - result := []string{} - for k, v := range r.SetString { - result = append(result, "--set-string", k+"="+strings.Replace(v, ",", "\\,", -1)+"") - } - return result -} - -// getWait returns a partial helm command containing the helm wait flag (--wait) if the wait flag for the release was set to true -// Otherwise, retruns an empty string -func getWait(r *release) []string { - result := []string{} - if r.Wait { - result = append(result, "--wait") - } - return result -} - -// getDesiredNamespace returns the namespace of a release -func getDesiredNamespace(r *release) string { - - return r.Namespace -} - -// getCurrentNamespaceProtection returns the protection state for the namespace where a release is currently installed. -// It returns true if a namespace is defined as protected in the desired state file, false otherwise. -func getCurrentNamespaceProtection(rs releaseState) bool { - - return s.Namespaces[rs.Namespace].Protected -} - // isProtected checks if a release is protected or not. // A protected is release is either: a) deployed in a protected namespace b) flagged as protected in the desired state file // Any release in a protected namespace is protected by default regardless of its flag // returns true if a release is protected, false otherwise -func isProtected(r *release) bool { +func (cs *currentState) isProtected(r *release) bool { // if the release does not exist in the cluster, it is not protected - if ok := isReleaseExisting(r, ""); !ok { + if ok := cs.releaseExists(r, ""); !ok { return false } @@ -440,28 +285,16 @@ func isProtected(r *release) bool { } -// getDryRunFlags returns dry-run flag -func getDryRunFlags() []string { - if dryRun { - return []string{"--dry-run", "--debug"} - } - return []string{} -} +var chartNameExtractor = regexp.MustCompile(`[\\/]([^\\/]+)$`) -// getHelmFlags returns helm flags -func getHelmFlags(r *release) []string { - var flags []string +// extractChartName extracts the Helm chart name from full chart name in the desired state. +// example: it extracts "chartY" from "repoX/chartY" and "chartZ" from "c:\charts\chartZ" +func extractChartName(releaseChart string) string { - for _, flag := range r.HelmFlags { - flags = append(flags, flag) + m := chartNameExtractor.FindStringSubmatch(releaseChart) + if len(m) == 2 { + return m[1] } - return concat(getNoHooks(r), getTimeout(r), getDryRunFlags(), flags) -} -func checkChartDepUpdate(r *release) { - if updateDeps && isLocalChart(r.Chart) { - if ok, err := updateChartDep(r.Chart); !ok { - log.Fatal("helm dependency update failed: " + err) - } - } + return "" } diff --git a/internal/app/decision_maker_test.go b/internal/app/decision_maker_test.go index 17dd3ef8..efdc298b 100644 --- a/internal/app/decision_maker_test.go +++ b/internal/app/decision_maker_test.go @@ -69,7 +69,7 @@ func Test_getValuesFiles(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := getValuesFiles(tt.args.r); !reflect.DeepEqual(got, tt.want) { + if got := tt.args.r.getValuesFiles(); !reflect.DeepEqual(got, tt.want) { t.Errorf("getValuesFiles() = %v, want %v", got, tt.want) } }) @@ -79,7 +79,7 @@ func Test_getValuesFiles(t *testing.T) { func Test_inspectUpgradeScenario(t *testing.T) { type args struct { r *release - s *map[string]releaseState + s *map[string]helmRelease } tests := []struct { name string @@ -96,7 +96,7 @@ func Test_inspectUpgradeScenario(t *testing.T) { Chart: "./../../tests/chart-test", Enabled: true, }, - s: &map[string]releaseState{ + s: &map[string]helmRelease{ "release1-namespace": { Namespace: "namespace", Chart: "chart-1.0.0", @@ -109,10 +109,10 @@ func Test_inspectUpgradeScenario(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { outcome = plan{} - currentState = *tt.args.s + cs := currentState(*tt.args.s) // Act - inspectUpgradeScenario(tt.args.r) + cs.inspectUpgradeScenario(tt.args.r) got := outcome.Decisions[0].Type t.Log(outcome.Decisions[0].Description) @@ -205,6 +205,7 @@ func Test_decide(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { targetMap = make(map[string]bool) + cs := currentState(make(map[string]helmRelease)) for _, target := range tt.targetFlag { targetMap[target] = true @@ -213,7 +214,7 @@ func Test_decide(t *testing.T) { wg := sync.WaitGroup{} wg.Add(1) // Act - decide(tt.args.r, tt.args.s, &wg) + cs.decide(tt.args.r, tt.args.s, &wg) wg.Wait() got := outcome.Decisions[0].Type t.Log(outcome.Decisions[0].Description) @@ -230,7 +231,7 @@ func Test_decide_group(t *testing.T) { type args struct { r *release s *state - currentState *map[string]releaseState + currentState *map[string]helmRelease } tests := []struct { name string @@ -249,7 +250,7 @@ func Test_decide_group(t *testing.T) { Enabled: true, }, s: &state{}, - currentState: &map[string]releaseState{ + currentState: &map[string]helmRelease{ "release1-namespace": { Namespace: "namespace", Chart: "chart-1.0.0", @@ -271,7 +272,7 @@ func Test_decide_group(t *testing.T) { s: &state{ Context: "default", }, - currentState: &map[string]releaseState{ + currentState: &map[string]helmRelease{ "release2-namespace": { Name: "release2", Namespace: "namespace", @@ -288,7 +289,7 @@ func Test_decide_group(t *testing.T) { t.Run(tt.name, func(t *testing.T) { groupMap = make(map[string]bool) targetMap = make(map[string]bool) - currentState = *tt.args.currentState + cs := currentState(*tt.args.currentState) for _, target := range tt.targetFlag { groupMap[target] = true @@ -299,7 +300,7 @@ func Test_decide_group(t *testing.T) { outcome = plan{} wg := sync.WaitGroup{} wg.Add(1) - decide(tt.args.r, tt.args.s, &wg) + cs.decide(tt.args.r, tt.args.s, &wg) wg.Wait() got := outcome.Decisions[0].Type t.Log(outcome.Decisions[0].Description) diff --git a/internal/app/helm_helpers.go b/internal/app/helm_helpers.go index c3c24741..640a591f 100644 --- a/internal/app/helm_helpers.go +++ b/internal/app/helm_helpers.go @@ -1,51 +1,12 @@ package app import ( - "encoding/json" - "errors" - "fmt" "net/url" - "os" - "path/filepath" - "regexp" - "strconv" "strings" - "sync" - "time" "github.com/Praqma/helmsman/internal/gcs" ) -var currentState map[string]releaseState - -// releaseState represents the current state of a release -type releaseState struct { - Name string - Revision int - Updated time.Time - Status string - Chart string - Namespace string - HelmsmanContext string -} - -type releaseInfo struct { - Name string `json:"Name"` - Namespace string `json:"Namespace"` - Revision string `json:"Revision"` - Updated string `json:"Updated"` - Status string `json:"Status"` - Chart string `json:"Chart"` - AppVersion string `json:"AppVersion,omitempty"` -} - -type chartVersion struct { - Name string `json:"name"` - Version string `json:"version"` - AppVersion string `json:"app_version"` - Description string `json:"description"` -} - // getHelmClientVersion returns Helm client Version func getHelmVersion() string { cmd := command{ @@ -61,207 +22,38 @@ func getHelmVersion() string { return result } -// getHelmReleases fetches a list of all releases in a k8s cluster -func getHelmReleases() []releaseInfo { - var allReleases []releaseInfo +// helmPluginExists returns true if the plugin is present in the environment and false otherwise. +// It takes as input the plugin's name to check if it is recognizable or not. e.g. diff +func helmPluginExists(plugin string) bool { cmd := command{ Cmd: helmBin, - Args: []string{"list", "--all", "--max", "0", "--output", "json", "--all-namespaces"}, - Description: "Listing all existing releases...", - } - exitCode, result, _ := cmd.exec(debug, verbose) - if exitCode != 0 { - log.Fatal("Failed to list all releases: " + result) - } - if err := json.Unmarshal([]byte(result), &allReleases); err != nil { - log.Fatal(fmt.Sprintf("failed to unmarshal Helm CLI output: %s", err)) - } - return allReleases -} - -// buildState builds the currentState map containing information about all releases existing in a k8s cluster -func buildState() { - log.Info("Acquiring current Helm state from cluster...") - - currentState = make(map[string]releaseState) - rel := getHelmReleases() - - for i := 0; i < len(rel); i++ { - // we need to split the time into parts and make sure milliseconds len = 6, it happens to skip trailing zeros - updatedFields := strings.Fields(rel[i].Updated) - updatedHour := strings.Split(updatedFields[1], ".") - milliseconds := updatedHour[1] - for i := len(milliseconds); i < 9; i++ { - milliseconds = fmt.Sprintf("%s0", milliseconds) - } - date, err := time.Parse("2006-01-02 15:04:05.000000000 -0700 MST", - fmt.Sprintf("%s %s.%s %s %s", updatedFields[0], updatedHour[0], milliseconds, updatedFields[2], updatedFields[3])) - if err != nil { - log.Fatal("While converting release time: " + err.Error()) - } - revision, _ := strconv.Atoi(rel[i].Revision) - currentState[fmt.Sprintf("%s-%s", rel[i].Name, rel[i].Namespace)] = releaseState{ - Name: rel[i].Name, - Revision: revision, - Updated: date, - Status: rel[i].Status, - Chart: rel[i].Chart, - Namespace: rel[i].Namespace, - HelmsmanContext: getReleaseContext(rel[i].Name, rel[i].Namespace), - } + Args: []string{"plugin", "list"}, + Description: "Validating that [ " + plugin + " ] is installed", } -} -// isReleaseExisting checks if a Helm release is/was deployed in a k8s cluster. -// It searches the Current State for releases. -// The key format for releases uniqueness is: -// If status is provided as an input [deployed, deleted, failed], then the search will verify the release status matches the search status. -func isReleaseExisting(r *release, status string) bool { - v, ok := currentState[fmt.Sprintf("%s-%s", r.Name, r.Namespace)] - if !ok || v.HelmsmanContext != s.Context { - return false - } + exitCode, result, _ := cmd.exec(debug, false) - if status != "" { - if v.Status == status { - return true - } + if exitCode != 0 { return false } - return true -} - -// getReleaseRevision returns the revision number for a release -func getReleaseRevision(rs releaseState) string { - return strconv.Itoa(rs.Revision) -} -// getReleaseChartName extracts and returns the Helm chart name from the chart info in a release state. -// example: chart in release state is "jenkins-0.9.0" and this function will extract "jenkins" from it. -func getReleaseChartName(rs releaseState) string { - - chart := rs.Chart - runes := []rune(chart) - return string(runes[0:strings.LastIndexByte(chart[0:strings.IndexByte(chart, '.')], '-')]) -} - -// getReleaseChartVersion extracts and returns the Helm chart version from the chart info in a release state. -// example: chart in release state is returns "jenkins-0.9.0" and this functions will extract "0.9.0" from it. -// It should also handle semver-valid pre-release/meta information, example: in: jenkins-0.9.0-1, out: 0.9.0-1 -// in the event of an error, an empty string is returned. -func getReleaseChartVersion(rs releaseState) string { - chart := rs.Chart - re := regexp.MustCompile("-(v?[0-9]+\\.[0-9]+\\.[0-9]+.*)") - matches := re.FindStringSubmatch(chart) - if len(matches) > 1 { - return matches[1] - } - return "" -} - -// validateReleaseCharts validates if the charts defined in a release are valid. -// Valid charts are the ones that can be found in the defined repos. -// This function uses Helm search to verify if the chart can be found or not. -func validateReleaseCharts(apps map[string]*release) error { - versionExtractor := regexp.MustCompile(`version:\s?(.*)`) - - wg := sync.WaitGroup{} - c := make(chan string, len(apps)) - for app, r := range apps { - wg.Add(1) - go func(app string, r *release, wg *sync.WaitGroup, c chan string) { - defer wg.Done() - validateCurrentChart := true - if !r.isReleaseConsideredToRun() { - validateCurrentChart = false - } - if validateCurrentChart { - if isLocalChart(r.Chart) { - cmd := command{ - Cmd: helmBin, - Args: []string{"inspect", "chart", r.Chart}, - Description: "Validating [ " + r.Chart + " ] chart's availability", - } - - var output string - var exitCode int - if exitCode, output, _ = cmd.exec(debug, verbose); exitCode != 0 { - maybeRepo := filepath.Base(filepath.Dir(r.Chart)) - c <- "Chart [ " + r.Chart + " ] for app [" + app + "] can't be found. Did you mean to add a repo [ " + maybeRepo + " ]?" - return - } - matches := versionExtractor.FindStringSubmatch(output) - if len(matches) == 2 { - version := matches[1] - if r.Version != version { - c <- "Chart [ " + r.Chart + " ] with version [ " + r.Version + " ] is specified for " + - "app [" + app + "] but the chart found at that path has version [ " + version + " ] which does not match." - return - } - } - - } else { - version := r.Version - if len(version) == 0 { - version = "*" - } - cmd := command{ - Cmd: helmBin, - Args: []string{"search", "repo", r.Chart, "--version", version, "-l"}, - Description: "Validating [ " + r.Chart + " ] chart's version [ " + r.Version + " ] availability", - } - - if exitCode, result, _ := cmd.exec(debug, verbose); exitCode != 0 || strings.Contains(result, "No results found") { - c <- "Chart [ " + r.Chart + " ] with version [ " + r.Version + " ] is specified for " + - "app [" + app + "] but was not found" - return - } - } - } - }(app, r, &wg, c) - } - wg.Wait() - if len(c) > 0 { - err := <-c - if err != "" { - return errors.New(err) - } - } - return nil + return strings.Contains(result, plugin) } -// getChartVersion fetches the lastest chart version matching the semantic versioning constraints. -// If chart is local, returns the given release version -func getChartVersion(r *release) (string, string) { - if isLocalChart(r.Chart) { - return r.Version, "" - } +// updateChartDep updates dependencies for a local chart +func updateChartDep(chartPath string) (bool, string) { cmd := command{ Cmd: helmBin, - Args: []string{"search", "repo", r.Chart, "--version", r.Version, "-o", "json"}, - Description: "Getting latest chart's version " + r.Chart + "-" + r.Version + "", - } - - var ( - exitCode int - result string - ) - - if exitCode, result, _ = cmd.exec(debug, verbose); exitCode != 0 { - return "", "Chart [ " + r.Chart + " ] with version [ " + r.Version + " ] is specified but not found in the helm repositories" + Args: []string{"dependency", "update", chartPath}, + Description: "Updating dependency for local chart [ " + chartPath + " ]", } - chartVersions := make([]chartVersion, 0) - if err := json.Unmarshal([]byte(result), &chartVersions); err != nil { - log.Fatal(fmt.Sprint(err)) - } + exitCode, err, _ := cmd.exec(debug, verbose) - if len(chartVersions) < 1 { - return "", "Chart [ " + r.Chart + " ] with version [ " + r.Version + " ] is specified but not found in the helm repositories" - } else if len(chartVersions) > 1 { - return "", "Multiple versions of chart [ " + r.Chart + " ] with version [ " + r.Version + " ] found in the helm repositories" + if exitCode != 0 { + return false, err } - return chartVersions[0].Version, "" + return true, "" } // addHelmRepos adds repositories to Helm if they don't exist already. @@ -316,133 +108,3 @@ func addHelmRepos(repos map[string]string) (bool, string) { return true, "" } - -// cleanUntrackedReleases checks for any releases that are managed by Helmsman and are no longer tracked by the desired state -// It compares the currently deployed releases labeled with "MANAGED-BY=HELMSMAN" with Apps defined in the desired state -// For all untracked releases found, a decision is made to uninstall them and is added to the Helmsman plan -// NOTE: Untracked releases don't benefit from either namespace or application protection. -// NOTE: Removing/Commenting out an app from the desired state makes it untracked. -func cleanUntrackedReleases() { - toDelete := make(map[string]map[releaseState]bool) - log.Info("Checking if any Helmsman managed releases are no longer tracked by your desired state ...") - for ns, releases := range getHelmsmanReleases() { - for r := range releases { - tracked := false - for _, app := range s.Apps { - if app.Name == r.Name && app.Namespace == r.Namespace { - tracked = true - } - } - if !tracked { - if _, ok := toDelete[ns]; !ok { - toDelete[ns] = make(map[releaseState]bool) - } - toDelete[ns][r] = true - } - } - } - - if len(toDelete) == 0 { - log.Info("No untracked releases found") - } else { - for _, releases := range toDelete { - for r := range releases { - logDecision("Untracked release [ "+r.Name+" ] found and it will be deleted", -800, delete) - uninstallUntrackedRelease(r) - } - } - } -} - -// uninstallUntrackedRelease creates the helm command to uninstall an untracked release -func uninstallUntrackedRelease(release releaseState) { - cmd := command{ - Cmd: helmBin, - Args: concat([]string{"uninstall", release.Name, "--namespace", release.Namespace}, getDryRunFlags()), - Description: "Deleting untracked release [ " + release.Name + " ] in namespace [ " + release.Namespace + " ]", - } - - outcome.addCommand(cmd, -800, nil) -} - -// decrypt a helm secret file -func decryptSecret(name string) error { - cmd := helmBin - args := []string{"secrets", "dec", name} - - if settings.EyamlEnabled { - cmd = "eyaml" - args = []string{"decrypt", "-f", name} - if settings.EyamlPrivateKeyPath != "" && settings.EyamlPublicKeyPath != "" { - args = append(args, []string{"--pkcs7-private-key", settings.EyamlPrivateKeyPath, "--pkcs7-public-key", settings.EyamlPublicKeyPath}...) - } - } - - command := command{ - Cmd: cmd, - Args: args, - Description: "Decrypting " + name, - } - - exitCode, output, stderr := command.exec(debug, false) - if !settings.EyamlEnabled { - _, fileNotFound := os.Stat(name + ".dec") - if fileNotFound != nil && !isOfType(name, []string{".dec"}) { - return errors.New(output) - } - } - - if exitCode != 0 { - return errors.New(output) - } else if stderr != "" { - return errors.New(output) - } - - if settings.EyamlEnabled { - var outfile string - if isOfType(name, []string{".dec"}) { - outfile = name - } else { - outfile = name + ".dec" - } - err := writeStringToFile(outfile, output) - if err != nil { - log.Fatal("Can't write [ " + outfile + " ] file") - } - } - return nil -} - -// updateChartDep updates dependencies for a local chart -func updateChartDep(chartPath string) (bool, string) { - cmd := command{ - Cmd: helmBin, - Args: []string{"dependency", "update", chartPath}, - Description: "Updating dependency for local chart [ " + chartPath + " ]", - } - - exitCode, err, _ := cmd.exec(debug, verbose) - - if exitCode != 0 { - return false, err - } - return true, "" -} - -// helmPluginExists returns true if the plugin is present in the environment and false otherwise. -// It takes as input the plugin's name to check if it is recognizable or not. e.g. diff -func helmPluginExists(plugin string) bool { - cmd := command{ - Cmd: helmBin, - Args: []string{"plugin", "list"}, - Description: "Validating that [ " + plugin + " ] is installed", - } - - exitCode, result, _ := cmd.exec(debug, false) - - if exitCode != 0 { - return false - } - - return strings.Contains(result, plugin) -} diff --git a/internal/app/helm_helpers_test.go b/internal/app/helm_helpers_test.go deleted file mode 100644 index 40861917..00000000 --- a/internal/app/helm_helpers_test.go +++ /dev/null @@ -1,475 +0,0 @@ -package app - -import ( - "fmt" - "os" - "testing" - "time" -) - -func setupTestCase(t *testing.T) func(t *testing.T) { - t.Log("setup test case") - os.MkdirAll(os.TempDir()+"/helmsman-tests/myapp", os.ModePerm) - os.MkdirAll(os.TempDir()+"/helmsman-tests/dir-with space/myapp", os.ModePerm) - cmd := command{ - Cmd: helmBin, - Args: []string{"create", os.TempDir() + "/helmsman-tests/dir-with space/myapp"}, - Description: "creating an empty local chart directory", - } - if exitCode, msg, _ := cmd.exec(debug, verbose); exitCode != 0 { - log.Fatal(fmt.Sprintf("Command returned with exit code: %d. And error message: %s ", exitCode, msg)) - } - - return func(t *testing.T) { - t.Log("teardown test case") - //os.RemoveAll("/tmp/helmsman-tests/") - } -} -func Test_validateReleaseCharts(t *testing.T) { - type args struct { - apps map[string]*release - } - tests := []struct { - name string - targetFlag []string - groupFlag []string - args args - want bool - }{ - { - name: "test case 1: valid local path with no chart", - targetFlag: []string{}, - args: args{ - apps: map[string]*release{ - "app": &release{ - Name: "", - Description: "", - Namespace: "", - Enabled: true, - Chart: os.TempDir() + "/helmsman-tests/myapp", - Version: "", - ValuesFile: "", - ValuesFiles: []string{}, - SecretsFile: "", - SecretsFiles: []string{}, - Test: false, - Protected: false, - Wait: false, - Priority: 0, - Set: make(map[string]string), - SetString: make(map[string]string), - HelmFlags: []string{}, - NoHooks: false, - Timeout: 0, - }, - }, - }, - want: false, - }, { - name: "test case 2: invalid local path", - targetFlag: []string{}, - args: args{ - apps: map[string]*release{ - "app": &release{ - Name: "", - Description: "", - Namespace: "", - Enabled: true, - Chart: os.TempDir() + "/does-not-exist/myapp", - Version: "", - ValuesFile: "", - ValuesFiles: []string{}, - SecretsFile: "", - SecretsFiles: []string{}, - Test: false, - Protected: false, - Wait: false, - Priority: 0, - Set: make(map[string]string), - SetString: make(map[string]string), - HelmFlags: []string{}, - NoHooks: false, - Timeout: 0, - }, - }, - }, - want: false, - }, { - name: "test case 3: valid chart local path with whitespace", - targetFlag: []string{}, - args: args{ - apps: map[string]*release{ - "app": &release{ - Name: "", - Description: "", - Namespace: "", - Enabled: true, - Chart: os.TempDir() + "/helmsman-tests/dir-with space/myapp", - Version: "0.1.0", - ValuesFile: "", - ValuesFiles: []string{}, - SecretsFile: "", - SecretsFiles: []string{}, - Test: false, - Protected: false, - Wait: false, - Priority: 0, - Set: make(map[string]string), - SetString: make(map[string]string), - HelmFlags: []string{}, - NoHooks: false, - Timeout: 0, - }, - }, - }, - want: true, - }, { - name: "test case 4: valid chart from repo", - targetFlag: []string{}, - args: args{ - apps: map[string]*release{ - "app": &release{ - Name: "", - Description: "", - Namespace: "", - Enabled: true, - Chart: "stable/prometheus", - Version: "9.5.2", - ValuesFile: "", - ValuesFiles: []string{}, - SecretsFile: "", - SecretsFiles: []string{}, - Test: false, - Protected: false, - Wait: false, - Priority: 0, - Set: make(map[string]string), - SetString: make(map[string]string), - HelmFlags: []string{}, - NoHooks: false, - Timeout: 0, - }, - }, - }, - want: true, - }, { - name: "test case 5: invalid local path for chart ignored with -target flag, while other app was targeted", - targetFlag: []string{"notThisOne"}, - args: args{ - apps: map[string]*release{ - "app": &release{ - Name: "app", - Description: "", - Namespace: "", - Enabled: true, - Chart: os.TempDir() + "/does-not-exist/myapp", - Version: "", - ValuesFile: "", - ValuesFiles: []string{}, - SecretsFile: "", - SecretsFiles: []string{}, - Test: false, - Protected: false, - Wait: false, - Priority: 0, - Set: make(map[string]string), - SetString: make(map[string]string), - HelmFlags: []string{}, - NoHooks: false, - Timeout: 0, - }, - }, - }, - want: true, - }, { - name: "test case 6: invalid local path for chart included with -target flag", - targetFlag: []string{"app"}, - args: args{ - apps: map[string]*release{ - "app": &release{ - Name: "app", - Description: "", - Namespace: "", - Enabled: true, - Chart: os.TempDir() + "/does-not-exist/myapp", - Version: "", - ValuesFile: "", - ValuesFiles: []string{}, - SecretsFile: "", - SecretsFiles: []string{}, - Test: false, - Protected: false, - Wait: false, - Priority: 0, - Set: make(map[string]string), - SetString: make(map[string]string), - HelmFlags: []string{}, - NoHooks: false, - Timeout: 0, - }, - }, - }, - want: false, - }, - } - - teardownTestCase := setupTestCase(t) - defer teardownTestCase(t) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - targetMap = make(map[string]bool) - groupMap = make(map[string]bool) - for _, target := range tt.targetFlag { - targetMap[target] = true - } - for _, group := range tt.groupFlag { - groupMap[group] = true - } - err := validateReleaseCharts(tt.args.apps) - switch err.(type) { - case nil: - if tt.want != true { - t.Errorf("validateReleaseCharts() = %v, want error", err) - } - case error: - if tt.want != false { - t.Errorf("validateReleaseCharts() = %v, want nil", err) - } - } - }) - } -} - -func Test_getReleaseChartVersion(t *testing.T) { - // version string = the first semver-valid string after the last hypen in the chart string. - - type args struct { - r releaseState - } - tests := []struct { - name string - args args - want string - }{ - { - name: "test case 1: there is a pre-release version", - args: args{ - r: releaseState{ - Revision: 0, - Updated: time.Now(), - Status: "", - Chart: "elasticsearch-1.3.0-1", - Namespace: "", - }, - }, - want: "1.3.0-1", - }, { - name: "test case 2: normal case", - args: args{ - r: releaseState{ - Revision: 0, - Updated: time.Now(), - Status: "", - Chart: "elasticsearch-1.3.0", - Namespace: "", - }, - }, - want: "1.3.0", - }, { - name: "test case 3: there is a hypen in the name", - args: args{ - r: releaseState{ - Revision: 0, - Updated: time.Now(), - Status: "", - Chart: "elastic-search-1.3.0", - Namespace: "", - }, - }, - want: "1.3.0", - }, { - name: "test case 4: there is meta information", - args: args{ - r: releaseState{ - Revision: 0, - Updated: time.Now(), - Status: "", - Chart: "elastic-search-1.3.0+meta.info", - Namespace: "", - }, - }, - want: "1.3.0+meta.info", - }, { - name: "test case 5: an invalid string", - args: args{ - r: releaseState{ - Revision: 0, - Updated: time.Now(), - Status: "", - Chart: "foo", - Namespace: "", - }, - }, - want: "", - }, { - name: "test case 6: version includes v", - args: args{ - r: releaseState{ - Revision: 0, - Updated: time.Now(), - Status: "", - Chart: "cert-manager-v0.5.2", - Namespace: "", - }, - }, - want: "v0.5.2", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Log(tt.want) - if got := getReleaseChartVersion(tt.args.r); got != tt.want { - t.Errorf("getReleaseChartVersion() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_getChartVersion(t *testing.T) { - // version string = the first semver-valid string after the last hypen in the chart string. - type args struct { - r *release - } - tests := []struct { - name string - args args - want string - }{ - { - name: "getChartVersion - local chart should return given release version", - args: args{ - r: &release{ - Name: "release1", - Namespace: "namespace", - Version: "1.0.0", - Chart: "./../../tests/chart-test", - Enabled: true, - }, - }, - want: "1.0.0", - }, - { - name: "getChartVersion - unknown chart should error", - args: args{ - r: &release{ - Name: "release1", - Namespace: "namespace", - Version: "1.0.0", - Chart: "random-chart-name-1f8147", - Enabled: true, - }, - }, - want: "", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Log(tt.want) - got, _ := getChartVersion(tt.args.r) - if got != tt.want { - t.Errorf("getChartVersion() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_eyamlSecrets(t *testing.T) { - type args struct { - r *release - s *config - } - tests := []struct { - name string - args args - want bool - }{ - { - name: "decryptSecrets - valid eyaml-based secrets decryption", - args: args{ - s: &config{ - EyamlEnabled: true, - EyamlPublicKeyPath: "./../../tests/keys/public_key.pkcs7.pem", - EyamlPrivateKeyPath: "./../../tests/keys/private_key.pkcs7.pem", - }, - r: &release{ - Name: "release1", - Namespace: "namespace", - Version: "1.0.0", - Enabled: true, - SecretsFile: "./../../tests/secrets/valid_eyaml_secrets.yaml", - }, - }, - want: true, - }, - { - name: "decryptSecrets - not existing eyaml-based secrets file", - args: args{ - s: &config{ - EyamlEnabled: true, - EyamlPublicKeyPath: "./../../tests/keys/public_key.pkcs7.pem", - EyamlPrivateKeyPath: "./../../tests/keys/private_key.pkcs7.pem", - }, - r: &release{ - Name: "release1", - Namespace: "namespace", - Version: "1.0.0", - Enabled: true, - SecretsFile: "./../../tests/secrets/invalid_eyaml_secrets.yaml", - }, - }, - want: false, - }, - { - name: "decryptSecrets - not existing eyaml key", - args: args{ - s: &config{ - EyamlEnabled: true, - EyamlPublicKeyPath: "./../../tests/keys/public_key.pkcs7.pem2", - EyamlPrivateKeyPath: "./../../tests/keys/private_key.pkcs7.pem", - }, - r: &release{ - Name: "release1", - Namespace: "namespace", - Version: "1.0.0", - Enabled: true, - SecretsFile: "./../../tests/secrets/valid_eyaml_secrets.yaml", - }, - }, - want: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Log(tt.want) - defaultSettings := settings - defer func() { settings = defaultSettings }() - settings.EyamlEnabled = tt.args.s.EyamlEnabled - settings.EyamlPublicKeyPath = tt.args.s.EyamlPublicKeyPath - settings.EyamlPrivateKeyPath = tt.args.s.EyamlPrivateKeyPath - err := decryptSecret(tt.args.r.SecretsFile) - switch err.(type) { - case nil: - if tt.want != true { - t.Errorf("decryptSecret() = %v, want error", err) - } - case error: - if tt.want != false { - t.Errorf("decryptSecret() = %v, want nil", err) - } - } - if _, err := os.Stat(tt.args.r.SecretsFile + ".dec"); err == nil { - defer deleteFile(tt.args.r.SecretsFile + ".dec") - } - }) - } -} diff --git a/internal/app/helm_release.go b/internal/app/helm_release.go new file mode 100644 index 00000000..27b72dfb --- /dev/null +++ b/internal/app/helm_release.go @@ -0,0 +1,84 @@ +package app + +import ( + "encoding/json" + "fmt" + "regexp" + "strconv" + "strings" +) + +// helmRelease represents the current state of a release +type helmRelease struct { + Name string `json:"Name"` + Namespace string `json:"Namespace"` + Revision int `json:"Revision,string"` + Updated HelmTime `json:"Updated"` + Status string `json:"Status"` + Chart string `json:"Chart"` + AppVersion string `json:"AppVersion,omitempty"` + HelmsmanContext string +} + +// getHelmReleases fetches a list of all releases in a k8s cluster +func getHelmReleases() []helmRelease { + var allReleases []helmRelease + cmd := command{ + Cmd: helmBin, + Args: []string{"list", "--all", "--max", "0", "--output", "json", "--all-namespaces"}, + Description: "Listing all existing releases...", + } + exitCode, result, _ := cmd.exec(debug, verbose) + if exitCode != 0 { + log.Fatal("Failed to list all releases: " + result) + } + if err := json.Unmarshal([]byte(result), &allReleases); err != nil { + log.Fatal(fmt.Sprintf("failed to unmarshal Helm CLI output: %s", err)) + } + return allReleases +} + +// uninstall creates the helm command to uninstall an untracked release +func (r *helmRelease) uninstall() { + cmd := command{ + Cmd: helmBin, + Args: concat([]string{"uninstall", r.Name, "--namespace", r.Namespace}, getDryRunFlags()), + Description: "Deleting untracked release [ " + r.Name + " ] in namespace [ " + r.Namespace + " ]", + } + + outcome.addCommand(cmd, -800, nil) +} + +// getRevision returns the revision number for an existing helm release +func (rs *helmRelease) getRevision() string { + return strconv.Itoa(rs.Revision) +} + +// getChartName extracts and returns the Helm chart name from the chart info in a release state. +// example: chart in release state is "jenkins-0.9.0" and this function will extract "jenkins" from it. +func (rs *helmRelease) getChartName() string { + + chart := rs.Chart + runes := []rune(chart) + return string(runes[0:strings.LastIndexByte(chart[0:strings.IndexByte(chart, '.')], '-')]) +} + +// getChartVersion extracts and returns the Helm chart version from the chart info in a release state. +// example: chart in release state is returns "jenkins-0.9.0" and this functions will extract "0.9.0" from it. +// It should also handle semver-valid pre-release/meta information, example: in: jenkins-0.9.0-1, out: 0.9.0-1 +// in the event of an error, an empty string is returned. +func (rs *helmRelease) getChartVersion() string { + chart := rs.Chart + re := regexp.MustCompile(`-(v?[0-9]+\.[0-9]+\.[0-9]+.*)`) + matches := re.FindStringSubmatch(chart) + if len(matches) > 1 { + return matches[1] + } + return "" +} + +// getCurrentNamespaceProtection returns the protection state for the namespace where a release is currently installed. +// It returns true if a namespace is defined as protected in the desired state file, false otherwise. +func (rs *helmRelease) getCurrentNamespaceProtection() bool { + return s.Namespaces[rs.Namespace].Protected +} diff --git a/internal/app/helm_time.go b/internal/app/helm_time.go new file mode 100644 index 00000000..05cf6ca9 --- /dev/null +++ b/internal/app/helm_time.go @@ -0,0 +1,44 @@ +package app + +import ( + "fmt" + "strings" + "time" +) + +const ctLayout = "2006-01-02 15:04:05.000000000 -0700 MST" + +var nilTime = (time.Time{}).UnixNano() + +type HelmTime struct { + time.Time +} + +func (ht *HelmTime) UnmarshalJSON(b []byte) (err error) { + s := strings.Trim(string(b), "\"") + if s == "null" { + ht.Time = time.Time{} + return + } + // we need to split the time into parts and make sure milliseconds len = 6, it happens to skip trailing zeros + updatedFields := strings.Fields(s) + updatedHour := strings.Split(updatedFields[1], ".") + milliseconds := updatedHour[1] + for i := len(milliseconds); i < 9; i++ { + milliseconds = fmt.Sprintf("%s0", milliseconds) + } + s = fmt.Sprintf("%s %s.%s %s %s", updatedFields[0], updatedHour[0], milliseconds, updatedFields[2], updatedFields[3]) + ht.Time, err = time.Parse(ctLayout, s) + return +} + +func (ht *HelmTime) MarshalJSON() ([]byte, error) { + if ht.Time.UnixNano() == nilTime { + return []byte("null"), nil + } + return []byte(fmt.Sprintf("\"%s\"", ht.Time.Format(ctLayout))), nil +} + +func (ht *HelmTime) IsSet() bool { + return ht.UnixNano() != nilTime +} diff --git a/internal/app/kube_helpers.go b/internal/app/kube_helpers.go index e6a9a458..493a085a 100644 --- a/internal/app/kube_helpers.go +++ b/internal/app/kube_helpers.go @@ -29,7 +29,7 @@ func addNamespaces(namespaces map[string]namespace) { func overrideAppsNamespace(newNs string) { log.Info("Overriding apps namespaces with [ " + newNs + " ] ...") for _, r := range s.Apps { - overrideNamespace(r, newNs) + r.overrideNamespace(newNs) } } @@ -338,44 +338,6 @@ func getReleaseContext(releaseName string, namespace string) string { return strings.TrimSpace(out) } -// getHelmsmanReleases returns a map of all releases that are labeled with "MANAGED-BY=HELMSMAN" -// The releases are categorized by the namespaces in which they are deployed -// The returned map format is: map[:map[:true]] -func getHelmsmanReleases() map[string]map[releaseState]bool { - var lines []string - releases := make(map[string]map[releaseState]bool) - storageBackend := s.Settings.StorageBackend - - for ns := range s.Namespaces { - cmd := command{ - Cmd: "kubectl", - Args: []string{"get", storageBackend, "-n", ns, "-l", "MANAGED-BY=HELMSMAN", "-l", "HELMSMAN_CONTEXT=" + s.Context, "-o", "name"}, - Description: "Getting Helmsman-managed releases in namespace [ " + ns + " ]", - } - - exitCode, output, _ := cmd.exec(debug, verbose) - - if exitCode != 0 { - log.Fatal(output) - } - if strings.ToUpper("No resources found.") != strings.ToUpper(strings.TrimSpace(output)) { - lines = strings.Split(output, "\n") - } - - for _, r := range lines { - if r == "" { - continue - } - if _, ok := releases[ns]; !ok { - releases[ns] = make(map[releaseState]bool) - } - releaseName := strings.Split(strings.Split(r, "/")[1], ".")[4] - releases[ns][currentState[releaseName+"-"+ns]] = true - } - } - return releases -} - // getKubectlClientVersion returns kubectl client version func getKubectlClientVersion() string { cmd := command{ diff --git a/internal/app/main.go b/internal/app/main.go index ad6d9b43..891050d5 100644 --- a/internal/app/main.go +++ b/internal/app/main.go @@ -16,6 +16,12 @@ func (i *stringArray) Set(value string) error { return nil } +const appVersion = "v3.0.0-beta2" +const tempFilesDir = ".helmsman-tmp" +const stableHelmRepo = "https://kubernetes-charts.storage.googleapis.com" +const incubatorHelmRepo = "http://storage.googleapis.com/kubernetes-charts-incubator" +const defaultContextName = "default" + var s state var debug bool var files stringArray @@ -31,7 +37,6 @@ var noNs bool var nsOverride string var skipValidation bool var keepUntrackedReleases bool -var appVersion = "v3.0.0-beta1" var helmBin = "helm" var helmVersion string var kubectlVersion string @@ -52,10 +57,7 @@ var updateDeps bool var forceUpgrades bool var noDefaultRepos bool -const tempFilesDir = ".helmsman-tmp" -const stableHelmRepo = "https://kubernetes-charts.storage.googleapis.com" -const incubatorHelmRepo = "http://storage.googleapis.com/kubernetes-charts-incubator" -const defaultContextName = "default" +var settings config func init() { Cli() @@ -67,8 +69,9 @@ func Main() { defer os.RemoveAll(tempFilesDir) defer cleanup() + settings = s.Settings // set the kubecontext to be used Or create it if it does not exist - if !setKubeContext(s.Settings.KubeContext) { + if !setKubeContext(settings.KubeContext) { if r, msg := createContext(); !r { log.Fatal(msg) } @@ -100,9 +103,10 @@ func Main() { log.Info("--destroy is enabled. Your releases will be deleted!") } - p := makePlan(&s) + cs := buildState() + p := cs.makePlan(&s) if !keepUntrackedReleases { - cleanUntrackedReleases() + cs.cleanUntrackedReleases() } p.sortPlan() diff --git a/internal/app/namespace.go b/internal/app/namespace.go index e45d6cfb..636f4477 100644 --- a/internal/app/namespace.go +++ b/internal/app/namespace.go @@ -31,10 +31,7 @@ type namespace struct { // checkNamespaceDefined checks if a given namespace is defined in the namespaces section of the desired state file func checkNamespaceDefined(ns string, s state) bool { _, ok := s.Namespaces[ns] - if !ok { - return false - } - return true + return ok } // print prints the namespace diff --git a/internal/app/release.go b/internal/app/release.go index 9b9031de..4e7b3c68 100644 --- a/internal/app/release.go +++ b/internal/app/release.go @@ -1,9 +1,15 @@ package app import ( + "encoding/json" + "errors" "fmt" "os" + "path/filepath" + "regexp" + "strconv" "strings" + "sync" ) // release type representing Helm releases which are described in the desired state @@ -30,8 +36,15 @@ type release struct { Timeout int `yaml:"timeout"` } +type chartVersion struct { + Name string `json:"name"` + Version string `json:"version"` + AppVersion string `json:"app_version"` + Description string `json:"description"` +} + // isReleaseConsideredToRun checks if a release is being targeted for operations as specified by user cmd flags (--group or --target) -func (r *release) isReleaseConsideredToRun() bool { +func (r *release) isConsideredToRun() bool { if len(targetMap) > 0 { if _, ok := targetMap[r.Name]; ok { return true @@ -49,7 +62,7 @@ func (r *release) isReleaseConsideredToRun() bool { // validateRelease validates if a release inside a desired state meets the specifications or not. // check the full specification @ https://github.com/Praqma/helmsman/docs/desired_state_spec.md -func validateRelease(appLabel string, r *release, names map[string]map[string]bool, s state) (bool, string) { +func (r *release) validate(appLabel string, names map[string]map[string]bool, s state) (bool, string) { if r.Name == "" { r.Name = appLabel } @@ -120,8 +133,364 @@ func validateRelease(appLabel string, r *release, names map[string]map[string]bo return true, "" } +// validateReleaseCharts validates if the charts defined in a release are valid. +// Valid charts are the ones that can be found in the defined repos. +// This function uses Helm search to verify if the chart can be found or not. +func validateReleaseCharts(apps map[string]*release) error { + wg := sync.WaitGroup{} + c := make(chan string, len(apps)) + for app, r := range apps { + wg.Add(1) + go r.validateChart(app, &wg, c) + } + wg.Wait() + if len(c) > 0 { + err := <-c + if err != "" { + return errors.New(err) + } + } + return nil +} + +var versionExtractor = regexp.MustCompile(`version:\s?(.*)`) + +func (r *release) validateChart(app string, wg *sync.WaitGroup, c chan string) { + + defer wg.Done() + validateCurrentChart := true + if !r.isConsideredToRun() { + validateCurrentChart = false + } + if validateCurrentChart { + if isLocalChart(r.Chart) { + cmd := command{ + Cmd: helmBin, + Args: []string{"inspect", "chart", r.Chart}, + Description: "Validating [ " + r.Chart + " ] chart's availability", + } + + var output string + var exitCode int + if exitCode, output, _ = cmd.exec(debug, verbose); exitCode != 0 { + maybeRepo := filepath.Base(filepath.Dir(r.Chart)) + c <- "Chart [ " + r.Chart + " ] for app [" + app + "] can't be found. Did you mean to add a repo [ " + maybeRepo + " ]?" + return + } + matches := versionExtractor.FindStringSubmatch(output) + if len(matches) == 2 { + version := matches[1] + if r.Version != version { + c <- "Chart [ " + r.Chart + " ] with version [ " + r.Version + " ] is specified for " + + "app [" + app + "] but the chart found at that path has version [ " + version + " ] which does not match." + return + } + } + + } else { + version := r.Version + if len(version) == 0 { + version = "*" + } + cmd := command{ + Cmd: helmBin, + Args: []string{"search", "repo", r.Chart, "--version", version, "-l"}, + Description: "Validating [ " + r.Chart + " ] chart's version [ " + r.Version + " ] availability", + } + + if exitCode, result, _ := cmd.exec(debug, verbose); exitCode != 0 || strings.Contains(result, "No results found") { + c <- "Chart [ " + r.Chart + " ] with version [ " + r.Version + " ] is specified for " + + "app [" + app + "] but was not found" + return + } + } + } +} + +// getChartVersion fetches the lastest chart version matching the semantic versioning constraints. +// If chart is local, returns the given release version +func (r *release) getChartVersion() (string, string) { + if isLocalChart(r.Chart) { + return r.Version, "" + } + cmd := command{ + Cmd: helmBin, + Args: []string{"search", "repo", r.Chart, "--version", r.Version, "-o", "json"}, + Description: "Getting latest chart's version " + r.Chart + "-" + r.Version + "", + } + + var ( + exitCode int + result string + ) + + if exitCode, result, _ = cmd.exec(debug, verbose); exitCode != 0 { + return "", "Chart [ " + r.Chart + " ] with version [ " + r.Version + " ] is specified but not found in the helm repositories" + } + + chartVersions := make([]chartVersion, 0) + if err := json.Unmarshal([]byte(result), &chartVersions); err != nil { + log.Fatal(fmt.Sprint(err)) + } + + if len(chartVersions) < 1 { + return "", "Chart [ " + r.Chart + " ] with version [ " + r.Version + " ] is specified but not found in the helm repositories" + } else if len(chartVersions) > 1 { + return "", "Multiple versions of chart [ " + r.Chart + " ] with version [ " + r.Version + " ] found in the helm repositories" + } + return chartVersions[0].Version, "" +} + +// testRelease creates a Helm command to test a particular release. +func (r *release) test() { + cmd := command{ + Cmd: helmBin, + Args: []string{"test", "--namespace", r.Namespace, r.Name}, + Description: "Running tests for release [ " + r.Name + " ] in namespace [ " + r.Namespace + " ]", + } + outcome.addCommand(cmd, r.Priority, r) + logDecision("Release [ "+r.Name+" ] in namespace [ "+r.Namespace+" ] is required to be tested during installation", r.Priority, noop) +} + +// installRelease creates a Helm command to install a particular release in a particular namespace using a particular Tiller. +func (r *release) install() { + cmd := command{ + Cmd: helmBin, + Args: concat([]string{"install", r.Name, r.Chart, "--namespace", r.Namespace}, r.getValuesFiles(), []string{"--version", r.Version}, r.getSetValues(), r.getSetStringValues(), r.getWait(), r.getHelmFlags()), + Description: "Installing release [ " + r.Name + " ] in namespace [ " + r.Namespace + " ]", + } + outcome.addCommand(cmd, r.Priority, r) + logDecision("Release [ "+r.Name+" ] will be installed in [ "+r.Namespace+" ] namespace", r.Priority, create) + + if r.Test { + r.test() + } +} + +// uninstall deletes a release from a particular Tiller in a k8s cluster +func (r *release) uninstall() { + priority := r.Priority + if settings.ReverseDelete { + priority = priority * -1 + } + + cmd := command{ + Cmd: helmBin, + Args: concat([]string{"uninstall", "--namespace", r.Namespace, r.Name}, getDryRunFlags()), + Description: "Deleting release [ " + r.Name + " ] in namespace [ " + r.Namespace + " ]", + } + outcome.addCommand(cmd, priority, r) + logDecision(fmt.Sprintf("release [ %s ] is desired to be DELETED.", r.Name), r.Priority, delete) +} + +// diffRelease diffs an existing release with the specified values.yaml +func (r *release) diff() string { + exitCode := 0 + msg := "" + colorFlag := "" + diffContextFlag := []string{} + suppressDiffSecretsFlag := "" + if noColors { + colorFlag = "--no-color" + } + if diffContext != -1 { + diffContextFlag = []string{"--context", strconv.Itoa(diffContext)} + } + if suppressDiffSecrets { + suppressDiffSecretsFlag = "--suppress-secrets" + } + + cmd := command{ + Cmd: helmBin, + Args: concat([]string{"diff", colorFlag}, diffContextFlag, []string{suppressDiffSecretsFlag, "--namespace", r.Namespace, "upgrade", r.Name, r.Chart}, r.getValuesFiles(), []string{"--version", r.Version}, r.getSetValues(), r.getSetStringValues()), + Description: "Diffing release [ " + r.Name + " ] in namespace [ " + r.Namespace + " ]", + } + + if exitCode, msg, _ = cmd.exec(debug, verbose); exitCode != 0 { + log.Fatal(fmt.Sprintf("Command returned with exit code: %d. And error message: %s ", exitCode, msg)) + } else { + if (verbose || showDiff) && msg != "" { + fmt.Println(msg) + } + } + + return msg +} + +// upgradeRelease upgrades an existing release with the specified values.yaml +func (r *release) upgrade() { + var force string + if forceUpgrades { + force = "--force" + } + cmd := command{ + Cmd: helmBin, + Args: concat([]string{"upgrade", "--namespace", r.Namespace, r.Name, r.Chart}, r.getValuesFiles(), []string{"--version", r.Version, force}, r.getSetValues(), r.getSetStringValues(), r.getWait(), r.getHelmFlags()), + Description: "Upgrading release [ " + r.Name + " ] in namespace [ " + r.Namespace + " ]", + } + + outcome.addCommand(cmd, r.Priority, r) +} + +// reInstall purge deletes a release and reinstalls it. +// This is used when moving a release to another namespace or when changing the chart used for it. +func (r *release) reInstall(rs helmRelease) { + + delCmd := command{ + Cmd: helmBin, + Args: concat([]string{"delete", "--purge", r.Name}, getDryRunFlags()), + Description: "Deleting release [ " + r.Name + " ] in namespace [ " + r.Namespace + " ]", + } + outcome.addCommand(delCmd, r.Priority, r) + + installCmd := command{ + Cmd: helmBin, + Args: concat([]string{"install", r.Chart, "--version", r.Version, "-n", r.Name, "--namespace", r.Namespace}, r.getValuesFiles(), r.getSetValues(), r.getSetStringValues(), r.getWait(), r.getHelmFlags()), + Description: "Installing release [ " + r.Name + " ] in namespace [ " + r.Namespace + " ]", + } + outcome.addCommand(installCmd, r.Priority, r) +} + +// rollbackRelease evaluates if a rollback action needs to be taken for a given release. +// if the release is already deleted but from a different namespace than the one specified in input, +// it purge deletes it and create it in the specified namespace. +func (r *release) rollback(cs *currentState) { + rs, ok := (*cs)[fmt.Sprintf("%s-%s", r.Name, r.Namespace)] + if !ok { + return + } + + if r.Namespace == rs.Namespace { + + cmd := command{ + Cmd: helmBin, + Args: concat([]string{"rollback", r.Name, rs.getRevision()}, r.getWait(), r.getTimeout(), r.getNoHooks(), getDryRunFlags()), + Description: "Rolling back release [ " + r.Name + " ] in namespace [ " + r.Namespace + " ]", + } + outcome.addCommand(cmd, r.Priority, r) + r.upgrade() // this is to reflect any changes in values file(s) + logDecision("Release [ "+r.Name+" ] was deleted and is desired to be rolled back to "+ + "namespace [ "+r.Namespace+" ]", r.Priority, create) + } else { + r.reInstall(rs) + logDecision("Release [ "+r.Name+" ] is deleted BUT from namespace [ "+rs.Namespace+ + " ]. Will purge delete it from there and install it in namespace [ "+r.Namespace+" ]", r.Priority, create) + logDecision("WARNING: rolling back release [ "+r.Name+" ] from [ "+rs.Namespace+" ] to [ "+r.Namespace+ + " ] might not correctly connect to existing volumes. Check https://github.com/Praqma/helmsman/blob/master/docs/how_to/apps/moving_across_namespaces.md"+ + " for details if this release uses PV and PVC.", r.Priority, create) + + } +} + +// getNoHooks returns the no-hooks flag for install/upgrade commands +func (r *release) getNoHooks() []string { + if r.NoHooks { + return []string{"--no-hooks"} + } + return []string{} +} + +// getTimeout returns the timeout flag for install/upgrade commands +func (r *release) getTimeout() []string { + if r.Timeout != 0 { + return []string{"--timeout", strconv.Itoa(r.Timeout) + "s"} + } + return []string{} +} + +// getValuesFiles return partial install/upgrade release command to substitute the -f flag in Helm. +func (r *release) getValuesFiles() []string { + var fileList []string + + if r.ValuesFile != "" { + fileList = append(fileList, r.ValuesFile) + } else if len(r.ValuesFiles) > 0 { + fileList = append(fileList, r.ValuesFiles...) + } + + if r.SecretsFile != "" { + if !helmPluginExists("secrets") { + log.Fatal("helm secrets plugin is not installed/configured correctly. Aborting!") + } + if err := decryptSecret(r.SecretsFile); err != nil { + log.Fatal(err.Error()) + } + fileList = append(fileList, r.SecretsFile+".dec") + } else if len(r.SecretsFiles) > 0 { + if !helmPluginExists("secrets") { + log.Fatal("helm secrets plugin is not installed/configured correctly. Aborting!") + } + for i := 0; i < len(r.SecretsFiles); i++ { + if err := decryptSecret(r.SecretsFiles[i]); err != nil { + log.Fatal(err.Error()) + } + // if .dec extension is added before to the secret filename, don't add it again. + // This happens at upgrade time (where diff and upgrade both call this function) + if !isOfType(r.SecretsFiles[i], []string{".dec"}) { + r.SecretsFiles[i] = r.SecretsFiles[i] + ".dec" + } + } + fileList = append(fileList, r.SecretsFiles...) + } + + fileListArgs := []string{} + for _, file := range fileList { + fileListArgs = append(fileListArgs, "-f", file) + } + return fileListArgs +} + +// getSetValues returns --set params to be used with helm install/upgrade commands +func (r *release) getSetValues() []string { + result := []string{} + for k, v := range r.Set { + result = append(result, "--set", k+"="+strings.Replace(v, ",", "\\,", -1)+"") + } + return result +} + +// getSetStringValues returns --set-string params to be used with helm install/upgrade commands +func (r *release) getSetStringValues() []string { + result := []string{} + for k, v := range r.SetString { + result = append(result, "--set-string", k+"="+strings.Replace(v, ",", "\\,", -1)+"") + } + return result +} + +// getWait returns a partial helm command containing the helm wait flag (--wait) if the wait flag for the release was set to true +// Otherwise, retruns an empty string +func (r *release) getWait() []string { + result := []string{} + if r.Wait { + result = append(result, "--wait") + } + return result +} + +// getDesiredNamespace returns the namespace of a release +func (r *release) getDesiredNamespace() string { + return r.Namespace +} + +// getHelmFlags returns helm flags +func (r *release) getHelmFlags() []string { + var flags []string + + flags = append(flags, r.HelmFlags...) + return concat(r.getNoHooks(), r.getTimeout(), getDryRunFlags(), flags) +} + +func (r *release) checkChartDepUpdate() { + if updateDeps && isLocalChart(r.Chart) { + if ok, err := updateChartDep(r.Chart); !ok { + log.Fatal("helm dependency update failed: " + err) + } + } +} + // overrideNamespace overrides a release defined namespace with a new given one -func overrideNamespace(r *release, newNs string) { +func (r *release) overrideNamespace(newNs string) { log.Info("Overriding namespace for app: " + r.Name) r.Namespace = newNs } diff --git a/internal/app/release_test.go b/internal/app/release_test.go index fd5aad47..1af9d055 100644 --- a/internal/app/release_test.go +++ b/internal/app/release_test.go @@ -1,10 +1,31 @@ package app import ( + "fmt" + "os" "strings" "testing" ) +func setupTestCase(t *testing.T) func(t *testing.T) { + t.Log("setup test case") + os.MkdirAll(os.TempDir()+"/helmsman-tests/myapp", os.ModePerm) + os.MkdirAll(os.TempDir()+"/helmsman-tests/dir-with space/myapp", os.ModePerm) + cmd := command{ + Cmd: helmBin, + Args: []string{"create", os.TempDir() + "/helmsman-tests/dir-with space/myapp"}, + Description: "creating an empty local chart directory", + } + if exitCode, msg, _ := cmd.exec(debug, verbose); exitCode != 0 { + log.Fatal(fmt.Sprintf("Command returned with exit code: %d. And error message: %s ", exitCode, msg)) + } + + return func(t *testing.T) { + t.Log("teardown test case") + //os.RemoveAll("/tmp/helmsman-tests/") + } +} + func Test_validateRelease(t *testing.T) { st := state{ Metadata: make(map[string]string), @@ -270,7 +291,7 @@ func Test_validateRelease(t *testing.T) { names := make(map[string]map[string]bool) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, got1 := validateRelease("testApp", tt.args.r, names, tt.args.s) + got, got1 := tt.args.r.validate("testApp", names, tt.args.s) if got != tt.want { t.Errorf("validateRelease() got = %v, want %v", got, tt.want) } @@ -280,3 +301,361 @@ func Test_validateRelease(t *testing.T) { }) } } + +func Test_validateReleaseCharts(t *testing.T) { + type args struct { + apps map[string]*release + } + tests := []struct { + name string + targetFlag []string + groupFlag []string + args args + want bool + }{ + { + name: "test case 1: valid local path with no chart", + targetFlag: []string{}, + args: args{ + apps: map[string]*release{ + "app": &release{ + Name: "", + Description: "", + Namespace: "", + Enabled: true, + Chart: os.TempDir() + "/helmsman-tests/myapp", + Version: "", + ValuesFile: "", + ValuesFiles: []string{}, + SecretsFile: "", + SecretsFiles: []string{}, + Test: false, + Protected: false, + Wait: false, + Priority: 0, + Set: make(map[string]string), + SetString: make(map[string]string), + HelmFlags: []string{}, + NoHooks: false, + Timeout: 0, + }, + }, + }, + want: false, + }, { + name: "test case 2: invalid local path", + targetFlag: []string{}, + args: args{ + apps: map[string]*release{ + "app": &release{ + Name: "", + Description: "", + Namespace: "", + Enabled: true, + Chart: os.TempDir() + "/does-not-exist/myapp", + Version: "", + ValuesFile: "", + ValuesFiles: []string{}, + SecretsFile: "", + SecretsFiles: []string{}, + Test: false, + Protected: false, + Wait: false, + Priority: 0, + Set: make(map[string]string), + SetString: make(map[string]string), + HelmFlags: []string{}, + NoHooks: false, + Timeout: 0, + }, + }, + }, + want: false, + }, { + name: "test case 3: valid chart local path with whitespace", + targetFlag: []string{}, + args: args{ + apps: map[string]*release{ + "app": &release{ + Name: "", + Description: "", + Namespace: "", + Enabled: true, + Chart: os.TempDir() + "/helmsman-tests/dir-with space/myapp", + Version: "0.1.0", + ValuesFile: "", + ValuesFiles: []string{}, + SecretsFile: "", + SecretsFiles: []string{}, + Test: false, + Protected: false, + Wait: false, + Priority: 0, + Set: make(map[string]string), + SetString: make(map[string]string), + HelmFlags: []string{}, + NoHooks: false, + Timeout: 0, + }, + }, + }, + want: true, + }, { + name: "test case 4: valid chart from repo", + targetFlag: []string{}, + args: args{ + apps: map[string]*release{ + "app": &release{ + Name: "", + Description: "", + Namespace: "", + Enabled: true, + Chart: "stable/prometheus", + Version: "9.5.2", + ValuesFile: "", + ValuesFiles: []string{}, + SecretsFile: "", + SecretsFiles: []string{}, + Test: false, + Protected: false, + Wait: false, + Priority: 0, + Set: make(map[string]string), + SetString: make(map[string]string), + HelmFlags: []string{}, + NoHooks: false, + Timeout: 0, + }, + }, + }, + want: true, + }, { + name: "test case 5: invalid local path for chart ignored with -target flag, while other app was targeted", + targetFlag: []string{"notThisOne"}, + args: args{ + apps: map[string]*release{ + "app": &release{ + Name: "app", + Description: "", + Namespace: "", + Enabled: true, + Chart: os.TempDir() + "/does-not-exist/myapp", + Version: "", + ValuesFile: "", + ValuesFiles: []string{}, + SecretsFile: "", + SecretsFiles: []string{}, + Test: false, + Protected: false, + Wait: false, + Priority: 0, + Set: make(map[string]string), + SetString: make(map[string]string), + HelmFlags: []string{}, + NoHooks: false, + Timeout: 0, + }, + }, + }, + want: true, + }, { + name: "test case 6: invalid local path for chart included with -target flag", + targetFlag: []string{"app"}, + args: args{ + apps: map[string]*release{ + "app": &release{ + Name: "app", + Description: "", + Namespace: "", + Enabled: true, + Chart: os.TempDir() + "/does-not-exist/myapp", + Version: "", + ValuesFile: "", + ValuesFiles: []string{}, + SecretsFile: "", + SecretsFiles: []string{}, + Test: false, + Protected: false, + Wait: false, + Priority: 0, + Set: make(map[string]string), + SetString: make(map[string]string), + HelmFlags: []string{}, + NoHooks: false, + Timeout: 0, + }, + }, + }, + want: false, + }, + } + + teardownTestCase := setupTestCase(t) + defer teardownTestCase(t) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + targetMap = make(map[string]bool) + groupMap = make(map[string]bool) + for _, target := range tt.targetFlag { + targetMap[target] = true + } + for _, group := range tt.groupFlag { + groupMap[group] = true + } + err := validateReleaseCharts(tt.args.apps) + switch err.(type) { + case nil: + if tt.want != true { + t.Errorf("validateReleaseCharts() = %v, want error", err) + } + case error: + if tt.want != false { + t.Errorf("validateReleaseCharts() = %v, want nil", err) + } + } + }) + } +} + +func Test_getReleaseChartVersion(t *testing.T) { + // version string = the first semver-valid string after the last hypen in the chart string. + + type args struct { + r helmRelease + } + tests := []struct { + name string + args args + want string + }{ + { + name: "test case 1: there is a pre-release version", + args: args{ + r: helmRelease{ + Revision: 0, + Updated: HelmTime{}, + Status: "", + Chart: "elasticsearch-1.3.0-1", + Namespace: "", + }, + }, + want: "1.3.0-1", + }, { + name: "test case 2: normal case", + args: args{ + r: helmRelease{ + Revision: 0, + Updated: HelmTime{}, + Status: "", + Chart: "elasticsearch-1.3.0", + Namespace: "", + }, + }, + want: "1.3.0", + }, { + name: "test case 3: there is a hypen in the name", + args: args{ + r: helmRelease{ + Revision: 0, + Updated: HelmTime{}, + Status: "", + Chart: "elastic-search-1.3.0", + Namespace: "", + }, + }, + want: "1.3.0", + }, { + name: "test case 4: there is meta information", + args: args{ + r: helmRelease{ + Revision: 0, + Updated: HelmTime{}, + Status: "", + Chart: "elastic-search-1.3.0+meta.info", + Namespace: "", + }, + }, + want: "1.3.0+meta.info", + }, { + name: "test case 5: an invalid string", + args: args{ + r: helmRelease{ + Revision: 0, + Updated: HelmTime{}, + Status: "", + Chart: "foo", + Namespace: "", + }, + }, + want: "", + }, { + name: "test case 6: version includes v", + args: args{ + r: helmRelease{ + Revision: 0, + Updated: HelmTime{}, + Status: "", + Chart: "cert-manager-v0.5.2", + Namespace: "", + }, + }, + want: "v0.5.2", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Log(tt.want) + if got := tt.args.r.getChartVersion(); got != tt.want { + t.Errorf("getReleaseChartVersion() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getChartVersion(t *testing.T) { + // version string = the first semver-valid string after the last hypen in the chart string. + type args struct { + r *release + } + tests := []struct { + name string + args args + want string + }{ + { + name: "getChartVersion - local chart should return given release version", + args: args{ + r: &release{ + Name: "release1", + Namespace: "namespace", + Version: "1.0.0", + Chart: "./../../tests/chart-test", + Enabled: true, + }, + }, + want: "1.0.0", + }, + { + name: "getChartVersion - unknown chart should error", + args: args{ + r: &release{ + Name: "release1", + Namespace: "namespace", + Version: "1.0.0", + Chart: "random-chart-name-1f8147", + Enabled: true, + }, + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Log(tt.want) + got, _ := tt.args.r.getChartVersion() + if got != tt.want { + t.Errorf("getChartVersion() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/app/state.go b/internal/app/state.go index 031e8cc8..5f7832b2 100644 --- a/internal/app/state.go +++ b/internal/app/state.go @@ -139,7 +139,7 @@ func (s state) validate() error { names := make(map[string]map[string]bool) for appLabel, r := range s.Apps { - result, errMsg := validateRelease(appLabel, r, names, s) + result, errMsg := r.validate(appLabel, names, s) if !result { return errors.New("apps validation failed -- for app [" + appLabel + " ]. " + errMsg) } diff --git a/internal/app/utils.go b/internal/app/utils.go index 09c93396..c7633124 100644 --- a/internal/app/utils.go +++ b/internal/app/utils.go @@ -2,6 +2,7 @@ package app import ( "bytes" + "errors" "fmt" "io" "io/ioutil" @@ -450,19 +451,19 @@ func notifySlack(content string, url string, failure bool, executing bool) bool ] }`) req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr)) + if err != nil { + log.Errorf("Failed to send slack message: %w", err) + } req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { - log.Fatal("while sending notifications to slack" + err.Error()) + log.Errorf("Failed to send notification to slack: %w", err) } defer resp.Body.Close() - if resp.StatusCode == 200 { - return true - } - return false + return resp.StatusCode == 200 } // getBucketElements returns a map containing the bucket name and the file path inside the bucket @@ -508,10 +509,7 @@ func Indent(s, prefix string) string { // isLocalChart checks if a chart specified in the DSF is a local directory or not func isLocalChart(chart string) bool { _, err := os.Stat(chart) - if err == nil { - return true - } - return false + return err == nil } // concat appends all slices to a single slice @@ -536,3 +534,51 @@ func writeStringToFile(filename string, data string) error { } return file.Sync() } + +// decrypt a eyaml secret file +func decryptSecret(name string) error { + cmd := helmBin + args := []string{"secrets", "dec", name} + + if settings.EyamlEnabled { + cmd = "eyaml" + args = []string{"decrypt", "-f", name} + if settings.EyamlPrivateKeyPath != "" && settings.EyamlPublicKeyPath != "" { + args = append(args, []string{"--pkcs7-private-key", settings.EyamlPrivateKeyPath, "--pkcs7-public-key", settings.EyamlPublicKeyPath}...) + } + } + + command := command{ + Cmd: cmd, + Args: args, + Description: "Decrypting " + name, + } + + exitCode, output, stderr := command.exec(debug, false) + if !settings.EyamlEnabled { + _, fileNotFound := os.Stat(name + ".dec") + if fileNotFound != nil && !isOfType(name, []string{".dec"}) { + return errors.New(output) + } + } + + if exitCode != 0 { + return errors.New(output) + } else if stderr != "" { + return errors.New(output) + } + + if settings.EyamlEnabled { + var outfile string + if isOfType(name, []string{".dec"}) { + outfile = name + } else { + outfile = name + ".dec" + } + err := writeStringToFile(outfile, output) + if err != nil { + log.Fatal("Can't write [ " + outfile + " ] file") + } + } + return nil +} diff --git a/internal/app/utils_test.go b/internal/app/utils_test.go index 31def8e6..971abca0 100644 --- a/internal/app/utils_test.go +++ b/internal/app/utils_test.go @@ -369,6 +369,97 @@ func Test_readFile(t *testing.T) { } } +func Test_eyamlSecrets(t *testing.T) { + type args struct { + r *release + s *config + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "decryptSecrets - valid eyaml-based secrets decryption", + args: args{ + s: &config{ + EyamlEnabled: true, + EyamlPublicKeyPath: "./../../tests/keys/public_key.pkcs7.pem", + EyamlPrivateKeyPath: "./../../tests/keys/private_key.pkcs7.pem", + }, + r: &release{ + Name: "release1", + Namespace: "namespace", + Version: "1.0.0", + Enabled: true, + SecretsFile: "./../../tests/secrets/valid_eyaml_secrets.yaml", + }, + }, + want: true, + }, + { + name: "decryptSecrets - not existing eyaml-based secrets file", + args: args{ + s: &config{ + EyamlEnabled: true, + EyamlPublicKeyPath: "./../../tests/keys/public_key.pkcs7.pem", + EyamlPrivateKeyPath: "./../../tests/keys/private_key.pkcs7.pem", + }, + r: &release{ + Name: "release1", + Namespace: "namespace", + Version: "1.0.0", + Enabled: true, + SecretsFile: "./../../tests/secrets/invalid_eyaml_secrets.yaml", + }, + }, + want: false, + }, + { + name: "decryptSecrets - not existing eyaml key", + args: args{ + s: &config{ + EyamlEnabled: true, + EyamlPublicKeyPath: "./../../tests/keys/public_key.pkcs7.pem2", + EyamlPrivateKeyPath: "./../../tests/keys/private_key.pkcs7.pem", + }, + r: &release{ + Name: "release1", + Namespace: "namespace", + Version: "1.0.0", + Enabled: true, + SecretsFile: "./../../tests/secrets/valid_eyaml_secrets.yaml", + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Log(tt.want) + defaultSettings := settings + defer func() { settings = defaultSettings }() + settings.EyamlEnabled = tt.args.s.EyamlEnabled + settings.EyamlPublicKeyPath = tt.args.s.EyamlPublicKeyPath + settings.EyamlPrivateKeyPath = tt.args.s.EyamlPrivateKeyPath + err := decryptSecret(tt.args.r.SecretsFile) + switch err.(type) { + case nil: + if tt.want != true { + t.Errorf("decryptSecret() = %v, want error", err) + } + case error: + if tt.want != false { + t.Errorf("decryptSecret() = %v, want nil", err) + } + } + if _, err := os.Stat(tt.args.r.SecretsFile + ".dec"); err == nil { + defer deleteFile(tt.args.r.SecretsFile + ".dec") + } + }) + } +} + // func Test_printHelp(t *testing.T) { // tests := []struct { // name string diff --git a/release-notes.md b/release-notes.md index 174da7e4..66731b64 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,14 +1,15 @@ -# v3.0.0-beta1 +# v3.0.0-beta2 This is a major release to support Helm v3. It is recommended you read the [Helm 3 migration guide](https://helm.sh/docs/topics/v2_v3_migration/) before using this release. > Starting from this release, support for Helmsman v1.x will be limited to bug fixes. -The following are the most important changes: +The following are the most important changes: - A new and improved logger. - Restructuring the code. -- Introducing the `context` stanza to define a context for each DSF. More details [here](docs/misc/merge_desired_state_files). +- Parallelized decision making +- Introducing the `context` stanza to define a context for each DSF. More details [here](docs/misc/merge_desired_state_files). - Deprecating all the DSF stanzas related to Tiller. - Deprecating the `purge` option for releases. - The default value for `storageBackend` is now `secret`.