Skip to content

Commit

Permalink
Support for createNamespace
Browse files Browse the repository at this point in the history
- createNamespace is a new attribute that can be added to helmDefaults
  or an individual release to enforce the creation of a release namespace
  during sync if the namespace does not exist. This leverages helm's
  (3.2+) --create-namespace flag for the install/upgrade command. If
  running helm < 3.2, the createNamespace attribute has no effect.

Resolves roboll#891
Resolves roboll#1140
  • Loading branch information
cdunford committed Apr 26, 2020
1 parent b119050 commit 0d8812c
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 26 deletions.
34 changes: 19 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ repositories:

# context: kube-context # this directive is deprecated, please consider using helmDefaults.kubeContext

# Default values to set for args along with dedicated keys that can be set by contributors, cli args take precedence over these.
# Default values to set for args along with dedicated keys that can be set by contributors, cli args take precedence over these.
# In other words, unset values results in no flags passed to helm.
# See the helm usage (helm SUBCOMMAND -h) for more info on default values when those flags aren't provided.
helmDefaults:
Expand All @@ -91,15 +91,18 @@ helmDefaults:
# forces resource update through delete/recreate if needed (default false)
force: false
# enable TLS for request to Tiller (default false)
tls: true
tls: true
# path to TLS CA certificate file (default "$HELM_HOME/ca.pem")
tlsCACert: "path/to/ca.pem"
# path to TLS certificate file (default "$HELM_HOME/cert.pem")
tlsCert: "path/to/cert.pem"
# path to TLS key file (default "$HELM_HOME/key.pem")
tlsKey: "path/to/key.pem"
# limit the maximum number of revisions saved per release. Use 0 for no limit. (default 10)
# limit the maximum number of revisions saved per release. Use 0 for no limit. (default 10)
historyMax: 10
# when using helm 3.2+, automatically create release namespaces if they do not exist (default true)
createNamespace: true


# The desired states of Helm releases.
#
Expand All @@ -108,6 +111,7 @@ releases:
# Published chart example
- name: vault # name of this release
namespace: vault # target namespace
createNamespace: true # helm 3.2+ automatically create release namespace (default true)
labels: # Arbitrary key value pairs for filtering releases
foo: bar
chart: roboll/vault-secret-manager # the chart being installed to create this release, referenced by `repository/chart` syntax
Expand Down Expand Up @@ -152,21 +156,21 @@ releases:
value: {{ .Namespace }}
# will attempt to decrypt it using helm-secrets plugin
secrets:
- vault_secret.yaml
# Override helmDefaults options for verify, wait, timeout, recreatePods and force.
verify: true
wait: true
timeout: 60
recreatePods: true
force: false
- vault_secret.yaml
# Override helmDefaults options for verify, wait, timeout, recreatePods and force.
verify: true
wait: true
timeout: 60
recreatePods: true
force: false
# set `false` to uninstall this release on sync. (default true)
installed: true
# restores previous state in case of failed release (default false)
atomic: true
atomic: true
# when true, cleans up any new resources created during a failed release (default false)
cleanupOnFail: false
# name of the tiller namespace (default "")
tillerNamespace: vault
cleanupOnFail: false
# name of the tiller namespace (default "")
tillerNamespace: vault
# if true, will use the helm-tiller plugin (default false)
tillerless: false
# enable TLS for request to Tiller (default false)
Expand Down Expand Up @@ -280,7 +284,7 @@ bases:
# 'helmfile template' renders releases locally without querying an actual cluster,
# and in this case `.Capabilities.APIVersions` cannot be populated.
# When a chart queries for a specific CRD, this can lead to unexpected results.
#
#
# Configure a fixed list of api versions to pass to 'helm template' via the --api-versions flag:
apiVersions:
- example/v1
Expand Down
8 changes: 8 additions & 0 deletions pkg/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2168,6 +2168,14 @@ func (helm *mockHelmExec) IsHelm3() bool {
return false
}

func (helm *mockHelmExec) GetVersion() helmexec.Version {
return helmexec.Version{}
}

func (helm *mockHelmExec) IsVersionAtLeast(major int, minor int) bool {
return false
}

