github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/internal/fnruntime/runner_test.go (about)

     1  // Copyright 2019 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  
    15  // Package pipeline provides struct definitions for Pipeline and utility
    16  // methods to read and write a pipeline resource.
    17  package fnruntime
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"os"
    23  	"path"
    24  	"strings"
    25  	"testing"
    26  
    27  	"github.com/GoogleContainerTools/kpt/internal/printer"
    28  	"github.com/GoogleContainerTools/kpt/internal/types"
    29  	kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
    30  	"github.com/stretchr/testify/assert"
    31  	"sigs.k8s.io/kustomize/kyaml/filesys"
    32  	"sigs.k8s.io/kustomize/kyaml/fn/framework"
    33  	"sigs.k8s.io/kustomize/kyaml/kio"
    34  	"sigs.k8s.io/kustomize/kyaml/yaml"
    35  )
    36  
    37  func TestFunctionConfig(t *testing.T) {
    38  	type input struct {
    39  		name              string
    40  		fn                kptfilev1.Function
    41  		configFileContent string
    42  		expected          string
    43  	}
    44  
    45  	cases := []input{
    46  		{
    47  			name:     "no config",
    48  			fn:       kptfilev1.Function{},
    49  			expected: "",
    50  		},
    51  		{
    52  			name: "file config",
    53  			fn:   kptfilev1.Function{},
    54  			configFileContent: `apiVersion: cft.dev/v1alpha1
    55  kind: ResourceHierarchy
    56  metadata:
    57    name: root-hierarchy
    58    namespace: hierarchy`,
    59  			expected: `apiVersion: cft.dev/v1alpha1
    60  kind: ResourceHierarchy
    61  metadata:
    62    name: root-hierarchy
    63    namespace: hierarchy
    64  `,
    65  		},
    66  		{
    67  			name: "map config",
    68  			fn: kptfilev1.Function{
    69  				ConfigMap: map[string]string{
    70  					"foo": "bar",
    71  				},
    72  			},
    73  			expected: `apiVersion: v1
    74  kind: ConfigMap
    75  metadata:
    76    name: function-input
    77  data: {foo: bar}
    78  `,
    79  		},
    80  	}
    81  
    82  	for _, c := range cases {
    83  		c := c
    84  		t.Run(c.name, func(t *testing.T) {
    85  			if c.configFileContent != "" {
    86  				tmp, err := os.CreateTemp("", "kpt-pipeline-*")
    87  				assert.NoError(t, err, "unexpected error")
    88  				_, err = tmp.WriteString(c.configFileContent)
    89  				assert.NoError(t, err, "unexpected error")
    90  				c.fn.ConfigPath = path.Base(tmp.Name())
    91  			}
    92  			fsys := filesys.MakeFsOnDisk()
    93  			cn, err := newFnConfig(fsys, &c.fn, types.UniquePath(os.TempDir()))
    94  			assert.NoError(t, err, "unexpected error")
    95  			actual, err := cn.String()
    96  			assert.NoError(t, err, "unexpected error")
    97  			assert.Equal(t, c.expected, actual, "unexpected result")
    98  		})
    99  	}
   100  }
   101  
   102  func TestMultilineFormatter(t *testing.T) {
   103  	type testcase struct {
   104  		ml       *multiLineFormatter
   105  		expected string
   106  	}
   107  
   108  	testcases := map[string]testcase{
   109  		"multiline should format lines and truncate": {
   110  			ml: &multiLineFormatter{
   111  				Title: "Results",
   112  				Lines: []string{
   113  					"line-1",
   114  					"line-2",
   115  					"line-3",
   116  					"line-4",
   117  					"line-5",
   118  				},
   119  				MaxLines:       3,
   120  				TruncateOutput: true,
   121  			},
   122  			expected: `  Results:
   123      line-1
   124      line-2
   125      line-3
   126      ...(2 line(s) truncated, use '--truncate-output=false' to disable)
   127  `,
   128  		},
   129  		"multiline should format without truncate": {
   130  			ml: &multiLineFormatter{
   131  				Title: "Results",
   132  				Lines: []string{
   133  					"line-1",
   134  					"line-2",
   135  					"line-3",
   136  					"line-4",
   137  					"line-5",
   138  				},
   139  			},
   140  			expected: `  Results:
   141      line-1
   142      line-2
   143      line-3
   144      line-4
   145      line-5
   146  `,
   147  		},
   148  	}
   149  	for name, c := range testcases {
   150  		c := c
   151  		t.Run(name, func(t *testing.T) {
   152  			assert.Equal(t, c.expected, c.ml.String())
   153  		})
   154  	}
   155  }
   156  
   157  func TestEnforcePathInvariants(t *testing.T) {
   158  	tests := map[string]struct {
   159  		input       string // input
   160  		expectedErr string // expected result
   161  	}{
   162  		"duplicate": {
   163  			input: `apiVersion: v1
   164  kind: Custom
   165  metadata:
   166    name: a
   167    annotations:
   168      config.kubernetes.io/path: 'my/path/custom.yaml'
   169      config.kubernetes.io/index: '0'
   170  ---
   171  apiVersion: v1
   172  kind: Custom
   173  metadata:
   174    name: b
   175    annotations:
   176      config.kubernetes.io/path: 'my/path/custom.yaml'
   177      config.kubernetes.io/index: '0'
   178  `,
   179  			expectedErr: `resource at path "my/path/custom.yaml" and index "0" already exists`,
   180  		},
   181  		"duplicate with `./` prefix": {
   182  			input: `apiVersion: v1
   183  kind: Custom
   184  metadata:
   185    name: a
   186    annotations:
   187      config.kubernetes.io/path: 'my/path/custom.yaml'
   188      config.kubernetes.io/index: '0'
   189  ---
   190  apiVersion: v1
   191  kind: Custom
   192  metadata:
   193    name: b
   194    annotations:
   195      config.kubernetes.io/path: './my/path/custom.yaml'
   196      config.kubernetes.io/index: '0'
   197  `,
   198  			expectedErr: `resource at path "my/path/custom.yaml" and index "0" already exists`,
   199  		},
   200  		"duplicate path, not index": {
   201  			input: `apiVersion: v1
   202  kind: Custom
   203  metadata:
   204    name: a
   205    annotations:
   206      config.kubernetes.io/path: 'my/path/custom.yaml'
   207      config.kubernetes.io/index: '0'
   208  ---
   209  apiVersion: v1
   210  kind: Custom
   211  metadata:
   212    name: b
   213    annotations:
   214      config.kubernetes.io/path: 'my/path/custom.yaml'
   215      config.kubernetes.io/index: '1'
   216  `,
   217  		},
   218  		"duplicate index, not path": {
   219  			input: `apiVersion: v1
   220  kind: Custom
   221  metadata:
   222    name: a
   223    annotations:
   224      config.kubernetes.io/path: 'my/path/a.yaml'
   225      config.kubernetes.io/index: '0'
   226  ---
   227  apiVersion: v1
   228  kind: Custom
   229  metadata:
   230    name: b
   231    annotations:
   232      config.kubernetes.io/path: 'my/path/b.yaml'
   233      config.kubernetes.io/index: '0'
   234  `,
   235  		},
   236  		"larger number of resources with duplicate": {
   237  			input: `apiVersion: v1
   238  kind: Custom
   239  metadata:
   240    name: a
   241    annotations:
   242      config.kubernetes.io/path: 'my/path/a.yaml'
   243      config.kubernetes.io/index: '0'
   244  ---
   245  apiVersion: v1
   246  kind: Custom
   247  metadata:
   248    name: b
   249    annotations:
   250      config.kubernetes.io/path: 'my/path/a.yaml'
   251      config.kubernetes.io/index: '1'
   252  ---
   253  apiVersion: v1
   254  kind: Custom
   255  metadata:
   256    name: b
   257    annotations:
   258      config.kubernetes.io/path: 'my/path/b.yaml'
   259      config.kubernetes.io/index: '0'
   260  ---
   261  apiVersion: v1
   262  kind: Custom
   263  metadata:
   264    name: b
   265    annotations:
   266      config.kubernetes.io/path: 'my/path/b.yaml'
   267      config.kubernetes.io/index: '1'
   268  ---
   269  apiVersion: v1
   270  kind: Custom
   271  metadata:
   272    name: b
   273    annotations:
   274      config.kubernetes.io/path: 'my/path/b.yaml'
   275      config.kubernetes.io/index: '2'
   276  ---
   277  apiVersion: v1
   278  kind: Custom
   279  metadata:
   280    name: b
   281    annotations:
   282      config.kubernetes.io/path: 'my/path/c.yaml'
   283      config.kubernetes.io/index: '0'
   284  ---
   285  apiVersion: v1
   286  kind: Custom
   287  metadata:
   288    name: b
   289    annotations:
   290      config.kubernetes.io/path: 'my/path/c.yaml'
   291      config.kubernetes.io/index: '1'
   292  ---
   293  apiVersion: v1
   294  kind: Custom
   295  metadata:
   296    name: b
   297    annotations:
   298      config.kubernetes.io/path: 'my/path/b.yaml'
   299      config.kubernetes.io/index: '1'
   300  `,
   301  			expectedErr: `resource at path "my/path/b.yaml" and index "1" already exists`,
   302  		},
   303  		"larger number of resources without duplicates": {
   304  			input: `apiVersion: v1
   305  kind: Custom
   306  metadata:
   307    name: a
   308    annotations:
   309      config.kubernetes.io/path: 'my/path/a.yaml'
   310      config.kubernetes.io/index: '0'
   311  ---
   312  apiVersion: v1
   313  kind: Custom
   314  metadata:
   315    name: b
   316    annotations:
   317      config.kubernetes.io/path: 'my/path/a.yaml'
   318      config.kubernetes.io/index: '1'
   319  ---
   320  apiVersion: v1
   321  kind: Custom
   322  metadata:
   323    name: b
   324    annotations:
   325      config.kubernetes.io/path: 'my/path/b.yaml'
   326      config.kubernetes.io/index: '0'
   327  ---
   328  apiVersion: v1
   329  kind: Custom
   330  metadata:
   331    name: b
   332    annotations:
   333      config.kubernetes.io/path: 'my/path/b.yaml'
   334      config.kubernetes.io/index: '1'
   335  ---
   336  apiVersion: v1
   337  kind: Custom
   338  metadata:
   339    name: b
   340    annotations:
   341      config.kubernetes.io/path: 'my/path/b.yaml'
   342      config.kubernetes.io/index: '2'
   343  ---
   344  apiVersion: v1
   345  kind: Custom
   346  metadata:
   347    name: b
   348    annotations:
   349      config.kubernetes.io/path: 'my/path/c.yaml'
   350      config.kubernetes.io/index: '0'
   351  ---
   352  apiVersion: v1
   353  kind: Custom
   354  metadata:
   355    name: b
   356    annotations:
   357      config.kubernetes.io/path: 'my/path/c.yaml'
   358      config.kubernetes.io/index: '1'
   359  ---
   360  apiVersion: v1
   361  kind: Custom
   362  metadata:
   363    name: b
   364    annotations:
   365      config.kubernetes.io/path: 'my/path/b.yaml'
   366      config.kubernetes.io/index: '3'
   367  `,
   368  		},
   369  
   370  		"no error": {
   371  			input: `
   372  apiVersion: apps/v1
   373  kind: StatefulSet
   374  metadata:
   375    name: my-stateful-set
   376    annotations:
   377      config.kubernetes.io/path: my-stateful-set.yaml
   378  spec:
   379    replicas: 3
   380  `,
   381  		},
   382  		"with ../ prefix": {
   383  			input: `
   384  apiVersion: apps/v1
   385  kind: StatefulSet
   386  metadata:
   387    name: my-stateful-set
   388    annotations:
   389      config.kubernetes.io/path: ../my-stateful-set.yaml
   390  spec:
   391    replicas: 3
   392  
   393  `,
   394  			expectedErr: "function must not modify resources outside of package: resource has path ../my-stateful-set.yaml",
   395  		},
   396  		"with nested ../ in path": {
   397  			input: `
   398  apiVersion: apps/v1
   399  kind: StatefulSet
   400  metadata:
   401    name: my-stateful-set
   402    annotations:
   403      config.kubernetes.io/path: a/b/../../../my-stateful-set.yaml
   404  spec:
   405    replicas: 3
   406  `,
   407  			expectedErr: "function must not modify resources outside of package: resource has path a/b/../../../my-stateful-set.yaml",
   408  		},
   409  	}
   410  	for _, tc := range tests {
   411  		out := &bytes.Buffer{}
   412  		r := kio.ByteReadWriter{
   413  			Reader:                bytes.NewBufferString(tc.input),
   414  			Writer:                out,
   415  			KeepReaderAnnotations: true,
   416  			OmitReaderAnnotations: true,
   417  			WrapBareSeqNode:       true,
   418  		}
   419  		n, err := r.Read()
   420  		if err != nil {
   421  			t.FailNow()
   422  		}
   423  		err = enforcePathInvariants(n)
   424  		if err != nil && tc.expectedErr == "" {
   425  			t.Errorf("unexpected error %s", err.Error())
   426  			t.FailNow()
   427  		}
   428  		if tc.expectedErr != "" && err == nil {
   429  			t.Errorf("expected error %s", tc.expectedErr)
   430  			t.FailNow()
   431  		}
   432  		if tc.expectedErr != "" && !strings.Contains(err.Error(), tc.expectedErr) {
   433  			t.Errorf("wanted error %s, got %s", tc.expectedErr, err.Error())
   434  			t.FailNow()
   435  		}
   436  	}
   437  }
   438  
   439  func TestGetResourceRefMetadata(t *testing.T) {
   440  	tests := map[string]struct {
   441  		input    string // input
   442  		expected string // expected result
   443  	}{
   444  		"new format with name": {
   445  			input: `
   446  message: selector is required
   447  severity: error
   448  resourceRef:
   449    apiVersion: apps/v1
   450    kind: Deployment
   451    name: nginx-deployment
   452  field:
   453    path: selector
   454  file:
   455    path: resources.yaml
   456  `,
   457  			expected: `message: selector is required
   458  severity: error
   459  resourceRef:
   460    apiVersion: apps/v1
   461    kind: Deployment
   462    name: nginx-deployment
   463  field:
   464    path: selector
   465  file:
   466    path: resources.yaml
   467  `,
   468  		},
   469  		"new format with namespace": {
   470  			input: `
   471  message: selector is required
   472  severity: error
   473  resourceRef:
   474    apiVersion: apps/v1
   475    kind: Deployment
   476    name: nginx-deployment
   477    namespace: my-namespace
   478  field:
   479    path: selector
   480  file:
   481    path: resources.yaml
   482  `,
   483  			expected: `message: selector is required
   484  severity: error
   485  resourceRef:
   486    apiVersion: apps/v1
   487    kind: Deployment
   488    name: nginx-deployment
   489    namespace: my-namespace
   490  field:
   491    path: selector
   492  file:
   493    path: resources.yaml
   494  `,
   495  		},
   496  		"old format with name": {
   497  			input: `
   498  message: selector is required
   499  severity: error
   500  resourceRef:
   501    apiVersion: apps/v1
   502    kind: Deployment
   503    metadata:
   504      name: nginx-deployment
   505  field:
   506    path: selector
   507  file:
   508    path: resources.yaml
   509  `,
   510  			expected: `message: selector is required
   511  severity: error
   512  resourceRef:
   513    apiVersion: apps/v1
   514    kind: Deployment
   515    name: nginx-deployment
   516  field:
   517    path: selector
   518  file:
   519    path: resources.yaml
   520  `,
   521  		},
   522  		"old format with namespace": {
   523  			input: `
   524  message: selector is required
   525  severity: error
   526  resourceRef:
   527    apiVersion: apps/v1
   528    kind: Deployment
   529    metadata:
   530      name: nginx-deployment
   531      namespace: my-namespace
   532  field:
   533    path: selector
   534  file:
   535    path: resources.yaml
   536  `,
   537  			expected: `message: selector is required
   538  severity: error
   539  resourceRef:
   540    apiVersion: apps/v1
   541    kind: Deployment
   542    name: nginx-deployment
   543    namespace: my-namespace
   544  field:
   545    path: selector
   546  file:
   547    path: resources.yaml
   548  `,
   549  		},
   550  		"no resourceRef": {
   551  			input: `
   552  message: selector is required
   553  severity: error
   554  field:
   555    path: selector
   556  file:
   557    path: resources.yaml
   558  `,
   559  			expected: `message: selector is required
   560  severity: error
   561  field:
   562    path: selector
   563  file:
   564    path: resources.yaml
   565  `,
   566  		},
   567  	}
   568  	for _, tc := range tests {
   569  		yml, err := yaml.Parse(tc.input)
   570  		assert.NoError(t, err)
   571  
   572  		result := &framework.Result{}
   573  		err = yaml.Unmarshal([]byte(tc.input), result)
   574  		assert.NoError(t, err)
   575  		assert.NoError(t, populateResourceRef(yml, result))
   576  
   577  		out, err := yaml.Marshal(result)
   578  		assert.NoError(t, err)
   579  		assert.Equal(t, tc.expected, string(out))
   580  	}
   581  }
   582  
   583  func TestPrintFnStderr(t *testing.T) {
   584  	tests := map[string]struct {
   585  		input          string // input
   586  		truncateOutput bool   // whether to truncate output
   587  		expected       string // expected result
   588  	}{
   589  		"no output": {
   590  			input:          ``,
   591  			truncateOutput: true,
   592  			expected:       ``,
   593  		},
   594  		"truncated output": {
   595  			input: `0
   596  1
   597  2
   598  3
   599  4
   600  5`,
   601  			truncateOutput: true,
   602  			expected: `  Stderr:
   603      "0"
   604      "1"
   605      "2"
   606      "3"
   607      ...(2 line(s) truncated, use '--truncate-output=false' to disable)
   608  `,
   609  		},
   610  		"non-truncated output": {
   611  			input: `0
   612  1
   613  2
   614  3
   615  4
   616  5`,
   617  			truncateOutput: false,
   618  			expected: `  Stderr:
   619      "0"
   620      "1"
   621      "2"
   622      "3"
   623      "4"
   624      "5"
   625  `,
   626  		},
   627  	}
   628  	cleanupFunc := func() func() {
   629  		origTruncateOutput := printer.TruncateOutput
   630  		return func() {
   631  			printer.TruncateOutput = origTruncateOutput
   632  		}
   633  	}()
   634  	defer cleanupFunc()
   635  	for testName, tc := range tests {
   636  		t.Run(testName, func(t *testing.T) {
   637  			printer.TruncateOutput = tc.truncateOutput
   638  			out := &bytes.Buffer{}
   639  			err := &bytes.Buffer{}
   640  			ctx := printer.WithContext(context.Background(), printer.New(out, err))
   641  
   642  			printFnStderr(ctx, tc.input)
   643  
   644  			assert.Equal(t, tc.expected, err.String())
   645  			assert.Equal(t, "", out.String())
   646  		})
   647  	}
   648  }