Support for createNamespace (#1226)

- 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 #891
Resolves #1140
This commit is contained in:
Craig Dunford 2020-04-25 21:41:40 -04:00 committed by GitHub
parent b1190508b2
commit eeb61e6174
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 270 additions and 26 deletions

View File

@ -69,7 +69,7 @@ repositories:
# context: kube-context # this directive is deprecated, please consider using helmDefaults.kubeContext # 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. # 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. # See the helm usage (helm SUBCOMMAND -h) for more info on default values when those flags aren't provided.
helmDefaults: helmDefaults:
@ -91,15 +91,18 @@ helmDefaults:
# forces resource update through delete/recreate if needed (default false) # forces resource update through delete/recreate if needed (default false)
force: false force: false
# enable TLS for request to Tiller (default false) # enable TLS for request to Tiller (default false)
tls: true tls: true
# path to TLS CA certificate file (default "$HELM_HOME/ca.pem") # path to TLS CA certificate file (default "$HELM_HOME/ca.pem")
tlsCACert: "path/to/ca.pem" tlsCACert: "path/to/ca.pem"
# path to TLS certificate file (default "$HELM_HOME/cert.pem") # path to TLS certificate file (default "$HELM_HOME/cert.pem")
tlsCert: "path/to/cert.pem" tlsCert: "path/to/cert.pem"
# path to TLS key file (default "$HELM_HOME/key.pem") # path to TLS key file (default "$HELM_HOME/key.pem")
tlsKey: "path/to/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 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. # The desired states of Helm releases.
# #
@ -108,6 +111,7 @@ releases:
# Published chart example # Published chart example
- name: vault # name of this release - name: vault # name of this release
namespace: vault # target namespace namespace: vault # target namespace
createNamespace: true # helm 3.2+ automatically create release namespace (default true)
labels: # Arbitrary key value pairs for filtering releases labels: # Arbitrary key value pairs for filtering releases
foo: bar foo: bar
chart: roboll/vault-secret-manager # the chart being installed to create this release, referenced by `repository/chart` syntax chart: roboll/vault-secret-manager # the chart being installed to create this release, referenced by `repository/chart` syntax
@ -152,21 +156,21 @@ releases:
value: {{ .Namespace }} value: {{ .Namespace }}
# will attempt to decrypt it using helm-secrets plugin # will attempt to decrypt it using helm-secrets plugin
secrets: secrets:
- vault_secret.yaml - vault_secret.yaml
# Override helmDefaults options for verify, wait, timeout, recreatePods and force. # Override helmDefaults options for verify, wait, timeout, recreatePods and force.
verify: true verify: true
wait: true wait: true
timeout: 60 timeout: 60
recreatePods: true recreatePods: true
force: false force: false
# set `false` to uninstall this release on sync. (default true) # set `false` to uninstall this release on sync. (default true)
installed: true installed: true
# restores previous state in case of failed release (default false) # 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) # when true, cleans up any new resources created during a failed release (default false)
cleanupOnFail: false cleanupOnFail: false
# name of the tiller namespace (default "") # name of the tiller namespace (default "")
tillerNamespace: vault tillerNamespace: vault
# if true, will use the helm-tiller plugin (default false) # if true, will use the helm-tiller plugin (default false)
tillerless: false tillerless: false
# enable TLS for request to Tiller (default false) # enable TLS for request to Tiller (default false)
@ -280,7 +284,7 @@ bases:
# 'helmfile template' renders releases locally without querying an actual cluster, # 'helmfile template' renders releases locally without querying an actual cluster,
# and in this case `.Capabilities.APIVersions` cannot be populated. # and in this case `.Capabilities.APIVersions` cannot be populated.
# When a chart queries for a specific CRD, this can lead to unexpected results. # 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: # Configure a fixed list of api versions to pass to 'helm template' via the --api-versions flag:
apiVersions: apiVersions:
- example/v1 - example/v1

View File

@ -2168,6 +2168,14 @@ func (helm *mockHelmExec) IsHelm3() bool {
return false 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) { func TestTemplate_SingleStateFile(t *testing.T) {
files := map[string]string{ files := map[string]string{
"/path/to/helmfile.yaml": ` "/path/to/helmfile.yaml": `

View File

@ -82,3 +82,13 @@ func (helm *noCallHelmExec) IsHelm3() bool {
helm.doPanic() helm.doPanic()
return false 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
}

View File

@ -30,6 +30,7 @@ type Helm struct {
Diffed []Release Diffed []Release
FailOnUnexpectedDiff bool FailOnUnexpectedDiff bool
FailOnUnexpectedList bool FailOnUnexpectedList bool
Version *helmexec.Version
UpdateDepsCallbacks map[string]func(string) error UpdateDepsCallbacks map[string]func(string) error
@ -161,6 +162,22 @@ func (helm *Helm) IsHelm3() bool {
return false 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()) { func (helm *Helm) sync(m *sync.Mutex, f func()) {
if m != nil { if m != nil {
m.Lock() m.Lock()

View File

@ -6,6 +6,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -21,7 +22,7 @@ type decryptedSecret struct {
type execer struct { type execer struct {
helmBinary string helmBinary string
isHelm3 bool version Version
runner Runner runner Runner
logger *zap.SugaredLogger logger *zap.SugaredLogger
kubeContext string kubeContext string
@ -47,25 +48,62 @@ func NewLogger(writer io.Writer, logLevel string) *zap.SugaredLogger {
return zap.New(core).Sugar() return zap.New(core).Sugar()
} }
func detectHelm3(helmBinary string, logger *zap.SugaredLogger, runner Runner) bool { func getHelmVersion(helmBinary string, logger *zap.SugaredLogger, runner Runner) Version {
// Support explicit opt-in via environment variable
if os.Getenv("HELMFILE_HELM3") != "" {
return true
}
// Autodetect from `helm verison` // Autodetect from `helm verison`
bytes, err := runner.Execute(helmBinary, []string{"version", "--client", "--short"}, nil) bytes, err := runner.Execute(helmBinary, []string{"version", "--client", "--short"}, nil)
if err != nil { if err != nil {
panic(err) 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 // New for running helm commands
func New(helmBinary string, logger *zap.SugaredLogger, kubeContext string, runner Runner) *execer { func New(helmBinary string, logger *zap.SugaredLogger, kubeContext string, runner Runner) *execer {
return &execer{ return &execer{
helmBinary: helmBinary, helmBinary: helmBinary,
isHelm3: detectHelm3(helmBinary, logger, runner), version: getHelmVersion(helmBinary, logger, runner),
logger: logger, logger: logger,
kubeContext: kubeContext, kubeContext: kubeContext,
runner: runner, runner: runner,
@ -349,5 +387,13 @@ func (helm *execer) write(out []byte) {
} }
func (helm *execer) IsHelm3() bool { 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
} }

View File

@ -528,4 +528,45 @@ func Test_IsHelm3(t *testing.T) {
if !helm.IsHelm3() { if !helm.IsHelm3() {
t.Error("helmexec.IsHelm3() - Failed to detect Helm 3") 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")
}
} }

View File

@ -1,5 +1,12 @@
package helmexec package helmexec
// Version represents the version of helm
type Version struct {
Major int
Minor int
Patch int
}
// Interface for executing helm commands // Interface for executing helm commands
type Interface interface { type Interface interface {
SetExtraArgs(args ...string) SetExtraArgs(args ...string)
@ -20,6 +27,8 @@ type Interface interface {
List(context HelmContext, filter string, flags ...string) (string, error) List(context HelmContext, filter string, flags ...string) (string, error)
DecryptSecret(context HelmContext, name string, flags ...string) (string, error) DecryptSecret(context HelmContext, name string, flags ...string) (string, error)
IsHelm3() bool IsHelm3() bool
GetVersion() Version
IsVersionAtLeast(major int, minor int) bool
} }
type DependencyUpdater interface { type DependencyUpdater interface {

View File

@ -114,6 +114,8 @@ type HelmSpec struct {
CleanupOnFail bool `yaml:"cleanupOnFail,omitempty"` CleanupOnFail bool `yaml:"cleanupOnFail,omitempty"`
// HistoryMax, limit the maximum number of revisions saved per release. Use 0 for no limit (default 10) // HistoryMax, limit the maximum number of revisions saved per release. Use 0 for no limit (default 10)
HistoryMax *int `yaml:"historyMax,omitempty"` 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"` TLS bool `yaml:"tls"`
TLSCACert string `yaml:"tlsCACert,omitempty"` TLSCACert string `yaml:"tlsCACert,omitempty"`
@ -158,6 +160,8 @@ type ReleaseSpec struct {
HistoryMax *int `yaml:"historyMax,omitempty"` 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, 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"` 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. // 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". // The default value for MissingFileHandler is "Error".
@ -1635,6 +1639,12 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp
flags = append(flags, "--cleanup-on-fail") 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) flags = st.appendConnectionFlags(flags, release)
var err error var err error

View File

@ -163,6 +163,7 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
version *helmexec.Version
defaults HelmSpec defaults HelmSpec
release *ReleaseSpec release *ReleaseSpec
want []string want []string
@ -573,6 +574,101 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
"--tls-ca-cert", "ca.pem", "--tls-ca-cert", "ca.pem",
}, },
}, },
{
name: "create-namespace-default-helm3.2",
defaults: HelmSpec{
Verify: false,
},
version: &helmexec.Version{
Major: 3,
Minor: 2,
Patch: 0,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--create-namespace",
"--namespace", "test-namespace",
},
},
{
name: "create-namespace-disabled-helm3.2",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: &helmexec.Version{
Major: 3,
Minor: 2,
Patch: 0,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "create-namespace-release-override-enabled-helm3.2",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: &helmexec.Version{
Major: 3,
Minor: 2,
Patch: 0,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &enable,
},
want: []string{
"--version", "0.1",
"--create-namespace",
"--namespace", "test-namespace",
},
},
{
name: "create-namespace-release-override-disabled-helm3.2",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: &helmexec.Version{
Major: 3,
Minor: 2,
Patch: 0,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
} }
for i := range tests { for i := range tests {
tt := tests[i] tt := tests[i]
@ -584,10 +680,13 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
HelmDefaults: tt.defaults, HelmDefaults: tt.defaults,
valsRuntime: valsRuntime, valsRuntime: valsRuntime,
} }
helm := helmexec.New("helm", logger, "default", &mockRunner{}) helm := &exectest.Helm{
Version: tt.version,
}
args, err := state.flagsForUpgrade(helm, tt.release, 0) args, err := state.flagsForUpgrade(helm, tt.release, 0)
if err != nil { if err != nil {
t.Errorf("unexpected error flagsForUpgade: %v", err) t.Errorf("unexpected error flagsForUpgrade: %v", err)
} }
if !reflect.DeepEqual(args, tt.want) { if !reflect.DeepEqual(args, tt.want) {
t.Errorf("flagsForUpgrade returned = %v, want %v", args, tt.want) t.Errorf("flagsForUpgrade returned = %v, want %v", args, tt.want)