From 52becafe38fb906391cbc9dbdb12eef3d4b45aa4 Mon Sep 17 00:00:00 2001 From: Mujib Ahasan Date: Sun, 1 Feb 2026 01:44:55 +0530 Subject: [PATCH 1/4] Check namespae before creating it Signed-off-by: Mujib Ahasan --- pkg/action/install.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/pkg/action/install.go b/pkg/action/install.go index 580b8a0cb..51a524d40 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -447,11 +447,22 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st return nil, err } - if _, err := i.cfg.KubeClient.Create( - resourceList, - kube.ClientCreateOptionServerSideApply(i.ServerSideApply, false)); err != nil && !apierrors.IsAlreadyExists(err) { + namespaceExist := false + _, err = i.cfg.KubeClient.Get(resourceList, false) + if err == nil { + namespaceExist = true + } else if !apierrors.IsNotFound(err) { return nil, err } + + if !namespaceExist { + if _, err := i.cfg.KubeClient.Create( + resourceList, + kube.ClientCreateOptionServerSideApply(i.ServerSideApply, false)); err != nil && !apierrors.IsAlreadyExists(err) { + return nil, err + } + } + } // If Replace is true, we need to supersede the last release. From 09c7cc4c3f6940e16f0723e0c16e53d92eb486ea Mon Sep 17 00:00:00 2001 From: Mujib Ahasan Date: Tue, 3 Feb 2026 01:15:08 +0530 Subject: [PATCH 2/4] Test Cases Added Signed-off-by: Mujib Ahasan --- pkg/action/install_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index ede39e552..7d02000dc 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -1294,3 +1294,41 @@ func TestInstallRelease_WaitOptionsPassedDownstream(t *testing.T) { // Verify that WaitOptions were passed to GetWaiter is.NotEmpty(failer.RecordedWaitOptions, "WaitOptions should be passed to GetWaiter") } + +func TestInstall_CreateNamespaceAlreadyExists(t *testing.T) { + is := assert.New(t) + + // Namespace already exists in cluster + config := actionConfigFixtureWithDummyResources(t, createDummyResourceList(true)) + + instAction := installActionWithConfig(config) + instAction.CreateNamespace = true + instAction.Namespace = "spaced" + + resi, err := instAction.Run(buildChart(), nil) + is.NoError(err) + + res, err := releaserToV1Release(resi) + is.NoError(err) + is.Equal("spaced", res.Namespace) + is.Equal("Install complete", res.Info.Description) +} + +func TestInstall_CreateNamespaceNotExists(t *testing.T) { + is := assert.New(t) + + // Empty cluster state, thats mean namespace not present + config := actionConfigFixture(t) + + instAction := installActionWithConfig(config) + instAction.CreateNamespace = true + instAction.Namespace = "spaced" + + resi, err := instAction.Run(buildChart(), nil) + is.NoError(err) + + res, err := releaserToV1Release(resi) + is.NoError(err) + is.Equal("spaced", res.Namespace) + is.Equal("Install complete", res.Info.Description) +} From 1a6021c1df971f04f0300a030fa9adaff5b2598b Mon Sep 17 00:00:00 2001 From: Mujib Ahasan Date: Sat, 13 Jun 2026 00:00:44 +0530 Subject: [PATCH 3/4] test: add coverage for create-namespace existence checks Signed-off-by: Mujib Ahasan --- pkg/action/install.go | 32 ++++++++-- pkg/action/install_test.go | 125 +++++++++++++++++++++++++++++++++++-- 2 files changed, 146 insertions(+), 11 deletions(-) diff --git a/pkg/action/install.go b/pkg/action/install.go index 51a524d40..116abc2b1 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -447,15 +447,12 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st return nil, err } - namespaceExist := false - _, err = i.cfg.KubeClient.Get(resourceList, false) - if err == nil { - namespaceExist = true - } else if !apierrors.IsNotFound(err) { + exists, err := namespaceExists(resourceList) + if err != nil { return nil, err } - if !namespaceExist { + if !exists { if _, err := i.cfg.KubeClient.Create( resourceList, kube.ClientCreateOptionServerSideApply(i.ServerSideApply, false)); err != nil && !apierrors.IsAlreadyExists(err) { @@ -487,6 +484,29 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st return rel, err } +func namespaceExists(resourceList kube.ResourceList) (bool, error) { + exists := false + + err := resourceList.Visit(func(info *resource.Info, err error) error { + if err != nil { + return err + } + + _, err = resource.NewHelper(info.Client, info.Mapping).Get(info.Namespace, info.Name) + if apierrors.IsNotFound(err) { + return nil + } + if err != nil { + return err + } + + exists = true + return nil + }) + + return exists, err +} + func (i *Install) performInstallCtx(ctx context.Context, rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) (*release.Release, error) { type Msg struct { r *release.Release diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 7d02000dc..46deaf175 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -35,6 +35,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -1295,15 +1296,118 @@ func TestInstallRelease_WaitOptionsPassedDownstream(t *testing.T) { is.NotEmpty(failer.RecordedWaitOptions, "WaitOptions should be passed to GetWaiter") } -func TestInstall_CreateNamespaceAlreadyExists(t *testing.T) { +func fakeNamespaceResourceList(name string, statusCode int) kube.ResourceList { + body := kuberuntime.Object(&v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{ + "custom-label": "keep-me", + }, + Annotations: map[string]string{ + "custom-annotation": "keep-me", + }, + }, + }) + + if statusCode == http.StatusNotFound { + body = &metav1.Status{ + Status: metav1.StatusFailure, + Reason: metav1.StatusReasonNotFound, + Code: http.StatusNotFound, + } + } + + restClient := &fake.RESTClient{ + GroupVersion: schema.GroupVersion{Version: "v1"}, + NegotiatedSerializer: scheme.Codecs.WithoutConversion(), + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + return newNamespaceResponse(statusCode, body), nil + }), + } + + return kube.ResourceList{ + &resource.Info{ + Client: restClient, + Mapping: &meta.RESTMapping{ + Resource: schema.GroupVersionResource{ + Version: "v1", + Resource: "namespaces", + }, + GroupVersionKind: schema.GroupVersionKind{ + Version: "v1", + Kind: "Namespace", + }, + Scope: meta.RESTScopeRoot, + }, + Name: name, + Object: &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + }, + }, + } +} + +func newNamespaceResponse(code int, obj kuberuntime.Object) *http.Response { + body := kuberuntime.EncodeOrDie( + scheme.Codecs.LegacyCodec(schema.GroupVersion{Version: "v1"}), + obj, + ) + + return &http.Response{ + StatusCode: code, + Header: map[string][]string{ + "Content-Type": {"application/json"}, + }, + Body: io.NopCloser(strings.NewReader(body)), + } +} + +type namespaceInstallKubeClient struct { + *kubefake.FailingKubeClient + + namespaceStatusCode int + namespaceCreateCalled bool +} + +func (c *namespaceInstallKubeClient) Build(reader io.Reader, validate bool) (kube.ResourceList, error) { + b, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + + if strings.Contains(string(b), "kind: Namespace") { + return fakeNamespaceResourceList("spaced", c.namespaceStatusCode), nil + } + + return kube.ResourceList{}, nil +} + +func (c *namespaceInstallKubeClient) Create(resources kube.ResourceList, options ...kube.ClientCreateOption) (*kube.Result, error) { + for _, info := range resources { + if info.Name == "spaced" && info.Mapping != nil && info.Mapping.GroupVersionKind.Kind == "Namespace" { + c.namespaceCreateCalled = true + } + } + + return &kube.Result{}, nil +} + +func TestInstall_CreateNamespaceAlreadyExistsSkipsCreate(t *testing.T) { is := assert.New(t) - // Namespace already exists in cluster - config := actionConfigFixtureWithDummyResources(t, createDummyResourceList(true)) + config := actionConfigFixture(t) + client := &namespaceInstallKubeClient{ + FailingKubeClient: &kubefake.FailingKubeClient{ + PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, + }, + namespaceStatusCode: http.StatusOK, + } + config.KubeClient = client instAction := installActionWithConfig(config) instAction.CreateNamespace = true instAction.Namespace = "spaced" + instAction.ServerSideApply = true resi, err := instAction.Run(buildChart(), nil) is.NoError(err) @@ -1312,17 +1416,26 @@ func TestInstall_CreateNamespaceAlreadyExists(t *testing.T) { is.NoError(err) is.Equal("spaced", res.Namespace) is.Equal("Install complete", res.Info.Description) + + is.False(client.namespaceCreateCalled, "existing namespace should not be created/applied again") } -func TestInstall_CreateNamespaceNotExists(t *testing.T) { +func TestInstall_CreateNamespaceNotExistsCreatesNamespace(t *testing.T) { is := assert.New(t) - // Empty cluster state, thats mean namespace not present config := actionConfigFixture(t) + client := &namespaceInstallKubeClient{ + FailingKubeClient: &kubefake.FailingKubeClient{ + PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, + }, + namespaceStatusCode: http.StatusNotFound, + } + config.KubeClient = client instAction := installActionWithConfig(config) instAction.CreateNamespace = true instAction.Namespace = "spaced" + instAction.ServerSideApply = true resi, err := instAction.Run(buildChart(), nil) is.NoError(err) @@ -1331,4 +1444,6 @@ func TestInstall_CreateNamespaceNotExists(t *testing.T) { is.NoError(err) is.Equal("spaced", res.Namespace) is.Equal("Install complete", res.Info.Description) + + is.True(client.namespaceCreateCalled, "missing namespace should be created") } From 877cfd3c71f96f850e85d5cd9f4b9e1e151f854f Mon Sep 17 00:00:00 2001 From: Mujib Ahasan Date: Sat, 13 Jun 2026 00:12:04 +0530 Subject: [PATCH 4/4] fix lint error Signed-off-by: Mujib Ahasan --- pkg/action/install.go | 1 - pkg/action/install_test.go | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/action/install.go b/pkg/action/install.go index 116abc2b1..933685764 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -459,7 +459,6 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st return nil, err } } - } // If Replace is true, we need to supersede the last release. diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 46deaf175..b9f9dd038 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -1320,7 +1320,7 @@ func fakeNamespaceResourceList(name string, statusCode int) kube.ResourceList { restClient := &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: "v1"}, NegotiatedSerializer: scheme.Codecs.WithoutConversion(), - Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + Client: fake.CreateHTTPClient(func(_ *http.Request) (*http.Response, error) { return newNamespaceResponse(statusCode, body), nil }), } @@ -1369,7 +1369,7 @@ type namespaceInstallKubeClient struct { namespaceCreateCalled bool } -func (c *namespaceInstallKubeClient) Build(reader io.Reader, validate bool) (kube.ResourceList, error) { +func (c *namespaceInstallKubeClient) Build(reader io.Reader, _ bool) (kube.ResourceList, error) { b, err := io.ReadAll(reader) if err != nil { return nil, err @@ -1382,7 +1382,7 @@ func (c *namespaceInstallKubeClient) Build(reader io.Reader, validate bool) (kub return kube.ResourceList{}, nil } -func (c *namespaceInstallKubeClient) Create(resources kube.ResourceList, options ...kube.ClientCreateOption) (*kube.Result, error) { +func (c *namespaceInstallKubeClient) Create(resources kube.ResourceList, _ ...kube.ClientCreateOption) (*kube.Result, error) { for _, info := range resources { if info.Name == "spaced" && info.Mapping != nil && info.Mapping.GroupVersionKind.Kind == "Namespace" { c.namespaceCreateCalled = true