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:
parent
b1190508b2
commit
eeb61e6174
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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": `
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue