github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/internal/util/update/update.go (about) 1 // Copyright 2019 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package update contains libraries for updating packages. 16 package update 17 18 import ( 19 "context" 20 "fmt" 21 "os" 22 "path" 23 "path/filepath" 24 "strings" 25 26 "github.com/GoogleContainerTools/kpt/internal/errors" 27 "github.com/GoogleContainerTools/kpt/internal/gitutil" 28 "github.com/GoogleContainerTools/kpt/internal/pkg" 29 "github.com/GoogleContainerTools/kpt/internal/printer" 30 "github.com/GoogleContainerTools/kpt/internal/types" 31 "github.com/GoogleContainerTools/kpt/internal/util/addmergecomment" 32 "github.com/GoogleContainerTools/kpt/internal/util/fetch" 33 "github.com/GoogleContainerTools/kpt/internal/util/git" 34 "github.com/GoogleContainerTools/kpt/internal/util/pkgutil" 35 "github.com/GoogleContainerTools/kpt/internal/util/stack" 36 kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" 37 "github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil" 38 "sigs.k8s.io/kustomize/kyaml/copyutil" 39 "sigs.k8s.io/kustomize/kyaml/filesys" 40 ) 41 42 // PkgNotGitRepoError is the error type returned if the package being updated is not inside 43 // a git repository. 44 type PkgNotGitRepoError struct { 45 Path types.UniquePath 46 } 47 48 func (p *PkgNotGitRepoError) Error() string { 49 return fmt.Sprintf("package %q is not a git repository", p.Path.String()) 50 } 51 52 // PkgRepoDirtyError is the error type returned if the package being updated contains 53 // uncommitted changes. 54 type PkgRepoDirtyError struct { 55 Path types.UniquePath 56 } 57 58 func (p *PkgRepoDirtyError) Error() string { 59 return fmt.Sprintf("package %q contains uncommitted changes", p.Path.String()) 60 } 61 62 type Options struct { 63 // RelPackagePath is the relative path of a subpackage to the root. If the 64 // package is root, the value here will be ".". 65 RelPackagePath string 66 67 // LocalPath is the absolute path to the package on the local fork. 68 LocalPath string 69 70 // OriginPath is the absolute path to the package in the on-disk clone 71 // of the origin ref of the repo. 72 OriginPath string 73 74 // UpdatedPath is the absolute path to the package in the on-disk clone 75 // of the updated ref of the repo. 76 UpdatedPath string 77 78 // IsRoot is true if the package is the root, i.e. the clones of 79 // updated and origin were fetched based on the information in the 80 // Kptfile from this package. 81 IsRoot bool 82 } 83 84 // Updater updates a local package 85 type Updater interface { 86 Update(options Options) error 87 } 88 89 var strategies = map[kptfilev1.UpdateStrategyType]func() Updater{ 90 kptfilev1.FastForward: func() Updater { return FastForwardUpdater{} }, 91 kptfilev1.ForceDeleteReplace: func() Updater { return ReplaceUpdater{} }, 92 kptfilev1.ResourceMerge: func() Updater { return ResourceMergeUpdater{} }, 93 } 94 95 // Command updates the contents of a local package to a different version. 96 type Command struct { 97 // Pkg captures information about the package that should be updated. 98 Pkg *pkg.Pkg 99 100 // Ref is the ref to update to 101 Ref string 102 103 // Strategy is the update strategy to use 104 Strategy kptfilev1.UpdateStrategyType 105 106 // cachedUpstreamRepos is an upstream repo already fetched for a given repoSpec CloneRef 107 cachedUpstreamRepos map[string]*gitutil.GitUpstreamRepo 108 } 109 110 // Run runs the Command. 111 func (u *Command) Run(ctx context.Context) error { 112 const op errors.Op = "update.Run" 113 pr := printer.FromContextOrDie(ctx) 114 115 if u.Pkg == nil { 116 return errors.E(op, errors.MissingParam, "pkg must be provided") 117 } 118 119 rootKf, err := u.Pkg.Kptfile() 120 if err != nil { 121 return errors.E(op, u.Pkg.UniquePath, err) 122 } 123 124 if rootKf.Upstream == nil || rootKf.Upstream.Git == nil { 125 return errors.E(op, u.Pkg.UniquePath, 126 fmt.Errorf("package must have an upstream reference")) 127 } 128 originalRootKfRef := rootKf.Upstream.Git.Ref 129 if u.Ref != "" { 130 rootKf.Upstream.Git.Ref = u.Ref 131 } 132 if u.Strategy != "" { 133 rootKf.Upstream.UpdateStrategy = u.Strategy 134 } 135 err = kptfileutil.WriteFile(u.Pkg.UniquePath.String(), rootKf) 136 if err != nil { 137 return errors.E(op, u.Pkg.UniquePath, err) 138 } 139 if u.cachedUpstreamRepos == nil { 140 u.cachedUpstreamRepos = make(map[string]*gitutil.GitUpstreamRepo) 141 } 142 packageCount := 0 143 144 // Use stack to keep track of paths with a Kptfile that might contain 145 // information about remote subpackages. 146 s := stack.NewPkgStack() 147 s.Push(u.Pkg) 148 149 for s.Len() > 0 { 150 p := s.Pop() 151 packageCount++ 152 153 if err := u.updateRootPackage(ctx, p); err != nil { 154 return errors.E(op, p.UniquePath, err) 155 } 156 157 subPkgs, err := p.DirectSubpackages() 158 if err != nil { 159 return errors.E(op, p.UniquePath, err) 160 } 161 for _, subPkg := range subPkgs { 162 subKf, err := subPkg.Kptfile() 163 if err != nil { 164 return errors.E(op, p.UniquePath, err) 165 } 166 167 if subKf.Upstream != nil && subKf.Upstream.Git != nil { 168 // update subpackage kf ref/strategy if current pkg is a subpkg of root pkg or is root pkg 169 // and if original root pkg ref matches the subpkg ref 170 if shouldUpdateSubPkgRef(subKf, rootKf, originalRootKfRef) { 171 updateSubKf(subKf, u.Ref, u.Strategy) 172 err = kptfileutil.WriteFile(subPkg.UniquePath.String(), subKf) 173 if err != nil { 174 return errors.E(op, subPkg.UniquePath, err) 175 } 176 } 177 s.Push(subPkg) 178 } 179 } 180 } 181 pr.Printf("\nUpdated %d package(s).\n", packageCount) 182 183 // finally, make sure that the merge comments are added to all resources in the updated package 184 if err := addmergecomment.Process(string(u.Pkg.UniquePath)); err != nil { 185 return errors.E(op, u.Pkg.UniquePath, err) 186 } 187 return nil 188 } 189 190 // GetCachedUpstreamRepos returns repos cached during update 191 func (u Command) GetCachedUpstreamRepos() map[string]*gitutil.GitUpstreamRepo { 192 return u.cachedUpstreamRepos 193 } 194 195 // updateSubKf updates subpackage with given ref and update strategy 196 func updateSubKf(subKf *kptfilev1.KptFile, ref string, strategy kptfilev1.UpdateStrategyType) { 197 // check if explicit ref provided 198 if ref != "" { 199 subKf.Upstream.Git.Ref = ref 200 } 201 if strategy != "" { 202 subKf.Upstream.UpdateStrategy = strategy 203 } 204 } 205 206 // shouldUpdateSubPkgRef checks if subpkg ref should be updated. 207 // This is true if pkg has the same upstream repo, upstream directory is within or equal to root pkg directory and original root pkg ref matches the subpkg ref. 208 func shouldUpdateSubPkgRef(subKf, rootKf *kptfilev1.KptFile, originalRootKfRef string) bool { 209 return subKf.Upstream.Git.Repo == rootKf.Upstream.Git.Repo && 210 subKf.Upstream.Git.Ref == originalRootKfRef && 211 strings.HasPrefix(path.Clean(subKf.Upstream.Git.Directory), path.Clean(rootKf.Upstream.Git.Directory)) 212 } 213 214 // repoClone is an interface that represents a clone of a repo on the local 215 // disk. 216 type repoClone interface { 217 AbsPath() string 218 } 219 220 // newNilRepoClone creates a new nilRepoClone that implements the repoClone 221 // interface 222 func newNilRepoClone() (*nilRepoClone, error) { 223 const op errors.Op = "update.newNilRepoClone" 224 dir, err := os.MkdirTemp("", "kpt-empty-") 225 if err != nil { 226 return nil, errors.E(op, errors.IO, fmt.Errorf("errors creating a temporary directory: %w", err)) 227 } 228 return &nilRepoClone{ 229 dir: dir, 230 }, nil 231 } 232 233 // nilRepoClone is an implementation of the repoClone interface, but that 234 // just represents an empty directory. This simplifies the logic for update 235 // since we don't have to special case situations where we don't have 236 // upstream and/or origin. 237 type nilRepoClone struct { 238 dir string 239 } 240 241 // AbsPath returns the absolute path to the local directory for the repo. For 242 // the nilRepoClone, this will always be an empty directory. 243 func (nrc *nilRepoClone) AbsPath() string { 244 return nrc.dir 245 } 246 247 // updateRootPackage updates a local package. It will use the information 248 // about upstream in the Kptfile to fetch upstream and origin, and then 249 // recursively traverse the hierarchy to add/update/delete packages. 250 func (u Command) updateRootPackage(ctx context.Context, p *pkg.Pkg) error { 251 const op errors.Op = "update.updateRootPackage" 252 kf, err := p.Kptfile() 253 if err != nil { 254 return errors.E(op, p.UniquePath, err) 255 } 256 257 pr := printer.FromContextOrDie(ctx) 258 pr.PrintPackage(p, !(p == u.Pkg)) 259 260 g := kf.Upstream.Git 261 updated := &git.RepoSpec{OrgRepo: g.Repo, Path: g.Directory, Ref: g.Ref} 262 pr.Printf("Fetching upstream from %s@%s\n", kf.Upstream.Git.Repo, kf.Upstream.Git.Ref) 263 cloner := fetch.NewCloner(updated, fetch.WithCachedRepo(u.cachedUpstreamRepos)) 264 if err := cloner.ClonerUsingGitExec(ctx); err != nil { 265 return errors.E(op, p.UniquePath, err) 266 } 267 defer os.RemoveAll(updated.AbsPath()) 268 269 var origin repoClone 270 if kf.UpstreamLock != nil { 271 gLock := kf.UpstreamLock.Git 272 originRepoSpec := &git.RepoSpec{OrgRepo: gLock.Repo, Path: gLock.Directory, Ref: gLock.Commit} 273 pr.Printf("Fetching origin from %s@%s\n", kf.Upstream.Git.Repo, kf.Upstream.Git.Ref) 274 if err := fetch.NewCloner(originRepoSpec, fetch.WithCachedRepo(u.cachedUpstreamRepos)).ClonerUsingGitExec(ctx); err != nil { 275 return errors.E(op, p.UniquePath, err) 276 } 277 origin = originRepoSpec 278 } else { 279 origin, err = newNilRepoClone() 280 if err != nil { 281 return errors.E(op, p.UniquePath, err) 282 } 283 } 284 defer os.RemoveAll(origin.AbsPath()) 285 286 s := stack.New() 287 s.Push(".") 288 289 for s.Len() > 0 { 290 relPath := s.Pop() 291 localPath := filepath.Join(p.UniquePath.String(), relPath) 292 updatedPath := filepath.Join(updated.AbsPath(), relPath) 293 originPath := filepath.Join(origin.AbsPath(), relPath) 294 295 isRoot := false 296 if relPath == "." { 297 isRoot = true 298 } 299 300 if err := u.updatePackage(ctx, relPath, localPath, updatedPath, originPath, isRoot); err != nil { 301 return errors.E(op, p.UniquePath, err) 302 } 303 304 paths, err := pkgutil.FindSubpackagesForPaths(pkg.Remote, false, 305 localPath, updatedPath, originPath) 306 if err != nil { 307 return errors.E(op, p.UniquePath, err) 308 } 309 for _, path := range paths { 310 s.Push(filepath.Join(relPath, path)) 311 } 312 } 313 314 if err := kptfileutil.UpdateUpstreamLockFromGit(p.UniquePath.String(), updated); err != nil { 315 return errors.E(op, p.UniquePath, err) 316 } 317 return nil 318 } 319 320 // updatePackage takes care of updating a single package. The absolute paths to 321 // the local, updated and origin packages are provided, as well as the path to the 322 // package relative to the root. 323 // The last parameter tells if this package is the root, i.e. the package 324 // from which we got the information about upstream and origin. 325 // 326 //nolint:gocyclo 327 func (u Command) updatePackage(ctx context.Context, subPkgPath, localPath, updatedPath, originPath string, isRootPkg bool) error { 328 const op errors.Op = "update.updatePackage" 329 pr := printer.FromContextOrDie(ctx) 330 331 localExists, err := pkg.IsPackageDir(filesys.FileSystemOrOnDisk{}, localPath) 332 if err != nil { 333 return errors.E(op, types.UniquePath(localPath), err) 334 } 335 336 // We need to handle the root package special here, since the copies 337 // from updated and origin might not have a Kptfile at the root. 338 updatedExists := isRootPkg 339 if !isRootPkg { 340 updatedExists, err = pkg.IsPackageDir(filesys.FileSystemOrOnDisk{}, updatedPath) 341 if err != nil { 342 return errors.E(op, types.UniquePath(localPath), err) 343 } 344 } 345 346 originExists := isRootPkg 347 if !isRootPkg { 348 originExists, err = pkg.IsPackageDir(filesys.FileSystemOrOnDisk{}, originPath) 349 if err != nil { 350 return errors.E(op, types.UniquePath(localPath), err) 351 } 352 } 353 354 switch { 355 case !originExists && !localExists && !updatedExists: 356 break 357 // Check if subpackage has been added both in upstream and in local. We 358 // can't make a sane merge here, so we treat it as an error. 359 case !originExists && localExists && updatedExists: 360 pr.Printf("Package %q added in both local and upstream.\n", packageName(localPath)) 361 return errors.E(op, types.UniquePath(localPath), 362 fmt.Errorf("subpackage %q added in both upstream and local", subPkgPath)) 363 364 // Package added in upstream. We just copy the package. If the package 365 // contains any unfetched subpackages, those will be handled when we traverse 366 // the package hierarchy and that package is the root. 367 case !originExists && !localExists && updatedExists: 368 pr.Printf("Adding package %q from upstream.\n", packageName(localPath)) 369 if err := pkgutil.CopyPackage(updatedPath, localPath, !isRootPkg, pkg.None); err != nil { 370 return errors.E(op, types.UniquePath(localPath), err) 371 } 372 373 // Package added locally, so no action needed. 374 case !originExists && localExists && !updatedExists: 375 break 376 377 // Package deleted from both upstream and local, so no action needed. 378 case originExists && !localExists && !updatedExists: 379 break 380 381 // Package deleted from local 382 // In this case we assume the user knows what they are doing, so 383 // we don't re-add the updated package from upstream. 384 case originExists && !localExists && updatedExists: 385 pr.Printf("Ignoring package %q in upstream since it is deleted from local.\n", packageName(localPath)) 386 387 // Package deleted from upstream 388 case originExists && localExists && !updatedExists: 389 // Check the diff. If there are local changes, we keep the subpackage. 390 diff, err := copyutil.Diff(originPath, localPath) 391 if err != nil { 392 return errors.E(op, types.UniquePath(localPath), err) 393 } 394 if diff.Len() == 0 { 395 pr.Printf("Deleting package %q from local since it is removed in upstream.\n", packageName(localPath)) 396 if err := os.RemoveAll(localPath); err != nil { 397 return errors.E(op, types.UniquePath(localPath), err) 398 } 399 } else { 400 pr.Printf("Package %q deleted from upstream, but keeping local since it has changes.\n", packageName(localPath)) 401 } 402 default: 403 if err := u.mergePackage(ctx, localPath, updatedPath, originPath, subPkgPath, isRootPkg); err != nil { 404 return errors.E(op, types.UniquePath(localPath), err) 405 } 406 } 407 return nil 408 } 409 410 func (u Command) mergePackage(ctx context.Context, localPath, updatedPath, originPath, relPath string, isRootPkg bool) error { 411 const op errors.Op = "update.mergePackage" 412 pr := printer.FromContextOrDie(ctx) 413 // at this point, the localPath, updatedPath and originPath exists and are about to be merged 414 // make sure that the merge comments are added to all of them so that they are merged accurately 415 if err := addmergecomment.Process(localPath, updatedPath, originPath); err != nil { 416 return errors.E(op, types.UniquePath(localPath), 417 fmt.Errorf("failed to add merge comments %q", err.Error())) 418 } 419 updatedUnfetched, err := pkg.IsPackageUnfetched(updatedPath) 420 if err != nil { 421 if !errors.Is(err, os.ErrNotExist) || !isRootPkg { 422 return errors.E(op, types.UniquePath(localPath), err) 423 } 424 // For root packages, there might not be a Kptfile in the upstream repo. 425 updatedUnfetched = false 426 } 427 428 originUnfetched, err := pkg.IsPackageUnfetched(originPath) 429 if err != nil { 430 if !errors.Is(err, os.ErrNotExist) || !isRootPkg { 431 return errors.E(op, types.UniquePath(localPath), err) 432 } 433 // For root packages, there might not be a Kptfile in origin. 434 originUnfetched = false 435 } 436 437 switch { 438 case updatedUnfetched && originUnfetched: 439 fallthrough 440 case updatedUnfetched && !originUnfetched: 441 // updated is unfetched, so can't have changes except for Kptfile. 442 // we can just merge that one. 443 return kptfileutil.UpdateKptfile(localPath, updatedPath, originPath, true) 444 case !updatedUnfetched && originUnfetched: 445 // This means that the package was unfetched when local forked from upstream, 446 // so the local fork and upstream might have fetched different versions of 447 // the package. We just return an error here. 448 // We might be able to compare the commit SHAs from local and updated 449 // to determine if they share the common upstream and then fetch origin 450 // using the common commit SHA. But this is a very advanced scenario, 451 // so we just return the error for now. 452 return errors.E(op, types.UniquePath(localPath), fmt.Errorf("no origin available for package")) 453 default: 454 // Both exists, so just go ahead as normal. 455 } 456 457 pkgKf, err := pkg.ReadKptfile(filesys.FileSystemOrOnDisk{}, localPath) 458 if err != nil { 459 return errors.E(op, types.UniquePath(localPath), err) 460 } 461 updater, found := strategies[pkgKf.Upstream.UpdateStrategy] 462 if !found { 463 return errors.E(op, types.UniquePath(localPath), 464 fmt.Errorf("unrecognized update strategy %s", u.Strategy)) 465 } 466 pr.Printf("Updating package %q with strategy %q.\n", packageName(localPath), pkgKf.Upstream.UpdateStrategy) 467 if err := updater().Update(Options{ 468 RelPackagePath: relPath, 469 LocalPath: localPath, 470 UpdatedPath: updatedPath, 471 OriginPath: originPath, 472 IsRoot: isRootPkg, 473 }); err != nil { 474 return errors.E(op, types.UniquePath(localPath), err) 475 } 476 477 return nil 478 } 479 480 func packageName(path string) string { 481 return filepath.Base(path) 482 }