@ -35,6 +35,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -43,6 +44,7 @@ import (
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest/fake"
"k8s.io/client-go/rest/fake"
"sigs.k8s.io/yaml"
ci "helm.sh/helm/v4/pkg/chart"
ci "helm.sh/helm/v4/pkg/chart"
@ -1253,3 +1255,78 @@ func TestInstallRelease_WaitOptionsPassedDownstream(t *testing.T) {
// Verify that WaitOptions were passed to GetWaiter
// Verify that WaitOptions were passed to GetWaiter
is . NotEmpty ( failer . RecordedWaitOptions , "WaitOptions should be passed to GetWaiter" )
is . NotEmpty ( failer . RecordedWaitOptions , "WaitOptions should be passed to GetWaiter" )
}
}
// namespaceRecordingKubeClient records the resources passed to Create and
// answers with AlreadyExists for the namespace, the way the API server answers
// a client-side create for a namespace that is already there.
type namespaceRecordingKubeClient struct {
kubefake . PrintingKubeClient
createdNamespaces [ ] string
namespaceCreateOpts kube . ClientCreateOptions
}
// Build returns a resource for the namespace manifest so that the namespace
// reaches Create; everything else keeps the printing client behaviour.
func ( c * namespaceRecordingKubeClient ) Build ( r io . Reader , validate bool ) ( kube . ResourceList , error ) {
manifest , err := io . ReadAll ( r )
if err != nil {
return nil , err
}
if ! strings . Contains ( string ( manifest ) , "kind: Namespace" ) {
return c . PrintingKubeClient . Build ( bytes . NewReader ( manifest ) , validate )
}
var ns v1 . Namespace
if err := yaml . Unmarshal ( manifest , & ns ) ; err != nil {
return nil , err
}
return kube . ResourceList { {
Name : ns . Name ,
Mapping : & meta . RESTMapping {
GroupVersionKind : schema . GroupVersionKind { Version : "v1" , Kind : "Namespace" } ,
} ,
Object : & ns ,
} } , nil
}
func ( c * namespaceRecordingKubeClient ) Create ( resources kube . ResourceList , options ... kube . ClientCreateOption ) ( * kube . Result , error ) {
for _ , r := range resources {
if r . Mapping != nil && r . Mapping . GroupVersionKind . Kind == "Namespace" {
resolved , err := kube . ResolveClientCreateOptions ( options ... )
if err != nil {
return nil , err
}
c . createdNamespaces = append ( c . createdNamespaces , r . Name )
c . namespaceCreateOpts = resolved
return nil , apierrors . NewAlreadyExists ( schema . GroupResource { Resource : "namespaces" } , r . Name )
}
}
return c . PrintingKubeClient . Create ( resources , options ... )
}
// TestInstallReleaseWithCreateNamespaceExisting checks that installing into a
// namespace that already exists succeeds and that Helm only ever asks for the
// namespace to be created, so an existing namespace keeps the labels and
// annotations set by whoever created it.
func TestInstallReleaseWithCreateNamespaceExisting ( t * testing . T ) {
is := assert . New ( t )
req := require . New ( t )
config := actionConfigFixture ( t )
recorder := & namespaceRecordingKubeClient { PrintingKubeClient : kubefake . PrintingKubeClient { Out : io . Discard } }
config . KubeClient = recorder
instAction := installActionWithConfig ( config )
instAction . CreateNamespace = true
res , err := instAction . Run ( buildChart ( ) , map [ string ] any { } )
req . NoError ( err , "install into an existing namespace should succeed" )
rel , err := releaserToV1Release ( res )
req . NoError ( err )
is . Equal ( "spaced" , rel . Namespace )
is . Equal ( [ ] string { "spaced" } , recorder . createdNamespaces )
// A server-side apply would make Helm a field manager of the namespace and
// drop the labels and annotations it does not set itself.
is . False ( recorder . namespaceCreateOpts . ServerSideApply , "the namespace must be created client-side" )
}