github.com/GoogleContainerTools/kpt@v1.0.0-beta.50.0.20240520170205-c25345ffcbee/internal/pkg/pkg.go (about) 1 // Copyright 2020 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 pkg defines the concept of a kpt package. 16 package pkg 17 18 import ( 19 "bytes" 20 "fmt" 21 "io" 22 "os" 23 "path/filepath" 24 "sort" 25 26 "github.com/GoogleContainerTools/kpt/internal/errors" 27 "github.com/GoogleContainerTools/kpt/internal/types" 28 "github.com/GoogleContainerTools/kpt/internal/util/git" 29 "github.com/GoogleContainerTools/kpt/internal/util/pathutil" 30 kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" 31 rgfilev1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/resourcegroup/v1alpha1" 32 "k8s.io/apimachinery/pkg/runtime/schema" 33 "sigs.k8s.io/kustomize/kyaml/filesys" 34 "sigs.k8s.io/kustomize/kyaml/kio" 35 "sigs.k8s.io/kustomize/kyaml/kio/kioutil" 36 "sigs.k8s.io/kustomize/kyaml/sets" 37 "sigs.k8s.io/kustomize/kyaml/yaml" 38 ) 39 40 const CurDir = "." 41 const ParentDir = ".." 42 43 const ( 44 pkgPathAnnotation = "internal.config.kubernetes.io/package-path" 45 ) 46 47 var DeprecatedKptfileVersions = []schema.GroupVersionKind{ 48 kptfilev1.KptFileGVK().GroupKind().WithVersion("v1alpha1"), 49 kptfilev1.KptFileGVK().GroupKind().WithVersion("v1alpha2"), 50 } 51 52 // MatchAllKRM represents set of glob pattern to match all KRM 53 // resources including Kptfile. 54 var MatchAllKRM = append([]string{kptfilev1.KptFileName}, kio.MatchAll...) 55 56 var SupportedKptfileVersions = []schema.GroupVersionKind{ 57 kptfilev1.KptFileGVK(), 58 } 59 60 // KptfileError records errors regarding reading or parsing of a Kptfile. 61 type KptfileError struct { 62 Path types.UniquePath 63 Err error 64 } 65 66 func (k *KptfileError) Error() string { 67 return fmt.Sprintf("error reading Kptfile at %q: %v", k.Path.String(), k.Err) 68 } 69 70 func (k *KptfileError) Unwrap() error { 71 return k.Err 72 } 73 74 // RemoteKptfileError records errors regarding reading or parsing of a Kptfile 75 // in a remote repo. 76 type RemoteKptfileError struct { 77 RepoSpec *git.RepoSpec 78 Err error 79 } 80 81 func (e *RemoteKptfileError) Error() string { 82 return fmt.Sprintf("error reading Kptfile from %q: %v", e.RepoSpec.RepoRef(), e.Err) 83 } 84 85 func (e *RemoteKptfileError) Unwrap() error { 86 return e.Err 87 } 88 89 // DeprecatedKptfileError is an implementation of the error interface that is 90 // returned whenever kpt encounters a Kptfile using the legacy format. 91 type DeprecatedKptfileError struct { 92 Version string 93 } 94 95 func (e *DeprecatedKptfileError) Error() string { 96 return fmt.Sprintf("old resource version %q found in Kptfile", e.Version) 97 } 98 99 type UnknownKptfileResourceError struct { 100 GVK schema.GroupVersionKind 101 } 102 103 func (e *UnknownKptfileResourceError) Error() string { 104 return fmt.Sprintf("unknown resource type %q found in Kptfile", e.GVK.String()) 105 } 106 107 // RGError is an implementation of the error interface that is returned whenever 108 // kpt encounters errors reading a resourcegroup object file. 109 type RGError struct { 110 Path types.UniquePath 111 Err error 112 } 113 114 func (rg *RGError) Error() string { 115 return fmt.Sprintf("error reading ResourceGroup file at %q: %s", rg.Path.String(), rg.Err.Error()) 116 } 117 118 func (rg *RGError) Unwrap() error { 119 return rg.Err 120 } 121 122 // MultipleResourceGroupsError is the error returned if there are multiple 123 // inventories provided in a stream or package as ResourceGroup objects. 124 type MultipleResourceGroupsError struct{} 125 126 func (e *MultipleResourceGroupsError) Error() string { 127 return "multiple ResourceGroup objects found in package" 128 } 129 130 // MultipleKfInv is the error returned if there are multiple 131 // inventories provided in a stream or package as ResourceGroup objects. 132 type MultipleKfInv struct{} 133 134 func (e *MultipleKfInv) Error() string { 135 return "multiple Kptfile inventories found in package" 136 } 137 138 // MultipleInventoryInfoError is the error returned if there are multiple 139 // inventories provided in a stream or package contained with both Kptfile and 140 // ResourceGroup objects. 141 type MultipleInventoryInfoError struct{} 142 143 func (e *MultipleInventoryInfoError) Error() string { 144 return "inventory was found in both Kptfile and ResourceGroup object" 145 } 146 147 // NoInvInfoError is the error returned if there are no inventory information 148 // provided in either a stream or locally. 149 type NoInvInfoError struct{} 150 151 func (e *NoInvInfoError) Error() string { 152 return "no ResourceGroup object was provided within the stream or package" 153 } 154 155 type InvInfoInvalid struct{} 156 157 func (e *InvInfoInvalid) Error() string { 158 return "the provided ResourceGroup is not valid" 159 } 160 161 // warnInvInKptfile is the warning message when the inventory information is present within the Kptfile. 162 // 163 //nolint:lll 164 const warnInvInKptfile = "[WARN] The resourcegroup file was not found... Using Kptfile to gather inventory information. We recommend migrating to a resourcegroup file for inventories. Please migrate with `kpt live migrate`." 165 166 // Pkg represents a kpt package with a one-to-one mapping to a directory on the local filesystem. 167 type Pkg struct { 168 // fsys represents the FileSystem of the package, it may or may not be FileSystem on disk 169 fsys filesys.FileSystem 170 171 // UniquePath represents absolute unique OS-defined path to the package directory on the filesystem. 172 UniquePath types.UniquePath 173 174 // DisplayPath represents Slash-separated path to the package directory on the filesystem relative 175 // to parent directory of root package on which the command is invoked. 176 // root package is defined as the package on which the command is invoked by user 177 // This is not guaranteed to be unique (e.g. in presence of symlinks) and should only 178 // be used for display purposes and is subject to change. 179 DisplayPath types.DisplayPath 180 181 // rootPkgParentDirPath is the absolute path to the parent directory of root package, 182 // root package is defined as the package on which the command is invoked by user 183 // this must be same for all the nested subpackages in root package 184 rootPkgParentDirPath string 185 186 // A package can contain zero or one Kptfile meta resource. 187 // A nil value represents an implicit package. 188 kptfile *kptfilev1.KptFile 189 190 // A package can contain zero or one ResourceGroup object. 191 rgFile *rgfilev1alpha1.ResourceGroup 192 } 193 194 // New returns a pkg given an absolute OS-defined path. 195 // Use ReadKptfile or ReadPipeline on the return value to read meta resources from filesystem. 196 func New(fs filesys.FileSystem, path string) (*Pkg, error) { 197 if !filepath.IsAbs(path) { 198 return nil, fmt.Errorf("provided path %s must be absolute", path) 199 } 200 absPath := filepath.Clean(path) 201 pkg := &Pkg{ 202 fsys: fs, 203 UniquePath: types.UniquePath(absPath), 204 // by default, rootPkgParentDirPath should be the absolute path to the parent directory of package being instantiated 205 rootPkgParentDirPath: filepath.Dir(absPath), 206 // by default, DisplayPath should be the package name which is same as directory name 207 DisplayPath: types.DisplayPath(filepath.Base(absPath)), 208 } 209 return pkg, nil 210 } 211 212 // Kptfile returns the Kptfile meta resource by lazy loading it from the filesytem. 213 // A nil value represents an implicit package. 214 func (p *Pkg) Kptfile() (*kptfilev1.KptFile, error) { 215 if p.kptfile == nil { 216 kf, err := ReadKptfile(p.fsys, p.UniquePath.String()) 217 if err != nil { 218 return nil, err 219 } 220 p.kptfile = kf 221 } 222 return p.kptfile, nil 223 } 224 225 // ReadKptfile reads the KptFile in the given pkg. 226 // TODO(droot): This method exists for current version of Kptfile. 227 // Need to reconcile with the team how we want to handle multiple versions 228 // of Kptfile in code. One option is to follow Kubernetes approach to 229 // have an internal version of Kptfile that all the code uses. In that case, 230 // we will have to implement pieces for IO/Conversion with right interfaces. 231 func ReadKptfile(fs filesys.FileSystem, p string) (*kptfilev1.KptFile, error) { 232 f, err := fs.Open(filepath.Join(p, kptfilev1.KptFileName)) 233 if err != nil { 234 return nil, &KptfileError{ 235 Path: types.UniquePath(p), 236 Err: err, 237 } 238 } 239 defer f.Close() 240 241 kf, err := DecodeKptfile(f) 242 if err != nil { 243 return nil, &KptfileError{ 244 Path: types.UniquePath(p), 245 Err: err, 246 } 247 } 248 return kf, nil 249 } 250 251 func DecodeKptfile(in io.Reader) (*kptfilev1.KptFile, error) { 252 kf := &kptfilev1.KptFile{} 253 c, err := io.ReadAll(in) 254 if err != nil { 255 return kf, err 256 } 257 if err := CheckKptfileVersion(c); err != nil { 258 return kf, err 259 } 260 261 d := yaml.NewDecoder(bytes.NewBuffer(c)) 262 d.KnownFields(true) 263 if err := d.Decode(kf); err != nil { 264 return kf, err 265 } 266 return kf, nil 267 } 268 269 // CheckKptfileVersion verifies the apiVersion and kind of the resource 270 // within the Kptfile. If the legacy version is found, the DeprecatedKptfileError 271 // is returned. If the currently supported apiVersion and kind is found, no 272 // error is returned. 273 func CheckKptfileVersion(content []byte) error { 274 r, err := yaml.Parse(string(content)) 275 if err != nil { 276 return err 277 } 278 279 m, err := r.GetMeta() 280 if err != nil { 281 return err 282 } 283 284 kind := m.Kind 285 gv, err := schema.ParseGroupVersion(m.APIVersion) 286 if err != nil { 287 return err 288 } 289 gvk := gv.WithKind(kind) 290 291 switch { 292 // If the resource type matches what we are looking for, just return nil. 293 case isSupportedKptfileVersion(gvk): 294 return nil 295 // If the kind and group is correct and the version is a known deprecated 296 // schema for the Kptfile, return DeprecatedKptfileError. 297 case isDeprecatedKptfileVersion(gvk): 298 return &DeprecatedKptfileError{ 299 Version: gv.Version, 300 } 301 // If the combination of group, version and kind are unknown to us, return 302 // UnknownKptfileResourceError. 303 default: 304 return &UnknownKptfileResourceError{ 305 GVK: gv.WithKind(kind), 306 } 307 } 308 } 309 310 func isDeprecatedKptfileVersion(gvk schema.GroupVersionKind) bool { 311 for _, v := range DeprecatedKptfileVersions { 312 if v == gvk { 313 return true 314 } 315 } 316 return false 317 } 318 319 func isSupportedKptfileVersion(gvk schema.GroupVersionKind) bool { 320 for _, v := range SupportedKptfileVersions { 321 if v == gvk { 322 return true 323 } 324 } 325 return false 326 } 327 328 // Pipeline returns the Pipeline section of the pkg's Kptfile. 329 // if pipeline is not specified in a Kptfile, it returns Zero value of the pipeline. 330 func (p *Pkg) Pipeline() (*kptfilev1.Pipeline, error) { 331 kf, err := p.Kptfile() 332 if err != nil { 333 return nil, err 334 } 335 pl := kf.Pipeline 336 if pl == nil { 337 return &kptfilev1.Pipeline{}, nil 338 } 339 return pl, nil 340 } 341 342 // String returns the slash-separated relative path to the package. 343 func (p *Pkg) String() string { 344 return string(p.DisplayPath) 345 } 346 347 // RelativePathTo returns current package's path relative to a given package. 348 // It returns an error if relative path doesn't exist. 349 // In a nested package chain, one can use this method to get the relative 350 // path of a subpackage relative to an ancestor package up the chain. 351 // Example: rel, _ := subpkg.RelativePathTo(rootPkg) 352 // The returned relative path is compatible with the target operating 353 // system-defined file paths. 354 func (p *Pkg) RelativePathTo(ancestorPkg *Pkg) (string, error) { 355 return filepath.Rel(string(ancestorPkg.UniquePath), string(p.UniquePath)) 356 } 357 358 // DirectSubpackages returns subpackages of a pkg. It will return all direct 359 // subpackages, i.e. subpackages that aren't nested inside other subpackages 360 // under the current package. It will return packages that are nested inside 361 // directories of the current package. 362 // TODO: This does not support symlinks, so we need to figure out how 363 // we should support that with kpt. 364 func (p *Pkg) DirectSubpackages() ([]*Pkg, error) { 365 var subPkgs []*Pkg 366 367 packagePaths, err := Subpackages(p.fsys, p.UniquePath.String(), All, false) 368 if err != nil { 369 return subPkgs, err 370 } 371 372 for _, subPkgPath := range packagePaths { 373 subPkg, err := New(p.fsys, filepath.Join(p.UniquePath.String(), subPkgPath)) 374 if err != nil { 375 return subPkgs, fmt.Errorf("failed to read package at path %q: %w", subPkgPath, err) 376 } 377 if err := p.adjustDisplayPathForSubpkg(subPkg); err != nil { 378 return subPkgs, fmt.Errorf("failed to resolve display path for %q: %w", subPkgPath, err) 379 } 380 subPkgs = append(subPkgs, subPkg) 381 } 382 383 sort.Slice(subPkgs, func(i, j int) bool { 384 return subPkgs[i].DisplayPath < subPkgs[j].DisplayPath 385 }) 386 return subPkgs, nil 387 } 388 389 // adjustDisplayPathForSubpkg adjusts the display path of subPkg relative to the RootPkgUniquePath 390 // subPkg also inherits the RootPkgUniquePath value from parent package p 391 func (p *Pkg) adjustDisplayPathForSubpkg(subPkg *Pkg) error { 392 // inherit the rootPkgParentDirPath from the parent package 393 subPkg.rootPkgParentDirPath = p.rootPkgParentDirPath 394 // display path of subPkg should be relative to parent dir of rootPkg 395 // e.g. if mysql(subPkg) is direct subpackage of wordpress(p), DisplayPath of "mysql" should be "wordpress/mysql" 396 dp, err := filepath.Rel(subPkg.rootPkgParentDirPath, string(subPkg.UniquePath)) 397 if err != nil { 398 return err 399 } 400 // make sure that the DisplayPath is always Slash-separated os-agnostic 401 subPkg.DisplayPath = types.DisplayPath(filepath.ToSlash(dp)) 402 return nil 403 } 404 405 // SubpackageMatcher is type for specifying the types of subpackages which 406 // should be included when listing them. 407 type SubpackageMatcher string 408 409 const ( 410 // All means all types of subpackages will be returned. 411 All SubpackageMatcher = "ALL" 412 // Local means only local subpackages will be returned. 413 Local SubpackageMatcher = "LOCAL" 414 // remote means only remote subpackages will be returned. 415 Remote SubpackageMatcher = "REMOTE" 416 // None means that no subpackages will be returned. 417 None SubpackageMatcher = "NONE" 418 ) 419 420 // Subpackages returns a slice of paths to any subpackages of the provided path. 421 // The matcher parameter decides the types of subpackages should be considered(ALL/LOCAL/REMOTE/NONE), 422 // and the recursive parameter determines if only direct subpackages are 423 // considered. All returned paths will be relative to the provided rootPath. 424 // The top level package is not considered a subpackage. If the provided path 425 // doesn't exist, an empty slice will be returned. 426 // Symlinks are ignored. 427 // TODO: For now this accepts the path as a string type. See if we can leverage 428 // the package type here. 429 func Subpackages(fsys filesys.FileSystem, rootPath string, matcher SubpackageMatcher, recursive bool) ([]string, error) { 430 const op errors.Op = "pkg.Subpackages" 431 432 if !fsys.Exists(rootPath) { 433 return []string{}, nil 434 } 435 packagePaths := make(map[string]bool) 436 if err := fsys.Walk(rootPath, func(path string, info os.FileInfo, err error) error { 437 if err != nil { 438 return fmt.Errorf("failed to read package %s: %w", rootPath, err) 439 } 440 441 // Ignore the root folder 442 if path == rootPath { 443 return nil 444 } 445 446 // For every folder, we check if it is a kpt package 447 if info.IsDir() { 448 // Ignore anything inside the .git folder 449 // TODO: We eventually want to support user-defined ignore lists. 450 if info.Name() == ".git" { 451 return filepath.SkipDir 452 } 453 454 // Check if the directory is the root of a kpt package 455 isPkg, err := IsPackageDir(fsys, path) 456 if err != nil { 457 return err 458 } 459 460 // If the path is the root of a subpackage, add the 461 // path to the slice and return SkipDir since we don't need to 462 // walk any deeper into the directory. 463 if isPkg { 464 kf, err := ReadKptfile(fsys, path) 465 if err != nil { 466 return errors.E(op, types.UniquePath(path), err) 467 } 468 switch matcher { 469 case Local: 470 if kf.Upstream == nil { 471 packagePaths[path] = true 472 } 473 case Remote: 474 if kf.Upstream != nil { 475 packagePaths[path] = true 476 } 477 case All: 478 packagePaths[path] = true 479 } 480 if !recursive { 481 return filepath.SkipDir 482 } 483 return nil 484 } 485 } 486 return nil 487 }); err != nil { 488 return []string{}, fmt.Errorf("failed to read package at %s: %w", rootPath, err) 489 } 490 491 paths := []string{} 492 for subPkgPath := range packagePaths { 493 relPath, err := filepath.Rel(rootPath, subPkgPath) 494 if err != nil { 495 return paths, fmt.Errorf("failed to find relative path for %s: %w", subPkgPath, err) 496 } 497 paths = append(paths, relPath) 498 } 499 return paths, nil 500 } 501 502 // IsPackageDir checks if there exists a Kptfile on the provided path, i.e. 503 // whether the provided path is the root of a package. 504 func IsPackageDir(fsys filesys.FileSystem, path string) (bool, error) { 505 if !fsys.Exists(filepath.Join(path, kptfilev1.KptFileName)) { 506 return false, nil 507 } 508 return true, nil 509 } 510 511 // IsPackageUnfetched returns true if a package has Upstream information, 512 // but no UpstreamLock. For local packages that doesn't have Upstream 513 // information, it will always return false. 514 // If a Kptfile is not found on the provided path, an error will be returned. 515 func IsPackageUnfetched(path string) (bool, error) { 516 kf, err := ReadKptfile(filesys.FileSystemOrOnDisk{}, path) 517 if err != nil { 518 return false, err 519 } 520 return kf.Upstream != nil && kf.UpstreamLock == nil, nil 521 } 522 523 // LocalResources returns resources that belong to this package excluding the subpackage resources. 524 func (p *Pkg) LocalResources() (resources []*yaml.RNode, err error) { 525 const op errors.Op = "pkg.readResources" 526 527 var hasKptfile bool 528 hasKptfile, err = IsPackageDir(p.fsys, p.UniquePath.String()) 529 if err != nil { 530 return nil, errors.E(op, p.UniquePath, err) 531 } 532 if !hasKptfile { 533 return nil, nil 534 } 535 536 pkgReader := &kio.LocalPackageReader{ 537 PackagePath: string(p.UniquePath), 538 PackageFileName: kptfilev1.KptFileName, 539 IncludeSubpackages: false, 540 MatchFilesGlob: MatchAllKRM, 541 PreserveSeqIndent: true, 542 SetAnnotations: map[string]string{ 543 pkgPathAnnotation: string(p.UniquePath), 544 }, 545 WrapBareSeqNode: true, 546 FileSystem: filesys.FileSystemOrOnDisk{ 547 FileSystem: p.fsys, 548 }, 549 } 550 resources, err = pkgReader.Read() 551 if err != nil { 552 return resources, errors.E(op, p.UniquePath, err) 553 } 554 return resources, err 555 } 556 557 // Validates the package pipeline. 558 func (p *Pkg) ValidatePipeline() error { 559 pl, err := p.Pipeline() 560 if err != nil { 561 return err 562 } 563 564 if pl.IsEmpty() { 565 return nil 566 } 567 568 // read all resources including function pipeline. 569 resources, err := p.LocalResources() 570 if err != nil { 571 return err 572 } 573 574 resourcesByPath := sets.String{} 575 576 for _, r := range resources { 577 rPath, _, err := kioutil.GetFileAnnotations(r) 578 if err != nil { 579 return fmt.Errorf("resource missing path annotation err: %w", err) 580 } 581 resourcesByPath.Insert(filepath.Clean(rPath)) 582 } 583 584 for i, fn := range pl.Mutators { 585 if fn.ConfigPath != "" && !resourcesByPath.Has(filepath.Clean(fn.ConfigPath)) { 586 return &kptfilev1.ValidateError{ 587 Field: fmt.Sprintf("pipeline.%s[%d].configPath", "mutators", i), 588 Value: fn.ConfigPath, 589 Reason: "functionConfig must exist in the current package", 590 } 591 } 592 } 593 for i, fn := range pl.Validators { 594 if fn.ConfigPath != "" && !resourcesByPath.Has(filepath.Clean(fn.ConfigPath)) { 595 return &kptfilev1.ValidateError{ 596 Field: fmt.Sprintf("pipeline.%s[%d].configPath", "validators", i), 597 Value: fn.ConfigPath, 598 Reason: "functionConfig must exist in the current package", 599 } 600 } 601 } 602 return nil 603 } 604 605 // GetPkgPathAnnotation returns the package path annotation on 606 // a given resource. 607 func GetPkgPathAnnotation(rn *yaml.RNode) (string, error) { 608 meta, err := rn.GetMeta() 609 if err != nil { 610 return "", err 611 } 612 pkgPath := meta.Annotations[pkgPathAnnotation] 613 return pkgPath, nil 614 } 615 616 // SetPkgPathAnnotation sets package path on a given resource. 617 func SetPkgPathAnnotation(rn *yaml.RNode, pkgPath types.UniquePath) error { 618 return rn.PipeE(yaml.SetAnnotation(pkgPathAnnotation, string(pkgPath))) 619 } 620 621 // RemovePkgPathAnnotation removes the package path on a given resource. 622 func RemovePkgPathAnnotation(rn *yaml.RNode) error { 623 return rn.PipeE(yaml.ClearAnnotation(pkgPathAnnotation)) 624 } 625 626 // ReadRGFile returns the resourcegroup object by lazy loading it from the filesytem. 627 func (p *Pkg) ReadRGFile(rgfile string) (*rgfilev1alpha1.ResourceGroup, error) { 628 if p.rgFile == nil { 629 rg, err := ReadRGFile(p.UniquePath.String(), rgfile) 630 if err != nil { 631 return nil, err 632 } 633 p.rgFile = rg 634 } 635 return p.rgFile, nil 636 } 637 638 // TODO(rquitales): Consolidate both Kptfile and ResourceGroup file reading functions to use 639 // shared logic/function. 640 641 // ReadRGFile reads the resourcegroup inventory in the given pkg. 642 func ReadRGFile(pkgPath, rgfile string) (*rgfilev1alpha1.ResourceGroup, error) { 643 // Check to see if filename for ResourceGroup is a filepath, rather than being relative to the pkg path. 644 // If only a filename is provided, we assume that the resourcegroup file is relative to the pkg path. 645 var absPath string 646 if filepath.Base(rgfile) == rgfile { 647 absPath = filepath.Join(pkgPath, rgfile) 648 } else { 649 rgFilePath, _, err := pathutil.ResolveAbsAndRelPaths(rgfile) 650 if err != nil { 651 return nil, &RGError{ 652 Path: types.UniquePath(rgfile), 653 Err: err, 654 } 655 } 656 657 absPath = rgFilePath 658 } 659 660 f, err := os.Open(absPath) 661 if err != nil { 662 return nil, &RGError{ 663 Path: types.UniquePath(absPath), 664 Err: err, 665 } 666 } 667 defer f.Close() 668 669 rg, err := DecodeRGFile(f) 670 if err != nil { 671 return nil, &RGError{ 672 Path: types.UniquePath(absPath), 673 Err: err, 674 } 675 } 676 return rg, nil 677 } 678 679 // DecodeRGFile converts a string reader into structured a ResourceGroup object. 680 func DecodeRGFile(in io.Reader) (*rgfilev1alpha1.ResourceGroup, error) { 681 rg := &rgfilev1alpha1.ResourceGroup{} 682 c, err := io.ReadAll(in) 683 if err != nil { 684 return rg, err 685 } 686 687 d := yaml.NewDecoder(bytes.NewBuffer(c)) 688 d.KnownFields(true) 689 if err := d.Decode(rg); err != nil { 690 return rg, err 691 } 692 return rg, nil 693 } 694 695 // LocalInventory returns the package inventory stored within a package. If more than one, or no inventories are 696 // found, an error is returned instead. 697 func (p *Pkg) LocalInventory() (kptfilev1.Inventory, error) { 698 const op errors.Op = "pkg.LocalInventory" 699 700 pkgReader := &kio.LocalPackageReader{ 701 PackagePath: string(p.UniquePath), 702 PackageFileName: kptfilev1.KptFileName, 703 IncludeSubpackages: false, 704 MatchFilesGlob: kio.MatchAll, 705 PreserveSeqIndent: true, 706 SetAnnotations: map[string]string{ 707 pkgPathAnnotation: string(p.UniquePath), 708 }, 709 WrapBareSeqNode: true, 710 FileSystem: filesys.FileSystemOrOnDisk{ 711 FileSystem: p.fsys, 712 }, 713 } 714 resources, err := pkgReader.Read() 715 if err != nil { 716 return kptfilev1.Inventory{}, errors.E(op, p.UniquePath, err) 717 } 718 719 resources, err = filterResourceGroups(resources) 720 if err != nil { 721 return kptfilev1.Inventory{}, errors.E(op, p.UniquePath, err) 722 } 723 724 // Multiple ResourceGroups found. 725 if len(resources) > 1 { 726 return kptfilev1.Inventory{}, &MultipleResourceGroupsError{} 727 } 728 729 // Load Kptfile and check if we have any inventory information there. 730 var hasKptfile bool 731 hasKptfile, err = IsPackageDir(p.fsys, p.UniquePath.String()) 732 if err != nil { 733 return kptfilev1.Inventory{}, errors.E(op, p.UniquePath, err) 734 } 735 736 if !hasKptfile { 737 // Return the ResourceGroup object as inventory. 738 if len(resources) == 1 { 739 return kptfilev1.Inventory{ 740 Name: resources[0].GetName(), 741 Namespace: resources[0].GetNamespace(), 742 InventoryID: resources[0].GetLabels()[rgfilev1alpha1.RGInventoryIDLabel], 743 }, nil 744 } 745 746 // No inventory information found as ResourceGroup objects, and Kptfile does not exist. 747 return kptfilev1.Inventory{}, &NoInvInfoError{} 748 } 749 750 kf, err := p.Kptfile() 751 if err != nil { 752 return kptfilev1.Inventory{}, errors.E(op, p.UniquePath, err) 753 } 754 755 // No inventory found in either Kptfile or as ResourceGroup objects. 756 if kf.Inventory == nil && len(resources) == 0 { 757 return kptfilev1.Inventory{}, &NoInvInfoError{} 758 } 759 760 // Multiple inventories found, in both Kptfile and resourcegroup objects. 761 if kf.Inventory != nil && len(resources) > 0 { 762 return kptfilev1.Inventory{}, &MultipleInventoryInfoError{} 763 } 764 765 // ResourceGroup stores the inventory and Kptfile does not contain inventory. 766 if len(resources) == 1 { 767 return kptfilev1.Inventory{ 768 Name: resources[0].GetName(), 769 Namespace: resources[0].GetNamespace(), 770 InventoryID: resources[0].GetLabels()[rgfilev1alpha1.RGInventoryIDLabel], 771 }, nil 772 } 773 774 // Kptfile stores the inventory. 775 fmt.Println(warnInvInKptfile) 776 return *kf.Inventory, nil 777 } 778 779 // filterResourceGroups only retains ResourceGroup objects. 780 func filterResourceGroups(input []*yaml.RNode) (output []*yaml.RNode, err error) { 781 for _, r := range input { 782 meta, err := r.GetMeta() 783 if err != nil { 784 return nil, fmt.Errorf("failed to read metadata for resource %w", err) 785 } 786 // Filter out any non-ResourceGroup files. 787 if !(meta.APIVersion == rgfilev1alpha1.ResourceGroupGVK().GroupVersion().String() && meta.Kind == rgfilev1alpha1.ResourceGroupGVK().Kind) { 788 continue 789 } 790 791 output = append(output, r) 792 } 793 794 return output, nil 795 }