github.com/GoogleContainerTools/kpt@v1.0.0-beta.50.0.20240520170205-c25345ffcbee/thirdparty/kyaml/runfn/runfn_test.go (about) 1 // Copyright 2019 The Kubernetes Authors. 2 // SPDX-License-Identifier: Apache-2.0 3 4 package runfn 5 6 import ( 7 "bytes" 8 "os" 9 "path/filepath" 10 "runtime" 11 "testing" 12 13 fnresult "github.com/GoogleContainerTools/kpt/pkg/api/fnresult/v1" 14 v1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" 15 "github.com/GoogleContainerTools/kpt/pkg/printer/fake" 16 "github.com/stretchr/testify/assert" 17 18 "sigs.k8s.io/kustomize/kyaml/copyutil" 19 "sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil" 20 "sigs.k8s.io/kustomize/kyaml/kio" 21 "sigs.k8s.io/kustomize/kyaml/kio/filters" 22 "sigs.k8s.io/kustomize/kyaml/yaml" 23 ) 24 25 const ( 26 ValueReplacerYAMLData = `apiVersion: v1 27 kind: ValueReplacer 28 stringMatch: Deployment 29 replace: StatefulSet 30 ` 31 32 ValueReplacerFnConfigYAMLData = `apiVersion: v1 33 kind: ValueReplacer 34 metadata: 35 name: fn-config 36 stringMatch: Deployment 37 replace: ReplicaSet 38 ` 39 40 KptfileData = `apiVersion: kpt.dev/v1 41 kind: Kptfile 42 metadata: 43 name: kptfile 44 annotations: 45 foo: bar 46 ` 47 ) 48 49 func TestRunFns_Execute__initDefault(t *testing.T) { 50 // droot: This is not a useful test at all, so skipping this 51 t.Skip() 52 b := &bytes.Buffer{} 53 var tests = []struct { 54 instance RunFns 55 expected RunFns 56 name string 57 }{ 58 { 59 instance: RunFns{}, 60 name: "empty", 61 expected: RunFns{Output: os.Stdout, Input: os.Stdin}, 62 }, 63 { 64 name: "explicit output", 65 instance: RunFns{Output: b}, 66 expected: RunFns{Output: b, Input: os.Stdin}, 67 }, 68 { 69 name: "explicit input", 70 instance: RunFns{Input: b}, 71 expected: RunFns{Output: os.Stdout, Input: b}, 72 }, 73 { 74 name: "explicit functions -- no functions from input", 75 instance: RunFns{Function: nil}, 76 expected: RunFns{Output: os.Stdout, Input: os.Stdin, Function: nil}, 77 }, 78 { 79 name: "explicit functions -- yes functions from input", 80 instance: RunFns{Function: nil}, 81 expected: RunFns{Output: os.Stdout, Input: os.Stdin, Function: nil}, 82 }, 83 { 84 name: "explicit functions in paths -- no functions from input", 85 instance: RunFns{FnConfigPath: "/foo"}, 86 expected: RunFns{ 87 Output: os.Stdout, 88 Input: os.Stdin, 89 FnConfigPath: "/foo", 90 }, 91 }, 92 { 93 name: "functions in paths -- yes functions from input", 94 instance: RunFns{FnConfigPath: "/foo"}, 95 expected: RunFns{ 96 Output: os.Stdout, 97 Input: os.Stdin, 98 FnConfigPath: "/foo", 99 }, 100 }, 101 { 102 name: "explicit directories in mounts", 103 instance: RunFns{StorageMounts: []runtimeutil.StorageMount{{MountType: "volume", Src: "myvol", DstPath: "/local/"}}}, 104 expected: RunFns{ 105 Output: os.Stdout, 106 Input: os.Stdin, 107 StorageMounts: []runtimeutil.StorageMount{{MountType: "volume", Src: "myvol", DstPath: "/local/"}}, 108 }, 109 }, 110 } 111 for i := range tests { 112 tt := tests[i] 113 t.Run(tt.name, func(t *testing.T) { 114 assert.NoError(t, (&tt.instance).init()) 115 (&tt.instance).functionFilterProvider = nil 116 if !assert.Equal(t, tt.expected, tt.instance) { 117 t.FailNow() 118 } 119 }) 120 } 121 } 122 123 func TestCmd_Execute(t *testing.T) { 124 dir := setupTest(t) 125 defer os.RemoveAll(dir) 126 127 fnConfig, err := yaml.Parse(ValueReplacerYAMLData) 128 if err != nil { 129 t.Fatal(err) 130 } 131 fn := &runtimeutil.FunctionSpec{ 132 Container: runtimeutil.ContainerSpec{ 133 Image: "gcr.io/example.com/image:version", 134 }, 135 } 136 137 instance := RunFns{ 138 Ctx: fake.CtxWithDefaultPrinter(), 139 Path: dir, 140 functionFilterProvider: getFilterProvider(t), 141 Function: fn, 142 FnConfig: fnConfig, 143 fnResults: fnresult.NewResultList(), 144 } 145 if !assert.NoError(t, instance.Execute()) { 146 t.FailNow() 147 } 148 b, err := os.ReadFile( 149 filepath.Join(dir, "java", "java-deployment.resource.yaml")) 150 if !assert.NoError(t, err) { 151 t.FailNow() 152 } 153 assert.Contains(t, string(b), "kind: StatefulSet") 154 } 155 156 func TestCmd_Execute_includeMetaResources(t *testing.T) { 157 dir := setupTest(t) 158 defer os.RemoveAll(dir) 159 160 fnConfig, err := yaml.Parse(ValueReplacerYAMLData) 161 if err != nil { 162 t.Fatal(err) 163 } 164 fn := &runtimeutil.FunctionSpec{ 165 Container: runtimeutil.ContainerSpec{ 166 Image: "gcr.io/example.com/image:version", 167 }, 168 } 169 170 // write a Kptfile to the directory of configuration 171 if !assert.NoError(t, os.WriteFile( 172 filepath.Join(dir, v1.KptFileName), []byte(KptfileData), 0600)) { 173 return 174 } 175 176 instance := RunFns{ 177 Ctx: fake.CtxWithDefaultPrinter(), 178 Path: dir, 179 functionFilterProvider: getMetaResourceFilterProvider(), 180 Function: fn, 181 FnConfig: fnConfig, 182 fnResults: fnresult.NewResultList(), 183 } 184 if !assert.NoError(t, instance.Execute()) { 185 t.FailNow() 186 } 187 b, err := os.ReadFile( 188 filepath.Join(dir, v1.KptFileName)) 189 if !assert.NoError(t, err) { 190 t.FailNow() 191 } 192 assert.Contains(t, string(b), "foo: baz") 193 } 194 195 func TestCmd_Execute_notIncludeMetaResources(t *testing.T) { 196 dir := setupTest(t) 197 defer os.RemoveAll(dir) 198 199 // write a test filter to the directory of configuration 200 if !assert.NoError(t, os.WriteFile( 201 filepath.Join(dir, "filter.yaml"), []byte(ValueReplacerYAMLData), 0600)) { 202 return 203 } 204 205 // write a Kptfile to the directory of configuration 206 if !assert.NoError(t, os.WriteFile( 207 filepath.Join(dir, v1.KptFileName), []byte(KptfileData), 0600)) { 208 return 209 } 210 211 instance := RunFns{ 212 Ctx: fake.CtxWithDefaultPrinter(), 213 Path: dir, 214 functionFilterProvider: getMetaResourceFilterProvider(), 215 fnResults: fnresult.NewResultList(), 216 } 217 if !assert.NoError(t, instance.Execute()) { 218 t.FailNow() 219 } 220 b, err := os.ReadFile( 221 filepath.Join(dir, v1.KptFileName)) 222 if !assert.NoError(t, err) { 223 t.FailNow() 224 } 225 assert.EqualValues(t, string(b), KptfileData) 226 } 227 228 type TestFilter struct { 229 invoked bool 230 Exit error 231 } 232 233 func (f *TestFilter) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) { 234 f.invoked = true 235 return input, nil 236 } 237 238 func (f *TestFilter) GetExit() error { 239 return f.Exit 240 } 241 242 func getFnConfigPathFilterProvider(t *testing.T, r *RunFns) func(runtimeutil.FunctionSpec, *yaml.RNode, currentUserFunc) (kio.Filter, error) { 243 return func(f runtimeutil.FunctionSpec, node *yaml.RNode, currentUser currentUserFunc) (kio.Filter, error) { 244 // parse the filter from the input 245 filter := yaml.YFilter{} 246 b := &bytes.Buffer{} 247 e := yaml.NewEncoder(b) 248 var err error 249 if r.FnConfigPath != "" { 250 node, err = r.getFunctionConfig() 251 if err != nil { 252 t.Fatal(err) 253 } 254 } 255 if !assert.NoError(t, e.Encode(node.YNode())) { 256 t.FailNow() 257 } 258 e.Close() 259 d := yaml.NewDecoder(b) 260 if !assert.NoError(t, d.Decode(&filter)) { 261 t.FailNow() 262 } 263 264 return filters.Modifier{ 265 Filters: []yaml.YFilter{{Filter: yaml.Lookup("kind")}, filter}, 266 }, nil 267 } 268 } 269 270 func TestCmd_Execute_setFnConfigPath(t *testing.T) { 271 dir := setupTest(t) 272 defer os.RemoveAll(dir) 273 274 // write a test filter to a separate directory 275 tmpF, err := os.CreateTemp("", "filter*.yaml") 276 if !assert.NoError(t, err) { 277 return 278 } 279 os.RemoveAll(tmpF.Name()) 280 if !assert.NoError(t, os.WriteFile(tmpF.Name(), []byte(ValueReplacerFnConfigYAMLData), 0600)) { 281 return 282 } 283 284 fnConfig, err := yaml.Parse(ValueReplacerYAMLData) 285 if err != nil { 286 t.Fatal(err) 287 } 288 fn := &runtimeutil.FunctionSpec{ 289 Container: runtimeutil.ContainerSpec{ 290 Image: "gcr.io/example.com/image:version", 291 }, 292 } 293 294 // run the functions, providing the path to the directory of filters 295 instance := RunFns{ 296 Ctx: fake.CtxWithDefaultPrinter(), 297 FnConfigPath: tmpF.Name(), 298 Path: dir, 299 Function: fn, 300 FnConfig: fnConfig, 301 fnResults: fnresult.NewResultList(), 302 } 303 instance.functionFilterProvider = getFnConfigPathFilterProvider(t, &instance) 304 // initialize the defaults 305 assert.NoError(t, instance.init()) 306 307 err = instance.Execute() 308 if !assert.NoError(t, err) { 309 return 310 } 311 b, err := os.ReadFile( 312 filepath.Join(dir, "java", "java-deployment.resource.yaml")) 313 if !assert.NoError(t, err) { 314 return 315 } 316 assert.Contains(t, string(b), "kind: ReplicaSet") 317 } 318 319 // TestCmd_Execute_setOutput tests the execution of a filter using an io.Writer as output 320 func TestCmd_Execute_setOutput(t *testing.T) { 321 dir := setupTest(t) 322 defer os.RemoveAll(dir) 323 324 fnConfig, err := yaml.Parse(ValueReplacerYAMLData) 325 if err != nil { 326 t.Fatal(err) 327 } 328 fn := &runtimeutil.FunctionSpec{ 329 Container: runtimeutil.ContainerSpec{ 330 Image: "gcr.io/example.com/image:version", 331 }, 332 } 333 334 out := &bytes.Buffer{} 335 instance := RunFns{ 336 Ctx: fake.CtxWithDefaultPrinter(), 337 Output: out, // write to out 338 Path: dir, 339 functionFilterProvider: getFilterProvider(t), 340 Function: fn, 341 FnConfig: fnConfig, 342 fnResults: fnresult.NewResultList(), 343 } 344 // initialize the defaults 345 assert.NoError(t, instance.init()) 346 347 if !assert.NoError(t, instance.Execute()) { 348 return 349 } 350 b, err := os.ReadFile( 351 filepath.Join(dir, "java", "java-deployment.resource.yaml")) 352 if !assert.NoError(t, err) { 353 return 354 } 355 assert.NotContains(t, string(b), "kind: StatefulSet") 356 assert.Contains(t, out.String(), "kind: StatefulSet") 357 } 358 359 // TestCmd_Execute_setInput tests the execution of a filter using an io.Reader as input 360 func TestCmd_Execute_setInput(t *testing.T) { 361 dir := setupTest(t) 362 defer os.RemoveAll(dir) 363 fnConfig, err := yaml.Parse(ValueReplacerYAMLData) 364 if err != nil { 365 t.Fatal(err) 366 } 367 fn := &runtimeutil.FunctionSpec{ 368 Container: runtimeutil.ContainerSpec{ 369 Image: "gcr.io/example.com/image:version", 370 }, 371 } 372 373 read, err := kio.LocalPackageReader{PackagePath: dir, PreserveSeqIndent: true}.Read() 374 if !assert.NoError(t, err) { 375 t.FailNow() 376 } 377 input := &bytes.Buffer{} 378 if !assert.NoError(t, kio.ByteWriter{Writer: input}.Write(read)) { 379 t.FailNow() 380 } 381 382 outDir, err := os.MkdirTemp("", "kustomize-test") 383 if !assert.NoError(t, err) { 384 t.FailNow() 385 } 386 387 if !assert.NoError(t, os.WriteFile( 388 filepath.Join(dir, "filter.yaml"), []byte(ValueReplacerYAMLData), 0600)) { 389 return 390 } 391 392 instance := RunFns{ 393 Ctx: fake.CtxWithDefaultPrinter(), 394 Input: input, // read from input 395 Path: outDir, 396 functionFilterProvider: getFilterProvider(t), 397 Function: fn, 398 FnConfig: fnConfig, 399 fnResults: fnresult.NewResultList(), 400 } 401 // initialize the defaults 402 assert.NoError(t, instance.init()) 403 404 if !assert.NoError(t, instance.Execute()) { 405 return 406 } 407 b, err := os.ReadFile( 408 filepath.Join(outDir, "java", "java-deployment.resource.yaml")) 409 if !assert.NoError(t, err) { 410 t.FailNow() 411 } 412 assert.Contains(t, string(b), "kind: StatefulSet") 413 } 414 415 // setupTest initializes a temp test directory containing test data 416 func setupTest(t *testing.T) string { 417 dir, err := os.MkdirTemp("", "kustomize-kyaml-test") 418 if !assert.NoError(t, err) { 419 t.FailNow() 420 } 421 422 _, filename, _, ok := runtime.Caller(0) 423 if !assert.True(t, ok) { 424 t.FailNow() 425 } 426 ds, err := filepath.Abs(filepath.Join(filepath.Dir(filename), "test", "testdata")) 427 if !assert.NoError(t, err) { 428 t.FailNow() 429 } 430 if !assert.NoError(t, copyutil.CopyDir(ds, dir)) { 431 t.FailNow() 432 } 433 if !assert.NoError(t, os.Chdir(filepath.Dir(dir))) { 434 t.FailNow() 435 } 436 return dir 437 } 438 439 // getFilterProvider fakes the creation of a filter, replacing the ContainerFiler with 440 // a filter to s/kind: Deployment/kind: StatefulSet/g. 441 // this can be used to simulate running a filter. 442 func getFilterProvider(t *testing.T) func(runtimeutil.FunctionSpec, *yaml.RNode, currentUserFunc) (kio.Filter, error) { 443 return func(f runtimeutil.FunctionSpec, node *yaml.RNode, currentUser currentUserFunc) (kio.Filter, error) { 444 // parse the filter from the input 445 filter := yaml.YFilter{} 446 b := &bytes.Buffer{} 447 e := yaml.NewEncoder(b) 448 if !assert.NoError(t, e.Encode(node.YNode())) { 449 t.FailNow() 450 } 451 e.Close() 452 d := yaml.NewDecoder(b) 453 if !assert.NoError(t, d.Decode(&filter)) { 454 t.FailNow() 455 } 456 457 return filters.Modifier{ 458 Filters: []yaml.YFilter{{Filter: yaml.Lookup("kind")}, filter}, 459 }, nil 460 } 461 } 462 463 // getMetaResourceFilterProvider fakes the creation of a filter, replacing the 464 // ContainerFilter with replace the value for annotation "foo" to "baz" 465 func getMetaResourceFilterProvider() func(runtimeutil.FunctionSpec, *yaml.RNode, currentUserFunc) (kio.Filter, error) { 466 return func(f runtimeutil.FunctionSpec, node *yaml.RNode, currentUser currentUserFunc) (kio.Filter, error) { 467 return filters.Modifier{ 468 Filters: []yaml.YFilter{{Filter: yaml.SetAnnotation("foo", "baz")}}, 469 }, nil 470 } 471 } 472 473 func TestRunFns_mergeContainerEnv(t *testing.T) { 474 testcases := []struct { 475 name string 476 instance RunFns 477 inputEnvs []string 478 expect runtimeutil.ContainerEnv 479 }{ 480 { 481 name: "all empty", 482 instance: RunFns{}, 483 expect: *runtimeutil.NewContainerEnv(), 484 }, 485 { 486 name: "empty command line envs", 487 instance: RunFns{}, 488 inputEnvs: []string{"foo=bar"}, 489 expect: *runtimeutil.NewContainerEnvFromStringSlice([]string{"foo=bar"}), 490 }, 491 { 492 name: "empty declarative envs", 493 instance: RunFns{ 494 Env: []string{"foo=bar"}, 495 }, 496 expect: *runtimeutil.NewContainerEnvFromStringSlice([]string{"foo=bar"}), 497 }, 498 { 499 name: "same key", 500 instance: RunFns{ 501 Env: []string{"foo=bar", "foo"}, 502 }, 503 inputEnvs: []string{"foo=bar1", "bar"}, 504 expect: *runtimeutil.NewContainerEnvFromStringSlice([]string{"foo=bar", "bar", "foo"}), 505 }, 506 { 507 name: "same exported key", 508 instance: RunFns{ 509 Env: []string{"foo=bar", "foo"}, 510 }, 511 inputEnvs: []string{"foo1=bar1", "foo"}, 512 expect: *runtimeutil.NewContainerEnvFromStringSlice([]string{"foo=bar", "foo1=bar1", "foo"}), 513 }, 514 } 515 516 for i := range testcases { 517 tc := testcases[i] 518 t.Run(tc.name, func(t *testing.T) { 519 envs := tc.instance.mergeContainerEnv(tc.inputEnvs) 520 assert.Equal(t, tc.expect.GetDockerFlags(), runtimeutil.NewContainerEnvFromStringSlice(envs).GetDockerFlags()) 521 }) 522 } 523 }