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