/* 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" "io" "net/http" "testing" "helm.sh/helm/v3/pkg/kube" "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/api/meta" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "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" ) func newDeploymentResource(name, namespace string) *resource.Info { return &resource.Info{ Name: name, Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployment"}, GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, }, Object: &appsv1.Deployment{ ObjectMeta: v1.ObjectMeta{ Name: name, Namespace: namespace, }, }, } } func newMissingDeployment(name, namespace string) *resource.Info { info := &resource.Info{ Name: name, Namespace: namespace, Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployment"}, GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, Scope: meta.RESTScopeNamespace, }, Object: &appsv1.Deployment{ ObjectMeta: v1.ObjectMeta{ Name: name, Namespace: namespace, }, }, Client: fakeClientWith(http.StatusNotFound, appsV1GV, ""), } return info } func newDeploymentWithOwner(name, namespace string, labels map[string]string, annotations map[string]string) *resource.Info { obj := &appsv1.Deployment{ ObjectMeta: v1.ObjectMeta{ Name: name, Namespace: namespace, Labels: labels, Annotations: annotations, }, } return &resource.Info{ Name: name, Namespace: namespace, Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployment"}, GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, Scope: meta.RESTScopeNamespace, }, Object: obj, Client: fakeClientWith(http.StatusOK, appsV1GV, runtime.EncodeOrDie(appsv1Codec, obj)), } } var ( appsV1GV = schema.GroupVersion{Group: "apps", Version: "v1"} appsv1Codec = scheme.Codecs.CodecForVersions(scheme.Codecs.LegacyCodec(appsV1GV), scheme.Codecs.UniversalDecoder(appsV1GV), appsV1GV, appsV1GV) ) func stringBody(body string) io.ReadCloser { return io.NopCloser(bytes.NewReader([]byte(body))) } func fakeClientWith(code int, gv schema.GroupVersion, body string) *fake.RESTClient { return &fake.RESTClient{ GroupVersion: gv, NegotiatedSerializer: scheme.Codecs.WithoutConversion(), Client: fake.CreateHTTPClient(func(_ *http.Request) (*http.Response, error) { header := http.Header{} header.Set("Content-Type", runtime.ContentTypeJSON) return &http.Response{ StatusCode: code, Header: header, Body: stringBody(body), }, nil }), } } func TestRequireAdoption(t *testing.T) { var ( missing = newMissingDeployment("missing", "ns-a") existing = newDeploymentWithOwner("existing", "ns-a", nil, nil) resources = kube.ResourceList{missing, existing} ) // Verify that a resource that lacks labels/annotations can be adopted found, err := requireAdoption(resources) assert.NoError(t, err) assert.Len(t, found, 1) assert.Equal(t, found[0], existing) } func TestExistingResourceConflict(t *testing.T) { var ( releaseName = "rel-name" releaseNamespace = "rel-namespace" labels = map[string]string{ appManagedByLabel: appManagedByHelm, } annotations = map[string]string{ helmReleaseNameAnnotation: releaseName, helmReleaseNamespaceAnnotation: releaseNamespace, } missing = newMissingDeployment("missing", "ns-a") existing = newDeploymentWithOwner("existing", "ns-a", labels, annotations) conflict = newDeploymentWithOwner("conflict", "ns-a", nil, nil) resources = kube.ResourceList{missing, existing} ) // Verify only existing resources are returned found, err := existingResourceConflict(resources, releaseName, releaseNamespace) assert.NoError(t, err) assert.Len(t, found, 1) assert.Equal(t, found[0], existing) // Verify that an existing resource that lacks labels/annotations results in an error resources = append(resources, conflict) _, err = existingResourceConflict(resources, releaseName, releaseNamespace) assert.Error(t, err) } func TestCheckOwnership(t *testing.T) { deployFoo := newDeploymentResource("foo", "ns-a") // Verify that a resource that lacks labels/annotations is not owned err := checkOwnership(deployFoo.Object, "rel-a", "ns-a") assert.EqualError(t, err, `invalid ownership metadata; label validation error: missing key "app.kubernetes.io/managed-by": must be set to "Helm"; annotation validation error: missing key "meta.helm.sh/release-name": must be set to "rel-a"; annotation validation error: missing key "meta.helm.sh/release-namespace": must be set to "ns-a"`) // Set managed by label and verify annotation error message _ = accessor.SetLabels(deployFoo.Object, map[string]string{ appManagedByLabel: appManagedByHelm, }) err = checkOwnership(deployFoo.Object, "rel-a", "ns-a") assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: missing key "meta.helm.sh/release-name": must be set to "rel-a"; annotation validation error: missing key "meta.helm.sh/release-namespace": must be set to "ns-a"`) // Set only the release name annotation and verify missing release namespace error message _ = accessor.SetAnnotations(deployFoo.Object, map[string]string{ helmReleaseNameAnnotation: "rel-a", }) err = checkOwnership(deployFoo.Object, "rel-a", "ns-a") assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: missing key "meta.helm.sh/release-namespace": must be set to "ns-a"`) // Set both release name and namespace annotations and verify no ownership errors _ = accessor.SetAnnotations(deployFoo.Object, map[string]string{ helmReleaseNameAnnotation: "rel-a", helmReleaseNamespaceAnnotation: "ns-a", }) err = checkOwnership(deployFoo.Object, "rel-a", "ns-a") assert.NoError(t, err) // Verify ownership error for wrong release name err = checkOwnership(deployFoo.Object, "rel-b", "ns-a") assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: key "meta.helm.sh/release-name" must equal "rel-b": current value is "rel-a"`) // Verify ownership error for wrong release namespace err = checkOwnership(deployFoo.Object, "rel-a", "ns-b") assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: key "meta.helm.sh/release-namespace" must equal "ns-b": current value is "ns-a"`) // Verify ownership error for wrong manager label _ = accessor.SetLabels(deployFoo.Object, map[string]string{ appManagedByLabel: "helm", }) err = checkOwnership(deployFoo.Object, "rel-a", "ns-a") assert.EqualError(t, err, `invalid ownership metadata; label validation error: key "app.kubernetes.io/managed-by" must equal "Helm": current value is "helm"`) } func TestSetMetadataVisitor(t *testing.T) { var ( err error deployFoo = newDeploymentResource("foo", "ns-a") deployBar = newDeploymentResource("bar", "ns-a-system") resources = kube.ResourceList{deployFoo, deployBar} ) // Set release tracking metadata and verify no error err = resources.Visit(setMetadataVisitor("rel-a", "ns-a", true)) assert.NoError(t, err) // Verify that release "b" cannot take ownership of "a" err = resources.Visit(setMetadataVisitor("rel-b", "ns-a", false)) assert.Error(t, err) // Force release "b" to take ownership err = resources.Visit(setMetadataVisitor("rel-b", "ns-a", true)) assert.NoError(t, err) // Check that there is now no ownership error when setting metadata without force err = resources.Visit(setMetadataVisitor("rel-b", "ns-a", false)) assert.NoError(t, err) // Add a new resource that is missing ownership metadata and verify error resources.Append(newDeploymentResource("baz", "default")) err = resources.Visit(setMetadataVisitor("rel-b", "ns-a", false)) assert.Error(t, err) assert.Contains(t, err.Error(), `Deployment "baz" in namespace "" cannot be owned`) }