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

     1  package buildpack
     2  
     3  import (
     4  	"archive/tar"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/BurntSushi/toml"
    13  	"github.com/buildpacks/lifecycle/api"
    14  	"github.com/pkg/errors"
    15  
    16  	"github.com/buildpacks/pack/internal/style"
    17  	"github.com/buildpacks/pack/pkg/archive"
    18  	"github.com/buildpacks/pack/pkg/dist"
    19  )
    20  
    21  const (
    22  	KindBuildpack = "buildpack"
    23  	KindExtension = "extension"
    24  )
    25  
    26  //go:generate mockgen -package testmocks -destination ../testmocks/mock_build_module.go github.com/buildpacks/pack/pkg/buildpack BuildModule
    27  type BuildModule interface {
    28  	// Open returns a reader to a tar with contents structured as per the distribution spec
    29  	// (currently '/cnb/buildpacks/{ID}/{version}/*', all entries with a zeroed-out
    30  	// timestamp and root UID/GID).
    31  	Open() (io.ReadCloser, error)
    32  	Descriptor() Descriptor
    33  }
    34  
    35  type Descriptor interface {
    36  	API() *api.Version
    37  	EnsureStackSupport(stackID string, providedMixins []string, validateRunStageMixins bool) error
    38  	EnsureTargetSupport(os, arch, distroName, distroVersion string) error
    39  	EscapedID() string
    40  	Info() dist.ModuleInfo
    41  	Kind() string
    42  	Order() dist.Order
    43  	Stacks() []dist.Stack
    44  	Targets() []dist.Target
    45  }
    46  
    47  type Blob interface {
    48  	// Open returns a io.ReadCloser for the contents of the Blob in tar format.
    49  	Open() (io.ReadCloser, error)
    50  }
    51  
    52  type buildModule struct {
    53  	descriptor Descriptor
    54  	Blob       `toml:"-"`
    55  }
    56  
    57  func (b *buildModule) Descriptor() Descriptor {
    58  	return b.descriptor
    59  }
    60  
    61  // FromBlob constructs a buildpack or extension from a blob. It is assumed that the buildpack
    62  // contents are structured as per the distribution spec (currently '/cnb/buildpacks/{ID}/{version}/*' or
    63  // '/cnb/extensions/{ID}/{version}/*').
    64  func FromBlob(descriptor Descriptor, blob Blob) BuildModule {
    65  	return &buildModule{
    66  		Blob:       blob,
    67  		descriptor: descriptor,
    68  	}
    69  }
    70  
    71  // FromBuildpackRootBlob constructs a buildpack from a blob. It is assumed that the buildpack contents reside at the
    72  // root of the blob. The constructed buildpack contents will be structured as per the distribution spec (currently
    73  // a tar with contents under '/cnb/buildpacks/{ID}/{version}/*').
    74  func FromBuildpackRootBlob(blob Blob, layerWriterFactory archive.TarWriterFactory, logger Logger) (BuildModule, error) {
    75  	descriptor := dist.BuildpackDescriptor{}
    76  	descriptor.WithAPI = api.MustParse(dist.AssumedBuildpackAPIVersion)
    77  	undecodedKeys, err := readDescriptor(KindBuildpack, &descriptor, blob)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  	if len(undecodedKeys) > 0 {
    82  		logger.Warnf("Ignoring unexpected key(s) in descriptor for buildpack %s: %s", descriptor.EscapedID(), strings.Join(undecodedKeys, ","))
    83  	}
    84  	if err := detectPlatformSpecificValues(&descriptor, blob); err != nil {
    85  		return nil, err
    86  	}
    87  	if err := validateBuildpackDescriptor(descriptor); err != nil {
    88  		return nil, err
    89  	}
    90  	return buildpackFrom(&descriptor, blob, layerWriterFactory)
    91  }
    92  
    93  // FromExtensionRootBlob constructs an extension from a blob. It is assumed that the extension contents reside at the
    94  // root of the blob. The constructed extension contents will be structured as per the distribution spec (currently
    95  // a tar with contents under '/cnb/extensions/{ID}/{version}/*').
    96  func FromExtensionRootBlob(blob Blob, layerWriterFactory archive.TarWriterFactory, logger Logger) (BuildModule, error) {
    97  	descriptor := dist.ExtensionDescriptor{}
    98  	descriptor.WithAPI = api.MustParse(dist.AssumedBuildpackAPIVersion)
    99  	undecodedKeys, err := readDescriptor(KindExtension, &descriptor, blob)
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  	if len(undecodedKeys) > 0 {
   104  		logger.Warnf("Ignoring unexpected key(s) in descriptor for extension %s: %s", descriptor.EscapedID(), strings.Join(undecodedKeys, ","))
   105  	}
   106  	if err := validateExtensionDescriptor(descriptor); err != nil {
   107  		return nil, err
   108  	}
   109  	return buildpackFrom(&descriptor, blob, layerWriterFactory)
   110  }
   111  
   112  func readDescriptor(kind string, descriptor interface{}, blob Blob) (undecodedKeys []string, err error) {
   113  	rc, err := blob.Open()
   114  	if err != nil {
   115  		return undecodedKeys, errors.Wrapf(err, "open %s", kind)
   116  	}
   117  	defer rc.Close()
   118  
   119  	descriptorFile := kind + ".toml"
   120  
   121  	_, buf, err := archive.ReadTarEntry(rc, descriptorFile)
   122  	if err != nil {
   123  		return undecodedKeys, errors.Wrapf(err, "reading %s", descriptorFile)
   124  	}
   125  
   126  	md, err := toml.Decode(string(buf), descriptor)
   127  	if err != nil {
   128  		return undecodedKeys, errors.Wrapf(err, "decoding %s", descriptorFile)
   129  	}
   130  
   131  	undecoded := md.Undecoded()
   132  	for _, k := range undecoded {
   133  		undecodedKeys = append(undecodedKeys, k.String())
   134  	}
   135  
   136  	return undecodedKeys, nil
   137  }
   138  
   139  func detectPlatformSpecificValues(descriptor *dist.BuildpackDescriptor, blob Blob) error {
   140  	if val, err := hasFile(blob, path.Join("bin", "build")); val {
   141  		descriptor.WithLinuxBuild = true
   142  	} else if err != nil {
   143  		return err
   144  	}
   145  	if val, err := hasFile(blob, path.Join("bin", "build.bat")); val {
   146  		descriptor.WithWindowsBuild = true
   147  	} else if err != nil {
   148  		return err
   149  	}
   150  	if val, err := hasFile(blob, path.Join("bin", "build.exe")); val {
   151  		descriptor.WithWindowsBuild = true
   152  	} else if err != nil {
   153  		return err
   154  	}
   155  	return nil
   156  }
   157  
   158  func hasFile(blob Blob, file string) (bool, error) {
   159  	rc, err := blob.Open()
   160  	if err != nil {
   161  		return false, errors.Wrapf(err, "open %s", "buildpack bin/")
   162  	}
   163  	defer rc.Close()
   164  	_, _, err = archive.ReadTarEntry(rc, file)
   165  	return err == nil, nil
   166  }
   167  
   168  func buildpackFrom(descriptor Descriptor, blob Blob, layerWriterFactory archive.TarWriterFactory) (BuildModule, error) {
   169  	return &buildModule{
   170  		descriptor: descriptor,
   171  		Blob: &distBlob{
   172  			openFn: func() io.ReadCloser {
   173  				return archive.GenerateTarWithWriter(
   174  					func(tw archive.TarWriter) error {
   175  						return toDistTar(tw, descriptor, blob)
   176  					},
   177  					layerWriterFactory,
   178  				)
   179  			},
   180  		},
   181  	}, nil
   182  }
   183  
   184  type distBlob struct {
   185  	openFn func() io.ReadCloser
   186  }
   187  
   188  func (b *distBlob) Open() (io.ReadCloser, error) {
   189  	return b.openFn(), nil
   190  }
   191  
   192  func toDistTar(tw archive.TarWriter, descriptor Descriptor, blob Blob) error {
   193  	ts := archive.NormalizedDateTime
   194  
   195  	parentDir := dist.BuildpacksDir
   196  	if descriptor.Kind() == KindExtension {
   197  		parentDir = dist.ExtensionsDir
   198  	}
   199  
   200  	if err := tw.WriteHeader(&tar.Header{
   201  		Typeflag: tar.TypeDir,
   202  		Name:     path.Join(parentDir, descriptor.EscapedID()),
   203  		Mode:     0755,
   204  		ModTime:  ts,
   205  	}); err != nil {
   206  		return errors.Wrapf(err, "writing %s id dir header", descriptor.Kind())
   207  	}
   208  
   209  	baseTarDir := path.Join(parentDir, descriptor.EscapedID(), descriptor.Info().Version)
   210  	if err := tw.WriteHeader(&tar.Header{
   211  		Typeflag: tar.TypeDir,
   212  		Name:     baseTarDir,
   213  		Mode:     0755,
   214  		ModTime:  ts,
   215  	}); err != nil {
   216  		return errors.Wrapf(err, "writing %s version dir header", descriptor.Kind())
   217  	}
   218  
   219  	rc, err := blob.Open()
   220  	if err != nil {
   221  		return errors.Wrapf(err, "reading %s blob", descriptor.Kind())
   222  	}
   223  	defer rc.Close()
   224  
   225  	tr := tar.NewReader(rc)
   226  	for {
   227  		header, err := tr.Next()
   228  		if err == io.EOF {
   229  			break
   230  		}
   231  		if err != nil {
   232  			return errors.Wrap(err, "failed to get next tar entry")
   233  		}
   234  
   235  		archive.NormalizeHeader(header, true)
   236  		header.Name = path.Clean(header.Name)
   237  		if header.Name == "." || header.Name == "/" {
   238  			continue
   239  		}
   240  
   241  		header.Mode = calcFileMode(header)
   242  		header.Name = path.Join(baseTarDir, header.Name)
   243  
   244  		if header.Typeflag == tar.TypeLink {
   245  			header.Linkname = path.Join(baseTarDir, path.Clean(header.Linkname))
   246  		}
   247  		err = tw.WriteHeader(header)
   248  		if err != nil {
   249  			return errors.Wrapf(err, "failed to write header for '%s'", header.Name)
   250  		}
   251  
   252  		_, err = io.Copy(tw, tr)
   253  		if err != nil {
   254  			return errors.Wrapf(err, "failed to write contents to '%s'", header.Name)
   255  		}
   256  	}
   257  
   258  	return nil
   259  }
   260  
   261  func calcFileMode(header *tar.Header) int64 {
   262  	switch {
   263  	case header.Typeflag == tar.TypeDir:
   264  		return 0755
   265  	case nameOneOf(header.Name,
   266  		path.Join("bin", "build"),
   267  		path.Join("bin", "detect"),
   268  		path.Join("bin", "generate"),
   269  	):
   270  		return 0755
   271  	case anyExecBit(header.Mode):
   272  		return 0755
   273  	}
   274  
   275  	return 0644
   276  }
   277  
   278  func nameOneOf(name string, paths ...string) bool {
   279  	for _, p := range paths {
   280  		if name == p {
   281  			return true
   282  		}
   283  	}
   284  	return false
   285  }
   286  
   287  func anyExecBit(mode int64) bool {
   288  	return mode&0111 != 0
   289  }
   290  
   291  func validateBuildpackDescriptor(bpd dist.BuildpackDescriptor) error {
   292  	if bpd.Info().ID == "" {
   293  		return errors.Errorf("%s is required", style.Symbol("buildpack.id"))
   294  	}
   295  
   296  	if bpd.Info().Version == "" {
   297  		return errors.Errorf("%s is required", style.Symbol("buildpack.version"))
   298  	}
   299  
   300  	if len(bpd.Order()) >= 1 && (len(bpd.Stacks()) >= 1 || len(bpd.Targets()) >= 1) {
   301  		return errors.Errorf(
   302  			"buildpack %s: cannot have both %s/%s and an %s defined",
   303  			style.Symbol(bpd.Info().FullName()),
   304  			style.Symbol("targets"),
   305  			style.Symbol("stacks"),
   306  			style.Symbol("order"),
   307  		)
   308  	}
   309  
   310  	return nil
   311  }
   312  
   313  func validateExtensionDescriptor(extd dist.ExtensionDescriptor) error {
   314  	if extd.Info().ID == "" {
   315  		return errors.Errorf("%s is required", style.Symbol("extension.id"))
   316  	}
   317  
   318  	if extd.Info().Version == "" {
   319  		return errors.Errorf("%s is required", style.Symbol("extension.version"))
   320  	}
   321  
   322  	return nil
   323  }
   324  
   325  func ToLayerTar(dest string, module BuildModule) (string, error) {
   326  	descriptor := module.Descriptor()
   327  	modReader, err := module.Open()
   328  	if err != nil {
   329  		return "", errors.Wrap(err, "opening blob")
   330  	}
   331  	defer modReader.Close()
   332  
   333  	layerTar := filepath.Join(dest, fmt.Sprintf("%s.%s.tar", descriptor.EscapedID(), descriptor.Info().Version))
   334  	fh, err := os.Create(layerTar)
   335  	if err != nil {
   336  		return "", errors.Wrap(err, "create file for tar")
   337  	}
   338  	defer fh.Close()
   339  
   340  	if _, err := io.Copy(fh, modReader); err != nil {
   341  		return "", errors.Wrap(err, "writing blob to tar")
   342  	}
   343  
   344  	return layerTar, nil
   345  }
   346  
   347  func ToNLayerTar(dest string, module BuildModule) ([]ModuleTar, error) {
   348  	modReader, err := module.Open()
   349  	if err != nil {
   350  		return nil, errors.Wrap(err, "opening blob")
   351  	}
   352  	defer modReader.Close()
   353  
   354  	tarCollection := newModuleTarCollection(dest)
   355  	tr := tar.NewReader(modReader)
   356  
   357  	var (
   358  		header     *tar.Header
   359  		forWindows bool
   360  	)
   361  
   362  	for {
   363  		header, err = tr.Next()
   364  		if err != nil {
   365  			if err == io.EOF {
   366  				return handleEmptyModule(dest, module)
   367  			}
   368  			return nil, err
   369  		}
   370  		if _, err := sanitizePath(header.Name); err != nil {
   371  			return nil, err
   372  		}
   373  		if header.Name == "Files" {
   374  			forWindows = true
   375  		}
   376  		if strings.Contains(header.Name, `/cnb/buildpacks/`) || strings.Contains(header.Name, `\cnb\buildpacks\`) {
   377  			// Only for Windows, the first four headers are:
   378  			// - Files
   379  			// - Hives
   380  			// - Files/cnb
   381  			// - Files/cnb/buildpacks
   382  			// Skip over these until we find "Files/cnb/buildpacks/<buildpack-id>":
   383  			break
   384  		}
   385  	}
   386  	// The header should look like "/cnb/buildpacks/<buildpack-id>"
   387  	// The version should be blank because the first header is missing <buildpack-version>.
   388  	origID, origVersion := parseBpIDAndVersion(header)
   389  	if origVersion != "" {
   390  		return nil, fmt.Errorf("first header '%s' contained unexpected version", header.Name)
   391  	}
   392  
   393  	if err := toNLayerTar(origID, origVersion, header, tr, tarCollection, forWindows); err != nil {
   394  		return nil, err
   395  	}
   396  
   397  	errs := tarCollection.close()
   398  	if len(errs) > 0 {
   399  		return nil, errors.New("closing files")
   400  	}
   401  
   402  	return tarCollection.moduleTars(), nil
   403  }
   404  
   405  func toNLayerTar(origID, origVersion string, firstHeader *tar.Header, tr *tar.Reader, tc *moduleTarCollection, forWindows bool) error {
   406  	toWrite := []*tar.Header{firstHeader}
   407  	if origVersion == "" {
   408  		// the first header only contains the id - e.g., /cnb/buildpacks/<buildpack-id>,
   409  		// read the next header to get the version
   410  		secondHeader, err := tr.Next()
   411  		if err != nil {
   412  			return fmt.Errorf("getting second header: %w; first header was %s", err, firstHeader.Name)
   413  		}
   414  		if _, err := sanitizePath(secondHeader.Name); err != nil {
   415  			return err
   416  		}
   417  		nextID, nextVersion := parseBpIDAndVersion(secondHeader)
   418  		if nextID != origID || nextVersion == "" {
   419  			return fmt.Errorf("second header '%s' contained unexpected id or missing version", secondHeader.Name)
   420  		}
   421  		origVersion = nextVersion
   422  		toWrite = append(toWrite, secondHeader)
   423  	} else {
   424  		// the first header contains id and version - e.g., /cnb/buildpacks/<buildpack-id>/<buildpack-version>,
   425  		// we need to write the parent header - e.g., /cnb/buildpacks/<buildpack-id>
   426  		realFirstHeader := *firstHeader
   427  		realFirstHeader.Name = filepath.ToSlash(filepath.Dir(firstHeader.Name))
   428  		toWrite = append([]*tar.Header{&realFirstHeader}, toWrite...)
   429  	}
   430  	if forWindows {
   431  		toWrite = append(windowsPreamble(), toWrite...)
   432  	}
   433  	mt, err := tc.get(origID, origVersion)
   434  	if err != nil {
   435  		return fmt.Errorf("getting module from collection: %w", err)
   436  	}
   437  	for _, h := range toWrite {
   438  		if err := mt.writer.WriteHeader(h); err != nil {
   439  			return fmt.Errorf("failed to write header '%s': %w", h.Name, err)
   440  		}
   441  	}
   442  	// write the rest of the package
   443  	var header *tar.Header
   444  	for {
   445  		header, err = tr.Next()
   446  		if err != nil {
   447  			if err == io.EOF {
   448  				return nil
   449  			}
   450  			return fmt.Errorf("getting next header: %w", err)
   451  		}
   452  		if _, err := sanitizePath(header.Name); err != nil {
   453  			return err
   454  		}
   455  		nextID, nextVersion := parseBpIDAndVersion(header)
   456  		if nextID != origID || nextVersion != origVersion {
   457  			// we found a new module, recurse
   458  			return toNLayerTar(nextID, nextVersion, header, tr, tc, forWindows)
   459  		}
   460  
   461  		err = mt.writer.WriteHeader(header)
   462  		if err != nil {
   463  			return fmt.Errorf("failed to write header for '%s': %w", header.Name, err)
   464  		}
   465  
   466  		_, err = io.Copy(mt.writer, tr)
   467  		if err != nil {
   468  			return errors.Wrapf(err, "failed to write contents to '%s'", header.Name)
   469  		}
   470  	}
   471  }
   472  
   473  func sanitizePath(path string) (string, error) {
   474  	if strings.Contains(path, "..") {
   475  		return "", fmt.Errorf("path %s contains unexpected special elements", path)
   476  	}
   477  	return path, nil
   478  }
   479  
   480  func windowsPreamble() []*tar.Header {
   481  	return []*tar.Header{
   482  		{
   483  			Name:     "Files",
   484  			Typeflag: tar.TypeDir,
   485  		},
   486  		{
   487  			Name:     "Hives",
   488  			Typeflag: tar.TypeDir,
   489  		},
   490  		{
   491  			Name:     "Files/cnb",
   492  			Typeflag: tar.TypeDir,
   493  		},
   494  		{
   495  			Name:     "Files/cnb/buildpacks",
   496  			Typeflag: tar.TypeDir,
   497  		},
   498  	}
   499  }
   500  
   501  func parseBpIDAndVersion(hdr *tar.Header) (id, version string) {
   502  	// splitting "/cnb/buildpacks/{ID}/{version}/*" returns
   503  	// [0] = "" -> first element is empty or "Files" in windows
   504  	// [1] = "cnb"
   505  	// [2] = "buildpacks"
   506  	// [3] = "{ID}"
   507  	// [4] = "{version}"
   508  	// ...
   509  	parts := strings.Split(strings.ReplaceAll(filepath.Clean(hdr.Name), `\`, `/`), `/`)
   510  	size := len(parts)
   511  	switch {
   512  	case size < 4:
   513  		// error
   514  	case size == 4:
   515  		id = parts[3]
   516  	case size >= 5:
   517  		id = parts[3]
   518  		version = parts[4]
   519  	}
   520  	return id, version
   521  }
   522  
   523  func handleEmptyModule(dest string, module BuildModule) ([]ModuleTar, error) {
   524  	tarFile, err := ToLayerTar(dest, module)
   525  	if err != nil {
   526  		return nil, err
   527  	}
   528  	layerTar := &moduleTar{
   529  		info: module.Descriptor().Info(),
   530  		path: tarFile,
   531  	}
   532  	return []ModuleTar{layerTar}, nil
   533  }
   534  
   535  // Set returns a set of the given string slice.
   536  func Set(exclude []string) map[string]struct{} {
   537  	type void struct{}
   538  	var member void
   539  	var excludedModules = make(map[string]struct{})
   540  	for _, fullName := range exclude {
   541  		excludedModules[fullName] = member
   542  	}
   543  	return excludedModules
   544  }
   545  
   546  type ModuleTar interface {
   547  	Info() dist.ModuleInfo
   548  	Path() string
   549  }
   550  
   551  type moduleTar struct {
   552  	info   dist.ModuleInfo
   553  	path   string
   554  	writer archive.TarWriter
   555  }
   556  
   557  func (t *moduleTar) Info() dist.ModuleInfo {
   558  	return t.info
   559  }
   560  
   561  func (t *moduleTar) Path() string {
   562  	return t.path
   563  }
   564  
   565  func newModuleTar(dest, id, version string) (moduleTar, error) {
   566  	layerTar := filepath.Join(dest, fmt.Sprintf("%s.%s.tar", id, version))
   567  	fh, err := os.Create(layerTar)
   568  	if err != nil {
   569  		return moduleTar{}, errors.Wrapf(err, "creating file at path %s", layerTar)
   570  	}
   571  	return moduleTar{
   572  		info: dist.ModuleInfo{
   573  			ID:      id,
   574  			Version: version,
   575  		},
   576  		path:   layerTar,
   577  		writer: tar.NewWriter(fh),
   578  	}, nil
   579  }
   580  
   581  type moduleTarCollection struct {
   582  	rootPath string
   583  	modules  map[string]moduleTar
   584  }
   585  
   586  func newModuleTarCollection(rootPath string) *moduleTarCollection {
   587  	return &moduleTarCollection{
   588  		rootPath: rootPath,
   589  		modules:  map[string]moduleTar{},
   590  	}
   591  }
   592  
   593  func (m *moduleTarCollection) get(id, version string) (moduleTar, error) {
   594  	key := fmt.Sprintf("%s@%s", id, version)
   595  	if _, ok := m.modules[key]; !ok {
   596  		module, err := newModuleTar(m.rootPath, id, version)
   597  		if err != nil {
   598  			return moduleTar{}, err
   599  		}
   600  		m.modules[key] = module
   601  	}
   602  	return m.modules[key], nil
   603  }
   604  
   605  func (m *moduleTarCollection) moduleTars() []ModuleTar {
   606  	var modulesTar []ModuleTar
   607  	for _, v := range m.modules {
   608  		v := v
   609  		vv := &v
   610  		modulesTar = append(modulesTar, vv)
   611  	}
   612  	return modulesTar
   613  }
   614  
   615  func (m *moduleTarCollection) close() []error {
   616  	var errors []error
   617  	for _, v := range m.modules {
   618  		err := v.writer.Close()
   619  		if err != nil {
   620  			errors = append(errors, err)
   621  		}
   622  	}
   623  	return errors
   624  }