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  }