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  }