fix: Skip namespace creation in --create-namespace when the namespace

Refs #32511

Signed-off-by: SillyZir <269283839+SillyZir@users.noreply.github.com>
pull/32553/head
SillyZir 2 months ago
parent 2a29f1770b
commit c9fd7b8e86

@ -447,10 +447,20 @@ 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) {
return nil, err
// Skip creation if the namespace already exists, as the user may have
// permission to get namespaces but not to create them
toCreate := resourceList.Filter(func(info *resource.Info) bool {
helper := resource.NewHelper(info.Client, info.Mapping)
_, err := helper.Get(info.Namespace, info.Name)
return err != nil
})
if len(toCreate) > 0 {
if _, err := i.cfg.KubeClient.Create(
toCreate,
kube.ClientCreateOptionServerSideApply(i.ServerSideApply, false)); err != nil && !apierrors.IsAlreadyExists(err) {
return nil, err
}
}
}

@ -0,0 +1,142 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"bytes"
"errors"
"io"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "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"
kuberuntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest/fake"
"helm.sh/helm/v4/pkg/kube"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
rcommon "helm.sh/helm/v4/pkg/release/common"
)
// existingNamespaceResourceList returns a ResourceList for a namespace that
// already exists in the cluster: its REST client answers GETs with the
// namespace object.
func existingNamespaceResourceList(name string) kube.ResourceList {
obj := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
}
corev1GV := schema.GroupVersion{Version: "v1"}
corev1Codec := scheme.Codecs.CodecForVersions(scheme.Codecs.LegacyCodec(corev1GV), scheme.Codecs.UniversalDecoder(corev1GV), corev1GV, corev1GV)
body := []byte(kuberuntime.EncodeOrDie(corev1Codec, obj))
resInfo := resource.Info{
Name: name,
Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Version: "v1", Resource: "namespaces"},
GroupVersionKind: schema.GroupVersionKind{Version: "v1", Kind: "Namespace"},
Scope: meta.RESTScopeRoot,
},
Object: obj,
}
resInfo.Client = &fake.RESTClient{
GroupVersion: corev1GV,
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
Client: fake.CreateHTTPClient(func(_ *http.Request) (*http.Response, error) {
header := http.Header{}
header.Set("Content-Type", kuberuntime.ContentTypeJSON)
return &http.Response{
StatusCode: http.StatusOK,
Header: header,
Body: io.NopCloser(bytes.NewReader(body)),
}, nil
}),
}
var resourceList kube.ResourceList
resourceList.Append(&resInfo)
return resourceList
}
// namespaceCreateForbiddenKubeClient simulates a user whose RBAC role allows
// reading namespaces but not creating them: any attempt to create a Namespace
// is rejected with a Forbidden error, while the namespace itself already
// exists in the cluster.
type namespaceCreateForbiddenKubeClient struct {
kubefake.FailingKubeClient
nsResources kube.ResourceList
}
func (f *namespaceCreateForbiddenKubeClient) Build(r io.Reader, validate bool) (kube.ResourceList, error) {
buf, err := io.ReadAll(r)
if err != nil {
return nil, err
}
if strings.Contains(string(buf), "kind: Namespace") {
return f.nsResources, nil
}
return f.FailingKubeClient.Build(bytes.NewReader(buf), validate)
}
func (f *namespaceCreateForbiddenKubeClient) Create(resources kube.ResourceList, options ...kube.ClientCreateOption) (*kube.Result, error) {
for _, info := range resources {
if info.Mapping != nil && info.Mapping.GroupVersionKind.Kind == "Namespace" {
return nil, apierrors.NewForbidden(
schema.GroupResource{Resource: "namespaces"},
info.Name,
errors.New(`User "system:serviceaccount:mcp:mcp" cannot create resource "namespaces" in API group "" at the cluster scope`),
)
}
}
return f.FailingKubeClient.Create(resources, options...)
}
func TestInstallReleaseCreateNamespace_NamespaceExistsWithoutCreatePermission(t *testing.T) {
is := assert.New(t)
req := require.New(t)
config := actionConfigFixture(t)
config.KubeClient = &namespaceCreateForbiddenKubeClient{
FailingKubeClient: kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}},
nsResources: existingNamespaceResourceList("spaced"),
}
instAction := installActionWithConfig(config)
instAction.CreateNamespace = true
resi, err := instAction.Run(buildChart(), map[string]any{})
req.NoError(err, "--create-namespace must not fail when the namespace already exists and the user may not create namespaces")
res, err := releaserToV1Release(resi)
req.NoError(err)
r, err := instAction.cfg.Releases.Get(res.Name, res.Version)
req.NoError(err)
rel, err := releaserToV1Release(r)
req.NoError(err)
is.Equal(rcommon.StatusDeployed, rel.Info.Status)
is.Equal("Install complete", rel.Info.Description)
}
Loading…
Cancel
Save