github.com/SamarSidharth/kpt@v0.0.0-20231122062228-c7d747ae3ace/internal/testutil/pkgbuilder/builder.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 pkgbuilder
    16  
    17  import (
    18  	"fmt"
    19  	"os"
    20  	"path/filepath"
    21  	"regexp"
    22  	"strconv"
    23  	"testing"
    24  
    25  	kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
    26  	rgfilev1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/resourcegroup/v1alpha1"
    27  	"github.com/stretchr/testify/assert"
    28  	"sigs.k8s.io/kustomize/kyaml/yaml"
    29  )
    30  
    31  var (
    32  	deploymentResourceManifest = `
    33  apiVersion: apps/v1
    34  kind: Deployment
    35  metadata:
    36    namespace: myspace
    37    name: mysql-deployment
    38  spec:
    39    replicas: 3
    40    foo: bar
    41    template:
    42      spec:
    43        containers:
    44        - name: mysql
    45          image: mysql:1.7.9
    46  `
    47  
    48  	configMapResourceManifest = `
    49  apiVersion: v1
    50  kind: ConfigMap
    51  metadata:
    52    name: configmap
    53  data:
    54    foo: bar
    55  `
    56  
    57  	secretResourceManifest = `
    58  apiVersion: v1
    59  kind: Secret
    60  metadata:
    61    name: secret
    62  type: Opaque
    63  data:
    64    foo: bar
    65  `
    66  )
    67  
    68  var (
    69  	DeploymentResource = "deployment"
    70  	ConfigMapResource  = "configmap"
    71  	SecretResource     = "secret"
    72  	resources          = map[string]resourceInfo{
    73  		DeploymentResource: {
    74  			filename: "deployment.yaml",
    75  			manifest: deploymentResourceManifest,
    76  		},
    77  		ConfigMapResource: {
    78  			filename: "configmap.yaml",
    79  			manifest: configMapResourceManifest,
    80  		},
    81  		SecretResource: {
    82  			filename: "secret.yaml",
    83  			manifest: secretResourceManifest,
    84  		},
    85  	}
    86  )
    87  
    88  // Pkg represents a package that can be created on the file system
    89  // by using the Build function
    90  type pkg struct {
    91  	Kptfile *Kptfile
    92  
    93  	RGFile *RGFile
    94  
    95  	resources []resourceInfoWithMutators
    96  
    97  	files map[string]string
    98  
    99  	subPkgs []*SubPkg
   100  }
   101  
   102  // WithRGFile configures the current package to have a resourcegroup file.
   103  func (rp *RootPkg) WithRGFile(rg *RGFile) *RootPkg {
   104  	rp.pkg.RGFile = rg
   105  	return rp
   106  }
   107  
   108  // withKptfile configures the current package to have a Kptfile. Only
   109  // zero or one Kptfiles are accepted.
   110  func (p *pkg) withKptfile(kf ...*Kptfile) {
   111  	if len(kf) > 1 {
   112  		panic("only 0 or 1 Kptfiles are allowed")
   113  	}
   114  	if len(kf) == 0 {
   115  		p.Kptfile = NewKptfile()
   116  	} else {
   117  		p.Kptfile = kf[0]
   118  	}
   119  }
   120  
   121  // withResource configures the package to include the provided resource
   122  func (p *pkg) withResource(resourceName string, mutators ...yaml.Filter) {
   123  	resourceInfo, ok := resources[resourceName]
   124  	if !ok {
   125  		panic(fmt.Errorf("unknown resource %s", resourceName))
   126  	}
   127  	p.resources = append(p.resources, resourceInfoWithMutators{
   128  		resourceInfo: resourceInfo,
   129  		mutators:     mutators,
   130  	})
   131  }
   132  
   133  // withRawResource configures the package to include the provided resource
   134  func (p *pkg) withRawResource(resourceName, manifest string, mutators ...yaml.Filter) {
   135  	p.resources = append(p.resources, resourceInfoWithMutators{
   136  		resourceInfo: resourceInfo{
   137  			filename: resourceName,
   138  			manifest: manifest,
   139  		},
   140  		mutators: mutators,
   141  	})
   142  }
   143  
   144  // withFile configures the package to contain a file with the provided name
   145  // and the given content.
   146  func (p *pkg) withFile(name, content string) {
   147  	p.files[name] = content
   148  }
   149  
   150  // withSubPackages adds the provided packages as subpackages to the current
   151  // package
   152  func (p *pkg) withSubPackages(ps ...*SubPkg) {
   153  	p.subPkgs = append(p.subPkgs, ps...)
   154  }
   155  
   156  // allReferencedRepos traverses the root package and all subpackages to
   157  // capture all references to other repos.
   158  func (p *pkg) allReferencedRepos(collector map[string]bool) {
   159  	for i := range p.subPkgs {
   160  		p.subPkgs[i].pkg.allReferencedRepos(collector)
   161  	}
   162  	if p.Kptfile != nil && p.Kptfile.Upstream != nil {
   163  		collector[p.Kptfile.Upstream.RepoRef] = true
   164  	}
   165  }
   166  
   167  // RootPkg is a package without any parent package.
   168  type RootPkg struct {
   169  	pkg *pkg
   170  }
   171  
   172  // NewRootPkg creates a new package for testing.
   173  func NewRootPkg() *RootPkg {
   174  	return &RootPkg{
   175  		pkg: &pkg{
   176  			files: make(map[string]string),
   177  		},
   178  	}
   179  }
   180  
   181  // WithKptfile configures the current package to have a Kptfile. Only
   182  // zero or one Kptfiles are accepted.
   183  func (rp *RootPkg) WithKptfile(kf ...*Kptfile) *RootPkg {
   184  	rp.pkg.withKptfile(kf...)
   185  	return rp
   186  }
   187  
   188  // HasKptfile tells whether the package contains a Kptfile.
   189  func (rp *RootPkg) HasKptfile() bool {
   190  	return rp.pkg.Kptfile != nil
   191  }
   192  
   193  // AllReferencedRepos returns the name of all remote subpackages referenced
   194  // in the package (including any local subpackages).
   195  func (rp *RootPkg) AllReferencedRepos() []string {
   196  	repoNameMap := make(map[string]bool)
   197  	rp.pkg.allReferencedRepos(repoNameMap)
   198  
   199  	var repoNames []string
   200  	for n := range repoNameMap {
   201  		repoNames = append(repoNames, n)
   202  	}
   203  	return repoNames
   204  }
   205  
   206  // WithResource configures the package to include the provided resource
   207  func (rp *RootPkg) WithResource(resourceName string, mutators ...yaml.Filter) *RootPkg {
   208  	rp.pkg.withResource(resourceName, mutators...)
   209  	return rp
   210  }
   211  
   212  // WithRawResource configures the package to include the provided resource
   213  func (rp *RootPkg) WithRawResource(resourceName, manifest string, mutators ...yaml.Filter) *RootPkg {
   214  	rp.pkg.withRawResource(resourceName, manifest, mutators...)
   215  	return rp
   216  }
   217  
   218  // WithFile configures the package to contain a file with the provided name
   219  // and the given content.
   220  func (rp *RootPkg) WithFile(name, content string) *RootPkg {
   221  	rp.pkg.withFile(name, content)
   222  	return rp
   223  }
   224  
   225  // WithSubPackages adds the provided packages as subpackages to the current
   226  // package
   227  func (rp *RootPkg) WithSubPackages(ps ...*SubPkg) *RootPkg {
   228  	rp.pkg.withSubPackages(ps...)
   229  	return rp
   230  }
   231  
   232  // Build outputs the current data structure as a set of (nested) package
   233  // in the provided path.
   234  func (rp *RootPkg) Build(path string, pkgName string, reposInfo ReposInfo) error {
   235  	pkgPath := filepath.Join(path, pkgName)
   236  	err := os.Mkdir(pkgPath, 0700)
   237  	if err != nil {
   238  		return err
   239  	}
   240  	if rp == nil {
   241  		return nil
   242  	}
   243  	err = buildPkg(pkgPath, rp.pkg, pkgName, reposInfo)
   244  	if err != nil {
   245  		return err
   246  	}
   247  	for i := range rp.pkg.subPkgs {
   248  		subPkg := rp.pkg.subPkgs[i]
   249  		err := buildSubPkg(pkgPath, subPkg, reposInfo)
   250  		if err != nil {
   251  			return err
   252  		}
   253  	}
   254  	return nil
   255  }
   256  
   257  // SubPkg is a subpackage, so it is contained inside another package. The
   258  // name sets both the name of the directory in which the package is stored
   259  // and the metadata.name field in the Kptfile (if there is one).
   260  type SubPkg struct {
   261  	pkg *pkg
   262  
   263  	Name string
   264  }
   265  
   266  // NewSubPkg returns a new subpackage for testing.
   267  func NewSubPkg(name string) *SubPkg {
   268  	return &SubPkg{
   269  		pkg: &pkg{
   270  			files: make(map[string]string),
   271  		},
   272  		Name: name,
   273  	}
   274  }
   275  
   276  // WithKptfile configures the current package to have a Kptfile. Only
   277  // zero or one Kptfiles are accepted.
   278  func (sp *SubPkg) WithKptfile(kf ...*Kptfile) *SubPkg {
   279  	sp.pkg.withKptfile(kf...)
   280  	return sp
   281  }
   282  
   283  // WithResource configures the package to include the provided resource
   284  func (sp *SubPkg) WithResource(resourceName string, mutators ...yaml.Filter) *SubPkg {
   285  	sp.pkg.withResource(resourceName, mutators...)
   286  	return sp
   287  }
   288  
   289  // WithRawResource configures the package to include the provided resource
   290  func (sp *SubPkg) WithRawResource(resourceName, manifest string, mutators ...yaml.Filter) *SubPkg {
   291  	sp.pkg.withRawResource(resourceName, manifest, mutators...)
   292  	return sp
   293  }
   294  
   295  // WithFile configures the package to contain a file with the provided name
   296  // and the given content.
   297  func (sp *SubPkg) WithFile(name, content string) *SubPkg {
   298  	sp.pkg.withFile(name, content)
   299  	return sp
   300  }
   301  
   302  // WithSubPackages adds the provided packages as subpackages to the current
   303  // package
   304  func (sp *SubPkg) WithSubPackages(ps ...*SubPkg) *SubPkg {
   305  	sp.pkg.withSubPackages(ps...)
   306  	return sp
   307  }
   308  
   309  // RGFile represents a minimal resourcegroup.
   310  type RGFile struct {
   311  	Name, Namespace, ID string
   312  }
   313  
   314  func NewRGFile() *RGFile {
   315  	return &RGFile{}
   316  }
   317  
   318  func (rg *RGFile) WithInventory(inv Inventory) *RGFile {
   319  	rg.Name = inv.Name
   320  	rg.Namespace = inv.Namespace
   321  	rg.ID = inv.ID
   322  	return rg
   323  }
   324  
   325  // Kptfile represents the Kptfile of a package.
   326  type Kptfile struct {
   327  	Upstream     *Upstream
   328  	UpstreamLock *UpstreamLock
   329  	Pipeline     *Pipeline
   330  	Inventory    *Inventory
   331  }
   332  
   333  func NewKptfile() *Kptfile {
   334  	return &Kptfile{}
   335  }
   336  
   337  // WithUpstream adds information about the upstream information to the Kptfile.
   338  // The upstream section of the Kptfile is only added if this information is
   339  // provided.
   340  func (k *Kptfile) WithUpstream(repo, dir, ref, strategy string) *Kptfile {
   341  	k.Upstream = &Upstream{
   342  		Repo:     repo,
   343  		Dir:      dir,
   344  		Ref:      ref,
   345  		Strategy: strategy,
   346  	}
   347  	return k
   348  }
   349  
   350  // WithUpstreamRef adds information about the upstream information to the
   351  // Kptfile. Unlike WithUpstream, this function allows providing just a
   352  // reference to the repo rather than the actual path. The reference will
   353  // be resolved to an actual path when the package is written to disk.
   354  func (k *Kptfile) WithUpstreamRef(repoRef, dir, ref, strategy string) *Kptfile {
   355  	k.Upstream = &Upstream{
   356  		RepoRef:  repoRef,
   357  		Dir:      dir,
   358  		Ref:      ref,
   359  		Strategy: strategy,
   360  	}
   361  	return k
   362  }
   363  
   364  // WithUpstreamLock adds upstreamLock information to the Kptfile. If no
   365  // upstreamLock information is provided,
   366  func (k *Kptfile) WithUpstreamLock(repo, dir, ref, commit string) *Kptfile {
   367  	k.UpstreamLock = &UpstreamLock{
   368  		Repo:   repo,
   369  		Dir:    dir,
   370  		Ref:    ref,
   371  		Commit: commit,
   372  	}
   373  	return k
   374  }
   375  
   376  // WithUpstreamLockRef adds upstreamLock information to the Kptfile. But unlike
   377  // WithUpstreamLock, this function takes a the name to a repo and will resolve
   378  // the actual path when expanding the package. The commit SHA is also not provided,
   379  // but rather the index of a commit that will be resolved when expanding the
   380  // package.
   381  func (k *Kptfile) WithUpstreamLockRef(repoRef, dir, ref string, index int) *Kptfile {
   382  	k.UpstreamLock = &UpstreamLock{
   383  		RepoRef: repoRef,
   384  		Dir:     dir,
   385  		Ref:     ref,
   386  		Index:   index,
   387  	}
   388  	return k
   389  }
   390  
   391  type Upstream struct {
   392  	Repo     string
   393  	RepoRef  string
   394  	Dir      string
   395  	Ref      string
   396  	Strategy string
   397  }
   398  
   399  type UpstreamLock struct {
   400  	Repo    string
   401  	RepoRef string
   402  	Dir     string
   403  	Ref     string
   404  	Index   int
   405  	Commit  string
   406  }
   407  
   408  func (k *Kptfile) WithInventory(inv Inventory) *Kptfile {
   409  	k.Inventory = &inv
   410  	return k
   411  }
   412  
   413  type Inventory struct {
   414  	Name      string
   415  	Namespace string
   416  	ID        string
   417  }
   418  
   419  func (k *Kptfile) WithPipeline(functions ...Function) *Kptfile {
   420  	k.Pipeline = &Pipeline{
   421  		Functions: functions,
   422  	}
   423  	return k
   424  }
   425  
   426  type Pipeline struct {
   427  	Functions []Function
   428  }
   429  
   430  func NewFunction(image string) Function {
   431  	return Function{
   432  		Image: image,
   433  	}
   434  }
   435  
   436  type Function struct {
   437  	Image      string
   438  	ConfigPath string
   439  }
   440  
   441  func (f Function) WithConfigPath(configPath string) Function {
   442  	f.ConfigPath = configPath
   443  	return f
   444  }
   445  
   446  // RemoteSubpackage contains information about remote subpackages that should
   447  // be listed in the Kptfile.
   448  type RemoteSubpackage struct {
   449  	// Name is the name of the remote subpackage. It will be used as the value
   450  	// for the LocalDir property and also used to resolve the Repo path from
   451  	// other defined repos.
   452  	RepoRef   string
   453  	Repo      string
   454  	Directory string
   455  	Ref       string
   456  	Strategy  string
   457  	LocalDir  string
   458  }
   459  
   460  type resourceInfo struct {
   461  	filename string
   462  	manifest string
   463  }
   464  
   465  type resourceInfoWithMutators struct {
   466  	resourceInfo resourceInfo
   467  	mutators     []yaml.Filter
   468  }
   469  
   470  func buildSubPkg(path string, pkg *SubPkg, reposInfo ReposInfo) error {
   471  	pkgPath := filepath.Join(path, pkg.Name)
   472  	err := os.Mkdir(pkgPath, 0700)
   473  	if err != nil {
   474  		return err
   475  	}
   476  	err = buildPkg(pkgPath, pkg.pkg, pkg.Name, reposInfo)
   477  	if err != nil {
   478  		return err
   479  	}
   480  	for i := range pkg.pkg.subPkgs {
   481  		subPkg := pkg.pkg.subPkgs[i]
   482  		err := buildSubPkg(pkgPath, subPkg, reposInfo)
   483  		if err != nil {
   484  			return err
   485  		}
   486  	}
   487  	return nil
   488  }
   489  
   490  func buildPkg(pkgPath string, pkg *pkg, pkgName string, reposInfo ReposInfo) error {
   491  	if pkg.Kptfile != nil {
   492  		content := buildKptfile(pkg, pkgName, reposInfo)
   493  
   494  		err := os.WriteFile(filepath.Join(pkgPath, kptfilev1.KptFileName),
   495  			[]byte(content), 0600)
   496  		if err != nil {
   497  			return err
   498  		}
   499  	}
   500  
   501  	if pkg.RGFile != nil {
   502  		content := buildRGFile(pkg)
   503  
   504  		err := os.WriteFile(filepath.Join(pkgPath, rgfilev1alpha1.RGFileName),
   505  			[]byte(content), 0600)
   506  		if err != nil {
   507  			return err
   508  		}
   509  	}
   510  
   511  	for _, ri := range pkg.resources {
   512  		m := ri.resourceInfo.manifest
   513  		r := yaml.MustParse(m)
   514  
   515  		for _, m := range ri.mutators {
   516  			if err := r.PipeE(m); err != nil {
   517  				return err
   518  			}
   519  		}
   520  
   521  		filePath := filepath.Join(pkgPath, ri.resourceInfo.filename)
   522  		err := os.WriteFile(filePath, []byte(r.MustString()), 0600)
   523  		if err != nil {
   524  			return err
   525  		}
   526  	}
   527  
   528  	for name, content := range pkg.files {
   529  		filePath := filepath.Join(pkgPath, name)
   530  		_, err := os.Stat(filePath)
   531  		if err != nil && !os.IsNotExist(err) {
   532  			return err
   533  		}
   534  		if !os.IsNotExist(err) {
   535  			return fmt.Errorf("file %s already exists", name)
   536  		}
   537  		err = os.WriteFile(filePath, []byte(content), 0600)
   538  		if err != nil {
   539  			return err
   540  		}
   541  	}
   542  	return nil
   543  }
   544  
   545  // buildRGFile creates a ResourceGroup inventory file.
   546  func buildRGFile(pkg *pkg) string {
   547  	tmp := rgfilev1alpha1.ResourceGroup{ResourceMeta: rgfilev1alpha1.DefaultMeta}
   548  	tmp.ObjectMeta.Name = pkg.RGFile.Name
   549  	tmp.ObjectMeta.Namespace = pkg.RGFile.Namespace
   550  	if pkg.RGFile.ID != "" {
   551  		tmp.ObjectMeta.Labels = map[string]string{rgfilev1alpha1.RGInventoryIDLabel: pkg.RGFile.ID}
   552  	}
   553  
   554  	b, err := yaml.MarshalWithOptions(tmp, &yaml.EncoderOptions{SeqIndent: yaml.WideSequenceStyle})
   555  	if err != nil {
   556  		panic(err)
   557  	}
   558  
   559  	return string(b)
   560  }
   561  
   562  type ReposInfo interface {
   563  	ResolveRepoRef(repoRef string) (string, bool)
   564  	ResolveCommitIndex(repoRef string, index int) (string, bool)
   565  }
   566  
   567  func buildKptfile(pkg *pkg, pkgName string, reposInfo ReposInfo) string {
   568  	if pkg.Kptfile.Upstream != nil && len(pkg.Kptfile.Upstream.RepoRef) > 0 {
   569  		repoRef := pkg.Kptfile.Upstream.RepoRef
   570  		ref := pkg.Kptfile.Upstream.Ref
   571  		pkg.Kptfile.Upstream.Repo = resolveRepoRef(repoRef, reposInfo)
   572  
   573  		if newRef, ok := resolveCommitRef(repoRef, ref, reposInfo); ok {
   574  			pkg.Kptfile.Upstream.Ref = newRef
   575  		}
   576  	}
   577  	if pkg.Kptfile.UpstreamLock != nil && len(pkg.Kptfile.UpstreamLock.RepoRef) > 0 {
   578  		repoRef := pkg.Kptfile.UpstreamLock.RepoRef
   579  		ref := pkg.Kptfile.UpstreamLock.Ref
   580  		pkg.Kptfile.UpstreamLock.Repo = resolveRepoRef(repoRef, reposInfo)
   581  
   582  		index := pkg.Kptfile.UpstreamLock.Index
   583  		pkg.Kptfile.UpstreamLock.Commit = resolveCommitIndex(repoRef, index, reposInfo)
   584  
   585  		if newRef, ok := resolveCommitRef(repoRef, ref, reposInfo); ok {
   586  			pkg.Kptfile.UpstreamLock.Ref = newRef
   587  		}
   588  	}
   589  
   590  	kptfile := &kptfilev1.KptFile{}
   591  	kptfile.APIVersion, kptfile.Kind = kptfilev1.KptFileGVK().ToAPIVersionAndKind()
   592  	kptfile.ObjectMeta.Name = pkgName
   593  	if pkg.Kptfile.Upstream != nil {
   594  		kptfile.Upstream = &kptfilev1.Upstream{
   595  			Type: "git",
   596  			Git: &kptfilev1.Git{
   597  				Repo:      pkg.Kptfile.Upstream.Repo,
   598  				Directory: pkg.Kptfile.Upstream.Dir,
   599  				Ref:       pkg.Kptfile.Upstream.Ref,
   600  			},
   601  			UpdateStrategy: kptfilev1.UpdateStrategyType(pkg.Kptfile.Upstream.Strategy),
   602  		}
   603  	}
   604  	if pkg.Kptfile.UpstreamLock != nil {
   605  		kptfile.UpstreamLock = &kptfilev1.UpstreamLock{
   606  			Type: "git",
   607  			Git: &kptfilev1.GitLock{
   608  				Repo:      pkg.Kptfile.UpstreamLock.Repo,
   609  				Directory: pkg.Kptfile.UpstreamLock.Dir,
   610  				Ref:       pkg.Kptfile.UpstreamLock.Ref,
   611  				Commit:    pkg.Kptfile.UpstreamLock.Commit,
   612  			},
   613  		}
   614  	}
   615  	if pkg.Kptfile.Pipeline != nil {
   616  		kptfile.Pipeline = &kptfilev1.Pipeline{}
   617  		for _, fn := range pkg.Kptfile.Pipeline.Functions {
   618  			mutator := kptfilev1.Function{
   619  				Image: fn.Image,
   620  			}
   621  			if fn.ConfigPath != "" {
   622  				mutator.ConfigPath = fn.ConfigPath
   623  			}
   624  			kptfile.Pipeline.Mutators = append(kptfile.Pipeline.Mutators, mutator)
   625  		}
   626  	}
   627  
   628  	if inventory := pkg.Kptfile.Inventory; inventory != nil {
   629  		kptfile.Inventory = &kptfilev1.Inventory{}
   630  		if inventory.Name != "" {
   631  			kptfile.Inventory.Name = inventory.Name
   632  		}
   633  		if inventory.Namespace != "" {
   634  			kptfile.Inventory.Namespace = inventory.Namespace
   635  		}
   636  		if inventory.ID != "" {
   637  			kptfile.Inventory.InventoryID = inventory.ID
   638  		}
   639  	}
   640  	b, err := yaml.Marshal(kptfile)
   641  	if err != nil {
   642  		panic(err)
   643  	}
   644  	return string(b)
   645  }
   646  
   647  // resolveRepoRef looks up the repo path for a repo from the reposInfo
   648  // object based on the provided reference.
   649  func resolveRepoRef(repoRef string, reposInfo ReposInfo) string {
   650  	repo, found := reposInfo.ResolveRepoRef(repoRef)
   651  	if !found {
   652  		panic(fmt.Errorf("path for package %s not found", repoRef))
   653  	}
   654  	return repo
   655  }
   656  
   657  // resolveCommitIndex looks up the commit SHA for a specific commit in a repo.
   658  // It looks up the repo based on the provided repoRef and returns the commit for
   659  // the commit with the provided index.
   660  func resolveCommitIndex(repoRef string, index int, reposInfo ReposInfo) string {
   661  	commit, found := reposInfo.ResolveCommitIndex(repoRef, index)
   662  	if !found {
   663  		panic(fmt.Errorf("can't find commit for index %d in repo %s", index, repoRef))
   664  	}
   665  	return commit
   666  }
   667  
   668  // resolveCommitRef looks up the commit SHA for a commit with the index given
   669  // through a special string format as the ref. If the string value follows the
   670  // correct format, the commit will looked up from the repo given by the RepoRef
   671  // and returned with the second value being true. If the ref string does not
   672  // follow the correct format, the second return value will be false.
   673  func resolveCommitRef(repoRef, ref string, reposInfo ReposInfo) (string, bool) {
   674  	re := regexp.MustCompile(`^COMMIT-INDEX:([0-9]+)$`)
   675  	matches := re.FindStringSubmatch(ref)
   676  	if len(matches) != 2 {
   677  		return "", false
   678  	}
   679  	index, err := strconv.Atoi(matches[1])
   680  	if err != nil {
   681  		return "", false
   682  	}
   683  	return resolveCommitIndex(repoRef, index, reposInfo), true
   684  }
   685  
   686  // ExpandPkg writes the provided package to disk. The name of the root package
   687  // will just be set to "base".
   688  func (rp *RootPkg) ExpandPkg(t *testing.T, reposInfo ReposInfo) string {
   689  	return rp.ExpandPkgWithName(t, "base", reposInfo)
   690  }
   691  
   692  // ExpandPkgWithName writes the provided package to disk and uses the given
   693  // rootName to set the value of the package directory and the metadata.name
   694  // field of the root package.
   695  func (rp *RootPkg) ExpandPkgWithName(t *testing.T, rootName string, reposInfo ReposInfo) string {
   696  	dir, err := os.MkdirTemp("", "test-kpt-builder-")
   697  	if !assert.NoError(t, err) {
   698  		t.FailNow()
   699  	}
   700  	err = rp.Build(dir, rootName, reposInfo)
   701  	if !assert.NoError(t, err) {
   702  		t.FailNow()
   703  	}
   704  	return filepath.Join(dir, rootName)
   705  }