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.
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
1516package kptops
1617
1718import (
@@ -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.
3445type 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