github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/pkg/buildpack/builder.go (about)

     1  package buildpack
     2  
     3  import (
     4  	"archive/tar"
     5  	"compress/gzip"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  	"strconv"
    11  
    12  	"github.com/buildpacks/imgutil"
    13  	"github.com/buildpacks/imgutil/layer"
    14  	v1 "github.com/google/go-containerregistry/pkg/v1"
    15  	"github.com/google/go-containerregistry/pkg/v1/empty"
    16  	"github.com/google/go-containerregistry/pkg/v1/layout"
    17  	"github.com/google/go-containerregistry/pkg/v1/mutate"
    18  	"github.com/google/go-containerregistry/pkg/v1/tarball"
    19  	"github.com/pkg/errors"
    20  
    21  	"github.com/buildpacks/pack/pkg/logging"
    22  
    23  	"github.com/buildpacks/pack/internal/stack"
    24  	"github.com/buildpacks/pack/internal/style"
    25  	"github.com/buildpacks/pack/pkg/archive"
    26  	"github.com/buildpacks/pack/pkg/dist"
    27  )
    28  
    29  type ImageFactory interface {
    30  	NewImage(repoName string, local bool, imageOS string) (imgutil.Image, error)
    31  }
    32  
    33  type WorkableImage interface {
    34  	SetLabel(string, string) error
    35  	AddLayerWithDiffID(path, diffID string) error
    36  }
    37  
    38  type layoutImage struct {
    39  	v1.Image
    40  }
    41  
    42  type toAdd struct {
    43  	tarPath string
    44  	diffID  string
    45  	module  BuildModule
    46  }
    47  
    48  func (i *layoutImage) SetLabel(key string, val string) error {
    49  	configFile, err := i.ConfigFile()
    50  	if err != nil {
    51  		return err
    52  	}
    53  	config := *configFile.Config.DeepCopy()
    54  	if config.Labels == nil {
    55  		config.Labels = map[string]string{}
    56  	}
    57  	config.Labels[key] = val
    58  	i.Image, err = mutate.Config(i.Image, config)
    59  	return err
    60  }
    61  
    62  func (i *layoutImage) AddLayerWithDiffID(path, _ string) error {
    63  	tarLayer, err := tarball.LayerFromFile(path, tarball.WithCompressionLevel(gzip.DefaultCompression))
    64  	if err != nil {
    65  		return err
    66  	}
    67  	i.Image, err = mutate.AppendLayers(i.Image, tarLayer)
    68  	if err != nil {
    69  		return errors.Wrap(err, "add layer")
    70  	}
    71  	return nil
    72  }
    73  
    74  type PackageBuilderOption func(*options) error
    75  
    76  type options struct {
    77  	flatten bool
    78  	exclude []string
    79  	logger  logging.Logger
    80  	factory archive.TarWriterFactory
    81  }
    82  
    83  type PackageBuilder struct {
    84  	buildpack                BuildModule
    85  	extension                BuildModule
    86  	logger                   logging.Logger
    87  	layerWriterFactory       archive.TarWriterFactory
    88  	dependencies             ManagedCollection
    89  	imageFactory             ImageFactory
    90  	flattenAllBuildpacks     bool
    91  	flattenExcludeBuildpacks []string
    92  }
    93  
    94  // TODO: Rename to PackageBuilder
    95  func NewBuilder(imageFactory ImageFactory, ops ...PackageBuilderOption) *PackageBuilder {
    96  	opts := &options{}
    97  	for _, op := range ops {
    98  		if err := op(opts); err != nil {
    99  			return nil
   100  		}
   101  	}
   102  	moduleManager := NewManagedCollectionV1(opts.flatten)
   103  	return &PackageBuilder{
   104  		imageFactory:             imageFactory,
   105  		dependencies:             moduleManager,
   106  		flattenAllBuildpacks:     opts.flatten,
   107  		flattenExcludeBuildpacks: opts.exclude,
   108  		logger:                   opts.logger,
   109  		layerWriterFactory:       opts.factory,
   110  	}
   111  }
   112  
   113  func FlattenAll() PackageBuilderOption {
   114  	return func(o *options) error {
   115  		o.flatten = true
   116  		return nil
   117  	}
   118  }
   119  
   120  func DoNotFlatten(exclude []string) PackageBuilderOption {
   121  	return func(o *options) error {
   122  		o.flatten = true
   123  		o.exclude = exclude
   124  		return nil
   125  	}
   126  }
   127  
   128  func WithLogger(logger logging.Logger) PackageBuilderOption {
   129  	return func(o *options) error {
   130  		o.logger = logger
   131  		return nil
   132  	}
   133  }
   134  
   135  func WithLayerWriterFactory(factory archive.TarWriterFactory) PackageBuilderOption {
   136  	return func(o *options) error {
   137  		o.factory = factory
   138  		return nil
   139  	}
   140  }
   141  
   142  func (b *PackageBuilder) SetBuildpack(buildpack BuildModule) {
   143  	b.buildpack = buildpack
   144  }
   145  func (b *PackageBuilder) SetExtension(extension BuildModule) {
   146  	b.extension = extension
   147  }
   148  
   149  func (b *PackageBuilder) AddDependency(buildpack BuildModule) {
   150  	b.dependencies.AddModules(buildpack)
   151  }
   152  
   153  func (b *PackageBuilder) AddDependencies(main BuildModule, dependencies []BuildModule) {
   154  	b.dependencies.AddModules(main, dependencies...)
   155  }
   156  
   157  func (b *PackageBuilder) ShouldFlatten(module BuildModule) bool {
   158  	return b.flattenAllBuildpacks || (b.dependencies.ShouldFlatten(module))
   159  }
   160  
   161  func (b *PackageBuilder) FlattenedModules() [][]BuildModule {
   162  	return b.dependencies.FlattenedModules()
   163  }
   164  
   165  func (b *PackageBuilder) AllModules() []BuildModule {
   166  	all := b.dependencies.ExplodedModules()
   167  	for _, modules := range b.dependencies.FlattenedModules() {
   168  		all = append(all, modules...)
   169  	}
   170  	return all
   171  }
   172  
   173  func (b *PackageBuilder) finalizeImage(image WorkableImage, tmpDir string) error {
   174  	if err := dist.SetLabel(image, MetadataLabel, &Metadata{
   175  		ModuleInfo: b.buildpack.Descriptor().Info(),
   176  		Stacks:     b.resolvedStacks(),
   177  	}); err != nil {
   178  		return err
   179  	}
   180  
   181  	collectionToAdd := map[string]toAdd{}
   182  	var individualBuildModules []BuildModule
   183  
   184  	// Let's create the tarball for each flatten module
   185  	if len(b.FlattenedModules()) > 0 {
   186  		buildModuleWriter := NewBuildModuleWriter(b.logger, b.layerWriterFactory)
   187  		excludedModules := Set(b.flattenExcludeBuildpacks)
   188  
   189  		var (
   190  			finalTarPath string
   191  			err          error
   192  		)
   193  		for i, additionalModules := range b.FlattenedModules() {
   194  			modFlattenTmpDir := filepath.Join(tmpDir, fmt.Sprintf("buildpack-%s-flatten", strconv.Itoa(i)))
   195  			if err := os.MkdirAll(modFlattenTmpDir, os.ModePerm); err != nil {
   196  				return errors.Wrap(err, "creating flatten temp dir")
   197  			}
   198  
   199  			if b.flattenAllBuildpacks {
   200  				// include the buildpack itself
   201  				additionalModules = append(additionalModules, b.buildpack)
   202  			}
   203  			finalTarPath, individualBuildModules, err = buildModuleWriter.NToLayerTar(modFlattenTmpDir, fmt.Sprintf("buildpack-flatten-%s", strconv.Itoa(i)), additionalModules, excludedModules)
   204  			if err != nil {
   205  				return errors.Wrapf(err, "adding layer %s", finalTarPath)
   206  			}
   207  
   208  			diffID, err := dist.LayerDiffID(finalTarPath)
   209  			if err != nil {
   210  				return errors.Wrapf(err, "calculating diffID for layer %s", finalTarPath)
   211  			}
   212  
   213  			for _, module := range additionalModules {
   214  				collectionToAdd[module.Descriptor().Info().FullName()] = toAdd{
   215  					tarPath: finalTarPath,
   216  					diffID:  diffID.String(),
   217  					module:  module,
   218  				}
   219  			}
   220  		}
   221  	}
   222  
   223  	if !b.flattenAllBuildpacks || len(b.FlattenedModules()) == 0 {
   224  		individualBuildModules = append(individualBuildModules, b.buildpack)
   225  	}
   226  
   227  	// Let's create the tarball for each individual module
   228  	for _, bp := range append(b.dependencies.ExplodedModules(), individualBuildModules...) {
   229  		bpLayerTar, err := ToLayerTar(tmpDir, bp)
   230  		if err != nil {
   231  			return err
   232  		}
   233  
   234  		diffID, err := dist.LayerDiffID(bpLayerTar)
   235  		if err != nil {
   236  			return errors.Wrapf(err,
   237  				"getting content hashes for buildpack %s",
   238  				style.Symbol(bp.Descriptor().Info().FullName()),
   239  			)
   240  		}
   241  		collectionToAdd[bp.Descriptor().Info().FullName()] = toAdd{
   242  			tarPath: bpLayerTar,
   243  			diffID:  diffID.String(),
   244  			module:  bp,
   245  		}
   246  	}
   247  
   248  	bpLayers := dist.ModuleLayers{}
   249  	diffIDAdded := map[string]string{}
   250  
   251  	for key := range collectionToAdd {
   252  		module := collectionToAdd[key]
   253  		bp := module.module
   254  		addLayer := true
   255  		if b.ShouldFlatten(bp) {
   256  			if _, ok := diffIDAdded[module.diffID]; !ok {
   257  				diffIDAdded[module.diffID] = module.tarPath
   258  			} else {
   259  				addLayer = false
   260  			}
   261  		}
   262  		if addLayer {
   263  			if err := image.AddLayerWithDiffID(module.tarPath, module.diffID); err != nil {
   264  				return errors.Wrapf(err, "adding layer tar for buildpack %s", style.Symbol(bp.Descriptor().Info().FullName()))
   265  			}
   266  		}
   267  
   268  		dist.AddToLayersMD(bpLayers, bp.Descriptor(), module.diffID)
   269  	}
   270  
   271  	if err := dist.SetLabel(image, dist.BuildpackLayersLabel, bpLayers); err != nil {
   272  		return err
   273  	}
   274  
   275  	return nil
   276  }
   277  
   278  func (b *PackageBuilder) finalizeExtensionImage(image WorkableImage, tmpDir string) error {
   279  	if err := dist.SetLabel(image, MetadataLabel, &Metadata{
   280  		ModuleInfo: b.extension.Descriptor().Info(),
   281  	}); err != nil {
   282  		return err
   283  	}
   284  
   285  	exLayers := dist.ModuleLayers{}
   286  	exLayerTar, err := ToLayerTar(tmpDir, b.extension)
   287  	if err != nil {
   288  		return err
   289  	}
   290  
   291  	diffID, err := dist.LayerDiffID(exLayerTar)
   292  	if err != nil {
   293  		return errors.Wrapf(err,
   294  			"getting content hashes for extension %s",
   295  			style.Symbol(b.extension.Descriptor().Info().FullName()),
   296  		)
   297  	}
   298  
   299  	if err := image.AddLayerWithDiffID(exLayerTar, diffID.String()); err != nil {
   300  		return errors.Wrapf(err, "adding layer tar for extension %s", style.Symbol(b.extension.Descriptor().Info().FullName()))
   301  	}
   302  
   303  	dist.AddToLayersMD(exLayers, b.extension.Descriptor(), diffID.String())
   304  
   305  	if err := dist.SetLabel(image, dist.ExtensionLayersLabel, exLayers); err != nil {
   306  		return err
   307  	}
   308  
   309  	return nil
   310  }
   311  
   312  func (b *PackageBuilder) validate() error {
   313  	if b.buildpack == nil && b.extension == nil {
   314  		return errors.New("buildpack or extension must be set")
   315  	}
   316  
   317  	// we don't need to validate extensions because there are no order or stacks in extensions
   318  	if b.buildpack != nil && b.extension == nil {
   319  		if err := validateBuildpacks(b.buildpack, b.AllModules()); err != nil {
   320  			return err
   321  		}
   322  
   323  		if len(b.resolvedStacks()) == 0 {
   324  			return errors.Errorf("no compatible stacks among provided buildpacks")
   325  		}
   326  	}
   327  
   328  	return nil
   329  }
   330  
   331  func (b *PackageBuilder) resolvedStacks() []dist.Stack {
   332  	stacks := b.buildpack.Descriptor().Stacks()
   333  	if len(stacks) == 0 && len(b.buildpack.Descriptor().Order()) == 0 {
   334  		// For non-meta-buildpacks using targets, not stacks: assume any stack
   335  		stacks = append(stacks, dist.Stack{ID: "*"})
   336  	}
   337  	for _, bp := range b.AllModules() {
   338  		bpd := bp.Descriptor()
   339  		bpdStacks := bp.Descriptor().Stacks()
   340  		if len(bpdStacks) == 0 && len(bpd.Order()) == 0 {
   341  			// For non-meta-buildpacks using targets, not stacks: assume any stack
   342  			bpdStacks = append(bpdStacks, dist.Stack{ID: "*"})
   343  		}
   344  
   345  		if len(stacks) == 0 {
   346  			stacks = bpdStacks
   347  		} else if len(bpdStacks) > 0 { // skip over "meta-buildpacks"
   348  			stacks = stack.MergeCompatible(stacks, bpdStacks)
   349  		}
   350  	}
   351  
   352  	return stacks
   353  }
   354  
   355  func (b *PackageBuilder) SaveAsFile(path, imageOS string, labels map[string]string) error {
   356  	if err := b.validate(); err != nil {
   357  		return err
   358  	}
   359  
   360  	layoutImage, err := newLayoutImage(imageOS)
   361  	if err != nil {
   362  		return errors.Wrap(err, "creating layout image")
   363  	}
   364  
   365  	for labelKey, labelValue := range labels {
   366  		err = layoutImage.SetLabel(labelKey, labelValue)
   367  		if err != nil {
   368  			return errors.Wrapf(err, "adding label %s=%s", labelKey, labelValue)
   369  		}
   370  	}
   371  
   372  	tempDirName := ""
   373  	if b.buildpack != nil {
   374  		tempDirName = "package-buildpack"
   375  	} else if b.extension != nil {
   376  		tempDirName = "extension-buildpack"
   377  	}
   378  
   379  	tmpDir, err := os.MkdirTemp("", tempDirName)
   380  	if err != nil {
   381  		return err
   382  	}
   383  	defer os.RemoveAll(tmpDir)
   384  
   385  	if b.buildpack != nil {
   386  		if err := b.finalizeImage(layoutImage, tmpDir); err != nil {
   387  			return err
   388  		}
   389  	} else if b.extension != nil {
   390  		if err := b.finalizeExtensionImage(layoutImage, tmpDir); err != nil {
   391  			return err
   392  		}
   393  	}
   394  	layoutDir, err := os.MkdirTemp(tmpDir, "oci-layout")
   395  	if err != nil {
   396  		return errors.Wrap(err, "creating oci-layout temp dir")
   397  	}
   398  
   399  	p, err := layout.Write(layoutDir, empty.Index)
   400  	if err != nil {
   401  		return errors.Wrap(err, "writing index")
   402  	}
   403  
   404  	if err := p.AppendImage(layoutImage); err != nil {
   405  		return errors.Wrap(err, "writing layout")
   406  	}
   407  
   408  	outputFile, err := os.Create(path)
   409  	if err != nil {
   410  		return errors.Wrap(err, "creating output file")
   411  	}
   412  	defer outputFile.Close()
   413  
   414  	tw := tar.NewWriter(outputFile)
   415  	defer tw.Close()
   416  
   417  	return archive.WriteDirToTar(tw, layoutDir, "/", 0, 0, 0755, true, false, nil)
   418  }
   419  
   420  func newLayoutImage(imageOS string) (*layoutImage, error) {
   421  	i := empty.Image
   422  
   423  	configFile, err := i.ConfigFile()
   424  	if err != nil {
   425  		return nil, err
   426  	}
   427  
   428  	configFile.OS = imageOS
   429  	i, err = mutate.ConfigFile(i, configFile)
   430  	if err != nil {
   431  		return nil, err
   432  	}
   433  
   434  	if imageOS == "windows" {
   435  		opener := func() (io.ReadCloser, error) {
   436  			reader, err := layer.WindowsBaseLayer()
   437  			return io.NopCloser(reader), err
   438  		}
   439  
   440  		baseLayer, err := tarball.LayerFromOpener(opener, tarball.WithCompressionLevel(gzip.DefaultCompression))
   441  		if err != nil {
   442  			return nil, err
   443  		}
   444  
   445  		i, err = mutate.AppendLayers(i, baseLayer)
   446  		if err != nil {
   447  			return nil, err
   448  		}
   449  	}
   450  
   451  	return &layoutImage{Image: i}, nil
   452  }
   453  
   454  func (b *PackageBuilder) SaveAsImage(repoName string, publish bool, imageOS string, labels map[string]string) (imgutil.Image, error) {
   455  	if err := b.validate(); err != nil {
   456  		return nil, err
   457  	}
   458  
   459  	image, err := b.imageFactory.NewImage(repoName, !publish, imageOS)
   460  	if err != nil {
   461  		return nil, errors.Wrapf(err, "creating image")
   462  	}
   463  
   464  	for labelKey, labelValue := range labels {
   465  		err = image.SetLabel(labelKey, labelValue)
   466  		if err != nil {
   467  			return nil, errors.Wrapf(err, "adding label %s=%s", labelKey, labelValue)
   468  		}
   469  	}
   470  
   471  	tempDirName := ""
   472  	if b.buildpack != nil {
   473  		tempDirName = "package-buildpack"
   474  	} else if b.extension != nil {
   475  		tempDirName = "extension-buildpack"
   476  	}
   477  
   478  	tmpDir, err := os.MkdirTemp("", tempDirName)
   479  	if err != nil {
   480  		return nil, err
   481  	}
   482  	defer os.RemoveAll(tmpDir)
   483  	if b.buildpack != nil {
   484  		if err := b.finalizeImage(image, tmpDir); err != nil {
   485  			return nil, err
   486  		}
   487  	} else if b.extension != nil {
   488  		if err := b.finalizeExtensionImage(image, tmpDir); err != nil {
   489  			return nil, err
   490  		}
   491  	}
   492  
   493  	if err := image.Save(); err != nil {
   494  		return nil, err
   495  	}
   496  
   497  	return image, nil
   498  }
   499  
   500  func validateBuildpacks(mainBP BuildModule, depBPs []BuildModule) error {
   501  	depsWithRefs := map[string][]dist.ModuleInfo{}
   502  
   503  	for _, bp := range depBPs {
   504  		depsWithRefs[bp.Descriptor().Info().FullName()] = nil
   505  	}
   506  
   507  	for _, bp := range append([]BuildModule{mainBP}, depBPs...) { // List of everything
   508  		bpd := bp.Descriptor()
   509  		for _, orderEntry := range bpd.Order() {
   510  			for _, groupEntry := range orderEntry.Group {
   511  				bpFullName, err := groupEntry.ModuleInfo.FullNameWithVersion()
   512  				if err != nil {
   513  					return errors.Wrapf(
   514  						err,
   515  						"buildpack %s must specify a version when referencing buildpack %s",
   516  						style.Symbol(bpd.Info().FullName()),
   517  						style.Symbol(bpFullName),
   518  					)
   519  				}
   520  				if _, ok := depsWithRefs[bpFullName]; !ok {
   521  					return errors.Errorf(
   522  						"buildpack %s references buildpack %s which is not present",
   523  						style.Symbol(bpd.Info().FullName()),
   524  						style.Symbol(bpFullName),
   525  					)
   526  				}
   527  
   528  				depsWithRefs[bpFullName] = append(depsWithRefs[bpFullName], bpd.Info())
   529  			}
   530  		}
   531  	}
   532  
   533  	for bp, refs := range depsWithRefs {
   534  		if len(refs) == 0 {
   535  			return errors.Errorf(
   536  				"buildpack %s is not used by buildpack %s",
   537  				style.Symbol(bp),
   538  				style.Symbol(mainBP.Descriptor().Info().FullName()),
   539  			)
   540  		}
   541  	}
   542  
   543  	return nil
   544  }