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 }