github.com/SamarSidharth/kpt@v0.0.0-20231122062228-c7d747ae3ace/commands/pkg/update/cmdupdate_test.go (about)

     1  // Copyright 2019 The kpt Authors
     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 update_test
    16  
    17  import (
    18  	"bytes"
    19  	"os"
    20  	"path/filepath"
    21  	"regexp"
    22  	"runtime"
    23  	"strings"
    24  	"testing"
    25  	"text/template"
    26  
    27  	"github.com/GoogleContainerTools/kpt/commands/pkg/get"
    28  	"github.com/GoogleContainerTools/kpt/commands/pkg/update"
    29  	"github.com/GoogleContainerTools/kpt/internal/gitutil"
    30  	"github.com/GoogleContainerTools/kpt/internal/testutil"
    31  	"github.com/GoogleContainerTools/kpt/internal/testutil/pkgbuilder"
    32  	kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
    33  	"github.com/GoogleContainerTools/kpt/pkg/printer/fake"
    34  	"github.com/spf13/cobra"
    35  	"github.com/stretchr/testify/assert"
    36  	"sigs.k8s.io/kustomize/kyaml/yaml"
    37  )
    38  
    39  func TestMain(m *testing.M) {
    40  	os.Exit(testutil.ConfigureTestKptCache(m))
    41  }
    42  
    43  // TestCmd_execute verifies that update is correctly invoked.
    44  func TestCmd_execute(t *testing.T) {
    45  	g, w, clean := testutil.SetupRepoAndWorkspace(t, testutil.Content{
    46  		Data:   testutil.Dataset1,
    47  		Branch: "master",
    48  	})
    49  	defer clean()
    50  
    51  	defer testutil.Chdir(t, w.WorkspaceDirectory)()
    52  
    53  	dest := filepath.Join(w.WorkspaceDirectory, g.RepoName)
    54  
    55  	// clone the repo
    56  	getCmd := get.NewRunner(fake.CtxWithDefaultPrinter(), "kpt")
    57  	getCmd.Command.SetArgs([]string{"file://" + g.RepoDirectory + ".git", w.WorkspaceDirectory})
    58  	err := getCmd.Command.Execute()
    59  	if !assert.NoError(t, err) {
    60  		return
    61  	}
    62  	if !g.AssertEqual(t, filepath.Join(g.DatasetDirectory, testutil.Dataset1), dest, true) {
    63  		return
    64  	}
    65  	gitRunner, err := gitutil.NewLocalGitRunner(w.WorkspaceDirectory)
    66  	if !assert.NoError(t, err) {
    67  		t.FailNow()
    68  	}
    69  	_, err = gitRunner.Run(fake.CtxWithDefaultPrinter(), "add", ".")
    70  	if !assert.NoError(t, err) {
    71  		return
    72  	}
    73  	_, err = gitRunner.Run(fake.CtxWithDefaultPrinter(), "commit", "-m", "commit local package -- ds1")
    74  	if !assert.NoError(t, err) {
    75  		return
    76  	}
    77  
    78  	// update the master branch
    79  	if !assert.NoError(t, g.ReplaceData(testutil.Dataset2)) {
    80  		return
    81  	}
    82  	_, err = g.Commit("modify upstream package -- ds2")
    83  	if !assert.NoError(t, err) {
    84  		return
    85  	}
    86  
    87  	// update the cloned package
    88  	updateCmd := update.NewRunner(fake.CtxWithDefaultPrinter(), "kpt")
    89  	updateCmd.Command.SetArgs([]string{g.RepoName, "--strategy", "fast-forward"})
    90  	if !assert.NoError(t, updateCmd.Command.Execute()) {
    91  		return
    92  	}
    93  	if !g.AssertEqual(t, filepath.Join(g.DatasetDirectory, testutil.Dataset2), dest, true) {
    94  		return
    95  	}
    96  
    97  	commit, err := g.GetCommit()
    98  	if !assert.NoError(t, err) {
    99  		return
   100  	}
   101  	if !g.AssertKptfile(t, dest, kptfilev1.KptFile{
   102  		ResourceMeta: yaml.ResourceMeta{
   103  			ObjectMeta: yaml.ObjectMeta{
   104  				NameMeta: yaml.NameMeta{
   105  					Name: g.RepoName,
   106  				},
   107  			},
   108  			TypeMeta: yaml.TypeMeta{
   109  				APIVersion: kptfilev1.TypeMeta.APIVersion,
   110  				Kind:       kptfilev1.TypeMeta.Kind},
   111  		},
   112  		Upstream: &kptfilev1.Upstream{
   113  			Type: kptfilev1.GitOrigin,
   114  			Git: &kptfilev1.Git{
   115  				Repo:      "file://" + g.RepoDirectory,
   116  				Ref:       "master",
   117  				Directory: "/",
   118  			},
   119  			UpdateStrategy: kptfilev1.FastForward,
   120  		},
   121  		UpstreamLock: &kptfilev1.UpstreamLock{
   122  			Type: kptfilev1.GitOrigin,
   123  			Git: &kptfilev1.GitLock{
   124  				Repo:      "file://" + g.RepoDirectory,
   125  				Ref:       "master",
   126  				Directory: "/",
   127  				Commit:    commit,
   128  			},
   129  		},
   130  	}) {
   131  		return
   132  	}
   133  }
   134  
   135  func TestCmd_successUnCommitted(t *testing.T) {
   136  	g, w, clean := testutil.SetupRepoAndWorkspace(t, testutil.Content{
   137  		Data:   testutil.Dataset1,
   138  		Branch: "master",
   139  	})
   140  	defer clean()
   141  
   142  	defer testutil.Chdir(t, w.WorkspaceDirectory)()
   143  
   144  	dest := filepath.Join(w.WorkspaceDirectory, g.RepoName)
   145  
   146  	// clone the repo
   147  	getCmd := get.NewRunner(fake.CtxWithDefaultPrinter(), "kpt")
   148  	getCmd.Command.SetArgs([]string{"file://" + g.RepoDirectory + ".git", w.WorkspaceDirectory})
   149  	err := getCmd.Command.Execute()
   150  	if !assert.NoError(t, err) {
   151  		return
   152  	}
   153  	if !g.AssertEqual(t, filepath.Join(g.DatasetDirectory, testutil.Dataset1), dest, true) {
   154  		return
   155  	}
   156  
   157  	// update the master branch
   158  	if !assert.NoError(t, g.ReplaceData(testutil.Dataset2)) {
   159  		return
   160  	}
   161  
   162  	// commit the upstream but not the local
   163  	_, err = g.Commit("new dataset")
   164  	if !assert.NoError(t, err) {
   165  		return
   166  	}
   167  
   168  	// update the cloned package
   169  	updateCmd := update.NewRunner(fake.CtxWithDefaultPrinter(), "kpt")
   170  	updateCmd.Command.SetArgs([]string{g.RepoName})
   171  	err = updateCmd.Command.Execute()
   172  	if !assert.NoError(t, err) {
   173  		t.FailNow()
   174  	}
   175  
   176  	if !g.AssertEqual(t, filepath.Join(g.DatasetDirectory, testutil.Dataset2), dest, true) {
   177  		return
   178  	}
   179  }
   180  
   181  func TestCmd_successNoGit(t *testing.T) {
   182  	g, w, clean := testutil.SetupRepoAndWorkspace(t, testutil.Content{
   183  		Data:   testutil.Dataset1,
   184  		Branch: "master",
   185  	})
   186  	defer clean()
   187  
   188  	defer testutil.Chdir(t, w.WorkspaceDirectory)()
   189  
   190  	err := os.RemoveAll(".git")
   191  	if !assert.NoError(t, err) {
   192  		t.FailNow()
   193  	}
   194  	dest := filepath.Join(w.WorkspaceDirectory, g.RepoName)
   195  
   196  	// clone the repo
   197  	getCmd := get.NewRunner(fake.CtxWithDefaultPrinter(), "kpt")
   198  	getCmd.Command.SetArgs([]string{"file://" + g.RepoDirectory + ".git", w.WorkspaceDirectory})
   199  	err = getCmd.Command.Execute()
   200  	if !assert.NoError(t, err) {
   201  		return
   202  	}
   203  	if !g.AssertEqual(t, filepath.Join(g.DatasetDirectory, testutil.Dataset1), dest, true) {
   204  		return
   205  	}
   206  
   207  	// update the master branch
   208  	if !assert.NoError(t, g.ReplaceData(testutil.Dataset2)) {
   209  		return
   210  	}
   211  
   212  	// commit the upstream but not the local
   213  	_, err = g.Commit("new dataset")
   214  	if !assert.NoError(t, err) {
   215  		return
   216  	}
   217  
   218  	// update the cloned package
   219  	updateCmd := update.NewRunner(fake.CtxWithDefaultPrinter(), "kpt")
   220  	updateCmd.Command.SetArgs([]string{g.RepoName})
   221  	err = updateCmd.Command.Execute()
   222  	if !assert.NoError(t, err) {
   223  		t.FailNow()
   224  	}
   225  
   226  	if !g.AssertEqual(t, filepath.Join(g.DatasetDirectory, testutil.Dataset2), dest, true) {
   227  		return
   228  	}
   229  }
   230  
   231  func TestCmd_onlyVersionAsInput(t *testing.T) {
   232  	g, w, clean := testutil.SetupRepoAndWorkspace(t, testutil.Content{
   233  		Data:   testutil.Dataset1,
   234  		Branch: "master",
   235  	})
   236  	defer clean()
   237  
   238  	err := os.RemoveAll(".git")
   239  	if !assert.NoError(t, err) {
   240  		t.FailNow()
   241  	}
   242  	dest := filepath.Join(w.WorkspaceDirectory, g.RepoName)
   243  
   244  	// clone the repo
   245  	getCmd := get.NewRunner(fake.CtxWithDefaultPrinter(), "kpt")
   246  	getCmd.Command.SetArgs([]string{"file://" + g.RepoDirectory + ".git", w.WorkspaceDirectory})
   247  	err = getCmd.Command.Execute()
   248  	if !assert.NoError(t, err) {
   249  		return
   250  	}
   251  	if !g.AssertEqual(t, filepath.Join(g.DatasetDirectory, testutil.Dataset1), dest, true) {
   252  		return
   253  	}
   254  
   255  	// update the master branch
   256  	if !assert.NoError(t, g.ReplaceData(testutil.Dataset2)) {
   257  		return
   258  	}
   259  
   260  	// commit the upstream but not the local
   261  	_, err = g.Commit("new dataset")
   262  	if !assert.NoError(t, err) {
   263  		return
   264  	}
   265  
   266  	// update the cloned package
   267  	updateCmd := update.NewRunner(fake.CtxWithDefaultPrinter(), "kpt")
   268  	defer testutil.Chdir(t, dest)()
   269  	updateCmd.Command.SetArgs([]string{"@master"})
   270  	err = updateCmd.Command.Execute()
   271  	if !assert.NoError(t, err) {
   272  		t.FailNow()
   273  	}
   274  
   275  	if !g.AssertEqual(t, filepath.Join(g.DatasetDirectory, testutil.Dataset2), dest, true) {
   276  		return
   277  	}
   278  }
   279  
   280  // NoOpRunE is a noop function to replace the run function of a command.  Useful for testing argument parsing.
   281  var NoOpRunE = func(cmd *cobra.Command, args []string) error { return nil }
   282  
   283  // NoOpFailRunE causes the test to fail if run is called.  Useful for validating run isn't called for
   284  // errors.
   285  type NoOpFailRunE struct {
   286  	t *testing.T
   287  }
   288  
   289  func (t NoOpFailRunE) runE(_ *cobra.Command, _ []string) error {
   290  	assert.Fail(t.t, "run should not be called")
   291  	return nil
   292  }
   293  
   294  // TestCmd_Execute_flagAndArgParsing verifies that the flags and args are parsed into the correct Command fields
   295  func TestCmd_Execute_flagAndArgParsing(t *testing.T) {
   296  	failRun := NoOpFailRunE{t: t}.runE
   297  
   298  	dir := t.TempDir()
   299  	defer testutil.Chdir(t, filepath.Dir(dir))()
   300  
   301  	// verify the current working directory is used if no path is specified
   302  	r := update.NewRunner(fake.CtxWithDefaultPrinter(), "kpt")
   303  	r.Command.RunE = NoOpRunE
   304  	r.Command.SetArgs([]string{})
   305  	err := r.Command.Execute()
   306  	assert.NoError(t, err)
   307  	assert.Equal(t, "", r.Update.Ref)
   308  	assert.Equal(t, kptfilev1.ResourceMerge, r.Update.Strategy)
   309  
   310  	// verify an error is thrown if multiple paths are specified
   311  	r = update.NewRunner(fake.CtxWithDefaultPrinter(), "kpt")
   312  	r.Command.SilenceErrors = true
   313  	r.Command.RunE = failRun
   314  	r.Command.SetArgs([]string{"foo", "bar"})
   315  	err = r.Command.Execute()
   316  	assert.EqualError(t, err, "accepts at most 1 arg(s), received 2")
   317  	assert.Equal(t, "", r.Update.Ref)
   318  	assert.Equal(t, kptfilev1.UpdateStrategyType(""), r.Update.Strategy)
   319  
   320  	// verify the branch ref is set to the correct value
   321  	r = update.NewRunner(fake.CtxWithDefaultPrinter(), "kpt")
   322  	r.Command.RunE = NoOpRunE
   323  	r.Command.SetArgs([]string{dir + "@refs/heads/foo"})
   324  	err = r.Command.Execute()
   325  	assert.NoError(t, err)
   326  	assert.Equal(t, "refs/heads/foo", r.Update.Ref)
   327  	assert.Equal(t, kptfilev1.ResourceMerge, r.Update.Strategy)
   328  
   329  	// verify the branch ref is set to the correct value
   330  	r = update.NewRunner(fake.CtxWithDefaultPrinter(), "kpt")
   331  	r.Command.RunE = NoOpRunE
   332  	r.Command.SetArgs([]string{dir, "--strategy", "force-delete-replace"})
   333  	err = r.Command.Execute()
   334  	assert.NoError(t, err)
   335  	assert.Equal(t, kptfilev1.ForceDeleteReplace, r.Update.Strategy)
   336  	assert.Equal(t, "", r.Update.Ref)
   337  
   338  	r = update.NewRunner(fake.CtxWithDefaultPrinter(), "kpt")
   339  	r.Command.RunE = NoOpRunE
   340  	r.Command.SetArgs([]string{dir, "--strategy", "resource-merge"})
   341  	err = r.Command.Execute()
   342  	assert.NoError(t, err)
   343  	assert.Equal(t, kptfilev1.ResourceMerge, r.Update.Strategy)
   344  	assert.Equal(t, "", r.Update.Ref)
   345  }
   346  
   347  func TestCmd_flagAndArgParsing_Symlink(t *testing.T) {
   348  	dir := t.TempDir()
   349  	defer testutil.Chdir(t, dir)()
   350  
   351  	err := os.MkdirAll(filepath.Join(dir, "path", "to", "pkg", "dir"), 0700)
   352  	assert.NoError(t, err)
   353  	err = os.Symlink(filepath.Join("path", "to", "pkg", "dir"), "foo")
   354  	assert.NoError(t, err)
   355  
   356  	// verify the branch ref is set to the correct value
   357  	r := update.NewRunner(fake.CtxWithDefaultPrinter(), "kpt")
   358  	r.Command.RunE = NoOpRunE
   359  	r.Command.SetArgs([]string{"foo" + "@refs/heads/foo"})
   360  	err = r.Command.Execute()
   361  	assert.NoError(t, err)
   362  	assert.Equal(t, "refs/heads/foo", r.Update.Ref)
   363  	assert.Equal(t, kptfilev1.ResourceMerge, r.Update.Strategy)
   364  	cwd, err := os.Getwd()
   365  	assert.NoError(t, err)
   366  	assert.Equal(t, filepath.Join(cwd, "path", "to", "pkg", "dir"), r.Update.Pkg.UniquePath.String())
   367  }
   368  
   369  // TestCmd_fail verifies that that command returns an error when it fails rather than exiting the process
   370  func TestCmd_fail(t *testing.T) {
   371  	r := update.NewRunner(fake.CtxWithDefaultPrinter(), "kpt")
   372  	r.Command.SilenceErrors = true
   373  	r.Command.SilenceUsage = true
   374  	r.Command.SetArgs([]string{filepath.Join("not", "real", "dir")})
   375  	err := r.Command.Execute()
   376  	if assert.Error(t, err) {
   377  		assert.Contains(t, err.Error(), "no such file or directory")
   378  	}
   379  }
   380  
   381  func TestCmd_path(t *testing.T) {
   382  	var pathPrefix string
   383  	if runtime.GOOS == "darwin" {
   384  		pathPrefix = "/private"
   385  	}
   386  
   387  	dir := t.TempDir()
   388  
   389  	testCases := []struct {
   390  		name                    string
   391  		currentWD               string
   392  		path                    string
   393  		expectedPath            string
   394  		expectedFullPackagePath string
   395  		expectedErrMsg          string
   396  	}{
   397  		{
   398  			name:                    "update package in current directory",
   399  			currentWD:               dir,
   400  			path:                    ".",
   401  			expectedPath:            ".",
   402  			expectedFullPackagePath: filepath.Join(pathPrefix, dir),
   403  		},
   404  		{
   405  			name:                    "update package in subfolder of current directory",
   406  			currentWD:               filepath.Dir(dir),
   407  			path:                    filepath.Base(dir),
   408  			expectedPath:            filepath.Base(dir),
   409  			expectedFullPackagePath: filepath.Join(pathPrefix, dir),
   410  		},
   411  		{
   412  			name:                    "update package with full absolute path",
   413  			currentWD:               filepath.Dir(dir),
   414  			path:                    filepath.Join(pathPrefix, dir),
   415  			expectedPath:            filepath.Base(dir),
   416  			expectedFullPackagePath: filepath.Join(pathPrefix, dir),
   417  		},
   418  		{
   419  			name:           "package must exist as a subdirectory of cwd",
   420  			currentWD:      filepath.Dir(dir),
   421  			path:           filepath.Dir(filepath.Dir(dir)),
   422  			expectedErrMsg: "package path must be under current working directory",
   423  		},
   424  	}
   425  
   426  	for i := range testCases {
   427  		test := testCases[i]
   428  		t.Run(test.name, func(t *testing.T) {
   429  			defer testutil.Chdir(t, test.currentWD)()
   430  
   431  			r := update.NewRunner(fake.CtxWithDefaultPrinter(), "kpt")
   432  			r.Command.RunE = func(cmd *cobra.Command, args []string) error {
   433  				if !assert.Equal(t, test.expectedFullPackagePath, r.Update.Pkg.UniquePath.String()) {
   434  					t.FailNow()
   435  				}
   436  				return nil
   437  			}
   438  
   439  			r.Command.SetArgs([]string{test.path})
   440  			err := r.Command.Execute()
   441  
   442  			if test.expectedErrMsg != "" {
   443  				if !assert.Error(t, err) {
   444  					t.FailNow()
   445  				}
   446  				assert.Contains(t, err.Error(), test.expectedErrMsg)
   447  				return
   448  			}
   449  
   450  			if !assert.NoError(t, err) {
   451  				t.FailNow()
   452  			}
   453  		})
   454  	}
   455  }
   456  
   457  func TestCmd_output(t *testing.T) {
   458  	testCases := map[string]struct {
   459  		reposChanges   map[string][]testutil.Content
   460  		updatedLocal   testutil.Content
   461  		expectedLocal  *pkgbuilder.RootPkg
   462  		expectedOutput string
   463  	}{
   464  		"basic package": {
   465  			reposChanges: map[string][]testutil.Content{
   466  				testutil.Upstream: {
   467  					{
   468  						Pkg: pkgbuilder.NewRootPkg().
   469  							WithKptfile().
   470  							WithResource(pkgbuilder.DeploymentResource),
   471  						Branch: "master",
   472  					},
   473  					{
   474  						Pkg: pkgbuilder.NewRootPkg().
   475  							WithKptfile().
   476  							WithResource(pkgbuilder.SecretResource),
   477  					},
   478  				},
   479  			},
   480  			expectedLocal: pkgbuilder.NewRootPkg().
   481  				WithKptfile(
   482  					pkgbuilder.NewKptfile().
   483  						WithUpstreamRef(testutil.Upstream, "/", "master", "resource-merge").
   484  						WithUpstreamLockRef(testutil.Upstream, "/", "master", 1),
   485  				).
   486  				WithResource(pkgbuilder.SecretResource),
   487  			expectedOutput: `
   488  Package "{{ .PKG_NAME }}":
   489  Fetching upstream from {{ (index .REPOS "upstream").RepoDirectory }}@master
   490  <git_output>
   491  Fetching origin from {{ (index .REPOS "upstream").RepoDirectory }}@master
   492  <git_output>
   493  Updating package "{{ .PKG_NAME }}" with strategy "resource-merge".
   494  
   495  Updated 1 package(s).
   496  `,
   497  		},
   498  		"nested packages": {
   499  			reposChanges: map[string][]testutil.Content{
   500  				testutil.Upstream: {
   501  					{
   502  						Pkg: pkgbuilder.NewRootPkg().
   503  							WithKptfile().
   504  							WithResource(pkgbuilder.DeploymentResource).
   505  							WithSubPackages(
   506  								pkgbuilder.NewSubPkg("subpkg").
   507  									WithKptfile(
   508  										pkgbuilder.NewKptfile().
   509  											WithUpstreamRef("foo", "/", "master", "fast-forward").
   510  											WithUpstreamLockRef("foo", "/", "master", 0),
   511  									).
   512  									WithResource(pkgbuilder.DeploymentResource),
   513  							),
   514  						Branch: "master",
   515  					},
   516  					{
   517  						Pkg: pkgbuilder.NewRootPkg().
   518  							WithKptfile().
   519  							WithResource(pkgbuilder.SecretResource).
   520  							WithSubPackages(
   521  								pkgbuilder.NewSubPkg("subpkg").
   522  									WithKptfile(
   523  										pkgbuilder.NewKptfile().
   524  											WithUpstreamRef("foo", "/", "master", "fast-forward").
   525  											WithUpstreamLockRef("foo", "/", "master", 0),
   526  									).
   527  									WithResource(pkgbuilder.DeploymentResource),
   528  							),
   529  					},
   530  				},
   531  				"foo": {
   532  					{
   533  						Pkg: pkgbuilder.NewRootPkg().
   534  							WithKptfile().
   535  							WithResource(pkgbuilder.DeploymentResource),
   536  						Branch: "master",
   537  					},
   538  					{
   539  						Pkg: pkgbuilder.NewRootPkg().
   540  							WithKptfile().
   541  							WithResource(pkgbuilder.ConfigMapResource),
   542  					},
   543  				},
   544  			},
   545  			expectedLocal: pkgbuilder.NewRootPkg().
   546  				WithKptfile(
   547  					pkgbuilder.NewKptfile().
   548  						WithUpstreamRef(testutil.Upstream, "/", "master", "resource-merge").
   549  						WithUpstreamLockRef(testutil.Upstream, "/", "master", 1),
   550  				).
   551  				WithResource(pkgbuilder.SecretResource).
   552  				WithSubPackages(
   553  					pkgbuilder.NewSubPkg("subpkg").
   554  						WithKptfile(
   555  							pkgbuilder.NewKptfile().
   556  								WithUpstreamRef("foo", "/", "master", "fast-forward").
   557  								WithUpstreamLockRef("foo", "/", "master", 1),
   558  						).
   559  						WithResource(pkgbuilder.ConfigMapResource),
   560  				),
   561  			expectedOutput: `
   562  Package "{{ .PKG_NAME }}":
   563  Fetching upstream from {{ (index .REPOS "upstream").RepoDirectory }}@master
   564  <git_output>
   565  Fetching origin from {{ (index .REPOS "upstream").RepoDirectory }}@master
   566  <git_output>
   567  Updating package "{{ .PKG_NAME }}" with strategy "resource-merge".
   568  Updating package "subpkg" with strategy "fast-forward".
   569  
   570  Package "{{ .PKG_NAME }}/subpkg":
   571  Fetching upstream from {{ (index .REPOS "foo").RepoDirectory }}@master
   572  <git_output>
   573  Fetching origin from {{ (index .REPOS "foo").RepoDirectory }}@master
   574  <git_output>
   575  Updating package "subpkg" with strategy "fast-forward".
   576  
   577  Updated 2 package(s).
   578  `,
   579  		},
   580  		"subpackage deleted from upstream": {
   581  			reposChanges: map[string][]testutil.Content{
   582  				testutil.Upstream: {
   583  					{
   584  						Pkg: pkgbuilder.NewRootPkg().
   585  							WithKptfile().
   586  							WithResource(pkgbuilder.DeploymentResource).
   587  							WithSubPackages(
   588  								pkgbuilder.NewSubPkg("subpkg1").
   589  									WithKptfile(
   590  										pkgbuilder.NewKptfile().
   591  											WithUpstreamRef("foo", "/", "master", "resource-merge").
   592  											WithUpstreamLockRef("foo", "/", "master", 0),
   593  									).
   594  									WithResource(pkgbuilder.DeploymentResource),
   595  								pkgbuilder.NewSubPkg("subpkg2").
   596  									WithKptfile(
   597  										pkgbuilder.NewKptfile().
   598  											WithUpstreamRef("foo", "/", "master", "resource-merge").
   599  											WithUpstreamLockRef("foo", "/", "master", 0),
   600  									).
   601  									WithResource(pkgbuilder.DeploymentResource),
   602  							),
   603  						Branch: "master",
   604  					},
   605  					{
   606  						Pkg: pkgbuilder.NewRootPkg().
   607  							WithKptfile().
   608  							WithResource(pkgbuilder.SecretResource),
   609  					},
   610  				},
   611  				"foo": {
   612  					{
   613  						Pkg: pkgbuilder.NewRootPkg().
   614  							WithKptfile().
   615  							WithResource(pkgbuilder.DeploymentResource),
   616  						Branch: "master",
   617  					},
   618  
   619  					{
   620  						Pkg: pkgbuilder.NewRootPkg().
   621  							WithKptfile().
   622  							WithResource(pkgbuilder.DeploymentResource).
   623  							WithResource(pkgbuilder.ConfigMapResource),
   624  					},
   625  				},
   626  			},
   627  			updatedLocal: testutil.Content{
   628  				Pkg: pkgbuilder.NewRootPkg().
   629  					WithKptfile(
   630  						pkgbuilder.NewKptfile().
   631  							WithUpstreamRef(testutil.Upstream, "/", "master", "resource-merge").
   632  							WithUpstreamLockRef(testutil.Upstream, "/", "master", 0),
   633  					).
   634  					WithResource(pkgbuilder.DeploymentResource).
   635  					WithSubPackages(
   636  						pkgbuilder.NewSubPkg("subpkg1").
   637  							WithKptfile(
   638  								pkgbuilder.NewKptfile().
   639  									WithUpstreamRef("foo", "/", "master", "resource-merge").
   640  									WithUpstreamLockRef("foo", "/", "master", 0),
   641  							).
   642  							WithResource(pkgbuilder.DeploymentResource, pkgbuilder.SetFieldPath("5", "spec", "replicas")),
   643  						pkgbuilder.NewSubPkg("subpkg2").
   644  							WithKptfile(
   645  								pkgbuilder.NewKptfile().
   646  									WithUpstreamRef("foo", "/", "master", "resource-merge").
   647  									WithUpstreamLockRef("foo", "/", "master", 0),
   648  							).
   649  							WithResource(pkgbuilder.DeploymentResource),
   650  					),
   651  			},
   652  			expectedLocal: pkgbuilder.NewRootPkg().
   653  				WithKptfile(
   654  					pkgbuilder.NewKptfile().
   655  						WithUpstreamRef(testutil.Upstream, "/", "master", "resource-merge").
   656  						WithUpstreamLockRef(testutil.Upstream, "/", "master", 1),
   657  				).
   658  				WithResource(pkgbuilder.SecretResource).
   659  				WithSubPackages(
   660  					pkgbuilder.NewSubPkg("subpkg1").
   661  						WithKptfile(
   662  							pkgbuilder.NewKptfile().
   663  								WithUpstreamRef("foo", "/", "master", "resource-merge").
   664  								WithUpstreamLockRef("foo", "/", "master", 1),
   665  						).
   666  						WithResource(pkgbuilder.ConfigMapResource).
   667  						WithResource(pkgbuilder.DeploymentResource, pkgbuilder.SetFieldPath("5", "spec", "replicas")),
   668  				),
   669  			expectedOutput: `
   670  Package "{{ .PKG_NAME }}":
   671  Fetching upstream from {{ (index .REPOS "upstream").RepoDirectory }}@master
   672  <git_output>
   673  Fetching origin from {{ (index .REPOS "upstream").RepoDirectory }}@master
   674  <git_output>
   675  Updating package "{{ .PKG_NAME }}" with strategy "resource-merge".
   676  Deleting package "subpkg2" from local since it is removed in upstream.
   677  Package "subpkg1" deleted from upstream, but keeping local since it has changes.
   678  
   679  Package "{{ .PKG_NAME }}/subpkg1":
   680  Fetching upstream from {{ (index .REPOS "foo").RepoDirectory }}@master
   681  <git_output>
   682  Fetching origin from {{ (index .REPOS "foo").RepoDirectory }}@master
   683  <git_output>
   684  Updating package "subpkg1" with strategy "resource-merge".
   685  
   686  Updated 2 package(s).
   687  `,
   688  		},
   689  		"Adding package in upstream": {
   690  			reposChanges: map[string][]testutil.Content{
   691  				testutil.Upstream: {
   692  					{
   693  						Pkg: pkgbuilder.NewRootPkg().
   694  							WithKptfile().
   695  							WithResource(pkgbuilder.DeploymentResource),
   696  						Branch: "master",
   697  					},
   698  					{
   699  						Pkg: pkgbuilder.NewRootPkg().
   700  							WithKptfile().
   701  							WithResource(pkgbuilder.SecretResource).
   702  							WithSubPackages(
   703  								pkgbuilder.NewSubPkg("subpkg").
   704  									WithKptfile(
   705  										pkgbuilder.NewKptfile().
   706  											WithUpstreamRef("foo", "/", "v1", "force-delete-replace"),
   707  									),
   708  							),
   709  					},
   710  				},
   711  				"foo": {
   712  					{
   713  						Pkg: pkgbuilder.NewRootPkg().
   714  							WithKptfile().
   715  							WithResource(pkgbuilder.DeploymentResource),
   716  						Branch: "master",
   717  						Tag:    "v1",
   718  					},
   719  				},
   720  			},
   721  			expectedLocal: pkgbuilder.NewRootPkg().
   722  				WithKptfile(
   723  					pkgbuilder.NewKptfile().
   724  						WithUpstreamRef(testutil.Upstream, "/", "master", "resource-merge").
   725  						WithUpstreamLockRef(testutil.Upstream, "/", "master", 1),
   726  				).
   727  				WithResource(pkgbuilder.SecretResource).
   728  				WithSubPackages(
   729  					pkgbuilder.NewSubPkg("subpkg").
   730  						WithKptfile(
   731  							pkgbuilder.NewKptfile().
   732  								WithUpstreamRef("foo", "/", "v1", "force-delete-replace").
   733  								WithUpstreamLockRef("foo", "/", "v1", 0),
   734  						).
   735  						WithResource(pkgbuilder.DeploymentResource),
   736  				),
   737  			expectedOutput: `
   738  Package "{{ .PKG_NAME }}":
   739  Fetching upstream from {{ (index .REPOS "upstream").RepoDirectory }}@master
   740  <git_output>
   741  Fetching origin from {{ (index .REPOS "upstream").RepoDirectory }}@master
   742  <git_output>
   743  Updating package "{{ .PKG_NAME }}" with strategy "resource-merge".
   744  Adding package "subpkg" from upstream.
   745  
   746  Package "{{ .PKG_NAME }}/subpkg":
   747  Fetching upstream from {{ (index .REPOS "foo").RepoDirectory }}@v1
   748  <git_output>
   749  Updating package "subpkg" with strategy "force-delete-replace".
   750  
   751  Updated 2 package(s).
   752  `,
   753  		},
   754  	}
   755  
   756  	for tn, tc := range testCases {
   757  		t.Run(tn, func(t *testing.T) {
   758  			g := &testutil.TestSetupManager{
   759  				T:            t,
   760  				ReposChanges: tc.reposChanges,
   761  			}
   762  			defer g.Clean()
   763  			if tc.updatedLocal.Pkg != nil {
   764  				g.LocalChanges = []testutil.Content{
   765  					tc.updatedLocal,
   766  				}
   767  			}
   768  			if !g.Init() {
   769  				return
   770  			}
   771  
   772  			clean := testutil.Chdir(t, g.LocalWorkspace.FullPackagePath())
   773  			defer clean()
   774  
   775  			var outBuf bytes.Buffer
   776  			var errBuf bytes.Buffer
   777  
   778  			ctx := fake.CtxWithPrinter(&outBuf, &errBuf)
   779  			r := update.NewRunner(ctx, "kpt")
   780  			r.Command.SetArgs([]string{})
   781  			err := r.Command.Execute()
   782  			if !assert.NoError(t, err) {
   783  				t.FailNow()
   784  			}
   785  
   786  			assert.Empty(t, outBuf.String())
   787  
   788  			tmpl := template.Must(template.New("test").Parse(tc.expectedOutput))
   789  			var expected bytes.Buffer
   790  			err = tmpl.Execute(&expected, map[string]interface{}{
   791  				"PKG_PATH": g.LocalWorkspace.FullPackagePath(),
   792  				"PKG_NAME": g.LocalWorkspace.PackageDir,
   793  				"REPOS":    g.Repos,
   794  			})
   795  			if !assert.NoError(t, err) {
   796  				t.FailNow()
   797  			}
   798  			actual := scrubGitOutput(errBuf.String())
   799  
   800  			assert.Equal(t, strings.TrimSpace(expected.String()), strings.TrimSpace(actual))
   801  
   802  			expectedPath := tc.expectedLocal.ExpandPkgWithName(t, g.LocalWorkspace.PackageDir, testutil.ToReposInfo(g.Repos))
   803  			testutil.KptfileAwarePkgEqual(t, expectedPath, g.LocalWorkspace.FullPackagePath(), true)
   804  		})
   805  	}
   806  }
   807  
   808  const (
   809  	gitOutputPattern = `From \/.*(\r\n|\r|\n)( * .*(\r\n|\r|\n))+`
   810  )
   811  
   812  func scrubGitOutput(output string) string {
   813  	re := regexp.MustCompile(gitOutputPattern)
   814  	return re.ReplaceAllString(output, "<git_output>\n")
   815  }