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

@ -100,6 +100,9 @@ helmDefaults:
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

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)