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  }