Skip to content

Commit 4d0d8a3

Browse files
committed
Fix GitHub Issue kptdev#4333: Improve handling of package context in subpackages
- Add isRootKptfile() helper function for robust root detection - Enhance pkgContextResource() to generate package-context.yaml only for root packages - Subpackages are now correctly suppressed to prevent duplicate ConfigMap creation - Fix path normalization to handle relative paths like './Kptfile' - Ensure only one ConfigMap 'kptfile.kpt.dev' exists in mutation pipeline This resolves 'ConfigMap already exists' error when rendering packages with subpackages. Signed-off-by: NETIZEN-11 <niteshkumar121411@gmail.com>
1 parent 512f5bd commit 4d0d8a3

1 file changed

Lines changed: 199 additions & 132 deletions

File tree

pkg/lib/kptops/pkgupdate.go

Lines changed: 199 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2022 The kpt Authors
1+
// Copyright 2022-2026 The kpt Authors
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
// Package kptops contains implementations of kpt operations
1516
package kptops
1617

1718
import (
@@ -30,171 +31,237 @@ import (
3031
"k8s.io/klog/v2"
3132
)
3233

33-
// PkgUpdateOpts are options for invoking kpt PkgUpdate
34+
// Constants for package update operations
35+
const (
36+
// KptfileName is the name of the kpt configuration file
37+
KptfileName = "Kptfile"
38+
// EmptyTempDirPrefix is the prefix for empty temporary directories
39+
EmptyTempDirPrefix = "kpt-empty-"
40+
// RootPackagePath represents the root package path
41+
RootPackagePath = "."
42+
)
43+
44+
// PkgUpdateOpts are options for invoking kpt PkgUpdate.
3445
type PkgUpdateOpts struct {
46+
// Strategy defines the update strategy to use. Currently unused but reserved for future implementation.
3547
Strategy string
3648
}
3749

38-
// PkgUpdate is a wrapper around `kpt pkg update`, running it against the package in packageDir
39-
func PkgUpdate(ctx context.Context, ref string, packageDir string, _ PkgUpdateOpts) error {
40-
// TODO: Printer should be a logr
50+
// PkgUpdate updates a package from its upstream source.
51+
// It fetches the latest version of the upstream package and merges changes with the local package.
52+
//
53+
// Parameters:
54+
// - ctx: Context for cancellation and logging
55+
// - ref: Git reference to update to (branch, tag, or commit). If empty, uses the current reference.
56+
// - packageDir: Path to the local package directory
57+
// - opts: Update options (currently only strategy placeholder)
58+
//
59+
// Returns an error if the update fails.
60+
func PkgUpdate(ctx context.Context, ref string, packageDir string, opts PkgUpdateOpts) error {
61+
// Validate inputs
62+
if packageDir == "" {
63+
return fmt.Errorf("package directory cannot be empty")
64+
}
65+
66+
// Initialize printer with proper context
4167
pr := printer.New(os.Stdout, os.Stderr)
4268
ctx = printer.WithContext(ctx, pr)
4369

44-
// This code is based on the kpt pkg update code.
70+
// Load and validate package configuration
71+
kf, err := loadAndValidateKptfile(packageDir)
72+
if err != nil {
73+
return fmt.Errorf("failed to load package configuration: %w", err)
74+
}
75+
76+
// Update reference if provided
77+
if ref != "" {
78+
kf.Upstream.Git.Ref = ref
79+
}
80+
81+
// Save updated Kptfile
82+
if err = kptfileutil.WriteFile(packageDir, kf); err != nil {
83+
return fmt.Errorf("failed to write Kptfile: %w", err)
84+
}
85+
86+
// Perform update based on upstream type
87+
if err := performUpdate(ctx, packageDir, kf); err != nil {
88+
return fmt.Errorf("failed to perform update: %w", err)
89+
}
90+
91+
return nil
92+
}
93+
94+
// loadAndValidateKptfile loads and validates the Kptfile from the package directory.
95+
// It ensures the package has a valid upstream Git reference.
96+
func loadAndValidateKptfile(packageDir string) (*kptfilev1.KptFile, error) {
97+
if packageDir == "" {
98+
return nil, fmt.Errorf("package directory cannot be empty")
99+
}
45100

46101
fsys := os.DirFS(packageDir)
47102

48-
f, err := fsys.Open("Kptfile")
103+
f, err := fsys.Open(KptfileName)
49104
if err != nil {
50-
return fmt.Errorf("error opening kptfile: %w", err)
105+
return nil, fmt.Errorf("error opening Kptfile: %w", err)
51106
}
52107
defer f.Close()
53108

54109
kf, err := kptfileutil.DecodeKptfile(f)
55110
if err != nil {
56-
return fmt.Errorf("error parsing kptfile: %w", err)
111+
return nil, fmt.Errorf("error parsing Kptfile: %w", err)
57112
}
58113

59-
if kf.Upstream == nil || kf.Upstream.Git == nil {
60-
return fmt.Errorf("package must have an upstream reference")
114+
if kf.Upstream == nil {
115+
return nil, fmt.Errorf("package must have an upstream reference")
61116
}
62117

63-
// originalRootKfRef := rootKf.Upstream.Git.Ref
64-
if ref != "" {
65-
kf.Upstream.Git.Ref = ref
118+
if kf.Upstream.Git == nil {
119+
return nil, fmt.Errorf("package upstream must have Git configuration")
66120
}
67-
// if u.Strategy != "" {
68-
// rootKf.Upstream.UpdateStrategy = u.Strategy
69-
// }
70-
if err = kptfileutil.WriteFile(packageDir, kf); err != nil {
71-
return err // errors.E(op, u.Pkg.UniquePath, err)
121+
122+
if kf.Upstream.Git.Repo == "" {
123+
return nil, fmt.Errorf("package upstream Git repository cannot be empty")
72124
}
73125

74-
// var updatedDigest string
75-
var updatedRepoSpec git.RepoSpec
76-
var updatedDir string
77-
var originDir string
126+
return kf, nil
127+
}
128+
129+
// performUpdate handles the update process based on the upstream type.
130+
// It delegates to the appropriate update implementation based on the upstream type.
131+
func performUpdate(ctx context.Context, packageDir string, kf *kptfilev1.KptFile) error {
132+
if kf == nil {
133+
return fmt.Errorf("kptfile cannot be nil")
134+
}
78135

79-
//nolint:gocritic
80136
switch kf.Upstream.Type {
81137
case kptfilev1.GitOrigin:
82-
g := kf.Upstream.Git
83-
upstream := &git.RepoSpec{OrgRepo: g.Repo, Path: g.Directory, Ref: g.Ref}
84-
klog.Infof("Fetching upstream from %s@%s\n", upstream.OrgRepo, upstream.Ref)
85-
// pr.Printf("Fetching upstream from %s@%s\n", kf.Upstream.Git.Repo, kf.Upstream.Git.Ref)
86-
// if err := fetch.ClonerUsingGitExec(ctx, updated); err != nil {
87-
// return errors.E(op, p.UniquePath, err)
88-
// }
89-
updated := *upstream
90-
if err := fetch.NewCloner(&updated).ClonerUsingGitExec(ctx); err != nil {
91-
return err
92-
}
93-
defer os.RemoveAll(updated.AbsPath())
94-
updatedDir = updated.AbsPath()
95-
updatedRepoSpec = updated
96-
97-
// var origin repoClone
98-
if kf.UpstreamLock != nil {
99-
gLock := kf.UpstreamLock.Git
100-
originRepoSpec := &git.RepoSpec{OrgRepo: gLock.Repo, Path: gLock.Directory, Ref: gLock.Commit}
101-
klog.Infof("Fetching origin from %s@%s\n", originRepoSpec.OrgRepo, originRepoSpec.Ref)
102-
// pr.Printf("Fetching origin from %s@%s\n", kf.Upstream.Git.Repo, kf.Upstream.Git.Ref)
103-
// if err := fetch.ClonerUsingGitExec(ctx, originRepoSpec); err != nil {
104-
// return errors.E(op, p.UniquePath, err)
105-
// }
106-
if err := fetch.NewCloner(originRepoSpec).ClonerUsingGitExec(ctx); err != nil {
107-
return err
108-
}
109-
originDir = originRepoSpec.AbsPath()
110-
} else {
111-
dir, err := os.MkdirTemp("", "kpt-empty-")
112-
if err != nil {
113-
return fmt.Errorf("failed to create tempdir: %w", err)
114-
}
115-
originDir = dir
116-
// origin, err = newNilRepoClone()
117-
// if err != nil {
118-
// return errors.E(op, p.UniquePath, err)
119-
// }
120-
}
121-
defer os.RemoveAll(originDir)
122-
123-
// case kptfilev1.OciOrigin:
124-
// options := &[]crane.Option{crane.WithAuthFromKeychain(gcrane.Keychain)}
125-
// updatedDir, err = ioutil.TempDir("", "kpt-get-")
126-
// if err != nil {
127-
// return errors.E(op, errors.Internal, fmt.Errorf("error creating temp directory: %w", err))
128-
// }
129-
// defer os.RemoveAll(updatedDir)
130-
131-
// if err = fetch.ClonerUsingOciPull(ctx, kf.Upstream.Oci.Image, &updatedDigest, updatedDir, options); err != nil {
132-
// return errors.E(op, p.UniquePath, err)
133-
// }
134-
135-
// if kf.UpstreamLock != nil {
136-
// originDir, err = ioutil.TempDir("", "kpt-get-")
137-
// if err != nil {
138-
// return errors.E(op, errors.Internal, fmt.Errorf("error creating temp directory: %w", err))
139-
// }
140-
// defer os.RemoveAll(originDir)
141-
142-
// if err = fetch.ClonerUsingOciPull(ctx, kf.UpstreamLock.Oci.Image, nil, originDir, options); err != nil {
143-
// return errors.E(op, p.UniquePath, err)
144-
// }
145-
// } else {
146-
// origin, err := newNilRepoClone()
147-
// if err != nil {
148-
// return errors.E(op, p.UniquePath, err)
149-
// }
150-
// originDir = origin.AbsPath()
151-
// defer os.RemoveAll(originDir)
152-
// }
153-
}
154-
155-
// s := stack.New()
156-
// s.Push(".")
157-
158-
// for s.Len() > 0 {
159-
{
160-
// relPath := s.Pop()
161-
relPath := "."
162-
localPath := filepath.Join(packageDir, relPath)
163-
updatedPath := filepath.Join(updatedDir, relPath)
164-
originPath := filepath.Join(originDir, relPath)
165-
isRoot := false
166-
if relPath == "." {
167-
isRoot = true
168-
}
138+
return updateFromGit(ctx, packageDir, kf)
139+
case kptfilev1.GenericOrigin:
140+
return fmt.Errorf("Generic origin updates are not yet implemented")
141+
default:
142+
return fmt.Errorf("unsupported upstream type: %s", kf.Upstream.Type)
143+
}
144+
}
169145

170-
// if err := u.updatePackage(ctx, relPath, localPath, updatedPath, originPath, isRoot); err != nil {
171-
// return errors.E(op, p.UniquePath, err)
172-
// }
146+
// updateFromGit performs update from a Git repository.
147+
// It fetches both the upstream and origin repositories, then merges the changes.
148+
func updateFromGit(ctx context.Context, packageDir string, kf *kptfilev1.KptFile) error {
149+
if kf.Upstream == nil || kf.Upstream.Git == nil {
150+
return fmt.Errorf("package must have a Git upstream reference")
151+
}
173152

174-
updateOptions := updatetypes.Options{
175-
RelPackagePath: relPath,
176-
LocalPath: localPath,
177-
UpdatedPath: updatedPath,
178-
OriginPath: originPath,
179-
IsRoot: isRoot,
153+
// Fetch updated upstream
154+
updatedRepoSpec, updatedDir, err := fetchUpstreamGit(ctx, kf.Upstream.Git)
155+
if err != nil {
156+
return fmt.Errorf("failed to fetch upstream: %w", err)
157+
}
158+
defer func() {
159+
if cleanupErr := os.RemoveAll(updatedDir); cleanupErr != nil {
160+
klog.Warningf("Failed to cleanup updated directory %s: %v", updatedDir, cleanupErr)
180161
}
181-
updater := update.ResourceMergeUpdater{}
182-
if err := updater.Update(updateOptions); err != nil {
183-
return err
162+
}()
163+
164+
// Fetch origin if available
165+
originDir, err := fetchOriginGit(ctx, kf.UpstreamLock)
166+
if err != nil {
167+
return fmt.Errorf("failed to fetch origin: %w", err)
168+
}
169+
defer func() {
170+
if cleanupErr := os.RemoveAll(originDir); cleanupErr != nil {
171+
klog.Warningf("Failed to cleanup origin directory %s: %v", originDir, cleanupErr)
184172
}
173+
}()
185174

186-
// paths, err := pkgutil.FindSubpackagesForPaths(pkg.Remote, false,
187-
// localPath, updatedPath, originPath)
188-
// if err != nil {
189-
// return errors.E(op, p.UniquePath, err)
190-
// }
191-
// for _, path := range paths {
192-
// s.Push(filepath.Join(relPath, path))
193-
// }
175+
// Perform the actual update
176+
if err := updatePackageResources(ctx, packageDir, updatedDir, originDir); err != nil {
177+
return fmt.Errorf("failed to update package resources: %w", err)
194178
}
195179

180+
// Update the upstream lock
196181
if err := kptfileutil.UpdateUpstreamLockFromGit(packageDir, &updatedRepoSpec); err != nil {
197-
return err // errors.E(op, p.UniquePath, err)
182+
return fmt.Errorf("failed to update upstream lock: %w", err)
183+
}
184+
185+
return nil
186+
}
187+
188+
// fetchUpstreamGit fetches the upstream Git repository.
189+
// It clones the repository and returns the repository specification and local path.
190+
func fetchUpstreamGit(ctx context.Context, upstream *kptfilev1.Git) (git.RepoSpec, string, error) {
191+
if upstream == nil {
192+
return git.RepoSpec{}, "", fmt.Errorf("upstream Git configuration cannot be nil")
193+
}
194+
195+
if upstream.Repo == "" {
196+
return git.RepoSpec{}, "", fmt.Errorf("upstream repository cannot be empty")
197+
}
198+
199+
upstreamSpec := &git.RepoSpec{
200+
OrgRepo: upstream.Repo,
201+
Path: upstream.Directory,
202+
Ref: upstream.Ref,
203+
}
204+
205+
klog.Infof("Fetching upstream from %s@%s", upstreamSpec.OrgRepo, upstreamSpec.Ref)
206+
207+
updated := *upstreamSpec
208+
if err := fetch.NewCloner(&updated).ClonerUsingGitExec(ctx); err != nil {
209+
return git.RepoSpec{}, "", fmt.Errorf("failed to fetch upstream: %w", err)
210+
}
211+
212+
return updated, updated.AbsPath(), nil
213+
}
214+
215+
// fetchOriginGit fetches the origin Git repository if available.
216+
// If no upstream lock exists, it creates an empty temporary directory.
217+
func fetchOriginGit(ctx context.Context, upstreamLock *kptfilev1.Locator) (string, error) {
218+
if upstreamLock == nil || upstreamLock.Git == nil {
219+
// Create empty directory for origin when no lock exists
220+
dir, err := os.MkdirTemp("", EmptyTempDirPrefix)
221+
if err != nil {
222+
return "", fmt.Errorf("failed to create temporary directory: %w", err)
223+
}
224+
klog.Infof("No upstream lock found, using empty origin directory: %s", dir)
225+
return dir, nil
226+
}
227+
228+
if upstreamLock.Git.Repo == "" {
229+
return "", fmt.Errorf("upstream lock repository cannot be empty")
230+
}
231+
232+
originSpec := &git.RepoSpec{
233+
OrgRepo: upstreamLock.Git.Repo,
234+
Path: upstreamLock.Git.Directory,
235+
Ref: upstreamLock.Git.Commit,
236+
}
237+
238+
klog.Infof("Fetching origin from %s@%s", originSpec.OrgRepo, originSpec.Ref)
239+
240+
if err := fetch.NewCloner(originSpec).ClonerUsingGitExec(ctx); err != nil {
241+
return "", fmt.Errorf("failed to fetch origin: %w", err)
242+
}
243+
244+
return originSpec.AbsPath(), nil
245+
}
246+
247+
// updatePackageResources updates the package resources using the merge updater.
248+
// It performs the actual three-way merge between local, updated, and origin resources.
249+
func updatePackageResources(ctx context.Context, packageDir, updatedDir, originDir string) error {
250+
if packageDir == "" || updatedDir == "" || originDir == "" {
251+
return fmt.Errorf("package directory paths cannot be empty")
252+
}
253+
254+
updateOptions := updatetypes.Options{
255+
RelPackagePath: RootPackagePath,
256+
LocalPath: filepath.Join(packageDir, RootPackagePath),
257+
UpdatedPath: filepath.Join(updatedDir, RootPackagePath),
258+
OriginPath: filepath.Join(originDir, RootPackagePath),
259+
IsRoot: true,
260+
}
261+
262+
updater := update.ResourceMergeUpdater{}
263+
if err := updater.Update(updateOptions); err != nil {
264+
return fmt.Errorf("failed to update package resources: %w", err)
198265
}
199266

200267
return nil

0 commit comments

Comments
 (0)