github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/kubernetes/manifest/visitor_test.go (about)

     1  /*
     2  Copyright 2019 The Skaffold Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package manifest
    18  
    19  import (
    20  	"fmt"
    21  	"regexp"
    22  	"testing"
    23  
    24  	"k8s.io/apimachinery/pkg/runtime/schema"
    25  
    26  	"github.com/GoogleContainerTools/skaffold/testutil"
    27  )
    28  
    29  type mockVisitor struct {
    30  	visited     map[string]int
    31  	pivotKey    string
    32  	replaceWith interface{}
    33  }
    34  
    35  func (m *mockVisitor) Visit(gk schema.GroupKind, navpath string, o map[string]interface{}, k string, v interface{}, rs ResourceSelector) bool {
    36  	s := fmt.Sprintf("%+v", v)
    37  	if len(s) > 4 {
    38  		s = s[:4] + "..."
    39  	}
    40  	m.visited[fmt.Sprintf("%v=%s", k, s)]++
    41  	if fmt.Sprintf("%+v", o[k]) != fmt.Sprintf("%+v", v) {
    42  		panic(fmt.Sprintf("visitor.Visit() called with o[k] != v: o[%q] != %v", k, v))
    43  	}
    44  	if k == m.pivotKey {
    45  		if m.replaceWith != nil {
    46  			o[k] = m.replaceWith
    47  		}
    48  		return false
    49  	}
    50  	return true
    51  }
    52  
    53  func TestVisit(t *testing.T) {
    54  	tests := []struct {
    55  		description       string
    56  		pivotKey          string
    57  		replaceWith       interface{}
    58  		manifests         ManifestList
    59  		expectedManifests ManifestList
    60  		expected          []string
    61  		shouldErr         bool
    62  	}{
    63  		{
    64  			description: "correct with one level",
    65  			manifests:   ManifestList{[]byte(`test: foo`), []byte(`test: bar`)},
    66  			expected:    []string{"test=foo", "test=bar"},
    67  		},
    68  		{
    69  			description:       "omit empty manifest",
    70  			manifests:         ManifestList{[]byte(``), []byte(`test: bar`)},
    71  			expectedManifests: ManifestList{[]byte(`test: bar`)},
    72  			expected:          []string{"test=bar"},
    73  		},
    74  		{
    75  			description: "skip nested map",
    76  			manifests: ManifestList{[]byte(`nested:
    77    prop: x
    78  test: foo`)},
    79  			expected: []string{"test=foo", "nested=map[..."},
    80  		},
    81  		{
    82  			description: "skip nested map in Role",
    83  			manifests: ManifestList{[]byte(`apiVersion: rbac.authorization.k8s.io/v1
    84  kind: Role
    85  metadata:
    86    name: myrole
    87  rules:
    88  - apiGroups:
    89    - ""
    90    resources:
    91    - configmaps
    92    verbs:
    93    - list
    94    - get`)},
    95  			expected: []string{"apiVersion=rbac...", "kind=Role", "metadata=map[...", "rules=[map..."},
    96  		},
    97  		{
    98  			description: "nested map in Pod",
    99  			manifests: ManifestList{[]byte(`apiVersion: v1
   100  kind: Pod
   101  metadata:
   102    name: mpod
   103  spec:
   104    restartPolicy: Always`)},
   105  			expected: []string{"apiVersion=v1", "kind=Pod", "metadata=map[...", "name=mpod", "spec=map[...", "restartPolicy=Alwa..."},
   106  		},
   107  		{
   108  			description: "skip recursion at key",
   109  			pivotKey:    "metadata",
   110  			manifests: ManifestList{[]byte(`apiVersion: v1
   111  kind: Pod
   112  metadata:
   113    name: mpod
   114  spec:
   115    restartPolicy: Always`)},
   116  			expected: []string{"apiVersion=v1", "kind=Pod", "metadata=map[...", "spec=map[...", "restartPolicy=Alwa..."},
   117  		},
   118  		{
   119  			description: "nested array and map in Pod",
   120  			manifests: ManifestList{[]byte(`apiVersion: v1
   121  kind: Pod
   122  metadata:
   123    name: mpod
   124  spec:
   125    containers:
   126    - env:
   127        name: k
   128        value: v
   129      name: c1
   130    - name: c2
   131    restartPolicy: Always`)},
   132  			expected: []string{"apiVersion=v1", "kind=Pod", "metadata=map[...", "name=mpod",
   133  				"spec=map[...", "containers=[map...",
   134  				"name=c1", "env=map[...", "name=k", "value=v",
   135  				"name=c2", "restartPolicy=Alwa...",
   136  			},
   137  		},
   138  		{
   139  			description: "replace key",
   140  			pivotKey:    "name",
   141  			replaceWith: "repl",
   142  			manifests: ManifestList{[]byte(`apiVersion: apps/v1
   143  kind: Deployment
   144  metadata:
   145    labels:
   146      name: x
   147    name: app
   148  spec:
   149    replicas: 0`), []byte(`name: foo`)},
   150  			// This behaviour is questionable but implemented like this for simplicity.
   151  			// In practice this is not a problem (currently) since only the fields
   152  			// "metadata" and "image" are matched in known kinds without ambiguous field names.
   153  			expectedManifests: ManifestList{[]byte(`apiVersion: apps/v1
   154  kind: Deployment
   155  metadata:
   156    labels:
   157      name: repl
   158    name: repl
   159  spec:
   160    replicas: 0`), []byte(`name: repl`)},
   161  			expected: []string{"apiVersion=apps...", "kind=Depl...", "metadata=map[...", "name=app", "labels=map[...", "name=x", "spec=map[...", "replicas=0", "name=foo"},
   162  		},
   163  		{
   164  			description: "deprecated daemonset.extensions",
   165  			manifests: ManifestList{[]byte(`apiVersion: extensions/v1beta1
   166  kind: DaemonSet
   167  metadata:
   168    name: app
   169  spec:
   170    replicas: 0`)},
   171  			expected: []string{"apiVersion=exte...", "kind=Daem...", "metadata=map[...", "name=app", "spec=map[...", "replicas=0"},
   172  		},
   173  		{
   174  			description: "deprecated deployment.extensions",
   175  			manifests: ManifestList{[]byte(`apiVersion: extensions/v1beta1
   176  kind: Deployment
   177  metadata:
   178    name: app
   179  spec:
   180    replicas: 0`)},
   181  			expected: []string{"apiVersion=exte...", "kind=Depl...", "metadata=map[...", "name=app", "spec=map[...", "replicas=0"},
   182  		},
   183  		{
   184  			description: "deprecated replicaset.extensions",
   185  			manifests: ManifestList{[]byte(`apiVersion: extensions/v1beta1
   186  kind: ReplicaSet
   187  metadata:
   188    name: app
   189  spec:
   190    replicas: 0`)},
   191  			expected: []string{"apiVersion=exte...", "kind=Repl...", "metadata=map[...", "name=app", "spec=map[...", "replicas=0"},
   192  		},
   193  		{
   194  			description: "invalid input",
   195  			manifests:   ManifestList{[]byte(`test:bar`)},
   196  			shouldErr:   true,
   197  		},
   198  		{
   199  			description: "skip CRD fields",
   200  			manifests: ManifestList{[]byte(`apiVersion: apiextensions.k8s.io/v1beta1
   201  kind: CustomResourceDefinition
   202  metadata:
   203    name: mykind.mygroup.org
   204  spec:
   205    group: mygroup.org
   206    names:
   207      kind: MyKind`)},
   208  			expected: []string{"apiVersion=apie...", "kind=Cust...", "metadata=map[...", "spec=map[..."},
   209  		},
   210  		{
   211  			description: "a manifest with non string key",
   212  			manifests: ManifestList{[]byte(`apiVersion: v1
   213  data:
   214    1973: \"test/myservice:1973\"
   215  kind: ConfigMap
   216  metadata:
   217    labels:
   218      app: myapp
   219      chart: myapp-0.1.0
   220      release: myapp
   221    name: rel-nginx-ingress-tcp`)},
   222  			expected: []string{"apiVersion=v1", "kind=Conf...", "metadata=map[...", "data=map[..."},
   223  		},
   224  		{
   225  			description: "replace knative serving image",
   226  			manifests: ManifestList{[]byte(`apiVersion: serving.knative.dev/v1
   227  kind: Service
   228  metadata:
   229    name: mknservice
   230  spec:
   231    template:
   232      spec:
   233        containers:
   234        - image: orig`)},
   235  			pivotKey:    "image",
   236  			replaceWith: "repl",
   237  			expected: []string{"apiVersion=serv...", "kind=Serv...", "metadata=map[...", "name=mkns...",
   238  				"spec=map[...", "template=map[...", "spec=map[...",
   239  				"containers=[map...", "image=orig"},
   240  			expectedManifests: ManifestList{[]byte(`apiVersion: serving.knative.dev/v1
   241  kind: Service
   242  metadata:
   243    name: mknservice
   244  spec:
   245    template:
   246      spec:
   247        containers:
   248        - image: repl`)},
   249  		},
   250  	}
   251  	for _, test := range tests {
   252  		testutil.Run(t, test.description, func(t *testutil.T) {
   253  			visitor := &mockVisitor{map[string]int{}, test.pivotKey, test.replaceWith}
   254  			actual, err := test.manifests.Visit(visitor, NewResourceSelectorImages(TransformAllowlist, TransformDenylist))
   255  			expectedVisits := map[string]int{}
   256  			for _, visit := range test.expected {
   257  				expectedVisits[visit]++
   258  			}
   259  			t.CheckErrorAndDeepEqual(test.shouldErr, err, expectedVisits, visitor.visited)
   260  			if !test.shouldErr {
   261  				expectedManifests := test.expectedManifests
   262  				if expectedManifests == nil {
   263  					expectedManifests = test.manifests
   264  				}
   265  				t.CheckDeepEqual(expectedManifests.String(), actual.String(), testutil.YamlObj(t.T))
   266  			}
   267  		})
   268  	}
   269  }
   270  
   271  func TestWildcardedGroupKind(t *testing.T) {
   272  	tests := []struct {
   273  		description string
   274  		pattern     wildcardGroupKind
   275  		group       string
   276  		kind        string
   277  		expected    bool
   278  	}{
   279  		{
   280  			description: "exact match",
   281  			pattern:     wildcardGroupKind{Group: regexp.MustCompile("group"), Kind: regexp.MustCompile("kind")},
   282  			group:       "group",
   283  			kind:        "kind",
   284  			expected:    true,
   285  		},
   286  		{
   287  			description: "use real regexp",
   288  			pattern:     wildcardGroupKind{Group: regexp.MustCompile(".*"), Kind: regexp.MustCompile(".*")},
   289  			group:       "group",
   290  			kind:        "kind",
   291  			expected:    true,
   292  		},
   293  		{
   294  			description: "null group and kind should match all",
   295  			pattern:     wildcardGroupKind{},
   296  			group:       "group",
   297  			kind:        "kind",
   298  			expected:    true,
   299  		},
   300  		{
   301  			description: "null group should match all",
   302  			pattern:     wildcardGroupKind{Kind: regexp.MustCompile("kind")},
   303  			group:       "group",
   304  			kind:        "kind",
   305  			expected:    true,
   306  		},
   307  		{
   308  			description: "null kind should match all",
   309  			pattern:     wildcardGroupKind{Group: regexp.MustCompile("group")},
   310  			group:       "group",
   311  			kind:        "kind",
   312  			expected:    true,
   313  		},
   314  		{
   315  			description: "no match",
   316  			pattern:     wildcardGroupKind{Group: regexp.MustCompile("xxx"), Kind: regexp.MustCompile("xxx")},
   317  			group:       "group",
   318  			kind:        "kind",
   319  			expected:    false,
   320  		},
   321  		{
   322  			description: "no kind match",
   323  			pattern:     wildcardGroupKind{Group: regexp.MustCompile("group"), Kind: regexp.MustCompile("xxx")},
   324  			group:       "group",
   325  			kind:        "kind",
   326  			expected:    false,
   327  		},
   328  		{
   329  			description: "no group match",
   330  			pattern:     wildcardGroupKind{Group: regexp.MustCompile("xxx"), Kind: regexp.MustCompile("kind")},
   331  			group:       "group",
   332  			kind:        "kind",
   333  			expected:    false,
   334  		},
   335  	}
   336  	for _, test := range tests {
   337  		result := test.pattern.Matches(test.group, test.kind)
   338  		t.Run(test.description, func(t *testing.T) {
   339  			if result != test.expected {
   340  				t.Errorf("got %v, expected %v", result, test.expected)
   341  			}
   342  		})
   343  	}
   344  }
   345  
   346  func TestShouldTransformManifest(t *testing.T) {
   347  	tests := []struct {
   348  		manifest map[string]interface{}
   349  		expected bool
   350  	}{
   351  		{manifest: map[string]interface{}{}, expected: false},
   352  		{manifest: map[string]interface{}{"xxx": "v1", "yyy": "Pod"}, expected: false}, // non-KRM
   353  		{manifest: map[string]interface{}{"apiVersion": "v1", "kind": "Pod"}, expected: true},
   354  		{manifest: map[string]interface{}{"apiVersion": "apps/v1", "kind": "DaemonSet"}, expected: true},
   355  		{manifest: map[string]interface{}{"apiVersion": "apps/v1", "kind": "Deployment"}, expected: true},
   356  		{manifest: map[string]interface{}{"apiVersion": "apps/v1", "kind": "StatefulSet"}, expected: true},
   357  		{manifest: map[string]interface{}{"apiVersion": "apps/v1", "kind": "ReplicaSet"}, expected: true},
   358  		{manifest: map[string]interface{}{"apiVersion": "extensions/v1beta1", "kind": "Deployment"}, expected: true},
   359  		{manifest: map[string]interface{}{"apiVersion": "extensions/v1beta1", "kind": "DaemonSet"}, expected: true},
   360  		{manifest: map[string]interface{}{"apiVersion": "extensions/v1beta1", "kind": "ReplicaSet"}, expected: true},
   361  		{manifest: map[string]interface{}{"apiVersion": "batch/v1", "kind": "CronJob"}, expected: true},
   362  		{manifest: map[string]interface{}{"apiVersion": "batch/v1", "kind": "Job"}, expected: true},
   363  		{manifest: map[string]interface{}{"apiVersion": "serving.knative.dev/v1", "kind": "Service"}, expected: true},
   364  		{manifest: map[string]interface{}{"apiVersion": "agones.dev/v1", "kind": "Fleet"}, expected: true},
   365  		{manifest: map[string]interface{}{"apiVersion": "agones.dev/v1", "kind": "GameServer"}, expected: true},
   366  		{manifest: map[string]interface{}{"apiVersion": "argoproj.io/v1", "kind": "Rollout"}, expected: true},
   367  		{manifest: map[string]interface{}{"apiVersion": "argoproj.io/v1alpha1", "kind": "Workflow"}, expected: true},
   368  		{manifest: map[string]interface{}{"apiVersion": "argoproj.io/v1alpha1", "kind": "CronWorkflow"}, expected: true},
   369  		{manifest: map[string]interface{}{"apiVersion": "argoproj.io/v1alpha1", "kind": "WorkflowTemplate"}, expected: true},
   370  		{manifest: map[string]interface{}{"apiVersion": "argoproj.io/v1alpha1", "kind": "ClusterWorkflowTemplate"}, expected: true},
   371  		{manifest: map[string]interface{}{"apiVersion": "foo.cnrm.cloud.google.com/v1", "kind": "Service"}, expected: true},
   372  		{manifest: map[string]interface{}{"apiVersion": "foo.bar.cnrm.cloud.google.com/v1", "kind": "Service"}, expected: true},
   373  		{manifest: map[string]interface{}{"apiVersion": "foo/v1", "kind": "Blah"}, expected: false},
   374  		{manifest: map[string]interface{}{"apiVersion": "foo.bar.cnrm.cloud.google.com/v1", "kind": "Other"}, expected: true},
   375  	}
   376  	for _, test := range tests {
   377  		testutil.Run(t, fmt.Sprintf("%v", test.manifest), func(t *testutil.T) {
   378  			result := shouldTransformManifest(test.manifest, NewResourceSelectorImages(TransformAllowlist, TransformDenylist))
   379  			t.CheckDeepEqual(test.expected, result)
   380  		})
   381  	}
   382  }