...

Source file src/sigs.k8s.io/kustomize/kyaml/kio/byteio_readwriter_test.go

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

     1  // Copyright 2019 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package kio_test
     5  
     6  import (
     7  	"bytes"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/require"
    13  	"sigs.k8s.io/kustomize/kyaml/kio"
    14  	"sigs.k8s.io/kustomize/kyaml/yaml"
    15  )
    16  
    17  func TestByteReadWriter(t *testing.T) {
    18  	type testCase struct {
    19  		name           string
    20  		err            string
    21  		input          string
    22  		expectedOutput string
    23  		instance       kio.ByteReadWriter
    24  	}
    25  
    26  	testCases := []testCase{
    27  		{
    28  			name: "round_trip",
    29  			input: `
    30  apiVersion: config.kubernetes.io/v1
    31  kind: ResourceList
    32  items:
    33  - kind: Deployment
    34    spec:
    35      replicas: 1
    36  - kind: Service
    37    spec:
    38      selectors:
    39        foo: bar
    40  `,
    41  			expectedOutput: `
    42  apiVersion: config.kubernetes.io/v1
    43  kind: ResourceList
    44  items:
    45  - kind: Deployment
    46    spec:
    47      replicas: 1
    48  - kind: Service
    49    spec:
    50      selectors:
    51        foo: bar
    52  `,
    53  		},
    54  
    55  		{
    56  			name: "function_config",
    57  			input: `
    58  apiVersion: config.kubernetes.io/v1
    59  kind: ResourceList
    60  items:
    61  - kind: Deployment
    62    spec:
    63      replicas: 1
    64  - kind: Service
    65    spec:
    66      selectors:
    67        foo: bar
    68  functionConfig:
    69    a: b # something
    70  `,
    71  			expectedOutput: `
    72  apiVersion: config.kubernetes.io/v1
    73  kind: ResourceList
    74  items:
    75  - kind: Deployment
    76    spec:
    77      replicas: 1
    78  - kind: Service
    79    spec:
    80      selectors:
    81        foo: bar
    82  functionConfig:
    83    a: b # something
    84  `,
    85  		},
    86  
    87  		{
    88  			name: "results",
    89  			input: `
    90  apiVersion: config.kubernetes.io/v1
    91  kind: ResourceList
    92  items:
    93  - kind: Deployment
    94    spec:
    95      replicas: 1
    96  - kind: Service
    97    spec:
    98      selectors:
    99        foo: bar
   100  results:
   101    a: b # something
   102  `,
   103  			expectedOutput: `
   104  apiVersion: config.kubernetes.io/v1
   105  kind: ResourceList
   106  items:
   107  - kind: Deployment
   108    spec:
   109      replicas: 1
   110  - kind: Service
   111    spec:
   112      selectors:
   113        foo: bar
   114  results:
   115    a: b # something
   116  `,
   117  		},
   118  
   119  		{
   120  			name: "drop_invalid_resource_list_field",
   121  			input: `
   122  apiVersion: config.kubernetes.io/v1
   123  kind: ResourceList
   124  items:
   125  - kind: Deployment
   126    spec:
   127      replicas: 1
   128  - kind: Service
   129    spec:
   130      selectors:
   131        foo: bar
   132  foo:
   133    a: b # something
   134  `,
   135  			expectedOutput: `
   136  apiVersion: config.kubernetes.io/v1
   137  kind: ResourceList
   138  items:
   139  - kind: Deployment
   140    spec:
   141      replicas: 1
   142  - kind: Service
   143    spec:
   144      selectors:
   145        foo: bar
   146  `,
   147  		},
   148  
   149  		{
   150  			name: "list",
   151  			input: `
   152  apiVersion: v1
   153  kind: List
   154  items:
   155  - kind: Deployment
   156    spec:
   157      replicas: 1
   158  - kind: Service
   159    spec:
   160      selectors:
   161        foo: bar
   162  `,
   163  			expectedOutput: `
   164  apiVersion: v1
   165  kind: List
   166  items:
   167  - kind: Deployment
   168    spec:
   169      replicas: 1
   170  - kind: Service
   171    spec:
   172      selectors:
   173        foo: bar
   174  `,
   175  		},
   176  
   177  		{
   178  			name: "multiple_documents",
   179  			input: `
   180  kind: Deployment
   181  spec:
   182    replicas: 1
   183  ---
   184  kind: Service
   185  spec:
   186    selectors:
   187      foo: bar
   188  `,
   189  			expectedOutput: `
   190  kind: Deployment
   191  spec:
   192    replicas: 1
   193  ---
   194  kind: Service
   195  spec:
   196    selectors:
   197      foo: bar
   198  `,
   199  		},
   200  
   201  		{
   202  			name: "keep_annotations",
   203  			input: `
   204  kind: Deployment
   205  spec:
   206    replicas: 1
   207  ---
   208  kind: Service
   209  spec:
   210    selectors:
   211      foo: bar
   212  `,
   213  			expectedOutput: `
   214  kind: Deployment
   215  spec:
   216    replicas: 1
   217  metadata:
   218    annotations:
   219      config.kubernetes.io/index: '0'
   220      internal.config.kubernetes.io/index: '0'
   221  ---
   222  kind: Service
   223  spec:
   224    selectors:
   225      foo: bar
   226  metadata:
   227    annotations:
   228      config.kubernetes.io/index: '1'
   229      internal.config.kubernetes.io/index: '1'
   230  `,
   231  			instance: kio.ByteReadWriter{KeepReaderAnnotations: true},
   232  		},
   233  
   234  		{
   235  			name: "manual_override_wrap",
   236  			input: `
   237  apiVersion: config.kubernetes.io/v1
   238  kind: ResourceList
   239  items:
   240  - kind: Deployment
   241    spec:
   242      replicas: 1
   243  - kind: Service
   244    spec:
   245      selectors:
   246        foo: bar
   247  functionConfig:
   248    a: b # something
   249  `,
   250  			expectedOutput: `
   251  kind: Deployment
   252  spec:
   253    replicas: 1
   254  ---
   255  kind: Service
   256  spec:
   257    selectors:
   258      foo: bar
   259  `,
   260  			instance: kio.ByteReadWriter{NoWrap: true},
   261  		},
   262  
   263  		{
   264  			name: "manual_override_function_config",
   265  			input: `
   266  apiVersion: config.kubernetes.io/v1
   267  kind: ResourceList
   268  items:
   269  - kind: Deployment
   270    spec:
   271      replicas: 1
   272  - kind: Service
   273    spec:
   274      selectors:
   275        foo: bar
   276  functionConfig:
   277    a: b # something
   278  `,
   279  			expectedOutput: `
   280  apiVersion: config.kubernetes.io/v1
   281  kind: ResourceList
   282  items:
   283  - kind: Deployment
   284    spec:
   285      replicas: 1
   286  - kind: Service
   287    spec:
   288      selectors:
   289        foo: bar
   290  functionConfig:
   291    c: d
   292  `,
   293  			instance: kio.ByteReadWriter{FunctionConfig: yaml.MustParse(`c: d`)},
   294  		},
   295  		{
   296  			name: "anchors_not_inflated",
   297  			input: `
   298  kind: ConfigMap
   299  metadata:
   300    name: foo
   301  data:
   302    color: &color-used blue
   303    feeling: *color-used
   304  `,
   305  			// If YAML anchors were automagically inflated,
   306  			// the expectedOutput would be something like
   307  			//
   308  			// kind: ConfigMap
   309  			// metadata:
   310  			//   name: foo
   311  			// data:
   312  			//   color: blue
   313  			//   feeling: blue
   314  			expectedOutput: `
   315  kind: ConfigMap
   316  metadata:
   317    name: foo
   318  data:
   319    color: &color-used blue
   320    feeling: *color-used
   321  `,
   322  		},
   323  	}
   324  
   325  	for i := range testCases {
   326  		tc := testCases[i]
   327  		t.Run(tc.name, func(t *testing.T) {
   328  			var in, out bytes.Buffer
   329  			in.WriteString(tc.input)
   330  			w := tc.instance
   331  			w.Writer = &out
   332  			w.Reader = &in
   333  
   334  			nodes, err := w.Read()
   335  			if !assert.NoError(t, err) {
   336  				t.FailNow()
   337  			}
   338  
   339  			err = w.Write(nodes)
   340  			if !assert.NoError(t, err) {
   341  				t.FailNow()
   342  			}
   343  
   344  			if tc.err != "" {
   345  				if !assert.EqualError(t, err, tc.err) {
   346  					t.FailNow()
   347  				}
   348  				return
   349  			}
   350  
   351  			if !assert.Equal(t,
   352  				strings.TrimSpace(tc.expectedOutput), strings.TrimSpace(out.String())) {
   353  				t.FailNow()
   354  			}
   355  		})
   356  	}
   357  }
   358  
   359  func TestByteReadWriter_RetainSeqIndent(t *testing.T) {
   360  	type testCase struct {
   361  		name           string
   362  		err            string
   363  		input          string
   364  		expectedOutput string
   365  		instance       kio.ByteReadWriter
   366  	}
   367  
   368  	testCases := []testCase{
   369  		{
   370  			name: "round_trip with 2 space seq indent",
   371  			input: `
   372  apiVersion: apps/v1
   373  kind: Deployment
   374  spec:
   375    - foo
   376    - bar
   377  ---
   378  apiVersion: v1
   379  kind: Service
   380  spec:
   381    - foo
   382    - bar
   383  `,
   384  			expectedOutput: `
   385  apiVersion: apps/v1
   386  kind: Deployment
   387  spec:
   388    - foo
   389    - bar
   390  ---
   391  apiVersion: v1
   392  kind: Service
   393  spec:
   394    - foo
   395    - bar
   396  `,
   397  		},
   398  		{
   399  			name: "round_trip with 0 space seq indent",
   400  			input: `
   401  apiVersion: apps/v1
   402  kind: Deployment
   403  spec:
   404  - foo
   405  - bar
   406  ---
   407  apiVersion: v1
   408  kind: Service
   409  spec:
   410  - foo
   411  - bar
   412  `,
   413  			expectedOutput: `
   414  apiVersion: apps/v1
   415  kind: Deployment
   416  spec:
   417  - foo
   418  - bar
   419  ---
   420  apiVersion: v1
   421  kind: Service
   422  spec:
   423  - foo
   424  - bar
   425  `,
   426  		},
   427  		{
   428  			name: "round_trip with different indentations",
   429  			input: `
   430  apiVersion: apps/v1
   431  kind: Deployment
   432  spec:
   433    - foo
   434    - bar
   435    - baz
   436  ---
   437  apiVersion: v1
   438  kind: Service
   439  spec:
   440  - foo
   441  - bar
   442  `,
   443  			expectedOutput: `
   444  apiVersion: apps/v1
   445  kind: Deployment
   446  spec:
   447    - foo
   448    - bar
   449    - baz
   450  ---
   451  apiVersion: v1
   452  kind: Service
   453  spec:
   454  - foo
   455  - bar
   456  `,
   457  		},
   458  		{
   459  			name: "round_trip with mixed indentations in same resource, wide wins as it is first",
   460  			input: `
   461  apiVersion: apps/v1
   462  kind: Deployment
   463  spec:
   464    - foo
   465  env:
   466  - foo
   467  - bar
   468  `,
   469  			expectedOutput: `
   470  apiVersion: apps/v1
   471  kind: Deployment
   472  spec:
   473    - foo
   474  env:
   475    - foo
   476    - bar
   477  `,
   478  		},
   479  		{
   480  			name: "round_trip with mixed indentations in same resource, compact wins as it is first",
   481  			input: `
   482  apiVersion: apps/v1
   483  kind: Deployment
   484  spec:
   485  - foo
   486  env:
   487    - foo
   488    - bar
   489  `,
   490  			expectedOutput: `
   491  apiVersion: apps/v1
   492  kind: Deployment
   493  spec:
   494  - foo
   495  env:
   496  - foo
   497  - bar
   498  `,
   499  		},
   500  		{
   501  			name: "unwrap ResourceList with annotations",
   502  			input: `
   503  apiVersion: config.kubernetes.io/v1
   504  kind: ResourceList
   505  items:
   506    - kind: Deployment
   507      metadata:
   508        annotations:
   509          internal.config.kubernetes.io/seqindent: "compact"
   510      spec:
   511        - foo
   512        - bar
   513    - kind: Service
   514      metadata:
   515        annotations:
   516          internal.config.kubernetes.io/seqindent: "wide"
   517      spec:
   518        - foo
   519        - bar
   520  `,
   521  			expectedOutput: `
   522  kind: Deployment
   523  spec:
   524  - foo
   525  - bar
   526  ---
   527  kind: Service
   528  spec:
   529    - foo
   530    - bar
   531  `,
   532  		},
   533  		{
   534  			name: "round_trip with mixed indentations in same resource, wide wins as it is first",
   535  			input: `
   536  apiVersion: apps/v1
   537  kind: Deployment
   538  spec:
   539    - foo
   540    - bar
   541  env:
   542  - foo
   543  - bar
   544  - baz
   545  `,
   546  			expectedOutput: `
   547  apiVersion: apps/v1
   548  kind: Deployment
   549  spec:
   550    - foo
   551    - bar
   552  env:
   553    - foo
   554    - bar
   555    - baz
   556  `,
   557  		},
   558  	}
   559  
   560  	for i := range testCases {
   561  		tc := testCases[i]
   562  		t.Run(tc.name, func(t *testing.T) {
   563  			var in, out bytes.Buffer
   564  			in.WriteString(tc.input)
   565  			w := tc.instance
   566  			w.Writer = &out
   567  			w.Reader = &in
   568  			w.PreserveSeqIndent = true
   569  
   570  			nodes, err := w.Read()
   571  			if !assert.NoError(t, err) {
   572  				t.FailNow()
   573  			}
   574  
   575  			w.WrappingKind = ""
   576  			err = w.Write(nodes)
   577  			if !assert.NoError(t, err) {
   578  				t.FailNow()
   579  			}
   580  
   581  			if tc.err != "" {
   582  				if !assert.EqualError(t, err, tc.err) {
   583  					t.FailNow()
   584  				}
   585  				return
   586  			}
   587  
   588  			if !assert.Equal(t,
   589  				strings.TrimSpace(tc.expectedOutput), strings.TrimSpace(out.String())) {
   590  				t.FailNow()
   591  			}
   592  		})
   593  	}
   594  }
   595  
   596  func TestByteReadWriter_WrapBareSeqNode(t *testing.T) {
   597  	type testCase struct {
   598  		name            string
   599  		readerErr       string
   600  		writerErr       string
   601  		input           string
   602  		wrapBareSeqNode bool
   603  		expectedOutput  string
   604  		instance        kio.ByteReadWriter
   605  	}
   606  
   607  	testCases := []testCase{
   608  		{
   609  			name:            "round_trip bare seq node simple",
   610  			wrapBareSeqNode: true,
   611  			input: `
   612  - foo
   613  - bar
   614  `,
   615  			expectedOutput: `
   616  - foo
   617  - bar
   618  `,
   619  		},
   620  		{
   621  			name:            "round_trip bare seq node",
   622  			wrapBareSeqNode: true,
   623  			input: `# Use the old CRD because of the quantity validation issue:
   624  # https://github.com/kubeflow/kubeflow/issues/5722
   625  - op: replace
   626    path: /spec
   627    value:
   628      group: kubeflow.org
   629      names:
   630        kind: Notebook
   631        plural: notebooks
   632        singular: notebook
   633      scope: Namespaced
   634      subresources:
   635        status: {}
   636      versions:
   637      - name: v1alpha1
   638        served: true
   639        storage: false
   640  `,
   641  			expectedOutput: `# Use the old CRD because of the quantity validation issue:
   642  # https://github.com/kubeflow/kubeflow/issues/5722
   643  - op: replace
   644    path: /spec
   645    value:
   646      group: kubeflow.org
   647      names:
   648        kind: Notebook
   649        plural: notebooks
   650        singular: notebook
   651      scope: Namespaced
   652      subresources:
   653        status: {}
   654      versions:
   655      - name: v1alpha1
   656        served: true
   657        storage: false
   658  `,
   659  		},
   660  		{
   661  			name:            "error round_trip bare seq node simple",
   662  			wrapBareSeqNode: false,
   663  			input: `
   664  - foo
   665  - bar
   666  `,
   667  			readerErr: "wrong node kind: expected MappingNode but got SequenceNode",
   668  		},
   669  		{
   670  			name:            "error round_trip bare seq node",
   671  			wrapBareSeqNode: false,
   672  			input: `# Use the old CRD because of the quantity validation issue:
   673  # https://github.com/kubeflow/kubeflow/issues/5722
   674  - op: replace
   675    path: /spec
   676    value:
   677      group: kubeflow.org
   678      names:
   679        kind: Notebook
   680        plural: notebooks
   681        singular: notebook
   682      scope: Namespaced
   683      subresources:
   684        status: {}
   685      versions:
   686      - name: v1alpha1
   687        served: true
   688        storage: false
   689  `,
   690  			readerErr: "wrong node kind: expected MappingNode but got SequenceNode",
   691  		},
   692  		{
   693  			name:            "round_trip bare seq node json",
   694  			wrapBareSeqNode: true,
   695  			input:           `[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--namespaced"}]`,
   696  			expectedOutput:  `[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--namespaced"}]`,
   697  		},
   698  		{
   699  			name:            "error round_trip invalid yaml node",
   700  			wrapBareSeqNode: false,
   701  			input:           "I am not valid",
   702  			readerErr:       "wrong node kind: expected MappingNode but got ScalarNode",
   703  		},
   704  	}
   705  
   706  	for i := range testCases {
   707  		tc := testCases[i]
   708  		t.Run(tc.name, func(t *testing.T) {
   709  			var in, out bytes.Buffer
   710  			in.WriteString(tc.input)
   711  			w := tc.instance
   712  			w.Writer = &out
   713  			w.Reader = &in
   714  			w.PreserveSeqIndent = true
   715  			w.WrapBareSeqNode = tc.wrapBareSeqNode
   716  
   717  			nodes, err := w.Read()
   718  			if tc.readerErr != "" {
   719  				if !assert.Error(t, err) {
   720  					t.FailNow()
   721  				}
   722  				if !assert.Contains(t, err.Error(), tc.readerErr) {
   723  					t.FailNow()
   724  				}
   725  				return
   726  			}
   727  
   728  			w.WrappingKind = ""
   729  			err = w.Write(nodes)
   730  			if !assert.NoError(t, err) {
   731  				t.FailNow()
   732  			}
   733  
   734  			if tc.writerErr != "" {
   735  				if !assert.Error(t, err) {
   736  					t.FailNow()
   737  				}
   738  				if !assert.Contains(t, err.Error(), tc.writerErr) {
   739  					t.FailNow()
   740  				}
   741  				return
   742  			}
   743  
   744  			if !assert.Equal(t,
   745  				strings.TrimSpace(tc.expectedOutput), strings.TrimSpace(out.String())) {
   746  				t.FailNow()
   747  			}
   748  		})
   749  	}
   750  }
   751  
   752  func TestByteReadWriter_ResourceListWrapping(t *testing.T) {
   753  	singleDeployment := `kind: Deployment
   754  apiVersion: v1
   755  metadata:
   756    name: tester
   757    namespace: default
   758  spec:
   759    replicas: 0`
   760  	resourceList := `apiVersion: config.kubernetes.io/v1
   761  kind: ResourceList
   762  items:
   763  - kind: Deployment
   764    apiVersion: v1
   765    metadata:
   766      name: tester
   767      namespace: default
   768    spec:
   769      replicas: 0`
   770  	resourceListWithError := `apiVersion: config.kubernetes.io/v1
   771  kind: ResourceList
   772  items:
   773  - kind: Deployment
   774    apiVersion: v1
   775    metadata:
   776      name: tester
   777      namespace: default
   778    spec:
   779      replicas: 0
   780  results:
   781  - message: some error
   782    severity: error`
   783  	resourceListDifferentWrapper := strings.NewReplacer(
   784  		"kind: ResourceList", "kind: SomethingElse",
   785  		"apiVersion: config.kubernetes.io/v1", "apiVersion: fakeVersion",
   786  	).Replace(resourceList)
   787  
   788  	testCases := []struct {
   789  		desc           string
   790  		noWrap         bool
   791  		wrapKind       string
   792  		wrapAPIVersion string
   793  		input          string
   794  		want           string
   795  	}{
   796  		{
   797  			desc:  "resource list",
   798  			input: resourceList,
   799  			want:  resourceList,
   800  		},
   801  		{
   802  			desc:  "individual resources",
   803  			input: singleDeployment,
   804  			want:  singleDeployment,
   805  		},
   806  		{
   807  			desc:           "no nested wrapping",
   808  			wrapKind:       kio.ResourceListKind,
   809  			wrapAPIVersion: kio.ResourceListAPIVersion,
   810  			input:          resourceList,
   811  			want:           resourceList,
   812  		},
   813  		{
   814  			desc:   "unwrap resource list",
   815  			noWrap: true,
   816  			input:  resourceList,
   817  			want:   singleDeployment,
   818  		},
   819  		{
   820  			desc:           "wrap individual resources",
   821  			wrapKind:       kio.ResourceListKind,
   822  			wrapAPIVersion: kio.ResourceListAPIVersion,
   823  			input:          singleDeployment,
   824  			want:           resourceList,
   825  		},
   826  		{
   827  			desc:           "NoWrap has precedence",
   828  			noWrap:         true,
   829  			wrapKind:       kio.ResourceListKind,
   830  			wrapAPIVersion: kio.ResourceListAPIVersion,
   831  			input:          singleDeployment,
   832  			want:           singleDeployment,
   833  		},
   834  		{
   835  			desc:           "honor specified wrapping kind",
   836  			wrapKind:       "SomethingElse",
   837  			wrapAPIVersion: "fakeVersion",
   838  			input:          resourceList,
   839  			want:           resourceListDifferentWrapper,
   840  		},
   841  		{
   842  			desc:  "passthrough results",
   843  			input: resourceListWithError,
   844  			want:  resourceListWithError,
   845  		},
   846  	}
   847  
   848  	for i := range testCases {
   849  		tc := testCases[i]
   850  		t.Run(tc.desc, func(t *testing.T) {
   851  			var got bytes.Buffer
   852  			rw := kio.ByteReadWriter{
   853  				Reader:             strings.NewReader(tc.input),
   854  				Writer:             &got,
   855  				NoWrap:             tc.noWrap,
   856  				WrappingAPIVersion: tc.wrapAPIVersion,
   857  				WrappingKind:       tc.wrapKind,
   858  			}
   859  
   860  			rnodes, err := rw.Read()
   861  			require.NoError(t, err)
   862  
   863  			err = rw.Write(rnodes)
   864  			require.NoError(t, err)
   865  
   866  			assert.Equal(t, tc.want, strings.TrimSpace(got.String()))
   867  		})
   868  	}
   869  }
   870  

View as plain text