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