diff --git a/go.mod b/go.mod index 696c2b6d2..068da2fa5 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/stretchr/testify v1.4.0 github.com/xeipuuv/gojsonschema v1.1.0 golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d + honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc // indirect k8s.io/api v0.17.2 k8s.io/apiextensions-apiserver v0.17.2 k8s.io/apimachinery v0.17.2 diff --git a/pkg/action/install.go b/pkg/action/install.go index c4ee50857..7d7459781 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -38,6 +38,7 @@ import ( "helm.sh/helm/v3/pkg/downloader" "helm.sh/helm/v3/pkg/engine" "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/kube" kubefake "helm.sh/helm/v3/pkg/kube/fake" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/releaseutil" @@ -259,6 +260,11 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. return rel, nil } + // Check if chart contains definition for namespace resource which matches release namespace + if resources, err = checkReleaseNamespace(i.cfg.KubeClient, resources, i.Namespace); err != nil { + return i.failRelease(rel, err) + } + // If Replace is true, we need to supercede the last release. if i.Replace { if err := i.replaceRelease(rel); err != nil { @@ -322,6 +328,25 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. return rel, nil } +// checkReleaseNamepace checks if the release namespace is included as template resource. +// if it is: +// 1. attempt to create a namespace returning any/all errors +// 2. upon successful creation - remove release namespace template resource from the provided list +// if it is not: noop +func checkReleaseNamespace(kubeClient kube.Interface, resources kube.ResourceList, namespace string) (kube.ResourceList, error) { + for i, r := range resources { + ok := r.Object.GetObjectKind() + if gvk := ok.GroupVersionKind(); gvk.Kind == "Namespace" && r.Name == namespace { + // If matching namespace resource found create namespace and remove this resource from the resource list + if _, err := kubeClient.Create(kube.ResourceList{r}); err != nil { + return resources, err + } + return append(resources[:i], resources[i+1:]...), nil + } + } + return resources, nil +} + func (i *Install) failRelease(rel *release.Release, err error) (*release.Release, error) { rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error())) if i.Atomic { diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index ba350819d..434d2ff4e 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -26,10 +26,18 @@ import ( "strings" "testing" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/resource" "helm.sh/helm/v3/internal/test" "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/kube" kubefake "helm.sh/helm/v3/pkg/kube/fake" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/storage/driver" @@ -614,3 +622,127 @@ func TestNameAndChartGenerateName(t *testing.T) { }) } } + +func Test_checkReleaseNamespace(t *testing.T) { + type args struct { + kubeClient kube.Interface + resources kube.ResourceList + namespace string + } + type want struct { + err error + resources kube.ResourceList + } + + testResource := func(o runtime.Object, name string) *resource.Info { + gvk := o.GetObjectKind().GroupVersionKind() + return &resource.Info{ + Name: name, + Mapping: &meta.RESTMapping{ + Resource: schema.GroupVersionResource{Group: gvk.Group, Version: gvk.Version, Resource: gvk.Kind}, + }, + Object: o, + } + } + + tests := map[string]struct { + args args + want want + }{ + "NilResourceList": { + args: args{ + kubeClient: nil, + resources: nil, + namespace: "test", + }, + want: want{ + resources: nil, + }, + }, + "EmptyResourceList": { + args: args{ + kubeClient: nil, + resources: kube.ResourceList{}, + namespace: "test", + }, + want: want{ + resources: kube.ResourceList{}, + }, + }, + "ReleaseNamespaceIsNotDefined": { + args: args{ + kubeClient: nil, + resources: kube.ResourceList{testResource(&corev1.Pod{}, "test")}, + namespace: "test", + }, + want: want{ + resources: kube.ResourceList{testResource(&corev1.Pod{}, "test")}, + }, + }, + "ReleaseNamespaceIsDefinedButNotTheRelease": { + args: args{ + kubeClient: nil, + resources: kube.ResourceList{ + testResource(&corev1.Pod{}, "test"), + testResource(&corev1.Namespace{TypeMeta: metav1.TypeMeta{Kind: "Namespace"}}, "foo"), + }, + namespace: "test", + }, + want: want{ + resources: kube.ResourceList{ + testResource(&corev1.Pod{}, "test"), + testResource(&corev1.Namespace{TypeMeta: metav1.TypeMeta{Kind: "Namespace"}}, "foo"), + }, + }, + }, + "ReleaseNamespaceIsDefinedButFailedToCreate": { + args: args{ + kubeClient: &kubefake.FailingKubeClient{CreateError: errors.New("create-error")}, + resources: kube.ResourceList{ + testResource(&corev1.Pod{}, "test"), + testResource(&corev1.Namespace{TypeMeta: metav1.TypeMeta{Kind: "Namespace"}}, "foo"), + testResource(&corev1.Namespace{TypeMeta: metav1.TypeMeta{Kind: "Namespace"}}, "test"), + }, + namespace: "test", + }, + want: want{ + err: errors.New("create-error"), + resources: kube.ResourceList{ + testResource(&corev1.Pod{}, "test"), + testResource(&corev1.Namespace{TypeMeta: metav1.TypeMeta{Kind: "Namespace"}}, "foo"), + testResource(&corev1.Namespace{TypeMeta: metav1.TypeMeta{Kind: "Namespace"}}, "test"), + }, + }, + }, + "ReleaseNamespaceIsDefinedSuccessfulCreate": { + args: args{ + kubeClient: &kubefake.PrintingKubeClient{Out: ioutil.Discard}, + resources: kube.ResourceList{ + testResource(&corev1.Pod{}, "test"), + testResource(&corev1.Namespace{TypeMeta: metav1.TypeMeta{Kind: "Namespace"}}, "foo"), + testResource(&corev1.Namespace{TypeMeta: metav1.TypeMeta{Kind: "Namespace"}}, "test"), + }, + namespace: "test", + }, + want: want{ + resources: kube.ResourceList{ + testResource(&corev1.Pod{}, "test"), + testResource(&corev1.Namespace{TypeMeta: metav1.TypeMeta{Kind: "Namespace"}}, "foo"), + }, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + is := assert.New(t) + res, err := checkReleaseNamespace(tt.args.kubeClient, tt.args.resources, tt.args.namespace) + + if tt.want.err == nil { + is.Nil(err) + } else { + is.EqualError(err, tt.want.err.Error()) + } + is.Equal(tt.want.resources, res) + }) + } +}