...

Source file src/sigs.k8s.io/cli-utils/pkg/manifestreader/common_test.go

Documentation: sigs.k8s.io/cli-utils/pkg/manifestreader

     1  // Copyright 2020 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package manifestreader
     5  
     6  import (
     7  	"fmt"
     8  	"testing"
     9  
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/require"
    12  	"k8s.io/apimachinery/pkg/api/meta"
    13  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    14  	"k8s.io/apimachinery/pkg/runtime/schema"
    15  	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
    16  	"sigs.k8s.io/cli-utils/pkg/object"
    17  	"sigs.k8s.io/kustomize/kyaml/kio/filters"
    18  	"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
    19  	"sigs.k8s.io/kustomize/kyaml/yaml"
    20  )
    21  
    22  func TestSetNamespaces(t *testing.T) {
    23  	testCases := map[string]struct {
    24  		objs             []*unstructured.Unstructured
    25  		defaultNamespace string
    26  		enforceNamespace bool
    27  
    28  		expectedNamespaces []string
    29  		expectedErr        error
    30  	}{
    31  		"resources already have namespace": {
    32  			objs: []*unstructured.Unstructured{
    33  				toUnstructured(schema.GroupVersionKind{
    34  					Group:   "apps",
    35  					Version: "v1",
    36  					Kind:    "Deployment",
    37  				}, "default"),
    38  				toUnstructured(schema.GroupVersionKind{
    39  					Group:   "policy",
    40  					Version: "v1beta1",
    41  					Kind:    "PodDisruptionBudget",
    42  				}, "default"),
    43  			},
    44  			defaultNamespace: "foo",
    45  			enforceNamespace: false,
    46  			expectedNamespaces: []string{
    47  				"default",
    48  				"default",
    49  			},
    50  		},
    51  		"resources without namespace and mapping in RESTMapper": {
    52  			objs: []*unstructured.Unstructured{
    53  				toUnstructured(schema.GroupVersionKind{
    54  					Group:   "apps",
    55  					Version: "v1",
    56  					Kind:    "Deployment",
    57  				}, ""),
    58  			},
    59  			defaultNamespace:   "foo",
    60  			enforceNamespace:   false,
    61  			expectedNamespaces: []string{"foo"},
    62  		},
    63  		"resource with namespace that does match enforced ns": {
    64  			objs: []*unstructured.Unstructured{
    65  				toUnstructured(schema.GroupVersionKind{
    66  					Group:   "apps",
    67  					Version: "v1",
    68  					Kind:    "Deployment",
    69  				}, "bar"),
    70  			},
    71  			defaultNamespace:   "bar",
    72  			enforceNamespace:   true,
    73  			expectedNamespaces: []string{"bar"},
    74  		},
    75  		"resource with namespace that doesn't match enforced ns": {
    76  			objs: []*unstructured.Unstructured{
    77  				toUnstructured(schema.GroupVersionKind{
    78  					Group:   "apps",
    79  					Version: "v1",
    80  					Kind:    "Deployment",
    81  				}, "foo"),
    82  			},
    83  			defaultNamespace: "bar",
    84  			enforceNamespace: true,
    85  			expectedErr: &NamespaceMismatchError{
    86  				RequiredNamespace: "bar",
    87  				Namespace:         "foo",
    88  			},
    89  		},
    90  		"cluster-scoped CR with CRD": {
    91  			objs: []*unstructured.Unstructured{
    92  				toUnstructured(schema.GroupVersionKind{
    93  					Group:   "custom.io",
    94  					Version: "v1",
    95  					Kind:    "Custom",
    96  				}, ""),
    97  				toCRDUnstructured(schema.GroupVersionKind{
    98  					Group:   "apiextensions.k8s.io",
    99  					Version: "v1",
   100  					Kind:    "CustomResourceDefinition",
   101  				}, schema.GroupVersionKind{
   102  					Group:   "custom.io",
   103  					Version: "v1",
   104  					Kind:    "Custom",
   105  				}, "Cluster"),
   106  			},
   107  			defaultNamespace:   "bar",
   108  			enforceNamespace:   true,
   109  			expectedNamespaces: []string{"", ""},
   110  		},
   111  		"namespace-scoped CR with CRD": {
   112  			objs: []*unstructured.Unstructured{
   113  				toCRDUnstructured(schema.GroupVersionKind{
   114  					Group:   "apiextensions.k8s.io",
   115  					Version: "v1",
   116  					Kind:    "CustomResourceDefinition",
   117  				}, schema.GroupVersionKind{
   118  					Group:   "custom.io",
   119  					Version: "v1",
   120  					Kind:    "Custom",
   121  				}, "Namespaced"),
   122  				toUnstructured(schema.GroupVersionKind{
   123  					Group:   "custom.io",
   124  					Version: "v1",
   125  					Kind:    "Custom",
   126  				}, ""),
   127  			},
   128  			defaultNamespace:   "bar",
   129  			enforceNamespace:   true,
   130  			expectedNamespaces: []string{"", "bar"},
   131  		},
   132  		"unknown types in CRs": {
   133  			objs: []*unstructured.Unstructured{
   134  				toUnstructured(schema.GroupVersionKind{
   135  					Group:   "custom.io",
   136  					Version: "v1",
   137  					Kind:    "Custom",
   138  				}, ""),
   139  				toUnstructured(schema.GroupVersionKind{
   140  					Group:   "custom.io",
   141  					Version: "v1",
   142  					Kind:    "AnotherCustom",
   143  				}, ""),
   144  			},
   145  			expectedErr: &UnknownTypesError{
   146  				GroupVersionKinds: []schema.GroupVersionKind{
   147  					{
   148  						Group:   "custom.io",
   149  						Version: "v1",
   150  						Kind:    "Custom",
   151  					},
   152  					{
   153  						Group:   "custom.io",
   154  						Version: "v1",
   155  						Kind:    "AnotherCustom",
   156  					},
   157  				},
   158  			},
   159  		},
   160  	}
   161  
   162  	for tn, tc := range testCases {
   163  		t.Run(tn, func(t *testing.T) {
   164  			tf := cmdtesting.NewTestFactory().WithNamespace("namespace")
   165  			defer tf.Cleanup()
   166  
   167  			mapper, err := tf.ToRESTMapper()
   168  			require.NoError(t, err)
   169  			crdGV := schema.GroupVersion{Group: "apiextensions.k8s.io", Version: "v1"}
   170  			crdMapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{crdGV})
   171  			crdMapper.AddSpecific(crdGV.WithKind("CustomResourceDefinition"),
   172  				crdGV.WithResource("customresourcedefinitions"),
   173  				crdGV.WithResource("customresourcedefinition"), meta.RESTScopeRoot)
   174  			mapper = meta.MultiRESTMapper([]meta.RESTMapper{mapper, crdMapper})
   175  
   176  			err = SetNamespaces(mapper, tc.objs, tc.defaultNamespace, tc.enforceNamespace)
   177  
   178  			if tc.expectedErr != nil {
   179  				require.Error(t, err)
   180  				assert.Equal(t, tc.expectedErr, err)
   181  				return
   182  			}
   183  
   184  			require.NoError(t, err)
   185  
   186  			for i, obj := range tc.objs {
   187  				assert.Equal(t, tc.expectedNamespaces[i], obj.GetNamespace())
   188  			}
   189  		})
   190  	}
   191  }
   192  
   193  var (
   194  	depID = object.ObjMetadata{
   195  		GroupKind: schema.GroupKind{
   196  			Group: "apps",
   197  			Kind:  "Deployment",
   198  		},
   199  		Namespace: "default",
   200  		Name:      "foo",
   201  	}
   202  
   203  	clusterRoleID = object.ObjMetadata{
   204  		GroupKind: schema.GroupKind{
   205  			Group: "rbac.authorization.k8s.io",
   206  			Kind:  "ClusterRole",
   207  		},
   208  		Name: "bar",
   209  	}
   210  )
   211  
   212  func TestFilterLocalConfigs(t *testing.T) {
   213  	testCases := map[string]struct {
   214  		input    []*unstructured.Unstructured
   215  		expected []string
   216  	}{
   217  		"don't filter if no annotation": {
   218  			input: []*unstructured.Unstructured{
   219  				objMetaToUnstructured(depID),
   220  				objMetaToUnstructured(clusterRoleID),
   221  			},
   222  			expected: []string{
   223  				depID.Name,
   224  				clusterRoleID.Name,
   225  			},
   226  		},
   227  		"filter all if all have annotation": {
   228  			input: []*unstructured.Unstructured{
   229  				addAnnotation(t, objMetaToUnstructured(depID), filters.LocalConfigAnnotation, "true"),
   230  				addAnnotation(t, objMetaToUnstructured(clusterRoleID), filters.LocalConfigAnnotation, "false"),
   231  			},
   232  			expected: []string{},
   233  		},
   234  		"filter even if resource have other annotations": {
   235  			input: []*unstructured.Unstructured{
   236  				addAnnotation(t,
   237  					addAnnotation(
   238  						t, objMetaToUnstructured(depID),
   239  						filters.LocalConfigAnnotation, "true"),
   240  					"my-annotation", "foo"),
   241  			},
   242  			expected: []string{},
   243  		},
   244  	}
   245  
   246  	for tn, tc := range testCases {
   247  		t.Run(tn, func(t *testing.T) {
   248  			res := FilterLocalConfig(tc.input)
   249  
   250  			var names []string
   251  			for _, obj := range res {
   252  				names = append(names, obj.GetName())
   253  			}
   254  
   255  			// Avoid test failures due to nil slice and empty slice
   256  			// not being equal.
   257  			if len(tc.expected) == 0 && len(names) == 0 {
   258  				return
   259  			}
   260  			assert.Equal(t, tc.expected, names)
   261  		})
   262  	}
   263  }
   264  
   265  func TestRemoveAnnotations(t *testing.T) {
   266  	testCases := map[string]struct {
   267  		node                *yaml.RNode
   268  		removeAnnotations   []kioutil.AnnotationKey
   269  		expectedAnnotations []kioutil.AnnotationKey
   270  	}{
   271  		"filter both kioutil annotations": {
   272  			node: yaml.MustParse(`
   273  apiVersion: apps/v1
   274  kind: Deployment
   275  metadata:
   276    name: foo
   277    annotations:
   278      config.kubernetes.io/path: deployment.yaml
   279      config.kubernetes.io/index: 0
   280  `),
   281  			removeAnnotations: []kioutil.AnnotationKey{
   282  				kioutil.PathAnnotation,
   283  				kioutil.IndexAnnotation,
   284  			},
   285  		},
   286  		"filter only a subset of the annotations": {
   287  			node: yaml.MustParse(`
   288  apiVersion: apps/v1
   289  kind: Deployment
   290  metadata:
   291    name: foo
   292    annotations:
   293      internal.config.kubernetes.io/path: deployment.yaml
   294      internal.config.kubernetes.io/index: 0
   295  `),
   296  			removeAnnotations: []kioutil.AnnotationKey{
   297  				kioutil.IndexAnnotation,
   298  			},
   299  			expectedAnnotations: []kioutil.AnnotationKey{
   300  				kioutil.PathAnnotation,
   301  			},
   302  		},
   303  		"filter none of the annotations": {
   304  			node: yaml.MustParse(`
   305  apiVersion: apps/v1
   306  kind: Deployment
   307  metadata:
   308    name: foo
   309    annotations:
   310      internal.config.kubernetes.io/path: deployment.yaml
   311      internal.config.kubernetes.io/index: 0
   312  `),
   313  			removeAnnotations: []kioutil.AnnotationKey{},
   314  			expectedAnnotations: []kioutil.AnnotationKey{
   315  				kioutil.PathAnnotation,
   316  				kioutil.IndexAnnotation,
   317  			},
   318  		},
   319  	}
   320  
   321  	for tn, tc := range testCases {
   322  		t.Run(tn, func(t *testing.T) {
   323  			node := tc.node
   324  			err := RemoveAnnotations(node, tc.removeAnnotations...)
   325  			if !assert.NoError(t, err) {
   326  				t.FailNow()
   327  			}
   328  
   329  			for _, anno := range tc.removeAnnotations {
   330  				n, err := node.Pipe(yaml.GetAnnotation(anno))
   331  				if !assert.NoError(t, err) {
   332  					t.FailNow()
   333  				}
   334  				assert.Nil(t, n)
   335  			}
   336  			for _, anno := range tc.expectedAnnotations {
   337  				n, err := node.Pipe(yaml.GetAnnotation(anno))
   338  				if !assert.NoError(t, err) {
   339  					t.FailNow()
   340  				}
   341  				assert.NotNil(t, n)
   342  			}
   343  		})
   344  	}
   345  }
   346  
   347  func toUnstructured(gvk schema.GroupVersionKind, namespace string) *unstructured.Unstructured {
   348  	return &unstructured.Unstructured{
   349  		Object: map[string]interface{}{
   350  			"apiVersion": gvk.GroupVersion().String(),
   351  			"kind":       gvk.Kind,
   352  			"metadata": map[string]interface{}{
   353  				"namespace": namespace,
   354  			},
   355  		},
   356  	}
   357  }
   358  
   359  func toCRDUnstructured(crdGvk schema.GroupVersionKind, crGvk schema.GroupVersionKind,
   360  	scope string) *unstructured.Unstructured {
   361  	return &unstructured.Unstructured{
   362  		Object: map[string]interface{}{
   363  			"apiVersion": crdGvk.GroupVersion().String(),
   364  			"kind":       crdGvk.Kind,
   365  			"spec": map[string]interface{}{
   366  				"group": crGvk.Group,
   367  				"names": map[string]interface{}{
   368  					"kind": crGvk.Kind,
   369  				},
   370  				"scope": scope,
   371  				"versions": []interface{}{
   372  					map[string]interface{}{
   373  						"name": crGvk.Version,
   374  					},
   375  				},
   376  			},
   377  		},
   378  	}
   379  }
   380  
   381  func objMetaToUnstructured(id object.ObjMetadata) *unstructured.Unstructured {
   382  	return &unstructured.Unstructured{
   383  		Object: map[string]interface{}{
   384  			"apiVersion": fmt.Sprintf("%s/v1", id.GroupKind.Group),
   385  			"kind":       id.GroupKind.Kind,
   386  			"metadata": map[string]interface{}{
   387  				"namespace": id.Namespace,
   388  				"name":      id.Name,
   389  			},
   390  		},
   391  	}
   392  }
   393  
   394  func addAnnotation(t *testing.T, u *unstructured.Unstructured, name, val string) *unstructured.Unstructured {
   395  	annos, found, err := unstructured.NestedStringMap(u.Object, "metadata", "annotations")
   396  	if err != nil {
   397  		t.Fatal(err)
   398  	}
   399  	if !found {
   400  		annos = make(map[string]string)
   401  	}
   402  	annos[name] = val
   403  	err = unstructured.SetNestedStringMap(u.Object, annos, "metadata", "annotations")
   404  	if err != nil {
   405  		t.Fatal(err)
   406  	}
   407  	return u
   408  }
   409  

View as plain text