cli-utils/pkg/manifestreader/common_test.go

409 lines
10 KiB
Go

// Copyright 2020 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package manifestreader
import (
"fmt"
"testing"
"github.com/fluxcd/cli-utils/pkg/object"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
"sigs.k8s.io/kustomize/kyaml/kio/filters"
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
func TestSetNamespaces(t *testing.T) {
testCases := map[string]struct {
objs []*unstructured.Unstructured
defaultNamespace string
enforceNamespace bool
expectedNamespaces []string
expectedErr error
}{
"resources already have namespace": {
objs: []*unstructured.Unstructured{
toUnstructured(schema.GroupVersionKind{
Group: "apps",
Version: "v1",
Kind: "Deployment",
}, "default"),
toUnstructured(schema.GroupVersionKind{
Group: "policy",
Version: "v1beta1",
Kind: "PodDisruptionBudget",
}, "default"),
},
defaultNamespace: "foo",
enforceNamespace: false,
expectedNamespaces: []string{
"default",
"default",
},
},
"resources without namespace and mapping in RESTMapper": {
objs: []*unstructured.Unstructured{
toUnstructured(schema.GroupVersionKind{
Group: "apps",
Version: "v1",
Kind: "Deployment",
}, ""),
},
defaultNamespace: "foo",
enforceNamespace: false,
expectedNamespaces: []string{"foo"},
},
"resource with namespace that does match enforced ns": {
objs: []*unstructured.Unstructured{
toUnstructured(schema.GroupVersionKind{
Group: "apps",
Version: "v1",
Kind: "Deployment",
}, "bar"),
},
defaultNamespace: "bar",
enforceNamespace: true,
expectedNamespaces: []string{"bar"},
},
"resource with namespace that doesn't match enforced ns": {
objs: []*unstructured.Unstructured{
toUnstructured(schema.GroupVersionKind{
Group: "apps",
Version: "v1",
Kind: "Deployment",
}, "foo"),
},
defaultNamespace: "bar",
enforceNamespace: true,
expectedErr: &NamespaceMismatchError{
RequiredNamespace: "bar",
Namespace: "foo",
},
},
"cluster-scoped CR with CRD": {
objs: []*unstructured.Unstructured{
toUnstructured(schema.GroupVersionKind{
Group: "custom.io",
Version: "v1",
Kind: "Custom",
}, ""),
toCRDUnstructured(schema.GroupVersionKind{
Group: "apiextensions.k8s.io",
Version: "v1",
Kind: "CustomResourceDefinition",
}, schema.GroupVersionKind{
Group: "custom.io",
Version: "v1",
Kind: "Custom",
}, "Cluster"),
},
defaultNamespace: "bar",
enforceNamespace: true,
expectedNamespaces: []string{"", ""},
},
"namespace-scoped CR with CRD": {
objs: []*unstructured.Unstructured{
toCRDUnstructured(schema.GroupVersionKind{
Group: "apiextensions.k8s.io",
Version: "v1",
Kind: "CustomResourceDefinition",
}, schema.GroupVersionKind{
Group: "custom.io",
Version: "v1",
Kind: "Custom",
}, "Namespaced"),
toUnstructured(schema.GroupVersionKind{
Group: "custom.io",
Version: "v1",
Kind: "Custom",
}, ""),
},
defaultNamespace: "bar",
enforceNamespace: true,
expectedNamespaces: []string{"", "bar"},
},
"unknown types in CRs": {
objs: []*unstructured.Unstructured{
toUnstructured(schema.GroupVersionKind{
Group: "custom.io",
Version: "v1",
Kind: "Custom",
}, ""),
toUnstructured(schema.GroupVersionKind{
Group: "custom.io",
Version: "v1",
Kind: "AnotherCustom",
}, ""),
},
expectedErr: &UnknownTypesError{
GroupVersionKinds: []schema.GroupVersionKind{
{
Group: "custom.io",
Version: "v1",
Kind: "Custom",
},
{
Group: "custom.io",
Version: "v1",
Kind: "AnotherCustom",
},
},
},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("namespace")
defer tf.Cleanup()
mapper, err := tf.ToRESTMapper()
require.NoError(t, err)
crdGV := schema.GroupVersion{Group: "apiextensions.k8s.io", Version: "v1"}
crdMapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{crdGV})
crdMapper.AddSpecific(crdGV.WithKind("CustomResourceDefinition"),
crdGV.WithResource("customresourcedefinitions"),
crdGV.WithResource("customresourcedefinition"), meta.RESTScopeRoot)
mapper = meta.MultiRESTMapper([]meta.RESTMapper{mapper, crdMapper})
err = SetNamespaces(mapper, tc.objs, tc.defaultNamespace, tc.enforceNamespace)
if tc.expectedErr != nil {
require.Error(t, err)
assert.Equal(t, tc.expectedErr, err)
return
}
require.NoError(t, err)
for i, obj := range tc.objs {
assert.Equal(t, tc.expectedNamespaces[i], obj.GetNamespace())
}
})
}
}
var (
depID = object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "apps",
Kind: "Deployment",
},
Namespace: "default",
Name: "foo",
}
clusterRoleID = object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
},
Name: "bar",
}
)
func TestFilterLocalConfigs(t *testing.T) {
testCases := map[string]struct {
input []*unstructured.Unstructured
expected []string
}{
"don't filter if no annotation": {
input: []*unstructured.Unstructured{
objMetaToUnstructured(depID),
objMetaToUnstructured(clusterRoleID),
},
expected: []string{
depID.Name,
clusterRoleID.Name,
},
},
"filter all if all have annotation": {
input: []*unstructured.Unstructured{
addAnnotation(t, objMetaToUnstructured(depID), filters.LocalConfigAnnotation, "true"),
addAnnotation(t, objMetaToUnstructured(clusterRoleID), filters.LocalConfigAnnotation, "false"),
},
expected: []string{},
},
"filter even if resource have other annotations": {
input: []*unstructured.Unstructured{
addAnnotation(t,
addAnnotation(
t, objMetaToUnstructured(depID),
filters.LocalConfigAnnotation, "true"),
"my-annotation", "foo"),
},
expected: []string{},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
res := FilterLocalConfig(tc.input)
var names []string
for _, obj := range res {
names = append(names, obj.GetName())
}
// Avoid test failures due to nil slice and empty slice
// not being equal.
if len(tc.expected) == 0 && len(names) == 0 {
return
}
assert.Equal(t, tc.expected, names)
})
}
}
func TestRemoveAnnotations(t *testing.T) {
testCases := map[string]struct {
node *yaml.RNode
removeAnnotations []kioutil.AnnotationKey
expectedAnnotations []kioutil.AnnotationKey
}{
"filter both kioutil annotations": {
node: yaml.MustParse(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
annotations:
config.kubernetes.io/path: deployment.yaml
config.kubernetes.io/index: 0
`),
removeAnnotations: []kioutil.AnnotationKey{
kioutil.PathAnnotation,
kioutil.IndexAnnotation,
},
},
"filter only a subset of the annotations": {
node: yaml.MustParse(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
annotations:
internal.config.kubernetes.io/path: deployment.yaml
internal.config.kubernetes.io/index: 0
`),
removeAnnotations: []kioutil.AnnotationKey{
kioutil.IndexAnnotation,
},
expectedAnnotations: []kioutil.AnnotationKey{
kioutil.PathAnnotation,
},
},
"filter none of the annotations": {
node: yaml.MustParse(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
annotations:
internal.config.kubernetes.io/path: deployment.yaml
internal.config.kubernetes.io/index: 0
`),
removeAnnotations: []kioutil.AnnotationKey{},
expectedAnnotations: []kioutil.AnnotationKey{
kioutil.PathAnnotation,
kioutil.IndexAnnotation,
},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
node := tc.node
err := RemoveAnnotations(node, tc.removeAnnotations...)
if !assert.NoError(t, err) {
t.FailNow()
}
for _, anno := range tc.removeAnnotations {
n, err := node.Pipe(yaml.GetAnnotation(anno))
if !assert.NoError(t, err) {
t.FailNow()
}
assert.Nil(t, n)
}
for _, anno := range tc.expectedAnnotations {
n, err := node.Pipe(yaml.GetAnnotation(anno))
if !assert.NoError(t, err) {
t.FailNow()
}
assert.NotNil(t, n)
}
})
}
}
func toUnstructured(gvk schema.GroupVersionKind, namespace string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": gvk.GroupVersion().String(),
"kind": gvk.Kind,
"metadata": map[string]interface{}{
"namespace": namespace,
},
},
}
}
func toCRDUnstructured(crdGvk schema.GroupVersionKind, crGvk schema.GroupVersionKind,
scope string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": crdGvk.GroupVersion().String(),
"kind": crdGvk.Kind,
"spec": map[string]interface{}{
"group": crGvk.Group,
"names": map[string]interface{}{
"kind": crGvk.Kind,
},
"scope": scope,
"versions": []interface{}{
map[string]interface{}{
"name": crGvk.Version,
},
},
},
},
}
}
func objMetaToUnstructured(id object.ObjMetadata) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": fmt.Sprintf("%s/v1", id.GroupKind.Group),
"kind": id.GroupKind.Kind,
"metadata": map[string]interface{}{
"namespace": id.Namespace,
"name": id.Name,
},
},
}
}
func addAnnotation(t *testing.T, u *unstructured.Unstructured, name, val string) *unstructured.Unstructured {
annos, found, err := unstructured.NestedStringMap(u.Object, "metadata", "annotations")
if err != nil {
t.Fatal(err)
}
if !found {
annos = make(map[string]string)
}
annos[name] = val
err = unstructured.SetNestedStringMap(u.Object, annos, "metadata", "annotations")
if err != nil {
t.Fatal(err)
}
return u
}