...

Source file src/sigs.k8s.io/kustomize/kyaml/runfn/runfn_test.go

Documentation: sigs.k8s.io/kustomize/kyaml/runfn

     1  // Copyright 2019 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package runfn
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"os"
    10  	"os/user"
    11  	"path/filepath"
    12  	"runtime"
    13  	"strings"
    14  	"testing"
    15  
    16  	"github.com/stretchr/testify/assert"
    17  	"sigs.k8s.io/kustomize/kyaml/copyutil"
    18  	"sigs.k8s.io/kustomize/kyaml/errors"
    19  	"sigs.k8s.io/kustomize/kyaml/filesys"
    20  	"sigs.k8s.io/kustomize/kyaml/fn/runtime/container"
    21  	"sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil"
    22  	"sigs.k8s.io/kustomize/kyaml/kio"
    23  	"sigs.k8s.io/kustomize/kyaml/kio/filters"
    24  	"sigs.k8s.io/kustomize/kyaml/yaml"
    25  )
    26  
    27  const (
    28  	ValueReplacerYAMLData = `apiVersion: v1
    29  kind: ValueReplacer
    30  metadata:
    31    annotations:
    32      config.kubernetes.io/function: |
    33        container:
    34          image: gcr.io/example.com/image:version
    35      config.kubernetes.io/local-config: "true"
    36  stringMatch: Deployment
    37  replace: StatefulSet
    38  `
    39  )
    40  
    41  func currentUser() (*user.User, error) {
    42  	return &user.User{
    43  		Uid: "1",
    44  		Gid: "2",
    45  	}, nil
    46  }
    47  
    48  func TestRunFns_init(t *testing.T) {
    49  	instance := RunFns{}
    50  	instance.init()
    51  	if !assert.Equal(t, instance.Input, os.Stdin) {
    52  		t.FailNow()
    53  	}
    54  	if !assert.Equal(t, instance.Output, os.Stdout) {
    55  		t.FailNow()
    56  	}
    57  
    58  	api, err := yaml.Parse(`apiVersion: apps/v1
    59  kind: 
    60  `)
    61  	spec := runtimeutil.FunctionSpec{
    62  		Container: runtimeutil.ContainerSpec{
    63  			Image: "example.com:version",
    64  		},
    65  	}
    66  	if !assert.NoError(t, err) {
    67  		return
    68  	}
    69  	filter, _ := instance.functionFilterProvider(spec, api, currentUser)
    70  	c := container.NewContainer(runtimeutil.ContainerSpec{Image: "example.com:version"}, "nobody")
    71  	cf := &c
    72  	cf.Exec.FunctionConfig = api
    73  	assert.Equal(t, cf, filter)
    74  }
    75  
    76  func TestRunFns_initAsCurrentUser(t *testing.T) {
    77  	instance := RunFns{
    78  		AsCurrentUser: true,
    79  	}
    80  	instance.init()
    81  	if !assert.Equal(t, instance.Input, os.Stdin) {
    82  		t.FailNow()
    83  	}
    84  	if !assert.Equal(t, instance.Output, os.Stdout) {
    85  		t.FailNow()
    86  	}
    87  
    88  	api, err := yaml.Parse(`apiVersion: apps/v1
    89  kind: 
    90  `)
    91  	spec := runtimeutil.FunctionSpec{
    92  		Container: runtimeutil.ContainerSpec{
    93  			Image: "example.com:version",
    94  		},
    95  	}
    96  	if !assert.NoError(t, err) {
    97  		return
    98  	}
    99  	filter, _ := instance.functionFilterProvider(spec, api, currentUser)
   100  	c := container.NewContainer(runtimeutil.ContainerSpec{Image: "example.com:version"}, "1:2")
   101  	cf := &c
   102  	cf.Exec.FunctionConfig = api
   103  	assert.Equal(t, cf, filter)
   104  }
   105  
   106  func TestRunFns_Execute__initGlobalScope(t *testing.T) {
   107  	instance := RunFns{GlobalScope: true}
   108  	instance.init()
   109  	if !assert.Equal(t, instance.Input, os.Stdin) {
   110  		t.FailNow()
   111  	}
   112  	if !assert.Equal(t, instance.Output, os.Stdout) {
   113  		t.FailNow()
   114  	}
   115  	api, err := yaml.Parse(`apiVersion: apps/v1
   116  kind: 
   117  `)
   118  	if !assert.NoError(t, err) {
   119  		return
   120  	}
   121  
   122  	spec := runtimeutil.FunctionSpec{
   123  		Container: runtimeutil.ContainerSpec{
   124  			Image: "example.com:version",
   125  		},
   126  	}
   127  	if !assert.NoError(t, err) {
   128  		return
   129  	}
   130  	filter, _ := instance.functionFilterProvider(spec, api, currentUser)
   131  	c := container.NewContainer(runtimeutil.ContainerSpec{Image: "example.com:version"}, "nobody")
   132  	cf := &c
   133  	cf.Exec.FunctionConfig = api
   134  	cf.Exec.GlobalScope = true
   135  	assert.Equal(t, cf, filter)
   136  }
   137  
   138  func TestRunFns_Execute__initDefault(t *testing.T) {
   139  	b := &bytes.Buffer{}
   140  	var tests = []struct {
   141  		instance RunFns
   142  		expected RunFns
   143  		name     string
   144  	}{
   145  		{
   146  			instance: RunFns{},
   147  			name:     "empty",
   148  			expected: RunFns{Output: os.Stdout, Input: os.Stdin, NoFunctionsFromInput: getFalse()},
   149  		},
   150  		{
   151  			name:     "explicit output",
   152  			instance: RunFns{Output: b},
   153  			expected: RunFns{Output: b, Input: os.Stdin, NoFunctionsFromInput: getFalse()},
   154  		},
   155  		{
   156  			name:     "explicit input",
   157  			instance: RunFns{Input: b},
   158  			expected: RunFns{Output: os.Stdout, Input: b, NoFunctionsFromInput: getFalse()},
   159  		},
   160  		{
   161  			name:     "explicit functions -- no functions from input",
   162  			instance: RunFns{Functions: []*yaml.RNode{{}}},
   163  			expected: RunFns{Output: os.Stdout, Input: os.Stdin, NoFunctionsFromInput: getTrue(), Functions: []*yaml.RNode{{}}},
   164  		},
   165  		{
   166  			name:     "explicit functions -- yes functions from input",
   167  			instance: RunFns{Functions: []*yaml.RNode{{}}, NoFunctionsFromInput: getFalse()},
   168  			expected: RunFns{Output: os.Stdout, Input: os.Stdin, NoFunctionsFromInput: getFalse(), Functions: []*yaml.RNode{{}}},
   169  		},
   170  		{
   171  			name:     "explicit functions in paths -- no functions from input",
   172  			instance: RunFns{FunctionPaths: []string{"foo"}},
   173  			expected: RunFns{
   174  				Output:               os.Stdout,
   175  				Input:                os.Stdin,
   176  				NoFunctionsFromInput: getTrue(),
   177  				FunctionPaths:        []string{"foo"},
   178  			},
   179  		},
   180  		{
   181  			name:     "functions in paths -- yes functions from input",
   182  			instance: RunFns{FunctionPaths: []string{"foo"}, NoFunctionsFromInput: getFalse()},
   183  			expected: RunFns{
   184  				Output:               os.Stdout,
   185  				Input:                os.Stdin,
   186  				NoFunctionsFromInput: getFalse(),
   187  				FunctionPaths:        []string{"foo"},
   188  			},
   189  		},
   190  		{
   191  			name:     "explicit directories in mounts",
   192  			instance: RunFns{StorageMounts: []runtimeutil.StorageMount{{MountType: "volume", Src: "myvol", DstPath: "/local/"}}},
   193  			expected: RunFns{
   194  				Output:               os.Stdout,
   195  				Input:                os.Stdin,
   196  				NoFunctionsFromInput: getFalse(),
   197  				StorageMounts:        []runtimeutil.StorageMount{{MountType: "volume", Src: "myvol", DstPath: "/local/"}},
   198  			},
   199  		},
   200  	}
   201  	for i := range tests {
   202  		tt := tests[i]
   203  		t.Run(tt.name, func(t *testing.T) {
   204  			(&tt.instance).init()
   205  			(&tt.instance).functionFilterProvider = nil
   206  			if !assert.Equal(t, tt.expected, tt.instance) {
   207  				t.FailNow()
   208  			}
   209  		})
   210  	}
   211  }
   212  
   213  func getTrue() *bool {
   214  	t := true
   215  	return &t
   216  }
   217  
   218  func getFalse() *bool {
   219  	f := false
   220  	return &f
   221  }
   222  
   223  // TestRunFns_getFilters tests how filters are found and sorted
   224  func TestRunFns_getFilters(t *testing.T) {
   225  	type f struct {
   226  		// path to function file and string value to write
   227  		path, value string
   228  		// if true, create the function in a separate directory from
   229  		// the config, and provide it through FunctionPaths
   230  		outOfPackage bool
   231  
   232  		// if true, create the function as an explicit Functions input
   233  		explicitFunction bool
   234  
   235  		// if true and outOfPackage is true, create a new directory
   236  		// for this function separate from the previous one.  If
   237  		// false and outOfPackage is true, create the function in
   238  		// the directory created for the last outOfPackage function.
   239  		newFnPath bool
   240  	}
   241  	var tests = []struct {
   242  		// function files to write
   243  		in []f
   244  		// images to be run in a specific order
   245  		out []string
   246  
   247  		// images to be run in a specific order -- computed from directory path
   248  		outFn func(string) []string
   249  
   250  		// expected Error
   251  		error string
   252  
   253  		// name of the test
   254  		name string
   255  		// value to set for NoFunctionsFromInput
   256  		noFunctionsFromInput *bool
   257  
   258  		enableStarlark bool
   259  
   260  		disableContainers bool
   261  	}{
   262  		// Test
   263  		//
   264  		//
   265  		{name: "single implicit function",
   266  			in: []f{
   267  				{
   268  					path: filepath.Join("foo", "bar.yaml"),
   269  					value: `
   270  apiVersion: example.com/v1alpha1
   271  kind: ExampleFunction
   272  metadata:
   273    annotations:
   274      config.kubernetes.io/function: |
   275        container:
   276          image: gcr.io/example.com/image:v1.0.0
   277      config.kubernetes.io/local-config: "true"
   278  `,
   279  				},
   280  			},
   281  			out: []string{"gcr.io/example.com/image:v1.0.0"},
   282  		},
   283  
   284  		{
   285  			name: "no function spec",
   286  			in: []f{
   287  				{
   288  					explicitFunction: true,
   289  					value: `
   290  apiVersion: example.com/v1alpha1
   291  kind: ExampleFunction
   292  metadata:
   293    annotations:
   294      foo: bar
   295  `,
   296  				},
   297  			},
   298  		},
   299  		{
   300  			name: "invalid input object",
   301  			in: []f{
   302  				{
   303  					explicitFunction: true,
   304  					value: `
   305  foo: bar
   306  `,
   307  				},
   308  			},
   309  			error: "failed to get FunctionSpec: failed to get ResourceMeta: missing Resource metadata",
   310  		},
   311  
   312  		// Test
   313  		//
   314  		//
   315  		{name: "defer_failure",
   316  			in: []f{
   317  				{
   318  					path: filepath.Join("foo", "bar.yaml"),
   319  					value: `
   320  apiVersion: example.com/v1alpha1
   321  kind: ExampleFunction
   322  metadata:
   323    annotations:
   324      config.kubernetes.io/function: |
   325        deferFailure: true
   326        container:
   327          image: gcr.io/example.com/image:v1.0.0
   328      config.kubernetes.io/local-config: "true"
   329  `,
   330  				},
   331  			},
   332  			out: []string{"gcr.io/example.com/image:v1.0.0 deferFailure: true"},
   333  		},
   334  		{
   335  			name: "parse_failure",
   336  			in: []f{
   337  				{
   338  					path: filepath.Join("foo", "bar.yaml"),
   339  					value: `
   340  apiVersion: example.com/v1alpha1
   341  kind: ExampleFunction
   342  metadata:
   343    annotations:
   344      config.kubernetes.io/function: |
   345        containeeer:
   346          image: gcr.io/example.com/image:v1.0.0
   347  `,
   348  				},
   349  			},
   350  			error: "config.kubernetes.io/function unmarshal error: error unmarshaling JSON: while decoding JSON: json: unknown field \"containeeer\"",
   351  		},
   352  
   353  		{name: "disable containers",
   354  			in: []f{
   355  				{
   356  					path: filepath.Join("foo", "bar.yaml"),
   357  					value: `
   358  apiVersion: example.com/v1alpha1
   359  kind: ExampleFunction
   360  metadata:
   361    annotations:
   362      config.kubernetes.io/function: |
   363        container:
   364          image: gcr.io/example.com/image:v1.0.0
   365      config.kubernetes.io/local-config: "true"
   366  `,
   367  				},
   368  			},
   369  			out:               nil,
   370  			disableContainers: true,
   371  		},
   372  
   373  		// Test
   374  		//
   375  		//
   376  		{name: "sort functions -- deepest first",
   377  			in: []f{
   378  				{
   379  					path: "a.yaml",
   380  					value: `
   381  metadata:
   382    annotations:
   383      config.kubernetes.io/function: |
   384        container:
   385          image: a
   386  `,
   387  				},
   388  				{
   389  					path: filepath.Join("foo", "b.yaml"),
   390  					value: `
   391  metadata:
   392    annotations:
   393      config.kubernetes.io/function: |
   394        container:
   395          image: b
   396  `,
   397  				},
   398  			},
   399  			out: []string{"b", "a"},
   400  		},
   401  
   402  		// Test
   403  		//
   404  		//
   405  		{name: "sort functions -- skip implicit with output of package",
   406  			in: []f{
   407  				{
   408  					path:         filepath.Join("foo", "a.yaml"),
   409  					outOfPackage: true, // out of package is run last
   410  					value: `
   411  metadata:
   412    annotations:
   413      config.kubernetes.io/function: |
   414        container:
   415          image: a
   416  `,
   417  				},
   418  				{
   419  					path: "b.yaml",
   420  					value: `
   421  metadata:
   422    annotations:
   423      config.kubernetes.io/function: |
   424        container:
   425          image: b
   426  `,
   427  				},
   428  			},
   429  			out: []string{"a"},
   430  		},
   431  
   432  		// Test
   433  		//
   434  		//
   435  		{name: "sort functions -- skip implicit",
   436  			noFunctionsFromInput: getTrue(),
   437  			in: []f{
   438  				{
   439  					path: filepath.Join("foo", "a.yaml"),
   440  					value: `
   441  metadata:
   442    annotations:
   443      config.kubernetes.io/function: |
   444        container:
   445          image: a
   446  `,
   447  				},
   448  				{
   449  					path: "b.yaml",
   450  					value: `
   451  metadata:
   452    annotations:
   453      config.kubernetes.io/function: |
   454        container:
   455          image: b
   456  `,
   457  				},
   458  			},
   459  			out: nil,
   460  		},
   461  
   462  		// Test
   463  		//
   464  		//
   465  		{name: "sort functions -- include implicit",
   466  			noFunctionsFromInput: getFalse(),
   467  			in: []f{
   468  				{
   469  					path: filepath.Join("foo", "a.yaml"),
   470  					value: `
   471  metadata:
   472    annotations:
   473      config.kubernetes.io/function: |
   474        container:
   475          image: a
   476  `,
   477  				},
   478  				{
   479  					path: "b.yaml",
   480  					value: `
   481  metadata:
   482    annotations:
   483      config.kubernetes.io/function: |
   484        container:
   485          image: b
   486  `,
   487  				},
   488  			},
   489  			out: []string{"a", "b"},
   490  		},
   491  
   492  		// Test
   493  		//
   494  		//
   495  		{name: "sort functions -- implicit first",
   496  			noFunctionsFromInput: getFalse(),
   497  			in: []f{
   498  				{
   499  					path:         filepath.Join("foo", "a.yaml"),
   500  					outOfPackage: true, // out of package is run last
   501  					value: `
   502  metadata:
   503    annotations:
   504      config.kubernetes.io/function: |
   505        container:
   506          image: a
   507  `,
   508  				},
   509  				{
   510  					path: "b.yaml",
   511  					value: `
   512  metadata:
   513    annotations:
   514      config.kubernetes.io/function: |
   515        container:
   516          image: b
   517  `,
   518  				},
   519  			},
   520  			out: []string{"b", "a"},
   521  		},
   522  
   523  		// Test
   524  		//
   525  		//
   526  		{name: "explicit functions",
   527  			in: []f{
   528  				{
   529  					explicitFunction: true,
   530  					value: `
   531  metadata:
   532    annotations:
   533      config.kubernetes.io/function: |
   534        container:
   535          image: c
   536  `,
   537  				},
   538  				{
   539  					path: "b.yaml",
   540  					value: `
   541  metadata:
   542    annotations:
   543      config.kubernetes.io/function: |
   544        container:
   545          image: b
   546  `,
   547  				},
   548  			},
   549  			out: []string{"c"},
   550  		},
   551  
   552  		// Test
   553  		//
   554  		//
   555  		{name: "sort functions -- implicit first",
   556  			noFunctionsFromInput: getFalse(),
   557  			in: []f{
   558  				{
   559  					explicitFunction: true,
   560  					value: `
   561  metadata:
   562    annotations:
   563      config.kubernetes.io/function: |
   564        container:
   565          image: c
   566  `,
   567  				},
   568  				{
   569  					path:         filepath.Join("foo", "a.yaml"),
   570  					outOfPackage: true, // out of package is run last
   571  					value: `
   572  metadata:
   573    annotations:
   574      config.kubernetes.io/function: |
   575        container:
   576          image: a
   577  `,
   578  				},
   579  				{
   580  					path: "b.yaml",
   581  					value: `
   582  metadata:
   583    annotations:
   584      config.kubernetes.io/function: |
   585        container:
   586          image: b
   587  `,
   588  				},
   589  			},
   590  			out: []string{"b", "a", "c"},
   591  		},
   592  
   593  		// Test
   594  		//
   595  		//
   596  		{name: "starlark-function",
   597  			in: []f{
   598  				{
   599  					path: filepath.Join("foo", "bar.yaml"),
   600  					value: `
   601  apiVersion: example.com/v1alpha1
   602  kind: ExampleFunction
   603  metadata:
   604    annotations:
   605      config.kubernetes.io/function: |
   606        starlark:
   607          path: a/b/c
   608  `,
   609  				},
   610  			},
   611  			enableStarlark: true,
   612  			outFn: func(path string) []string {
   613  				return []string{
   614  					fmt.Sprintf("name:  path: %s/foo/a/b/c url:  program:", filepath.ToSlash(path))}
   615  			},
   616  		},
   617  
   618  		// Test
   619  		//
   620  		//
   621  		{name: "starlark-function-absolute",
   622  			in: []f{
   623  				{
   624  					path: filepath.Join("foo", "bar.yaml"),
   625  					value: `
   626  apiVersion: example.com/v1alpha1
   627  kind: ExampleFunction
   628  metadata:
   629    annotations:
   630      config.kubernetes.io/function: |
   631        starlark:
   632          path: /a/b/c
   633  `,
   634  				},
   635  			},
   636  			enableStarlark: true,
   637  			error:          "absolute function path /a/b/c not allowed",
   638  		},
   639  
   640  		// Test
   641  		//
   642  		//
   643  		{name: "starlark-function-escape-parent",
   644  			in: []f{
   645  				{
   646  					path: filepath.Join("foo", "bar.yaml"),
   647  					value: `
   648  apiVersion: example.com/v1alpha1
   649  kind: ExampleFunction
   650  metadata:
   651    annotations:
   652      config.kubernetes.io/function: |
   653        starlark:
   654          path: ../a/b/c
   655  `,
   656  				},
   657  			},
   658  			enableStarlark: true,
   659  			error:          "function path ../a/b/c not allowed to start with ../",
   660  		},
   661  
   662  		{name: "starlark-function-disabled",
   663  			in: []f{
   664  				{
   665  					path: filepath.Join("foo", "bar.yaml"),
   666  					value: `
   667  apiVersion: example.com/v1alpha1
   668  kind: ExampleFunction
   669  metadata:
   670    annotations:
   671      config.kubernetes.io/function: |
   672        starlark:
   673          path: a/b/c
   674  `,
   675  				},
   676  			},
   677  		},
   678  	}
   679  
   680  	for i := range tests {
   681  		tt := tests[i]
   682  		t.Run(tt.name, func(t *testing.T) {
   683  			// setup the test directory
   684  			d := setupTest(t)
   685  
   686  			// write the functions to files
   687  			var fnPaths []string
   688  			var parsedFns []*yaml.RNode
   689  			var fnPath string
   690  			var err error
   691  			for _, f := range tt.in {
   692  				// get the location for the file
   693  				var dir string
   694  				switch {
   695  				case f.outOfPackage:
   696  					// if out of package, write to a separate temp directory
   697  					if f.newFnPath || fnPath == "" {
   698  						// create a new fn directory
   699  						fnPath = t.TempDir()
   700  						fnPaths = append(fnPaths, fnPath)
   701  					}
   702  					dir = fnPath
   703  				case f.explicitFunction:
   704  					parsedFns = append(parsedFns, yaml.MustParse(f.value))
   705  				default:
   706  					// if in package, write to the dir containing the configs
   707  					dir = d
   708  				}
   709  
   710  				if !f.explicitFunction {
   711  					// create the parent dir and write the file
   712  					err = os.MkdirAll(filepath.Join(dir, filepath.Dir(f.path)), 0700)
   713  					if !assert.NoError(t, err) {
   714  						t.FailNow()
   715  					}
   716  					err := os.WriteFile(filepath.Join(dir, f.path), []byte(f.value), 0600)
   717  					if !assert.NoError(t, err) {
   718  						t.FailNow()
   719  					}
   720  				}
   721  			}
   722  
   723  			// init the instance
   724  			r := &RunFns{
   725  				EnableStarlark:       tt.enableStarlark,
   726  				DisableContainers:    tt.disableContainers,
   727  				FunctionPaths:        fnPaths,
   728  				Functions:            parsedFns,
   729  				Path:                 d,
   730  				NoFunctionsFromInput: tt.noFunctionsFromInput,
   731  			}
   732  			r.init()
   733  
   734  			// get the filters which would be run
   735  			var results []string
   736  			_, fltrs, _, err := r.getNodesAndFilters()
   737  
   738  			if tt.error != "" {
   739  				if !assert.EqualError(t, err, tt.error) {
   740  					t.FailNow()
   741  				}
   742  				return
   743  			}
   744  
   745  			if !assert.NoError(t, err) {
   746  				t.FailNow()
   747  			}
   748  			for _, f := range fltrs {
   749  				results = append(results, strings.TrimSpace(fmt.Sprintf("%v", f)))
   750  			}
   751  
   752  			// compare the actual ordering to the expected ordering
   753  			if tt.outFn != nil {
   754  				if !assert.Equal(t, tt.outFn(d), results) {
   755  					t.FailNow()
   756  				}
   757  			} else {
   758  				if !assert.Equal(t, tt.out, results) {
   759  					t.FailNow()
   760  				}
   761  			}
   762  		})
   763  	}
   764  }
   765  
   766  func TestRunFns_sortFns(t *testing.T) {
   767  	testCases := []struct {
   768  		name           string
   769  		nodes          []*yaml.RNode
   770  		expectedImages []string
   771  		expectedErrMsg string
   772  	}{
   773  		{
   774  			name: "multiple functions in the same file are ordered by index",
   775  			nodes: []*yaml.RNode{
   776  				yaml.MustParse(`
   777  metadata:
   778    annotations:
   779      config.kubernetes.io/path: functions.yaml
   780      config.kubernetes.io/index: 1
   781      config.kubernetes.io/function: |
   782        container:
   783          image: a
   784  `),
   785  				yaml.MustParse(`
   786  metadata:
   787    annotations:
   788      config.kubernetes.io/path: functions.yaml
   789      config.kubernetes.io/index: 0
   790      config.kubernetes.io/function: |
   791        container:
   792          image: b
   793  `),
   794  			},
   795  			expectedImages: []string{"b", "a"},
   796  		},
   797  		{
   798  			name: "non-integer value in index annotation is an error",
   799  			nodes: []*yaml.RNode{
   800  				yaml.MustParse(`
   801  metadata:
   802    annotations:
   803      config.kubernetes.io/path: functions.yaml
   804      config.kubernetes.io/index: 0
   805      config.kubernetes.io/function: |
   806        container:
   807          image: a
   808  `),
   809  				yaml.MustParse(`
   810  metadata:
   811    annotations:
   812      config.kubernetes.io/path: functions.yaml
   813      config.kubernetes.io/index: abc
   814      config.kubernetes.io/function: |
   815        container:
   816          image: b
   817  `),
   818  			},
   819  			expectedErrMsg: "strconv.Atoi: parsing \"abc\": invalid syntax",
   820  		},
   821  	}
   822  
   823  	for i := range testCases {
   824  		test := testCases[i]
   825  		t.Run(test.name, func(t *testing.T) {
   826  			packageBuff := &kio.PackageBuffer{
   827  				Nodes: test.nodes,
   828  			}
   829  
   830  			err := sortFns(packageBuff)
   831  			if test.expectedErrMsg != "" {
   832  				if !assert.Error(t, err) {
   833  					t.FailNow()
   834  				}
   835  				assert.Equal(t, test.expectedErrMsg, err.Error())
   836  				return
   837  			}
   838  
   839  			if !assert.NoError(t, err) {
   840  				t.FailNow()
   841  			}
   842  
   843  			var images []string
   844  			for _, n := range packageBuff.Nodes {
   845  				spec, err := runtimeutil.GetFunctionSpec(n)
   846  				if !assert.NoError(t, err) {
   847  					t.FailNow()
   848  				}
   849  				images = append(images, spec.Container.Image)
   850  			}
   851  
   852  			assert.Equal(t, test.expectedImages, images)
   853  		})
   854  	}
   855  }
   856  
   857  func TestRunFns_network(t *testing.T) {
   858  	tests := []struct {
   859  		name          string
   860  		input         string
   861  		network       bool
   862  		expectNetwork bool
   863  		error         string
   864  	}{
   865  		{
   866  			name: "imperative false, declarative false",
   867  			input: `
   868  metadata:
   869    annotations:
   870      config.kubernetes.io/function: |
   871        container:
   872          image: a
   873          network: false
   874  `,
   875  			network:       false,
   876  			expectNetwork: false,
   877  		},
   878  		{
   879  			name: "imperative true, declarative false",
   880  			input: `
   881  metadata:
   882    annotations:
   883      config.kubernetes.io/function: |
   884        container:
   885          image: a
   886          network: false
   887  `,
   888  			network:       true,
   889  			expectNetwork: false,
   890  		},
   891  		{
   892  			name: "imperative true, declarative true",
   893  			input: `
   894  metadata:
   895    annotations:
   896      config.kubernetes.io/function: |
   897        container:
   898          image: a
   899          network: true
   900  `,
   901  			network:       true,
   902  			expectNetwork: true,
   903  		},
   904  		{
   905  			name: "imperative false, declarative true",
   906  			input: `
   907  metadata:
   908    annotations:
   909      config.kubernetes.io/function: |
   910        container:
   911          image: a
   912          network: true
   913  `,
   914  			network: false,
   915  			error:   "network required but not enabled with --network",
   916  		},
   917  	}
   918  
   919  	for i := range tests {
   920  		tt := tests[i]
   921  		fn := yaml.MustParse(tt.input)
   922  		t.Run(tt.name, func(t *testing.T) {
   923  			// init the instance
   924  			r := &RunFns{
   925  				Functions: []*yaml.RNode{fn},
   926  				Network:   tt.network,
   927  			}
   928  			r.init()
   929  
   930  			_, fltrs, _, err := r.getNodesAndFilters()
   931  			if tt.error != "" {
   932  				if !assert.EqualError(t, err, tt.error) {
   933  					t.FailNow()
   934  				}
   935  				return
   936  			}
   937  			if !assert.NoError(t, err) {
   938  				t.FailNow()
   939  			}
   940  
   941  			fltr := fltrs[0].(*container.Filter)
   942  			if !assert.Equal(t, tt.expectNetwork, fltr.Network) {
   943  				t.FailNow()
   944  			}
   945  		})
   946  	}
   947  }
   948  
   949  func TestCmd_Execute(t *testing.T) {
   950  	dir := setupTest(t)
   951  
   952  	// write a test filter to the directory of configuration
   953  	if !assert.NoError(t, os.WriteFile(
   954  		filepath.Join(dir, "filter.yaml"), []byte(ValueReplacerYAMLData), 0600)) {
   955  		return
   956  	}
   957  
   958  	instance := RunFns{Path: dir, functionFilterProvider: getFilterProvider(t)}
   959  	if !assert.NoError(t, instance.Execute()) {
   960  		t.FailNow()
   961  	}
   962  	b, err := os.ReadFile(
   963  		filepath.Join(dir, "java", "java-deployment.resource.yaml"))
   964  	if !assert.NoError(t, err) {
   965  		t.FailNow()
   966  	}
   967  	assert.Contains(t, string(b), "kind: StatefulSet")
   968  }
   969  
   970  type TestFilter struct {
   971  	invoked bool
   972  	Exit    error
   973  }
   974  
   975  func (f *TestFilter) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) {
   976  	f.invoked = true
   977  	return input, nil
   978  }
   979  
   980  func (f *TestFilter) GetExit() error {
   981  	return f.Exit
   982  }
   983  
   984  func TestCmd_Execute_deferFailure(t *testing.T) {
   985  	dir := setupTest(t)
   986  
   987  	// write a test filter to the directory of configuration
   988  	if !assert.NoError(t, os.WriteFile(
   989  		filepath.Join(dir, "filter1.yaml"), []byte(`apiVersion: v1
   990  kind: ValueReplacer
   991  metadata:
   992    annotations:
   993      config.kubernetes.io/function: |
   994        container:
   995          image: 1
   996      config.kubernetes.io/local-config: "true"
   997  stringMatch: Deployment
   998  replace: StatefulSet
   999  `), 0600)) {
  1000  		t.FailNow()
  1001  	}
  1002  
  1003  	// write a test filter to the directory of configuration
  1004  	if !assert.NoError(t, os.WriteFile(
  1005  		filepath.Join(dir, "filter2.yaml"), []byte(`apiVersion: v1
  1006  kind: ValueReplacer
  1007  metadata:
  1008    annotations:
  1009      config.kubernetes.io/function: |
  1010        container:
  1011          image: 2
  1012      config.kubernetes.io/local-config: "true"
  1013  stringMatch: Deployment
  1014  replace: StatefulSet
  1015  `), 0600)) {
  1016  		t.FailNow()
  1017  	}
  1018  
  1019  	var fltrs []*TestFilter
  1020  	instance := RunFns{
  1021  		Path: dir,
  1022  		functionFilterProvider: func(f runtimeutil.FunctionSpec, node *yaml.RNode, currentUser currentUserFunc) (kio.Filter, error) {
  1023  			tf := &TestFilter{
  1024  				Exit: errors.Errorf("message: %s", f.Container.Image),
  1025  			}
  1026  			fltrs = append(fltrs, tf)
  1027  			return tf, nil
  1028  		},
  1029  	}
  1030  	instance.init()
  1031  
  1032  	err := instance.Execute()
  1033  
  1034  	// make sure all filters were run
  1035  	if !assert.Equal(t, 2, len(fltrs)) {
  1036  		t.FailNow()
  1037  	}
  1038  	for i := range fltrs {
  1039  		if !assert.True(t, fltrs[i].invoked) {
  1040  			t.FailNow()
  1041  		}
  1042  	}
  1043  
  1044  	if !assert.EqualError(t, err, "message: 1\n---\nmessage: 2") {
  1045  		t.FailNow()
  1046  	}
  1047  	b, err := os.ReadFile(
  1048  		filepath.Join(dir, "java", "java-deployment.resource.yaml"))
  1049  	if !assert.NoError(t, err) {
  1050  		t.FailNow()
  1051  	}
  1052  	// files weren't changed because there was an error
  1053  	assert.Contains(t, string(b), "kind: Deployment")
  1054  }
  1055  
  1056  // TestCmd_Execute_setOutput tests the execution of a filter reading and writing to a dir
  1057  func TestCmd_Execute_setFunctionPaths(t *testing.T) {
  1058  	dir := setupTest(t)
  1059  
  1060  	// write a test filter to a separate directory
  1061  	tmpF, err := os.CreateTemp("", "filter*.yaml")
  1062  	if !assert.NoError(t, err) {
  1063  		return
  1064  	}
  1065  	os.RemoveAll(tmpF.Name())
  1066  	if !assert.NoError(t, os.WriteFile(tmpF.Name(), []byte(ValueReplacerYAMLData), 0600)) {
  1067  		return
  1068  	}
  1069  
  1070  	// run the functions, providing the path to the directory of filters
  1071  	instance := RunFns{
  1072  		FunctionPaths:          []string{tmpF.Name()},
  1073  		Path:                   dir,
  1074  		functionFilterProvider: getFilterProvider(t),
  1075  	}
  1076  	// initialize the defaults
  1077  	instance.init()
  1078  
  1079  	err = instance.Execute()
  1080  	if !assert.NoError(t, err) {
  1081  		return
  1082  	}
  1083  	b, err := os.ReadFile(
  1084  		filepath.Join(dir, "java", "java-deployment.resource.yaml"))
  1085  	if !assert.NoError(t, err) {
  1086  		return
  1087  	}
  1088  	assert.Contains(t, string(b), "kind: StatefulSet")
  1089  }
  1090  
  1091  // TestCmd_Execute_setOutput tests the execution of a filter using an io.Writer as output
  1092  func TestCmd_Execute_setOutput(t *testing.T) {
  1093  	dir := setupTest(t)
  1094  
  1095  	// write a test filter
  1096  	if !assert.NoError(t, os.WriteFile(
  1097  		filepath.Join(dir, "filter.yaml"), []byte(ValueReplacerYAMLData), 0600)) {
  1098  		return
  1099  	}
  1100  
  1101  	out := &bytes.Buffer{}
  1102  	instance := RunFns{
  1103  		Output:                 out, // write to out
  1104  		Path:                   dir,
  1105  		functionFilterProvider: getFilterProvider(t),
  1106  	}
  1107  	// initialize the defaults
  1108  	instance.init()
  1109  
  1110  	if !assert.NoError(t, instance.Execute()) {
  1111  		return
  1112  	}
  1113  	b, err := os.ReadFile(
  1114  		filepath.Join(dir, "java", "java-deployment.resource.yaml"))
  1115  	if !assert.NoError(t, err) {
  1116  		return
  1117  	}
  1118  	assert.NotContains(t, string(b), "kind: StatefulSet")
  1119  	assert.Contains(t, out.String(), "kind: StatefulSet")
  1120  }
  1121  
  1122  // TestCmd_Execute_setInput tests the execution of a filter using an io.Reader as input
  1123  func TestCmd_Execute_setInput(t *testing.T) {
  1124  	dir := setupTest(t)
  1125  	if !assert.NoError(t, os.WriteFile(
  1126  		filepath.Join(dir, "filter.yaml"), []byte(ValueReplacerYAMLData), 0600)) {
  1127  		return
  1128  	}
  1129  
  1130  	read, err := kio.LocalPackageReader{PackagePath: dir}.Read()
  1131  	if !assert.NoError(t, err) {
  1132  		t.FailNow()
  1133  	}
  1134  	input := &bytes.Buffer{}
  1135  	if !assert.NoError(t, kio.ByteWriter{Writer: input}.Write(read)) {
  1136  		t.FailNow()
  1137  	}
  1138  
  1139  	outDir := t.TempDir()
  1140  
  1141  	if !assert.NoError(t, os.WriteFile(
  1142  		filepath.Join(dir, "filter.yaml"), []byte(ValueReplacerYAMLData), 0600)) {
  1143  		return
  1144  	}
  1145  
  1146  	instance := RunFns{
  1147  		Input:                  input, // read from input
  1148  		Path:                   outDir,
  1149  		functionFilterProvider: getFilterProvider(t),
  1150  	}
  1151  	// initialize the defaults
  1152  	instance.init()
  1153  
  1154  	if !assert.NoError(t, instance.Execute()) {
  1155  		return
  1156  	}
  1157  	b, err := os.ReadFile(
  1158  		filepath.Join(outDir, "java", "java-deployment.resource.yaml"))
  1159  	if !assert.NoError(t, err) {
  1160  		t.FailNow()
  1161  	}
  1162  	assert.Contains(t, string(b), "kind: StatefulSet")
  1163  }
  1164  
  1165  // TestCmd_Execute_enableLogSteps tests the execution of a filter with LogSteps enabled.
  1166  func TestCmd_Execute_enableLogSteps(t *testing.T) {
  1167  	dir := setupTest(t)
  1168  
  1169  	// write a test filter to the directory of configuration
  1170  	if !assert.NoError(t, os.WriteFile(
  1171  		filepath.Join(dir, "filter.yaml"), []byte(ValueReplacerYAMLData), 0600)) {
  1172  		return
  1173  	}
  1174  
  1175  	logs := &bytes.Buffer{}
  1176  	instance := RunFns{
  1177  		Path:                   dir,
  1178  		functionFilterProvider: getFilterProvider(t),
  1179  		LogSteps:               true,
  1180  		LogWriter:              logs,
  1181  	}
  1182  	if !assert.NoError(t, instance.Execute()) {
  1183  		t.FailNow()
  1184  	}
  1185  	b, err := os.ReadFile(
  1186  		filepath.Join(dir, "java", "java-deployment.resource.yaml"))
  1187  	if !assert.NoError(t, err) {
  1188  		t.FailNow()
  1189  	}
  1190  	assert.Contains(t, string(b), "kind: StatefulSet")
  1191  	assert.Equal(t, "Running unknown-type function\n", logs.String())
  1192  }
  1193  
  1194  func getGeneratorFilterProvider(t *testing.T) func(runtimeutil.FunctionSpec, *yaml.RNode, currentUserFunc) (kio.Filter, error) {
  1195  	t.Helper()
  1196  	return func(f runtimeutil.FunctionSpec, node *yaml.RNode, currentUser currentUserFunc) (kio.Filter, error) {
  1197  		return kio.FilterFunc(func(items []*yaml.RNode) ([]*yaml.RNode, error) {
  1198  			if f.Container.Image == "generate" {
  1199  				node, err := yaml.Parse("kind: generated")
  1200  				if !assert.NoError(t, err) {
  1201  					t.FailNow()
  1202  				}
  1203  				return append(items, node), nil
  1204  			}
  1205  			return items, nil
  1206  		}), nil
  1207  	}
  1208  }
  1209  func TestRunFns_ContinueOnEmptyResult(t *testing.T) {
  1210  	fn1, err := yaml.Parse(`
  1211  kind: fakefn
  1212  metadata:
  1213    annotations:
  1214      config.kubernetes.io/function: |
  1215        container:
  1216          image: pass
  1217  `)
  1218  	if !assert.NoError(t, err) {
  1219  		t.FailNow()
  1220  	}
  1221  	fn2, err := yaml.Parse(`
  1222  kind: fakefn
  1223  metadata:
  1224    annotations:
  1225      config.kubernetes.io/function: |
  1226        container:
  1227          image: generate
  1228  `)
  1229  	if !assert.NoError(t, err) {
  1230  		t.FailNow()
  1231  	}
  1232  
  1233  	var test = []struct {
  1234  		ContinueOnEmptyResult bool
  1235  		ExpectedOutput        string
  1236  	}{
  1237  		{
  1238  			ContinueOnEmptyResult: false,
  1239  			ExpectedOutput:        "",
  1240  		},
  1241  		{
  1242  			ContinueOnEmptyResult: true,
  1243  			ExpectedOutput:        "kind: generated\n",
  1244  		},
  1245  	}
  1246  	for i := range test {
  1247  		ouputBuffer := bytes.Buffer{}
  1248  		instance := RunFns{
  1249  			Input:                  bytes.NewReader([]byte{}),
  1250  			Output:                 &ouputBuffer,
  1251  			Functions:              []*yaml.RNode{fn1, fn2},
  1252  			functionFilterProvider: getGeneratorFilterProvider(t),
  1253  			ContinueOnEmptyResult:  test[i].ContinueOnEmptyResult,
  1254  		}
  1255  		if !assert.NoError(t, instance.Execute()) {
  1256  			t.FailNow()
  1257  		}
  1258  		assert.Equal(t, test[i].ExpectedOutput, ouputBuffer.String())
  1259  	}
  1260  }
  1261  
  1262  // setupTest initializes a temp test directory containing test data
  1263  func setupTest(t *testing.T) string {
  1264  	t.Helper()
  1265  	dir := t.TempDir()
  1266  
  1267  	_, filename, _, ok := runtime.Caller(0)
  1268  	if !assert.True(t, ok) {
  1269  		t.FailNow()
  1270  	}
  1271  	ds, err := filepath.Abs(filepath.Join(filepath.Dir(filename), "test", "testdata"))
  1272  	if !assert.NoError(t, err) {
  1273  		t.FailNow()
  1274  	}
  1275  	if !assert.NoError(t, copyutil.CopyDir(filesys.MakeFsOnDisk(), ds, dir)) {
  1276  		t.FailNow()
  1277  	}
  1278  
  1279  	cwd, err := os.Getwd()
  1280  	if !assert.NoError(t, err) {
  1281  		t.FailNow()
  1282  	}
  1283  
  1284  	if !assert.NoError(t, os.Chdir(filepath.Dir(dir))) {
  1285  		t.FailNow()
  1286  	}
  1287  
  1288  	// Change back the current working directory when the test finishes
  1289  	t.Cleanup(func() {
  1290  		if !assert.NoError(t, os.Chdir(cwd)) {
  1291  			t.FailNow()
  1292  		}
  1293  	})
  1294  
  1295  	return dir
  1296  }
  1297  
  1298  // getFilterProvider fakes the creation of a filter, replacing the ContainerFiler with
  1299  // a filter to s/kind: Deployment/kind: StatefulSet/g.
  1300  // this can be used to simulate running a filter.
  1301  func getFilterProvider(t *testing.T) func(runtimeutil.FunctionSpec, *yaml.RNode, currentUserFunc) (kio.Filter, error) {
  1302  	t.Helper()
  1303  	return func(f runtimeutil.FunctionSpec, node *yaml.RNode, currentUser currentUserFunc) (kio.Filter, error) {
  1304  		// parse the filter from the input
  1305  		filter := yaml.YFilter{}
  1306  		b := &bytes.Buffer{}
  1307  		e := yaml.NewEncoder(b)
  1308  		if !assert.NoError(t, e.Encode(node.YNode())) {
  1309  			t.FailNow()
  1310  		}
  1311  		e.Close()
  1312  		d := yaml.NewDecoder(b)
  1313  		if !assert.NoError(t, d.Decode(&filter)) {
  1314  			t.FailNow()
  1315  		}
  1316  
  1317  		return filters.Modifier{
  1318  			Filters: []yaml.YFilter{{Filter: yaml.Lookup("kind")}, filter},
  1319  		}, nil
  1320  	}
  1321  }
  1322  
  1323  func TestRunFns_mergeContainerEnv(t *testing.T) {
  1324  	testcases := []struct {
  1325  		name      string
  1326  		instance  RunFns
  1327  		inputEnvs []string
  1328  		expect    runtimeutil.ContainerEnv
  1329  	}{
  1330  		{
  1331  			name:     "all empty",
  1332  			instance: RunFns{},
  1333  			expect:   *runtimeutil.NewContainerEnv(),
  1334  		},
  1335  		{
  1336  			name:      "empty command line envs",
  1337  			instance:  RunFns{},
  1338  			inputEnvs: []string{"foo=bar"},
  1339  			expect:    *runtimeutil.NewContainerEnvFromStringSlice([]string{"foo=bar"}),
  1340  		},
  1341  		{
  1342  			name: "empty declarative envs",
  1343  			instance: RunFns{
  1344  				Env: []string{"foo=bar"},
  1345  			},
  1346  			expect: *runtimeutil.NewContainerEnvFromStringSlice([]string{"foo=bar"}),
  1347  		},
  1348  		{
  1349  			name: "same key",
  1350  			instance: RunFns{
  1351  				Env: []string{"foo=bar", "foo"},
  1352  			},
  1353  			inputEnvs: []string{"foo=bar1", "bar"},
  1354  			expect:    *runtimeutil.NewContainerEnvFromStringSlice([]string{"foo=bar", "bar", "foo"}),
  1355  		},
  1356  		{
  1357  			name: "same exported key",
  1358  			instance: RunFns{
  1359  				Env: []string{"foo=bar", "foo"},
  1360  			},
  1361  			inputEnvs: []string{"foo1=bar1", "foo"},
  1362  			expect:    *runtimeutil.NewContainerEnvFromStringSlice([]string{"foo=bar", "foo1=bar1", "foo"}),
  1363  		},
  1364  	}
  1365  
  1366  	for i := range testcases {
  1367  		tc := testcases[i]
  1368  		t.Run(tc.name, func(t *testing.T) {
  1369  			envs := tc.instance.mergeContainerEnv(tc.inputEnvs)
  1370  			assert.Equal(t, tc.expect.GetDockerFlags(), runtimeutil.NewContainerEnvFromStringSlice(envs).GetDockerFlags())
  1371  		})
  1372  	}
  1373  }
  1374  

View as plain text