func TestTemplate_SingleStateFile(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
Expand Down
10 changes: 10 additions & 0 deletions pkg/app/mocks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,13 @@ func (helm *noCallHelmExec) IsHelm3() bool {
helm.doPanic()
return false
}

func (helm *noCallHelmExec) GetVersion() helmexec.Version {
helm.doPanic()
return helmexec.Version{}
}

func (helm *noCallHelmExec) IsVersionAtLeast(major int, minor int) bool {
helm.doPanic()
return false
}
17 changes: 17 additions & 0 deletions pkg/exectest/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Helm struct {
Diffed []Release
FailOnUnexpectedDiff bool
FailOnUnexpectedList bool
Version *helmexec.Version

UpdateDepsCallbacks map[string]func(string) error

Expand Down Expand Up @@ -161,6 +162,22 @@ func (helm *Helm) IsHelm3() bool {
return false
}

func (helm *Helm) GetVersion() helmexec.Version {
if helm.Version != nil {
return *helm.Version
}

return helmexec.Version{}
}

func (helm *Helm) IsVersionAtLeast(major int, minor int) bool {
if helm.Version == nil {
return false
}

return helm.Version.Major >= major && minor >= helm.Version.Minor
}

func (helm *Helm) sync(m *sync.Mutex, f func()) {
if m != nil {
m.Lock()
Expand Down
64 changes: 55 additions & 9 deletions pkg/helmexec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
Expand All @@ -21,7 +22,7 @@ type decryptedSecret struct {

type execer struct {
helmBinary string
isHelm3 bool
version Version
runner Runner
logger *zap.SugaredLogger
kubeContext string
Expand All @@ -47,25 +48,62 @@ func NewLogger(writer io.Writer, logLevel string) *zap.SugaredLogger {
return zap.New(core).Sugar()
}

func detectHelm3(helmBinary string, logger *zap.SugaredLogger, runner Runner) bool {
// Support explicit opt-in via environment variable
if os.Getenv("HELMFILE_HELM3") != "" {
return true
}
func getHelmVersion(helmBinary string, logger *zap.SugaredLogger, runner Runner) Version {

// Autodetect from `helm verison`
bytes, err := runner.Execute(helmBinary, []string{"version", "--client", "--short"}, nil)
if err != nil {
panic(err)
}
return strings.HasPrefix(string(bytes), "v3.")

if bytes == nil || len(bytes) == 0 {
return Version{}
}

re := regexp.MustCompile("v(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)")
matches := re.FindStringSubmatch(string(bytes))

result := make(map[string]string)
for i, name := range re.SubexpNames() {
result[name] = matches[i]
}

major, err := strconv.Atoi(result["major"])
if err != nil {
panic(err)
}

minor, err := strconv.Atoi(result["minor"])
if err != nil {
panic(err)
}

patch, err := strconv.Atoi(result["patch"])
if err != nil {
panic(err)
}

// Support explicit helm3 opt-in via environment variable
if os.Getenv("HELMFILE_HELM3") != "" && major < 3 {
return Version{
Major: 3,
Minor: 0,
Patch: 0,
}
}

return Version{
Major: major,
Minor: minor,
Patch: patch,
}
}

// New for running helm commands
func New(helmBinary string, logger *zap.SugaredLogger, kubeContext string, runner Runner) *execer {
return &execer{
helmBinary: helmBinary,
isHelm3: detectHelm3(helmBinary, logger, runner),
version: getHelmVersion(helmBinary, logger, runner),
logger: logger,
kubeContext: kubeContext,
runner: runner,
Expand Down Expand Up @@ -349,5 +387,13 @@ func (helm *execer) write(out []byte) {
}

func (helm *execer) IsHelm3() bool {
return helm.isHelm3
return helm.version.Major == 3
}

func (helm *execer) GetVersion() Version {
return helm.version
}

func (helm *execer) IsVersionAtLeast(major int, minor int) bool {
return helm.version.Major >= major && helm.version.Minor >= minor
}
41 changes: 41 additions & 0 deletions pkg/helmexec/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -528,4 +528,45 @@ func Test_IsHelm3(t *testing.T) {
if !helm.IsHelm3() {
t.Error("helmexec.IsHelm3() - Failed to detect Helm 3")
}

os.Setenv("HELMFILE_HELM3", "1")
helm2Runner = mockRunner{output: []byte("Client: v2.16.0+ge13bc94\n")}
helm = New("helm", NewLogger(os.Stdout, "info"), "dev", &helm2Runner)
if !helm.IsHelm3() {
t.Error("helmexec.IsHelm3() - Helm3 not detected when HELMFILE_HELM3 is set")
}
os.Setenv("HELMFILE_HELM3", "")
}

func Test_GetVersion(t *testing.T) {
helm2Runner := mockRunner{output: []byte("Client: v2.16.1+ge13bc94\n")}
helm := New("helm", NewLogger(os.Stdout, "info"), "dev", &helm2Runner)
ver := helm.GetVersion()
if ver.Major != 2 || ver.Minor != 16 || ver.Patch != 1 {
t.Error(fmt.Sprintf("helmexec.GetVersion - did not detect correct Helm2 version; it was: %+v", ver))
}

helm3Runner := mockRunner{output: []byte("v3.2.4+ge29ce2a\n")}
helm = New("helm", NewLogger(os.Stdout, "info"), "dev", &helm3Runner)
ver = helm.GetVersion()
if ver.Major != 3 || ver.Minor != 2 || ver.Patch != 4 {
t.Error(fmt.Sprintf("helmexec.GetVersion - did not detect correct Helm3 version; it was: %+v", ver))
}
}

func Test_IsVersionAtLeast(t *testing.T) {
helm2Runner := mockRunner{output: []byte("Client: v2.16.1+ge13bc94\n")}
helm := New("helm", NewLogger(os.Stdout, "info"), "dev", &helm2Runner)
if !helm.IsVersionAtLeast(2, 1) {
t.Error("helmexec.IsVersionAtLeast - 2.16.1 not atleast 2.1")
}

if helm.IsVersionAtLeast(2, 19) {
t.Error("helmexec.IsVersionAtLeast - 2.16.1 is atleast 2.19")
}

if helm.IsVersionAtLeast(3, 2) {
t.Error("helmexec.IsVersionAtLeast - 2.16.1 is atleast 3.2")
}

}
9 changes: 9 additions & 0 deletions pkg/helmexec/helmexec.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package helmexec

// Version represents the version of helm
type Version struct {
Major int
Minor int
Patch int
}

// Interface for executing helm commands
type Interface interface {
SetExtraArgs(args ...string)
Expand All @@ -20,6 +27,8 @@ type Interface interface {
List(context HelmContext, filter string, flags ...string) (string, error)
DecryptSecret(context HelmContext, name string, flags ...string) (string, error)
IsHelm3() bool
GetVersion() Version
IsVersionAtLeast(major int, minor int) bool
}

type DependencyUpdater interface {
Expand Down
10 changes: 10 additions & 0 deletions pkg/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ type HelmSpec struct {
CleanupOnFail bool `yaml:"cleanupOnFail,omitempty"`
// HistoryMax, limit the maximum number of revisions saved per release. Use 0 for no limit (default 10)
HistoryMax *int `yaml:"historyMax,omitempty"`
// CreateNamespace, when set to true (default), --create-namespace is passed to helm3 on install/upgrade (ignored for helm2)
CreateNamespace *bool `yaml:"createNamespace,omitempty"`

TLS bool `yaml:"tls"`
TLSCACert string `yaml:"tlsCACert,omitempty"`
Expand Down Expand Up @@ -158,6 +160,8 @@ type ReleaseSpec struct {
HistoryMax *int `yaml:"historyMax,omitempty"`
// Condition, when set, evaluate the mapping specified in this string to a boolean which decides whether or not to process the release
Condition string `yaml:"condition,omitempty"`
// CreateNamespace, when set to true (default), --create-namespace is passed to helm3 on install (ignored for helm2)
CreateNamespace *bool `yaml:"createNamespace,omitempty"`

// MissingFileHandler is set to either "Error" or "Warn". "Error" instructs helmfile to fail when unable to find a values or secrets file. When "Warn", it prints the file and continues.
// The default value for MissingFileHandler is "Error".
Expand Down Expand Up @@ -1635,6 +1639,12 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp
flags = append(flags, "--cleanup-on-fail")
}

if helm.IsVersionAtLeast(3, 2) &&
(release.CreateNamespace != nil && *release.CreateNamespace ||
release.CreateNamespace == nil && (st.HelmDefaults.CreateNamespace == nil || *st.HelmDefaults.CreateNamespace)) {
flags = append(flags, "--create-namespace")
}

flags = st.appendConnectionFlags(flags, release)

var err error
Expand Down
Loading

0 comments on commit 0d8812c

Please sign in to comment.