github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/pkg/api/kptfile/v1/validation_test.go (about) 1 // Copyright 2021 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 package v1 15 16 import ( 17 "os" 18 "path/filepath" 19 "testing" 20 21 "github.com/GoogleContainerTools/kpt/internal/types" 22 "github.com/stretchr/testify/assert" 23 "sigs.k8s.io/kustomize/kyaml/filesys" 24 "sigs.k8s.io/kustomize/kyaml/yaml" 25 ) 26 27 func TestKptfileValidate(t *testing.T) { 28 type input struct { 29 name string 30 kptfile KptFile 31 valid bool 32 } 33 34 cases := []input{ 35 { 36 name: "pipeline: empty", 37 kptfile: KptFile{ 38 Pipeline: &Pipeline{}, 39 }, 40 valid: true, 41 }, 42 { 43 name: "pipeline: validcase", 44 kptfile: KptFile{ 45 Pipeline: &Pipeline{ 46 Mutators: []Function{ 47 { 48 Image: "patch-strategic-merge", 49 }, 50 { 51 Image: "gcr.io/kpt-fn/set-annotations:v0.1", 52 ConfigMap: map[string]string{ 53 "environment": "dev", 54 }, 55 }, 56 }, 57 Validators: []Function{ 58 { 59 Image: "gcr.io/kpt-fn/gatekeeper", 60 }, 61 }, 62 }, 63 }, 64 valid: true, 65 }, 66 { 67 name: "pipeline: invalid image name", 68 kptfile: KptFile{ 69 Pipeline: &Pipeline{ 70 Mutators: []Function{ 71 { 72 Image: "patch@_@strategic-merge", 73 }, 74 }, 75 }, 76 }, 77 valid: false, 78 }, 79 { 80 name: "pipeline: more than 1 config", 81 kptfile: KptFile{ 82 Pipeline: &Pipeline{ 83 Mutators: []Function{ 84 { 85 Image: "image", 86 ConfigPath: "./config.yaml", 87 ConfigMap: map[string]string{ 88 "foo": "bar", 89 }, 90 }, 91 }, 92 }, 93 }, 94 valid: false, 95 }, 96 { 97 name: "pipeline: absolute config path", 98 kptfile: KptFile{ 99 Pipeline: &Pipeline{ 100 Mutators: []Function{ 101 { 102 Image: "image", 103 ConfigPath: "/config.yaml", 104 }, 105 }, 106 }, 107 }, 108 valid: false, 109 }, 110 { 111 name: "pipeline: configpath referring file in parent", 112 kptfile: KptFile{ 113 Pipeline: &Pipeline{ 114 Mutators: []Function{ 115 { 116 Image: "image", 117 ConfigPath: "../config.yaml", 118 }, 119 }, 120 }, 121 }, 122 valid: false, 123 }, 124 { 125 name: "pipeline: cleaned configpath contains ..", 126 kptfile: KptFile{ 127 Pipeline: &Pipeline{ 128 Mutators: []Function{ 129 { 130 Image: "image", 131 ConfigPath: "a/b/../../../config.yaml", 132 }, 133 }, 134 }, 135 }, 136 valid: false, 137 }, 138 { 139 name: "pipeline: configpath contains invalid .. references", 140 kptfile: KptFile{ 141 Pipeline: &Pipeline{ 142 Mutators: []Function{ 143 { 144 Image: "image", 145 ConfigPath: "a/.../config.yaml", 146 }, 147 }, 148 }, 149 }, 150 valid: false, 151 }, 152 } 153 154 for _, c := range cases { 155 c := c 156 t.Run(c.name, func(t *testing.T) { 157 err := c.kptfile.Validate(filesys.FileSystemOrOnDisk{}, "") 158 if c.valid && err != nil { 159 t.Fatalf("kptfile should be valid, %s", err) 160 } 161 if !c.valid && err == nil { 162 t.Fatal("kptfile should not be valid") 163 } 164 }) 165 } 166 } 167 168 func TestValidateFunctionName(t *testing.T) { 169 type input struct { 170 Name string 171 Valid bool 172 } 173 inputs := []input{ 174 { 175 "gcr.io/kpt-fn/generate-folders", 176 true, 177 }, 178 { 179 "patch-strategic-merge", 180 true, 181 }, 182 { 183 "gcr.io/kpt-fn/generate-folders:unstable", 184 true, 185 }, 186 { 187 "patch-strategic-merge:v1.3_beta", 188 true, 189 }, 190 { 191 "gcr.io/kpt-fn/generate-folders:v1.2.3-alpha1", 192 true, 193 }, 194 { 195 "patch-strategic-merge:x.y.z", 196 true, 197 }, 198 { 199 "patch-strategic-merge::@!", 200 false, 201 }, 202 { 203 "patch-strategic-merge:", 204 false, 205 }, 206 { 207 "a.b.c:1234/foo/bar/generate-folders", 208 true, 209 }, 210 { 211 "ab-.b/c", 212 false, 213 }, 214 { 215 "a/a/", 216 false, 217 }, 218 { 219 "a//a/a", 220 false, 221 }, 222 { 223 "example.com/.dots/myimage", 224 false, 225 }, 226 { 227 "registry.io/foo/project--id.module--name.ver---sion--name", 228 true, 229 }, 230 { 231 "Foo/FarB", 232 false, 233 }, 234 { 235 "example.com/foo/generate-folders@sha256:3434a5299f8fcb2c2ade9975e56ca5a622427b9d5a9a971640765e830fb90a0e", 236 true, 237 }, 238 } 239 240 for _, n := range inputs { 241 n := n 242 t.Run(n.Name, func(t *testing.T) { 243 err := ValidateFunctionImageURL(n.Name) 244 if n.Valid && err != nil { 245 t.Fatalf("function name %s should be valid", n.Name) 246 } 247 if !n.Valid && err == nil { 248 t.Fatalf("function name %s should not be valid", n.Name) 249 } 250 }) 251 } 252 } 253 254 func TestValidatePath(t *testing.T) { 255 type input struct { 256 Path string 257 Valid bool 258 } 259 260 cases := []input{ 261 { 262 "a/b/c", 263 true, 264 }, 265 { 266 "a/b/", 267 true, 268 }, 269 { 270 "/a/b", 271 false, 272 }, 273 { 274 "./a", 275 true, 276 }, 277 { 278 "./a/.../b", 279 false, 280 }, 281 { 282 ".", 283 true, 284 }, 285 { 286 "a\\b", 287 true, 288 }, 289 { 290 "a\b", 291 true, 292 }, 293 { 294 "a\v", 295 true, 296 }, 297 { 298 "a:\\b\\c", 299 true, 300 }, 301 { 302 "../a/../b", 303 false, 304 }, 305 { 306 "a//b", 307 true, 308 }, 309 { 310 "a/b/.", 311 true, 312 }, 313 { 314 "././././", 315 true, 316 }, 317 { 318 "", 319 false, 320 }, 321 { 322 "\t \n", 323 false, 324 }, 325 { 326 "a/b/../config.yaml", 327 true, 328 }, 329 } 330 331 for _, c := range cases { 332 ret := validateFnConfigPathSyntax(c.Path) 333 if (ret == nil) != c.Valid { 334 t.Fatalf("returned value for path %q should be %t, got %t", 335 c.Path, c.Valid, (ret == nil)) 336 } 337 } 338 } 339 340 func TestIsKustomization(t *testing.T) { 341 testcases := []struct { 342 name string 343 input string 344 exp bool 345 }{ 346 { 347 "resource in a kustomization file is a kustomization", 348 ` 349 metadata: 350 annotations: 351 config.kubernetes.io/path: kustomization.yaml 352 `, 353 true, 354 }, 355 { 356 "resource in a kustomization file with .yml extn is a kustomization", 357 ` 358 metadata: 359 annotations: 360 config.kubernetes.io/path: kustomization.yml 361 `, 362 true, 363 }, 364 { 365 "resource in a kustomization file in a subdir is a kustomization", 366 ` 367 metadata: 368 annotations: 369 config.kubernetes.io/path: subdir/kustomization.yaml 370 `, 371 true, 372 }, 373 { 374 "resource in a non-kustomization file, with empty apigroup and Kustomization kind is a kustomization", 375 `apiVersion: 376 kind: Kustomization 377 `, 378 true, 379 }, 380 { 381 "resource in a non-kustomization file with Kustomization APIGroup is a kustomization", 382 `apiVersion: kustomize.config.k8s.io/v1beta1 383 `, 384 true, 385 }, 386 { 387 "resource in a non-kustomization file with non-kustomize apigroup is not a kustomization", 388 `apiVersion: non.kubernetes.io/v1 389 `, 390 false, 391 }, 392 } 393 for _, tc := range testcases { 394 tc := tc 395 t.Run(tc.name, func(t *testing.T) { 396 got := isKustomization(yaml.MustParse(tc.input)) 397 if got != tc.exp { 398 t.Fatalf("got %v expected %v", got, tc.exp) 399 } 400 }) 401 } 402 } 403 404 func TestGetValidatedFnConfigFromPath(t *testing.T) { 405 testcases := []struct { 406 name string 407 input string 408 exp string 409 errMsg string 410 }{ 411 { 412 name: "normal resource", 413 input: ` 414 apiVersion: v1 415 kind: Service 416 metadata: 417 name: myService 418 spec: 419 selector: 420 app: bar 421 `, 422 exp: `apiVersion: v1 423 kind: Service 424 metadata: 425 name: myService 426 annotations: 427 config.kubernetes.io/index: '0' 428 internal.config.kubernetes.io/index: '0' 429 internal.config.kubernetes.io/seqindent: 'compact' 430 spec: 431 selector: 432 app: bar 433 `, 434 }, 435 { 436 name: "multiple resources wrapped in List", 437 input: ` 438 apiVersion: v1 439 kind: List 440 metadata: 441 name: upsert-multiple-resources-config 442 items: 443 - apiVersion: v1 444 kind: Service 445 metadata: 446 name: myService 447 namespace: mySpace 448 spec: 449 selector: 450 app: bar 451 - apiVersion: apps/v1 452 kind: Deployment 453 metadata: 454 name: myDeployment2 455 namespace: mySpace 456 spec: 457 replicas: 10 458 `, 459 exp: `apiVersion: v1 460 kind: List 461 metadata: 462 name: upsert-multiple-resources-config 463 annotations: 464 config.kubernetes.io/index: '0' 465 internal.config.kubernetes.io/index: '0' 466 internal.config.kubernetes.io/seqindent: 'compact' 467 items: 468 - apiVersion: v1 469 kind: Service 470 metadata: 471 name: myService 472 namespace: mySpace 473 spec: 474 selector: 475 app: bar 476 - apiVersion: apps/v1 477 kind: Deployment 478 metadata: 479 name: myDeployment2 480 namespace: mySpace 481 spec: 482 replicas: 10 483 `, 484 }, 485 { 486 name: "error for multiple resources", 487 input: ` 488 apiVersion: v1 489 kind: Service 490 metadata: 491 name: myService 492 namespace: mySpace 493 --- 494 apiVersion: v1 495 kind: Service 496 metadata: 497 name: myService2 498 namespace: mySpace 499 `, 500 errMsg: `functionConfig "f1.yaml" must not contain more than one config, got 2`, 501 }, 502 } 503 for _, tc := range testcases { 504 tc := tc 505 t.Run(tc.name, func(t *testing.T) { 506 d := t.TempDir() 507 err := os.WriteFile(filepath.Join(d, "f1.yaml"), []byte(tc.input), 0700) 508 assert.NoError(t, err) 509 got, err := GetValidatedFnConfigFromPath(filesys.FileSystemOrOnDisk{}, types.UniquePath(d), "f1.yaml") 510 if tc.errMsg != "" { 511 assert.Error(t, err) 512 assert.Equal(t, tc.errMsg, err.Error()) 513 return 514 } 515 assert.NoError(t, err) 516 actual, err := got.String() 517 assert.NoError(t, err) 518 assert.Equal(t, tc.exp, actual) 519 }) 520 } 521 }