From 0aff930ad5758667336a4804b851d7ddc05e9a15 Mon Sep 17 00:00:00 2001 From: liamfallon Date: Mon, 5 Jan 2026 13:04:02 +0000 Subject: [PATCH 1/5] Sync kpt type definitions across kpt and porch Signed-off-by: liamfallon --- pkg/api/kptfile/v1/types.go | 13 +++++++++++++ pkg/api/kptfile/v1/validation_test.go | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/pkg/api/kptfile/v1/types.go b/pkg/api/kptfile/v1/types.go index aa3e971ac..a8a9bd476 100644 --- a/pkg/api/kptfile/v1/types.go +++ b/pkg/api/kptfile/v1/types.go @@ -24,6 +24,8 @@ import ( "sigs.k8s.io/kustomize/kyaml/yaml" ) +//go:generate go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.19.0 object:headerFile="../../../../../scripts/boilerplate.go.txt" + const ( KptFileName = "Kptfile" @@ -105,6 +107,8 @@ func ToUpdateStrategy(strategy string) (UpdateStrategyType, error) { return FastForward, nil case string(ForceDeleteReplace): return ForceDeleteReplace, nil + case string(CopyMerge): + return CopyMerge, nil default: return "", fmt.Errorf("unknown update strategy %q", strategy) } @@ -119,6 +123,8 @@ const ( FastForward UpdateStrategyType = "fast-forward" // ForceDeleteReplace wipes all local changes to the package. ForceDeleteReplace UpdateStrategyType = "force-delete-replace" + // CopyMerge copies the updated package into the local package. + CopyMerge UpdateStrategyType = "copy-merge" ) // UpdateStrategies is a slice with all the supported update strategies. @@ -126,6 +132,7 @@ var UpdateStrategies = []UpdateStrategyType{ ResourceMerge, FastForward, ForceDeleteReplace, + CopyMerge, } // UpdateStrategiesAsStrings returns a list of update strategies as strings. @@ -396,3 +403,9 @@ const ( ConditionFalse ConditionStatus = "False" ConditionUnknown ConditionStatus = "Unknown" ) + +// BFSRenderAnnotation is an annotation that can be used to indicate that a package +// should be hydrated from the root package to the subpackages in a Breadth-First Level Order manner. +const ( + BFSRenderAnnotation = "kpt.dev/bfs-rendering" +) diff --git a/pkg/api/kptfile/v1/validation_test.go b/pkg/api/kptfile/v1/validation_test.go index bb6c9a6bc..2c180a49a 100644 --- a/pkg/api/kptfile/v1/validation_test.go +++ b/pkg/api/kptfile/v1/validation_test.go @@ -179,7 +179,7 @@ func TestValidateFunctionName(t *testing.T) { true, }, { - "ghcr.io/kptdev/krm-functions-catalog/generate-folders:unstable", + "ghcr.io/kptdev/krm-functions-catalog/generate-folders:latest", true, }, { @@ -187,7 +187,7 @@ func TestValidateFunctionName(t *testing.T) { true, }, { - "ghcr.io/kptdev/krm-functions-catalog/generate-folders:latest-alpha1", + "ghcr.io/kptdev/krm-functions-catalog/generate-folders:v1.2.3-alpha1", true, }, { From efe7efc13a8030b65349fc906f1d5e9f708df59d Mon Sep 17 00:00:00 2001 From: liamfallon Date: Mon, 5 Jan 2026 14:39:22 +0000 Subject: [PATCH 2/5] Add copy-merge strategy to kpt Signed-off-by: liamfallon --- internal/util/pkgutil/pkgutil.go | 57 ++- internal/util/pkgutil/pkgutil_test.go | 45 ++- internal/util/update/copy-merge.go | 51 +++ internal/util/update/copy-merge_test.go | 446 ++++++++++++++++++++++++ internal/util/update/update.go | 1 + 5 files changed, 596 insertions(+), 4 deletions(-) create mode 100644 internal/util/update/copy-merge.go create mode 100644 internal/util/update/copy-merge_test.go diff --git a/internal/util/pkgutil/pkgutil.go b/internal/util/pkgutil/pkgutil.go index a072c4fc7..f24eec32d 100644 --- a/internal/util/pkgutil/pkgutil.go +++ b/internal/util/pkgutil/pkgutil.go @@ -169,6 +169,62 @@ func CopyPackage(src, dst string, copyRootKptfile bool, matcher pkg.SubpackageMa return nil } +// RemoveStaleItems removes files and directories from the dst package that were present in the org package, +// but are not present in the src package. It does not remove the root Kptfile of the dst package. +func RemoveStaleItems(org, src, dst string, copyRootKptfile bool, matcher pkg.SubpackageMatcher) error { + var dirsToDelete []string + walkErr := filepath.Walk(dst, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + // The root directory should never be deleted. + if path == dst { + return nil + } + + relPath, err := filepath.Rel(dst, path) + if err != nil { + return err + } + + // Skip the root Kptfile + if relPath == kptfilev1.KptFileName { + return nil + } + + srcPath := filepath.Join(src, relPath) + orgPath := filepath.Join(org, relPath) + + _, srcErr := os.Stat(srcPath) + _, orgErr := os.Stat(orgPath) + + // Only remove if: + // - not present in src (srcErr is os.IsNotExist) + // - present in org (orgErr is nil) + if os.IsNotExist(srcErr) && orgErr == nil { + if info.IsDir() { + dirsToDelete = append(dirsToDelete, path) + } else { + if err := os.Remove(path); err != nil { + return err + } + } + } + return nil + }) + if walkErr != nil { + return walkErr + } + sort.Slice(dirsToDelete, SubPkgFirstSorter(dirsToDelete)) + for _, dir := range dirsToDelete { + if err := os.Remove(dir); err != nil { + return err + } + } + + return nil +} + func RemovePackageContent(path string, removeRootKptfile bool) error { // Walk the package (while ignoring subpackages) and delete all files. // We capture the paths to any subdirectories in the package so we @@ -209,7 +265,6 @@ func RemovePackageContent(path string, removeRootKptfile bool) error { if err != nil { return err } - defer f.Close() // List up to one file or folder in the directory. _, err = f.Readdirnames(1) if err != nil && err != io.EOF { diff --git a/internal/util/pkgutil/pkgutil_test.go b/internal/util/pkgutil/pkgutil_test.go index c64b2ef81..59c817e01 100644 --- a/internal/util/pkgutil/pkgutil_test.go +++ b/internal/util/pkgutil/pkgutil_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 The kpt Authors +// Copyright 2021 The kpt and Nephio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -83,7 +83,7 @@ func TestWalkPackage(t *testing.T) { pkgPath := tc.pkg.ExpandPkg(t, testutil.EmptyReposInfo) var visited []string - if err := pkgutil.WalkPackage(pkgPath, func(s string, _ os.FileInfo, err error) error { + if err := pkgutil.WalkPackage(pkgPath, func(s string, info os.FileInfo, err error) error { if err != nil { return err } @@ -371,7 +371,7 @@ func TestCopyPackage(t *testing.T) { } var visited []string - if err = filepath.Walk(dest, func(s string, _ os.FileInfo, err error) error { + if err = filepath.Walk(dest, func(s string, info os.FileInfo, err error) error { if err != nil { return err } @@ -549,3 +549,42 @@ func TestFindLocalRecursiveSubpackagesForPaths(t *testing.T) { }) } } + +func TestRemoveStaleItems_RemovesFile(t *testing.T) { + org := t.TempDir() + src := t.TempDir() + dst := t.TempDir() + + // Create a file in org and dst, but not in src + fileName := "file.txt" + assert.NoError(t, os.WriteFile(filepath.Join(org, fileName), []byte("content"), 0644)) + assert.NoError(t, os.WriteFile(filepath.Join(dst, fileName), []byte("content"), 0644)) + + // Should remove file.txt from dst + err := pkgutil.RemoveStaleItems(org, src, dst, true, pkg.All) + assert.NoError(t, err) + _, err = os.Stat(filepath.Join(dst, fileName)) + assert.True(t, os.IsNotExist(err)) +} + +func TestRemoveStaleItems_ErrorOnRemove(t *testing.T) { + org := t.TempDir() + src := t.TempDir() + dst := t.TempDir() + + fileName := "file.txt" + filePathDst := filepath.Join(dst, fileName) + filePathOrg := filepath.Join(org, fileName) + + assert.NoError(t, os.WriteFile(filePathOrg, []byte("content"), 0644)) + assert.NoError(t, os.WriteFile(filePathDst, []byte("content"), 0644)) + + // Replace file in dst with a non-empty directory to force os.Remove error + assert.NoError(t, os.Remove(filePathDst)) + assert.NoError(t, os.Mkdir(filePathDst, 0755)) + assert.NoError(t, os.WriteFile(filepath.Join(filePathDst, "dummy"), []byte("x"), 0644)) + + err := pkgutil.RemoveStaleItems(org, src, dst, true, pkg.All) + assert.Error(t, err) + assert.Contains(t, err.Error(), "directory not empty") +} diff --git a/internal/util/update/copy-merge.go b/internal/util/update/copy-merge.go new file mode 100644 index 000000000..681119add --- /dev/null +++ b/internal/util/update/copy-merge.go @@ -0,0 +1,51 @@ +// Copyright 2025 The Nephio Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package update + +import ( + "github.com/kptdev/kpt/internal/errors" + "github.com/kptdev/kpt/internal/pkg" + "github.com/kptdev/kpt/internal/types" + "github.com/kptdev/kpt/internal/util/pkgutil" + "github.com/kptdev/kpt/pkg/kptfile/kptfileutil" +) + +// CopyMergeUpdater is responsible for synchronizing the destination package +// with the source package by updating the Kptfile and copying and replacing package contents. +type CopyMergeUpdater struct{} + +// Update synchronizes the destination/local package with the source/update package by updating the Kptfile +// and copying package contents. It deletes resources from the destination package if they were present +// in the original package, but not present anymore in the source package. +// It takes an Options struct as input, which specifies the paths +// and other parameters for the update operation. Returns an error if the update fails. +func (u CopyMergeUpdater) Update(options Options) error { + const op errors.Op = "update.Update" + + dst := options.LocalPath + src := options.UpdatedPath + org := options.OriginPath + + if err := kptfileutil.UpdateKptfile(dst, src, options.OriginPath, true); err != nil { + return errors.E(op, types.UniquePath(dst), err) + } + if err := pkgutil.CopyPackage(src, dst, options.IsRoot, pkg.All); err != nil { + return errors.E(op, types.UniquePath(dst), err) + } + if err := pkgutil.RemoveStaleItems(org, src, dst, options.IsRoot, pkg.All); err != nil { + return errors.E(op, types.UniquePath(dst), err) + } + return nil +} diff --git a/internal/util/update/copy-merge_test.go b/internal/util/update/copy-merge_test.go new file mode 100644 index 000000000..24def9f4a --- /dev/null +++ b/internal/util/update/copy-merge_test.go @@ -0,0 +1,446 @@ +// Copyright 2025 The Nephio Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package update_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/kptdev/kpt/internal/testutil" + "github.com/kptdev/kpt/internal/testutil/pkgbuilder" + . "github.com/kptdev/kpt/internal/util/update" + "github.com/stretchr/testify/assert" +) + +const copyMergeLiteral = "copy-merge" + +func TestCopyMerge(t *testing.T) { + testCases := map[string]struct { + origin *pkgbuilder.RootPkg + local *pkgbuilder.RootPkg + updated *pkgbuilder.RootPkg + relPackagePath string + isRoot bool + expected *pkgbuilder.RootPkg + }{ + "only kpt file update": { + origin: pkgbuilder.NewRootPkg(), + local: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "A0", "1", copyMergeLiteral), + ), + updated: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "A0", "22", copyMergeLiteral), + ), + relPackagePath: "/", + isRoot: true, + expected: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "A0", "22", copyMergeLiteral), + ), + }, + "new package and subpackage": { + origin: pkgbuilder.NewRootPkg(), + local: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "A", "1", copyMergeLiteral), + ), + updated: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "A", "22", copyMergeLiteral), + ). + WithResource(pkgbuilder.DeploymentResource). + WithSubPackages( + pkgbuilder.NewSubPkg("B"). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "b", "1", copyMergeLiteral), + ). + WithResource(pkgbuilder.DeploymentResource), + ), + relPackagePath: "/", + isRoot: true, + expected: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "A", "22", copyMergeLiteral), + ). + WithResource(pkgbuilder.DeploymentResource). + WithSubPackages( + pkgbuilder.NewSubPkg("B"). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "b", "1", copyMergeLiteral), + ). + WithResource(pkgbuilder.DeploymentResource), + ), + }, + "adds and update package": { + origin: pkgbuilder.NewRootPkg(), + local: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "A0", "1", copyMergeLiteral), + ). + WithResource(pkgbuilder.DeploymentResource). + WithSubPackages( + pkgbuilder.NewSubPkg("pkgA"). + WithResource(pkgbuilder.DeploymentResource), + pkgbuilder.NewSubPkg("pkgB"). + WithResource(pkgbuilder.DeploymentResource), + ), + updated: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "A0", "1", copyMergeLiteral), + ). + WithResource(pkgbuilder.DeploymentResource). + WithSubPackages( + pkgbuilder.NewSubPkg("pkgA"). + WithResource(pkgbuilder.ConfigMapResource), + pkgbuilder.NewSubPkg("pkgC"). + WithResource(pkgbuilder.ConfigMapResource), + ), + relPackagePath: "/", + isRoot: true, + expected: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "A0", "1", copyMergeLiteral), + ). + WithResource(pkgbuilder.DeploymentResource). + WithSubPackages( + pkgbuilder.NewSubPkg("pkgA"). + WithResource(pkgbuilder.ConfigMapResource). + WithResource(pkgbuilder.DeploymentResource), + pkgbuilder.NewSubPkg("pkgB"). + WithResource(pkgbuilder.DeploymentResource), + pkgbuilder.NewSubPkg("pkgC"). + WithResource(pkgbuilder.ConfigMapResource), + ), + }, + "updates local subpackages": { + origin: pkgbuilder.NewRootPkg(), + local: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "/", "master", copyMergeLiteral). + WithUpstreamLock(kptRepo, "/", "master", "A"), + ). + WithResource(pkgbuilder.DeploymentResource). + WithSubPackages( + pkgbuilder.NewSubPkg("foo"). + WithKptfile(). + WithResource(pkgbuilder.DeploymentResource), + ), + updated: pkgbuilder.NewRootPkg(). + WithKptfile(pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "/A", "newBranch", copyMergeLiteral). + WithUpstreamLock(kptRepo, "/A", "newBranch", "A"), + ). + WithResource(pkgbuilder.DeploymentResource). + WithSubPackages( + pkgbuilder.NewSubPkg("foo2"). + WithKptfile(). + WithResource(pkgbuilder.ConfigMapResource), + ), + relPackagePath: "/", + isRoot: true, + expected: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "/A", "newBranch", copyMergeLiteral). + WithUpstreamLock(kptRepo, "/A", "newBranch", "A"), + ). + WithResource(pkgbuilder.DeploymentResource). + WithSubPackages( + pkgbuilder.NewSubPkg("foo2"). + WithKptfile(). + WithResource(pkgbuilder.ConfigMapResource), + pkgbuilder.NewSubPkg("foo"). + WithKptfile(). + WithResource(pkgbuilder.DeploymentResource), + ), + }, + "file removal if file exists in origin but not in update": { + origin: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "/origin", "master", copyMergeLiteral), + ). + WithResource(pkgbuilder.DeploymentResource), + local: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "/origin", "master", copyMergeLiteral), + ). + WithResource(pkgbuilder.DeploymentResource), + updated: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "/origin", "master", copyMergeLiteral). + WithUpstreamLock(kptRepo, "/origin", "master", "abc123"), + ), + relPackagePath: "/", + isRoot: true, + expected: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "/origin", "master", copyMergeLiteral). + WithUpstreamLock(kptRepo, "/origin", "master", "abc123"), + ), + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + + repos := testutil.EmptyReposInfo + origin := tc.origin.ExpandPkg(t, repos) + local := tc.local.ExpandPkg(t, repos) + updated := tc.updated.ExpandPkg(t, repos) + expected := tc.expected.ExpandPkg(t, repos) + + updater := &CopyMergeUpdater{} + + err := updater.Update(Options{ + RelPackagePath: tc.relPackagePath, + OriginPath: filepath.Join(origin, tc.relPackagePath), + LocalPath: filepath.Join(local, tc.relPackagePath), + UpdatedPath: filepath.Join(updated, tc.relPackagePath), + IsRoot: tc.isRoot, + }) + if !assert.NoError(t, err) { + t.FailNow() + } + + testutil.KptfileAwarePkgEqual(t, local, expected, false) + + }) + } +} + +func TestCopyMergeError(t *testing.T) { + src := t.TempDir() + dst := t.TempDir() + + err := os.WriteFile(filepath.Join(src, "file.txt"), []byte("content"), 0644) + if err != nil { + t.Fatalf("Failed to write file: %v", err) + } + os.RemoveAll(src) + + updater := &CopyMergeUpdater{} + options := Options{ + UpdatedPath: src, + LocalPath: dst, + IsRoot: true, + } + + err = updater.Update(options) + assert.Error(t, err) + assert.Contains(t, err.Error(), "no such file or directory") + +} + +func TestCopyMergeErrorUpdatingKptfile(t *testing.T) { + src := t.TempDir() + dst := t.TempDir() + + err := os.WriteFile(filepath.Join(src, "Kptfile"), []byte(` +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: source-package +`), 0644) + assert.NoError(t, err) + + err = os.WriteFile(filepath.Join(dst, "Kptfile"), []byte(` +apiVersion: kpt.dev/v000 +kind: malformedKptfile +`), 0644) + assert.NoError(t, err) + + updater := &CopyMergeUpdater{} + options := Options{ + UpdatedPath: src, + LocalPath: dst, + IsRoot: true, + } + + err = updater.Update(options) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unknown resource type") +} + +func TestCopyMergeErrorCopyingFile(t *testing.T) { + src := t.TempDir() + dst := t.TempDir() + + err := os.WriteFile(filepath.Join(src, "file.txt"), []byte("content"), 0644) + if err != nil { + t.Fatalf("Failed to write file: %v", err) + } + + err = os.Mkdir(filepath.Join(dst, "file.txt"), 0755) + if err != nil { + t.Fatalf("Failed to create directory: %v", err) + } + + updater := &CopyMergeUpdater{} + options := Options{ + UpdatedPath: src, + LocalPath: dst, + IsRoot: true, + } + + err = updater.Update(options) + assert.Error(t, err) + assert.Contains(t, err.Error(), "is a directory") +} + +func TestCopyMergeDifferentMetadata(t *testing.T) { + testCases := map[string]struct { + origin *pkgbuilder.RootPkg + local *pkgbuilder.RootPkg + updated *pkgbuilder.RootPkg + relPackagePath string + isRoot bool + expected *pkgbuilder.RootPkg + }{ + "kpt metadata name": { + origin: pkgbuilder.NewRootPkg(), + local: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(), + ), + updated: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(), + ), + relPackagePath: "/", + isRoot: true, + expected: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(), + ), + }, + "sub folder with different kptfile": { + origin: pkgbuilder.NewRootPkg(), + local: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "root", "1", copyMergeLiteral), + ). + WithSubPackages( + pkgbuilder.NewSubPkg("pkgA"). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "A1", "1", copyMergeLiteral), + ), + ), + updated: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "root", "2", copyMergeLiteral), + ). + WithSubPackages( + pkgbuilder.NewSubPkg("pkgA"). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "A2", "2", copyMergeLiteral), + ), + ), + relPackagePath: "/", + isRoot: true, + expected: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "root", "2", copyMergeLiteral), + ). + WithSubPackages( + pkgbuilder.NewSubPkg("pkgA"). + WithKptfile( + pkgbuilder.NewKptfile(). + WithUpstream(kptRepo, "A2", "2", copyMergeLiteral), + ), + ), + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + + repos := testutil.EmptyReposInfo + origin := tc.origin.ExpandPkg(t, repos) //metadata.name: "base" + local := tc.local.ExpandPkgWithName(t, "local", repos) //metadata.name: "local" + updated := tc.updated.ExpandPkgWithName(t, "updated", repos) //metadata.name: "updated" + expected := tc.expected.ExpandPkgWithName(t, "local", repos) //metadata.name: "local" I am expeting this field to not change + + updater := &CopyMergeUpdater{} + + err := updater.Update(Options{ + RelPackagePath: tc.relPackagePath, + OriginPath: filepath.Join(origin, tc.relPackagePath), + LocalPath: filepath.Join(local, tc.relPackagePath), + UpdatedPath: filepath.Join(updated, tc.relPackagePath), + IsRoot: tc.isRoot, + }) + if !assert.NoError(t, err) { + t.FailNow() + } + + testutil.KptfileAwarePkgEqual(t, local, expected, false) + + }) + } +} + +func TestCopyMergeErrorRemovingFile(t *testing.T) { + src := t.TempDir() + dst := t.TempDir() + org := t.TempDir() + + // Create a file in org and dst, but not in src (so RemoveStaleItems will try to remove it) + fileName := "file.txt" + filePathDst := filepath.Join(dst, fileName) + filePathOrg := filepath.Join(org, fileName) + + assert.NoError(t, os.WriteFile(filePathDst, []byte("content"), 0644)) + assert.NoError(t, os.WriteFile(filePathOrg, []byte("content"), 0644)) + + assert.NoError(t, os.Remove(filePathDst)) + assert.NoError(t, os.Mkdir(filePathDst, 0755)) + assert.NoError(t, os.WriteFile(filepath.Join(filePathDst, "dummy"), []byte("x"), 0644)) + + updater := &CopyMergeUpdater{} + options := Options{ + OriginPath: org, + UpdatedPath: src, + LocalPath: dst, + IsRoot: true, + } + + err := updater.Update(options) + assert.Error(t, err) + assert.Contains(t, err.Error(), "directory not empty") +} diff --git a/internal/util/update/update.go b/internal/util/update/update.go index 2d9b4eabe..524e1c127 100644 --- a/internal/util/update/update.go +++ b/internal/util/update/update.go @@ -90,6 +90,7 @@ var strategies = map[kptfilev1.UpdateStrategyType]func() Updater{ kptfilev1.FastForward: func() Updater { return FastForwardUpdater{} }, kptfilev1.ForceDeleteReplace: func() Updater { return ReplaceUpdater{} }, kptfilev1.ResourceMerge: func() Updater { return ResourceMergeUpdater{} }, + kptfilev1.CopyMerge: func() Updater { return CopyMergeUpdater{} }, } // Command updates the contents of a local package to a different version. From 2219c1b393d5dd84658301cf32c30c58e441219a Mon Sep 17 00:00:00 2001 From: liamfallon Date: Mon, 5 Jan 2026 14:49:49 +0000 Subject: [PATCH 3/5] Fix copyrights Signed-off-by: liamfallon --- internal/util/pkgutil/pkgutil.go | 2 +- internal/util/pkgutil/pkgutil_test.go | 2 +- internal/util/update/copy-merge.go | 2 +- internal/util/update/copy-merge_test.go | 2 +- internal/util/update/update.go | 2 +- pkg/api/kptfile/v1/types.go | 4 +-- pkg/api/kptfile/v1/validation.go | 2 +- pkg/api/kptfile/v1/validation_test.go | 2 +- pkg/api/kptfile/v1/zz_generated.deepcopy.go | 2 +- release/formula/main_test.go | 2 +- rollouts/hack/boilerplate.go.txt | 28 ++++++++++----------- 11 files changed, 24 insertions(+), 26 deletions(-) diff --git a/internal/util/pkgutil/pkgutil.go b/internal/util/pkgutil/pkgutil.go index f24eec32d..5be0c030d 100644 --- a/internal/util/pkgutil/pkgutil.go +++ b/internal/util/pkgutil/pkgutil.go @@ -1,4 +1,4 @@ -// Copyright 2020 The kpt Authors +// Copyright 2020,2026 The kpt Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/util/pkgutil/pkgutil_test.go b/internal/util/pkgutil/pkgutil_test.go index 59c817e01..09cfab733 100644 --- a/internal/util/pkgutil/pkgutil_test.go +++ b/internal/util/pkgutil/pkgutil_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 The kpt and Nephio Authors +// Copyright 2021,2026 The kpt Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/util/update/copy-merge.go b/internal/util/update/copy-merge.go index 681119add..d38dfc1ec 100644 --- a/internal/util/update/copy-merge.go +++ b/internal/util/update/copy-merge.go @@ -1,4 +1,4 @@ -// Copyright 2025 The Nephio Authors +// Copyright 2026 The kpt Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/util/update/copy-merge_test.go b/internal/util/update/copy-merge_test.go index 24def9f4a..06fd77725 100644 --- a/internal/util/update/copy-merge_test.go +++ b/internal/util/update/copy-merge_test.go @@ -1,4 +1,4 @@ -// Copyright 2025 The Nephio Authors +// Copyright 2026 The kpt Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/util/update/update.go b/internal/util/update/update.go index 524e1c127..240c1834f 100644 --- a/internal/util/update/update.go +++ b/internal/util/update/update.go @@ -1,4 +1,4 @@ -// Copyright 2019 The kpt Authors +// Copyright 2019,2026 The kpt Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pkg/api/kptfile/v1/types.go b/pkg/api/kptfile/v1/types.go index a8a9bd476..03df46819 100644 --- a/pkg/api/kptfile/v1/types.go +++ b/pkg/api/kptfile/v1/types.go @@ -1,4 +1,4 @@ -// Copyright 2021 The kpt Authors +// Copyright 2021,2026 The kpt Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import ( "sigs.k8s.io/kustomize/kyaml/yaml" ) -//go:generate go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.19.0 object:headerFile="../../../../../scripts/boilerplate.go.txt" +//go:generate go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.19.0 object:headerFile="../../../../rollouts/hack/boilerplate.go.txt" const ( KptFileName = "Kptfile" diff --git a/pkg/api/kptfile/v1/validation.go b/pkg/api/kptfile/v1/validation.go index 08c316da7..6818938ea 100644 --- a/pkg/api/kptfile/v1/validation.go +++ b/pkg/api/kptfile/v1/validation.go @@ -1,4 +1,4 @@ -// Copyright 2021 The kpt Authors +// Copyright 2021,2026 The kpt Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pkg/api/kptfile/v1/validation_test.go b/pkg/api/kptfile/v1/validation_test.go index 2c180a49a..08c5e23d1 100644 --- a/pkg/api/kptfile/v1/validation_test.go +++ b/pkg/api/kptfile/v1/validation_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 The kpt Authors +// Copyright 2021,2026 The kpt Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pkg/api/kptfile/v1/zz_generated.deepcopy.go b/pkg/api/kptfile/v1/zz_generated.deepcopy.go index 8818c129b..922a20507 100644 --- a/pkg/api/kptfile/v1/zz_generated.deepcopy.go +++ b/pkg/api/kptfile/v1/zz_generated.deepcopy.go @@ -1,6 +1,6 @@ //go:build !ignore_autogenerated -// Copyright 2023 The kpt Authors +// Copyright 2026 The kpt Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/release/formula/main_test.go b/release/formula/main_test.go index 965eee283..d357a8475 100644 --- a/release/formula/main_test.go +++ b/release/formula/main_test.go @@ -1,4 +1,4 @@ -// Copyright 2022 The kpt Authors +// Copyright 2022,2026 The kpt Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rollouts/hack/boilerplate.go.txt b/rollouts/hack/boilerplate.go.txt index 29c55ecda..4f9733b81 100644 --- a/rollouts/hack/boilerplate.go.txt +++ b/rollouts/hack/boilerplate.go.txt @@ -1,15 +1,13 @@ -/* -Copyright 2022. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ \ No newline at end of file +// Copyright 2026 The kpt Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. From 9dd00042a6ea32fbf91e0b1dd2b19d973e145a8d Mon Sep 17 00:00:00 2001 From: liamfallon Date: Mon, 5 Jan 2026 16:22:50 +0000 Subject: [PATCH 4/5] Fix lint errors Signed-off-by: liamfallon --- internal/util/pkgutil/pkgutil.go | 3 +- internal/util/pkgutil/pkgutil_test.go | 4 +-- internal/util/update/copy-merge_test.go | 39 +++++++++++-------------- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/internal/util/pkgutil/pkgutil.go b/internal/util/pkgutil/pkgutil.go index 5be0c030d..5daedf9ab 100644 --- a/internal/util/pkgutil/pkgutil.go +++ b/internal/util/pkgutil/pkgutil.go @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package pkgutil contains utility functions for packages package pkgutil import ( @@ -171,7 +172,7 @@ func CopyPackage(src, dst string, copyRootKptfile bool, matcher pkg.SubpackageMa // RemoveStaleItems removes files and directories from the dst package that were present in the org package, // but are not present in the src package. It does not remove the root Kptfile of the dst package. -func RemoveStaleItems(org, src, dst string, copyRootKptfile bool, matcher pkg.SubpackageMatcher) error { +func RemoveStaleItems(org, src, dst string, _ bool, _ pkg.SubpackageMatcher) error { var dirsToDelete []string walkErr := filepath.Walk(dst, func(path string, info os.FileInfo, err error) error { if err != nil { diff --git a/internal/util/pkgutil/pkgutil_test.go b/internal/util/pkgutil/pkgutil_test.go index 09cfab733..06d1dbaff 100644 --- a/internal/util/pkgutil/pkgutil_test.go +++ b/internal/util/pkgutil/pkgutil_test.go @@ -83,7 +83,7 @@ func TestWalkPackage(t *testing.T) { pkgPath := tc.pkg.ExpandPkg(t, testutil.EmptyReposInfo) var visited []string - if err := pkgutil.WalkPackage(pkgPath, func(s string, info os.FileInfo, err error) error { + if err := pkgutil.WalkPackage(pkgPath, func(s string, _ os.FileInfo, err error) error { if err != nil { return err } @@ -371,7 +371,7 @@ func TestCopyPackage(t *testing.T) { } var visited []string - if err = filepath.Walk(dest, func(s string, info os.FileInfo, err error) error { + if err = filepath.Walk(dest, func(s string, _ os.FileInfo, err error) error { if err != nil { return err } diff --git a/internal/util/update/copy-merge_test.go b/internal/util/update/copy-merge_test.go index 06fd77725..203342f34 100644 --- a/internal/util/update/copy-merge_test.go +++ b/internal/util/update/copy-merge_test.go @@ -21,7 +21,7 @@ import ( "github.com/kptdev/kpt/internal/testutil" "github.com/kptdev/kpt/internal/testutil/pkgbuilder" - . "github.com/kptdev/kpt/internal/util/update" + "github.com/kptdev/kpt/internal/util/update" "github.com/stretchr/testify/assert" ) @@ -213,16 +213,15 @@ func TestCopyMerge(t *testing.T) { for tn, tc := range testCases { t.Run(tn, func(t *testing.T) { - repos := testutil.EmptyReposInfo origin := tc.origin.ExpandPkg(t, repos) local := tc.local.ExpandPkg(t, repos) updated := tc.updated.ExpandPkg(t, repos) expected := tc.expected.ExpandPkg(t, repos) - updater := &CopyMergeUpdater{} + updater := &update.CopyMergeUpdater{} - err := updater.Update(Options{ + err := updater.Update(update.Options{ RelPackagePath: tc.relPackagePath, OriginPath: filepath.Join(origin, tc.relPackagePath), LocalPath: filepath.Join(local, tc.relPackagePath), @@ -234,7 +233,6 @@ func TestCopyMerge(t *testing.T) { } testutil.KptfileAwarePkgEqual(t, local, expected, false) - }) } } @@ -249,8 +247,8 @@ func TestCopyMergeError(t *testing.T) { } os.RemoveAll(src) - updater := &CopyMergeUpdater{} - options := Options{ + updater := &update.CopyMergeUpdater{} + options := update.Options{ UpdatedPath: src, LocalPath: dst, IsRoot: true, @@ -259,7 +257,6 @@ func TestCopyMergeError(t *testing.T) { err = updater.Update(options) assert.Error(t, err) assert.Contains(t, err.Error(), "no such file or directory") - } func TestCopyMergeErrorUpdatingKptfile(t *testing.T) { @@ -280,8 +277,8 @@ kind: malformedKptfile `), 0644) assert.NoError(t, err) - updater := &CopyMergeUpdater{} - options := Options{ + updater := &update.CopyMergeUpdater{} + options := update.Options{ UpdatedPath: src, LocalPath: dst, IsRoot: true, @@ -306,8 +303,8 @@ func TestCopyMergeErrorCopyingFile(t *testing.T) { t.Fatalf("Failed to create directory: %v", err) } - updater := &CopyMergeUpdater{} - options := Options{ + updater := &update.CopyMergeUpdater{} + options := update.Options{ UpdatedPath: src, LocalPath: dst, IsRoot: true, @@ -389,16 +386,15 @@ func TestCopyMergeDifferentMetadata(t *testing.T) { for tn, tc := range testCases { t.Run(tn, func(t *testing.T) { - repos := testutil.EmptyReposInfo - origin := tc.origin.ExpandPkg(t, repos) //metadata.name: "base" - local := tc.local.ExpandPkgWithName(t, "local", repos) //metadata.name: "local" - updated := tc.updated.ExpandPkgWithName(t, "updated", repos) //metadata.name: "updated" - expected := tc.expected.ExpandPkgWithName(t, "local", repos) //metadata.name: "local" I am expeting this field to not change + origin := tc.origin.ExpandPkg(t, repos) // metadata.name: "base" + local := tc.local.ExpandPkgWithName(t, "local", repos) // metadata.name: "local" + updated := tc.updated.ExpandPkgWithName(t, "updated", repos) // metadata.name: "updated" + expected := tc.expected.ExpandPkgWithName(t, "local", repos) // metadata.name: "local" I am expeting this field to not change - updater := &CopyMergeUpdater{} + updater := &update.CopyMergeUpdater{} - err := updater.Update(Options{ + err := updater.Update(update.Options{ RelPackagePath: tc.relPackagePath, OriginPath: filepath.Join(origin, tc.relPackagePath), LocalPath: filepath.Join(local, tc.relPackagePath), @@ -410,7 +406,6 @@ func TestCopyMergeDifferentMetadata(t *testing.T) { } testutil.KptfileAwarePkgEqual(t, local, expected, false) - }) } } @@ -432,8 +427,8 @@ func TestCopyMergeErrorRemovingFile(t *testing.T) { assert.NoError(t, os.Mkdir(filePathDst, 0755)) assert.NoError(t, os.WriteFile(filepath.Join(filePathDst, "dummy"), []byte("x"), 0644)) - updater := &CopyMergeUpdater{} - options := Options{ + updater := &update.CopyMergeUpdater{} + options := update.Options{ OriginPath: org, UpdatedPath: src, LocalPath: dst, From 4cf169811757dd50917adf6c796fad27fef836d6 Mon Sep 17 00:00:00 2001 From: liamfallon Date: Tue, 6 Jan 2026 16:28:28 +0000 Subject: [PATCH 5/5] Use template YEAR in boilerplate Signed-off-by: liamfallon --- internal/util/update/copy-merge.go | 2 +- rollouts/hack/boilerplate.go.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/util/update/copy-merge.go b/internal/util/update/copy-merge.go index d38dfc1ec..350ac04f9 100644 --- a/internal/util/update/copy-merge.go +++ b/internal/util/update/copy-merge.go @@ -17,9 +17,9 @@ package update import ( "github.com/kptdev/kpt/internal/errors" "github.com/kptdev/kpt/internal/pkg" - "github.com/kptdev/kpt/internal/types" "github.com/kptdev/kpt/internal/util/pkgutil" "github.com/kptdev/kpt/pkg/kptfile/kptfileutil" + "github.com/kptdev/kpt/pkg/lib/types" ) // CopyMergeUpdater is responsible for synchronizing the destination package diff --git a/rollouts/hack/boilerplate.go.txt b/rollouts/hack/boilerplate.go.txt index 4f9733b81..ebc8a1d57 100644 --- a/rollouts/hack/boilerplate.go.txt +++ b/rollouts/hack/boilerplate.go.txt @@ -1,4 +1,4 @@ -// Copyright 2026 The kpt Authors +// Copyright YEAR The kpt Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License.