github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/internal/util/merge/merge3_test.go (about)

     1  // Copyright 2020 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 merge_test
    16  
    17  import (
    18  	"os"
    19  	"path/filepath"
    20  	"strings"
    21  	"testing"
    22  
    23  	"github.com/GoogleContainerTools/kpt/internal/testutil"
    24  	"github.com/GoogleContainerTools/kpt/internal/testutil/pkgbuilder"
    25  	"github.com/GoogleContainerTools/kpt/internal/util/merge"
    26  	"github.com/stretchr/testify/assert"
    27  	"sigs.k8s.io/kustomize/kyaml/copyutil"
    28  	"sigs.k8s.io/kustomize/kyaml/yaml"
    29  )
    30  
    31  func TestMerge3_Nested_packages(t *testing.T) {
    32  	annotationSetter := yaml.SetAnnotation("foo", "bar")
    33  	labelSetter := yaml.SetLabel("bar", "foo")
    34  
    35  	testCases := []struct {
    36  		name               string
    37  		includeSubPackages bool
    38  		original           *pkgbuilder.RootPkg
    39  		upstream           *pkgbuilder.RootPkg
    40  		local              *pkgbuilder.RootPkg
    41  		expected           *pkgbuilder.RootPkg
    42  	}{
    43  		{
    44  			name:               "subpackages are merged if included",
    45  			includeSubPackages: true,
    46  			original:           createPkg(),
    47  			upstream:           createPkg(annotationSetter),
    48  			local:              createPkg(labelSetter),
    49  			expected:           createPkg(labelSetter, annotationSetter),
    50  		},
    51  		{
    52  			name:               "subpackages are not merged if not included",
    53  			includeSubPackages: false,
    54  			original:           createPkg(),
    55  			upstream:           createPkg(annotationSetter),
    56  			local:              createPkg(labelSetter),
    57  			expected: createPkgMultipleMutators(
    58  				[]yaml.Filter{
    59  					labelSetter,
    60  					annotationSetter,
    61  				},
    62  				[]yaml.Filter{
    63  					labelSetter,
    64  				},
    65  			),
    66  		},
    67  		{
    68  			name:               "local copy defines the package boundaries if different from upstream",
    69  			includeSubPackages: false,
    70  			original: pkgbuilder.NewRootPkg().
    71  				WithKptfile().
    72  				WithResource(pkgbuilder.DeploymentResource).
    73  				WithSubPackages(
    74  					pkgbuilder.NewSubPkg("a").
    75  						WithKptfile().
    76  						WithResource(pkgbuilder.DeploymentResource),
    77  				),
    78  			upstream: pkgbuilder.NewRootPkg().
    79  				WithKptfile().
    80  				WithResource(pkgbuilder.DeploymentResource, annotationSetter).
    81  				WithSubPackages(
    82  					pkgbuilder.NewSubPkg("a").
    83  						WithResource(pkgbuilder.DeploymentResource, annotationSetter),
    84  				),
    85  			local: pkgbuilder.NewRootPkg().
    86  				WithKptfile().
    87  				WithResource(pkgbuilder.DeploymentResource, labelSetter).
    88  				WithSubPackages(
    89  					pkgbuilder.NewSubPkg("a").
    90  						WithKptfile().
    91  						WithResource(pkgbuilder.DeploymentResource, labelSetter),
    92  				),
    93  			expected: pkgbuilder.NewRootPkg().
    94  				WithKptfile().
    95  				WithResource(pkgbuilder.DeploymentResource, labelSetter, annotationSetter).
    96  				WithSubPackages(
    97  					pkgbuilder.NewSubPkg("a").
    98  						WithKptfile().
    99  						WithResource(pkgbuilder.DeploymentResource, labelSetter),
   100  				),
   101  		},
   102  		{
   103  			name:               "upstream changes not included if in a different package",
   104  			includeSubPackages: false,
   105  			original: pkgbuilder.NewRootPkg().
   106  				WithKptfile().
   107  				WithResource(pkgbuilder.DeploymentResource).
   108  				WithSubPackages(
   109  					pkgbuilder.NewSubPkg("a").
   110  						WithKptfile().
   111  						WithResource(pkgbuilder.DeploymentResource),
   112  				),
   113  			upstream: pkgbuilder.NewRootPkg().
   114  				WithKptfile().
   115  				WithResource(pkgbuilder.DeploymentResource, annotationSetter).
   116  				WithSubPackages(
   117  					pkgbuilder.NewSubPkg("a").
   118  						WithKptfile().
   119  						WithResource(pkgbuilder.DeploymentResource, annotationSetter),
   120  				),
   121  			local: pkgbuilder.NewRootPkg().
   122  				WithKptfile().
   123  				WithResource(pkgbuilder.DeploymentResource, labelSetter).
   124  				WithSubPackages(
   125  					pkgbuilder.NewSubPkg("a"). // No Kptfile
   126  									WithResource(pkgbuilder.DeploymentResource, labelSetter),
   127  				),
   128  			expected: pkgbuilder.NewRootPkg().
   129  				WithKptfile().
   130  				WithResource(pkgbuilder.DeploymentResource, labelSetter, annotationSetter).
   131  				WithSubPackages(
   132  					pkgbuilder.NewSubPkg("a").
   133  						WithResource(pkgbuilder.DeploymentResource, labelSetter),
   134  				),
   135  		},
   136  	}
   137  
   138  	for i := range testCases {
   139  		test := testCases[i]
   140  		t.Run(test.name, func(t *testing.T) {
   141  			original := test.original.ExpandPkg(t, testutil.EmptyReposInfo)
   142  			updated := test.upstream.ExpandPkg(t, testutil.EmptyReposInfo)
   143  			local := test.local.ExpandPkg(t, testutil.EmptyReposInfo)
   144  			expected := test.expected.ExpandPkg(t, testutil.EmptyReposInfo)
   145  			err := merge.Merge3{
   146  				OriginalPath:       original,
   147  				UpdatedPath:        updated,
   148  				DestPath:           local,
   149  				MergeOnPath:        true,
   150  				IncludeSubPackages: test.includeSubPackages,
   151  			}.Merge()
   152  			if !assert.NoError(t, err) {
   153  				t.FailNow()
   154  			}
   155  
   156  			diffs, err := copyutil.Diff(local, expected)
   157  			if !assert.NoError(t, err) {
   158  				t.FailNow()
   159  			}
   160  
   161  			if !assert.Empty(t, diffs.List()) {
   162  				t.FailNow()
   163  			}
   164  		})
   165  	}
   166  }
   167  
   168  func createPkg(mutators ...yaml.Filter) *pkgbuilder.RootPkg {
   169  	return createPkgMultipleMutators(mutators, mutators)
   170  }
   171  
   172  func createPkgMultipleMutators(packageMutators, subPackageMutators []yaml.Filter) *pkgbuilder.RootPkg {
   173  	return pkgbuilder.NewRootPkg().
   174  		WithKptfile().
   175  		WithResource(pkgbuilder.DeploymentResource, packageMutators...).
   176  		WithSubPackages(
   177  			pkgbuilder.NewSubPkg("a").
   178  				WithKptfile().
   179  				WithResource(pkgbuilder.DeploymentResource, subPackageMutators...),
   180  			pkgbuilder.NewSubPkg("b").
   181  				WithResource(pkgbuilder.DeploymentResource, packageMutators...).
   182  				WithSubPackages(
   183  					pkgbuilder.NewSubPkg("c").
   184  						WithKptfile().
   185  						WithResource(pkgbuilder.DeploymentResource, subPackageMutators...),
   186  				),
   187  		)
   188  }
   189  
   190  func TestMerge3_Merge_path(t *testing.T) {
   191  	testCases := map[string]struct {
   192  		origin   string
   193  		update   string
   194  		local    string
   195  		expected string
   196  		errMsg   string
   197  	}{
   198  		`Most common: add namespace and name-prefix on local, merge upstream changes`: {
   199  			origin: `
   200  apiVersion: apps/v1
   201  kind: Deployment
   202  metadata:
   203    name: nginx-deployment
   204  spec:
   205    replicas: 3`,
   206  			update: `
   207  apiVersion: apps/v1
   208  kind: Deployment
   209  metadata:
   210    name: nginx-deployment
   211  spec:
   212    replicas: 4`,
   213  			local: `
   214  apiVersion: apps/v1
   215  kind: Deployment
   216  metadata: # kpt-merge: /nginx-deployment
   217    name: dev-nginx-deployment
   218    namespace: my-space
   219  spec:
   220    replicas: 3
   221  `,
   222  			expected: `
   223  apiVersion: apps/v1
   224  kind: Deployment
   225  metadata: # kpt-merge: /nginx-deployment
   226    name: dev-nginx-deployment
   227    namespace: my-space
   228  spec:
   229    replicas: 4
   230  `},
   231  
   232  		`Add namespace and name-prefix on local manually without adding annotations, adds new resource`: {
   233  			origin: `
   234  apiVersion: apps/v1
   235  kind: Deployment
   236  metadata:
   237    name: nginx-deployment
   238  spec:
   239    replicas: 3`,
   240  			update: `
   241  apiVersion: apps/v1
   242  kind: Deployment
   243  metadata:
   244    name: nginx-deployment
   245  spec:
   246    replicas: 4`,
   247  			local: `
   248  apiVersion: apps/v1
   249  kind: Deployment
   250  metadata:
   251    name: dev-nginx-deployment
   252    namespace: my-space
   253  spec:
   254    replicas: 3
   255  `,
   256  			expected: `
   257  apiVersion: apps/v1
   258  kind: Deployment
   259  metadata:
   260    name: dev-nginx-deployment
   261    namespace: my-space
   262  spec:
   263    replicas: 3
   264  `},
   265  
   266  		`Conflict: User fetches package, copies a resource in same file, adds different name suffix`: {
   267  			origin: `
   268  apiVersion: apps/v1
   269  kind: Deployment
   270  metadata:
   271    name: nginx-deployment
   272  spec:
   273    replicas: 3`,
   274  			update: `
   275  apiVersion: apps/v1
   276  kind: Deployment
   277  metadata:
   278    name: nginx-deployment
   279  spec:
   280    replicas: 4`,
   281  			local: `
   282  apiVersion: apps/v1
   283  kind: Deployment
   284  metadata: # kpt-merge: default/nginx-deployment
   285    name: nginx-deployment-1
   286    namespace: my-space
   287  spec:
   288    replicas: 3
   289  ---
   290  apiVersion: apps/v1
   291  kind: Deployment
   292  metadata: # kpt-merge: default/nginx-deployment
   293    name: nginx-deployment-2
   294    namespace: my-space
   295  spec:
   296    replicas: 3
   297  `,
   298  			errMsg: `found duplicate "local" resources in file "f1.yaml"`},
   299  
   300  		`Publisher changes name in upstream but want to maintain original identity, no local customizations, fetch upstream changes`: {
   301  			origin: `
   302  apiVersion: apps/v1
   303  kind: Deployment
   304  metadata:
   305    name: nginx-deployment
   306  spec:
   307    replicas: 3`,
   308  			update: `
   309  apiVersion: apps/v1
   310  kind: Deployment
   311  metadata: # kpt-merge: /nginx-deployment
   312    name: nginx-deployment-new
   313  spec:
   314    replicas: 4`,
   315  			local: `
   316  apiVersion: apps/v1
   317  kind: Deployment
   318  metadata:
   319    name: nginx-deployment
   320  spec:
   321    replicas: 3
   322  `,
   323  			expected: `
   324  apiVersion: apps/v1
   325  kind: Deployment
   326  metadata: # kpt-merge: /nginx-deployment
   327    name: nginx-deployment-new
   328  spec:
   329    replicas: 4
   330  `},
   331  
   332  		`Publisher changes name in upstream but want to maintain original identity, consumer adds name-prefix on local, fetch upstream changes`: {
   333  			origin: `
   334  apiVersion: apps/v1
   335  kind: Deployment
   336  metadata:
   337    name: nginx-deployment
   338  spec:
   339    replicas: 3`,
   340  			update: `
   341  apiVersion: apps/v1
   342  kind: Deployment
   343  metadata: # kpt-merge: default/nginx-deployment
   344    name: nginx-deployment-new
   345  spec:
   346    replicas: 4`,
   347  			local: `
   348  apiVersion: apps/v1
   349  kind: Deployment
   350  metadata: # kpt-merge: default/nginx-deployment
   351    name: dev-nginx-deployment
   352    namespace: my-space
   353  spec:
   354    replicas: 3
   355  `,
   356  			expected: `
   357  apiVersion: apps/v1
   358  kind: Deployment
   359  metadata: # kpt-merge: default/nginx-deployment
   360    name: nginx-deployment-new
   361    namespace: my-space
   362  spec:
   363    replicas: 4
   364  `},
   365  
   366  		`Publisher changes name in upstream but don't want to maintain original identity which is equivalent 
   367  to delete existing resource and add new one, consumer adds name-prefix on local`: {
   368  			origin: `
   369  apiVersion: apps/v1
   370  kind: Deployment
   371  metadata:
   372    name: nginx-deployment
   373  spec:
   374    replicas: 3`,
   375  			update: `
   376  apiVersion: apps/v1
   377  kind: Deployment
   378  metadata:
   379    name: nginx-deployment-new
   380  spec:
   381    replicas: 4`,
   382  			local: `
   383  apiVersion: apps/v1
   384  kind: Deployment
   385  metadata: # kpt-merge: /nginx-deployment
   386    name: dev-nginx-deployment
   387    namespace: my-space
   388  spec:
   389    replicas: 3
   390  `,
   391  			expected: `
   392  apiVersion: apps/v1
   393  kind: Deployment
   394  metadata: # kpt-merge: /nginx-deployment
   395    name: dev-nginx-deployment
   396    namespace: my-space
   397  spec:
   398    replicas: 3
   399  ---
   400  apiVersion: apps/v1
   401  kind: Deployment
   402  metadata:
   403    name: nginx-deployment-new
   404  spec:
   405    replicas: 4
   406  `},
   407  
   408  		`Publisher changes name multiple times in upstream but maintains original identity, no local customizations,
   409  fetch upstream changes`: {
   410  			origin: `
   411  apiVersion: apps/v1
   412  kind: Deployment
   413  metadata: # kpt-merge: default/nginx-deployment
   414    name: nginx-deployment-new
   415  spec:
   416    replicas: 4`,
   417  			update: `
   418  apiVersion: apps/v1
   419  kind: Deployment
   420  metadata: # kpt-merge: default/nginx-deployment
   421    name: nginx-deployment-new-again
   422  spec:
   423    replicas: 5`,
   424  			local: `
   425  apiVersion: apps/v1
   426  kind: Deployment
   427  metadata: # kpt-merge: default/nginx-deployment
   428    name: nginx-deployment-new
   429  spec:
   430    replicas: 5
   431  `,
   432  			expected: `
   433  apiVersion: apps/v1
   434  kind: Deployment
   435  metadata: # kpt-merge: default/nginx-deployment
   436    name: nginx-deployment-new-again
   437  spec:
   438    replicas: 5
   439  `},
   440  
   441  		`Publisher changes name multiple times in upstream but maintains original identity, consumer adds name-prefix 
   442  on local, fetch upstream changes`: {
   443  			origin: `
   444  apiVersion: apps/v1
   445  kind: Deployment
   446  metadata: # kpt-merge: default/nginx-deployment
   447    name: nginx-deployment-new
   448  spec:
   449    replicas: 4`,
   450  			update: `
   451  apiVersion: apps/v1
   452  kind: Deployment
   453  metadata: # kpt-merge: default/nginx-deployment
   454    name: nginx-deployment-new-again
   455  spec:
   456    replicas: 5`,
   457  			local: `
   458  apiVersion: apps/v1
   459  kind: Deployment
   460  metadata: # kpt-merge: default/nginx-deployment
   461    name: dev-nginx-deployment
   462    namespace: my-space
   463  spec:
   464    replicas: 5
   465  `,
   466  			expected: `
   467  apiVersion: apps/v1
   468  kind: Deployment
   469  metadata: # kpt-merge: default/nginx-deployment
   470    name: nginx-deployment-new-again
   471    namespace: my-space
   472  spec:
   473    replicas: 5
   474  `},
   475  		`Publisher adds metadata.annotations in upstream in a non-identity kustomization resource, consumer adds changes to resource body
   476  on local, fetch upstream changes`: {
   477  			origin: `
   478  apiVersion: kustomize.config.k8s.io/v1beta1
   479  metadata:
   480    labels:
   481      color: blue
   482  commonLabels:
   483    app: dev`,
   484  			update: `
   485  apiVersion: kustomize.config.k8s.io/v1beta1
   486  kind: Kustomization
   487  metadata:
   488    labels:
   489      color: blue
   490    annotations:
   491      id.example.org: abcd
   492  commonLabels:
   493    app: dev`,
   494  			local: `
   495  apiVersion: kustomize.config.k8s.io/v1beta1
   496  kind: Kustomization
   497  metadata:
   498    labels:
   499      color: blue
   500  commonLabels:
   501    app: dev
   502    tier: backend
   503  `,
   504  			expected: `
   505  apiVersion: kustomize.config.k8s.io/v1beta1
   506  kind: Kustomization
   507  metadata:
   508    labels:
   509      color: blue
   510    annotations:
   511      id.example.org: abcd
   512  commonLabels:
   513    app: dev
   514    tier: backend
   515  `},
   516  		`Publisher adds commonLabels in upstream in a non-identity kustomization resource, consumer adds changes to resource body
   517  on local, fetch upstream changes`: {
   518  			origin: `
   519  commonLabels:
   520    app: dev`,
   521  			update: `
   522  commonLabels:
   523    tier: backend
   524    app: dev`,
   525  			local: `
   526  commonLabels:
   527    app: dev
   528    tier: backend
   529    db: mysql
   530  `,
   531  			expected: `
   532  commonLabels:
   533    app: dev
   534    tier: backend
   535    db: mysql
   536  `},
   537  
   538  		`Version changes are just like any other changes`: {
   539  			origin: `
   540  apiVersion: apps/v1
   541  kind: Deployment
   542  metadata:
   543    name: nginx-deployment
   544  spec:
   545    replicas: 3`,
   546  			update: `
   547  apiVersion: apps/v2
   548  kind: Deployment
   549  metadata:
   550    name: nginx-deployment
   551  spec:
   552    replicas: 4`,
   553  			local: `
   554  apiVersion: apps/v1
   555  kind: Deployment
   556  metadata:
   557    name: nginx-deployment
   558  spec:
   559    replicas: 3
   560  `,
   561  			expected: `
   562  apiVersion: apps/v2
   563  kind: Deployment
   564  metadata:
   565    name: nginx-deployment
   566  spec:
   567    replicas: 4
   568  `},
   569  	}
   570  
   571  	for tn, tc := range testCases {
   572  		t.Run(tn, func(t *testing.T) {
   573  			// setup the local directory
   574  			dir := t.TempDir()
   575  
   576  			err := os.MkdirAll(filepath.Join(dir, "localDir"), 0700)
   577  			if !assert.NoError(t, err) {
   578  				t.FailNow()
   579  			}
   580  
   581  			err = os.MkdirAll(filepath.Join(dir, "updatedDir"), 0700)
   582  			if !assert.NoError(t, err) {
   583  				t.FailNow()
   584  			}
   585  
   586  			err = os.MkdirAll(filepath.Join(dir, "originalDir"), 0700)
   587  			if !assert.NoError(t, err) {
   588  				t.FailNow()
   589  			}
   590  
   591  			err = os.WriteFile(filepath.Join(dir, "originalDir", "f1.yaml"), []byte(strings.TrimSpace(tc.origin)), 0700)
   592  			if !assert.NoError(t, err) {
   593  				t.FailNow()
   594  			}
   595  
   596  			err = os.WriteFile(filepath.Join(dir, "updatedDir", "f1.yaml"), []byte(strings.TrimSpace(tc.update)), 0700)
   597  			if !assert.NoError(t, err) {
   598  				t.FailNow()
   599  			}
   600  
   601  			err = os.WriteFile(filepath.Join(dir, "localDir", "f1.yaml"), []byte(strings.TrimSpace(tc.local)), 0700)
   602  			if !assert.NoError(t, err) {
   603  				t.FailNow()
   604  			}
   605  
   606  			err = merge.Merge3{
   607  				OriginalPath: filepath.Join(dir, "originalDir"),
   608  				UpdatedPath:  filepath.Join(dir, "updatedDir"),
   609  				DestPath:     filepath.Join(dir, "localDir"),
   610  				MergeOnPath:  true,
   611  			}.Merge()
   612  			if tc.errMsg == "" {
   613  				if !assert.NoError(t, err) {
   614  					t.FailNow()
   615  				}
   616  			} else {
   617  				if !assert.Error(t, err) {
   618  					t.FailNow()
   619  				}
   620  				if !assert.Contains(t, err.Error(), tc.errMsg) {
   621  					t.FailNow()
   622  				}
   623  				return
   624  			}
   625  
   626  			b, err := os.ReadFile(filepath.Join(dir, "localDir", "f1.yaml"))
   627  			if !assert.NoError(t, err) {
   628  				t.FailNow()
   629  			}
   630  			if !assert.Equal(t, strings.TrimSpace(tc.expected), strings.TrimSpace(string(b))) {
   631  				t.FailNow()
   632  			}
   633  		})
   634  	}
   635  }