github.com/YousefHaggyHeroku/pack@v1.5.5/internal/dist/buildpack.go (about)

     1  package dist
     2  
     3  import (
     4  	"archive/tar"
     5  	"io"
     6  	"path"
     7  
     8  	"github.com/BurntSushi/toml"
     9  	"github.com/buildpacks/lifecycle/api"
    10  	"github.com/pkg/errors"
    11  
    12  	"github.com/YousefHaggyHeroku/pack/internal/archive"
    13  	"github.com/YousefHaggyHeroku/pack/internal/style"
    14  )
    15  
    16  const AssumedBuildpackAPIVersion = "0.1"
    17  const BuildpacksDir = "/cnb/buildpacks"
    18  
    19  type Blob interface {
    20  	// Open returns a io.ReadCloser for the contents of the Blob in tar format.
    21  	Open() (io.ReadCloser, error)
    22  }
    23  
    24  type buildpack struct {
    25  	descriptor BuildpackDescriptor
    26  	Blob       `toml:"-"`
    27  }
    28  
    29  func (b *buildpack) Descriptor() BuildpackDescriptor {
    30  	return b.descriptor
    31  }
    32  
    33  //go:generate mockgen -package testmocks -destination testmocks/mock_buildpack.go github.com/YousefHaggyHeroku/pack/internal/dist Buildpack
    34  type Buildpack interface {
    35  	// Open returns a reader to a tar with contents structured as per the distribution spec
    36  	// (currently '/cnbs/buildpacks/{ID}/{version}/*', all entries with a zeroed-out
    37  	// timestamp and root UID/GID).
    38  	Open() (io.ReadCloser, error)
    39  	Descriptor() BuildpackDescriptor
    40  }
    41  
    42  type BuildpackInfo struct {
    43  	ID       string `toml:"id,omitempty" json:"id,omitempty" yaml:"id,omitempty"`
    44  	Version  string `toml:"version,omitempty" json:"version,omitempty" yaml:"version,omitempty"`
    45  	Homepage string `toml:"homepage,omitempty" json:"homepage,omitempty" yaml:"homepage,omitempty"`
    46  }
    47  
    48  func (b BuildpackInfo) FullName() string {
    49  	if b.Version != "" {
    50  		return b.ID + "@" + b.Version
    51  	}
    52  	return b.ID
    53  }
    54  
    55  // Satisfy stringer
    56  func (b BuildpackInfo) String() string { return b.FullName() }
    57  
    58  // Match compares two buildpacks by ID and Version
    59  func (b BuildpackInfo) Match(o BuildpackInfo) bool {
    60  	return b.ID == o.ID && b.Version == o.Version
    61  }
    62  
    63  type Stack struct {
    64  	ID     string   `json:"id"`
    65  	Mixins []string `json:"mixins,omitempty"`
    66  }
    67  
    68  // BuildpackFromBlob constructs a buildpack from a blob. It is assumed that the buildpack
    69  // contents are structured as per the distribution spec (currently '/cnbs/buildpacks/{ID}/{version}/*').
    70  func BuildpackFromBlob(bpd BuildpackDescriptor, blob Blob) Buildpack {
    71  	return &buildpack{
    72  		Blob:       blob,
    73  		descriptor: bpd,
    74  	}
    75  }
    76  
    77  // BuildpackFromRootBlob constructs a buildpack from a blob. It is assumed that the buildpack contents reside at the
    78  // root of the blob. The constructed buildpack contents will be structured as per the distribution spec (currently
    79  // a tar with contents under '/cnbs/buildpacks/{ID}/{version}/*').
    80  func BuildpackFromRootBlob(blob Blob, layerWriterFactory archive.TarWriterFactory) (Buildpack, error) {
    81  	bpd := BuildpackDescriptor{}
    82  	rc, err := blob.Open()
    83  	if err != nil {
    84  		return nil, errors.Wrap(err, "open buildpack")
    85  	}
    86  	defer rc.Close()
    87  
    88  	_, buf, err := archive.ReadTarEntry(rc, "buildpack.toml")
    89  	if err != nil {
    90  		return nil, errors.Wrap(err, "reading buildpack.toml")
    91  	}
    92  
    93  	bpd.API = api.MustParse(AssumedBuildpackAPIVersion)
    94  	_, err = toml.Decode(string(buf), &bpd)
    95  	if err != nil {
    96  		return nil, errors.Wrap(err, "decoding buildpack.toml")
    97  	}
    98  
    99  	err = validateDescriptor(bpd)
   100  	if err != nil {
   101  		return nil, errors.Wrap(err, "invalid buildpack.toml")
   102  	}
   103  
   104  	return &buildpack{
   105  		descriptor: bpd,
   106  		Blob: &distBlob{
   107  			openFn: func() io.ReadCloser {
   108  				return archive.GenerateTarWithWriter(
   109  					func(tw archive.TarWriter) error {
   110  						return toDistTar(tw, bpd, blob)
   111  					},
   112  					layerWriterFactory,
   113  				)
   114  			},
   115  		},
   116  	}, nil
   117  }
   118  
   119  type distBlob struct {
   120  	openFn func() io.ReadCloser
   121  }
   122  
   123  func (b *distBlob) Open() (io.ReadCloser, error) {
   124  	return b.openFn(), nil
   125  }
   126  
   127  func toDistTar(tw archive.TarWriter, bpd BuildpackDescriptor, blob Blob) error {
   128  	ts := archive.NormalizedDateTime
   129  
   130  	if err := tw.WriteHeader(&tar.Header{
   131  		Typeflag: tar.TypeDir,
   132  		Name:     path.Join(BuildpacksDir, bpd.EscapedID()),
   133  		Mode:     0755,
   134  		ModTime:  ts,
   135  	}); err != nil {
   136  		return errors.Wrapf(err, "writing buildpack id dir header")
   137  	}
   138  
   139  	baseTarDir := path.Join(BuildpacksDir, bpd.EscapedID(), bpd.Info.Version)
   140  	if err := tw.WriteHeader(&tar.Header{
   141  		Typeflag: tar.TypeDir,
   142  		Name:     baseTarDir,
   143  		Mode:     0755,
   144  		ModTime:  ts,
   145  	}); err != nil {
   146  		return errors.Wrapf(err, "writing buildpack version dir header")
   147  	}
   148  
   149  	rc, err := blob.Open()
   150  	if err != nil {
   151  		return errors.Wrap(err, "reading buildpack blob")
   152  	}
   153  	defer rc.Close()
   154  
   155  	tr := tar.NewReader(rc)
   156  	for {
   157  		header, err := tr.Next()
   158  		if err == io.EOF {
   159  			break
   160  		}
   161  		if err != nil {
   162  			return errors.Wrap(err, "failed to get next tar entry")
   163  		}
   164  
   165  		archive.NormalizeHeader(header, true)
   166  		header.Name = path.Clean(header.Name)
   167  		if header.Name == "." || header.Name == "/" {
   168  			continue
   169  		}
   170  
   171  		header.Mode = calcFileMode(header)
   172  		header.Name = path.Join(baseTarDir, header.Name)
   173  		err = tw.WriteHeader(header)
   174  		if err != nil {
   175  			return errors.Wrapf(err, "failed to write header for '%s'", header.Name)
   176  		}
   177  
   178  		_, err = io.Copy(tw, tr)
   179  		if err != nil {
   180  			return errors.Wrapf(err, "failed to write contents to '%s'", header.Name)
   181  		}
   182  	}
   183  
   184  	return nil
   185  }
   186  
   187  func calcFileMode(header *tar.Header) int64 {
   188  	switch {
   189  	case header.Typeflag == tar.TypeDir:
   190  		return 0755
   191  	case nameOneOf(header.Name,
   192  		path.Join("bin", "detect"),
   193  		path.Join("bin", "build"),
   194  	):
   195  		return 0755
   196  	case anyExecBit(header.Mode):
   197  		return 0755
   198  	}
   199  
   200  	return 0644
   201  }
   202  
   203  func nameOneOf(name string, paths ...string) bool {
   204  	for _, p := range paths {
   205  		if name == p {
   206  			return true
   207  		}
   208  	}
   209  	return false
   210  }
   211  
   212  func anyExecBit(mode int64) bool {
   213  	return mode&0111 != 0
   214  }
   215  
   216  func validateDescriptor(bpd BuildpackDescriptor) error {
   217  	if bpd.Info.ID == "" {
   218  		return errors.Errorf("%s is required", style.Symbol("buildpack.id"))
   219  	}
   220  
   221  	if bpd.Info.Version == "" {
   222  		return errors.Errorf("%s is required", style.Symbol("buildpack.version"))
   223  	}
   224  
   225  	if len(bpd.Order) == 0 && len(bpd.Stacks) == 0 {
   226  		return errors.Errorf(
   227  			"buildpack %s: must have either %s or an %s defined",
   228  			style.Symbol(bpd.Info.FullName()),
   229  			style.Symbol("stacks"),
   230  			style.Symbol("order"),
   231  		)
   232  	}
   233  
   234  	if len(bpd.Order) >= 1 && len(bpd.Stacks) >= 1 {
   235  		return errors.Errorf(
   236  			"buildpack %s: cannot have both %s and an %s defined",
   237  			style.Symbol(bpd.Info.FullName()),
   238  			style.Symbol("stacks"),
   239  			style.Symbol("order"),
   240  		)
   241  	}
   242  
   243  	return nil
   244  }