github.com/cloudfoundry/libcfbuildpack@v1.91.23/packager/cnbpackager/packager.go (about)

     1  /*
     2   * Copyright 2018-2020 the original author or authors.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *      https://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package cnbpackager
    18  
    19  import (
    20  	"archive/tar"
    21  	"compress/gzip"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"log"
    26  	"os"
    27  	"os/exec"
    28  	"path/filepath"
    29  	"sort"
    30  	"strings"
    31  	templ "text/template"
    32  
    33  	"github.com/Masterminds/semver"
    34  	"github.com/fatih/color"
    35  
    36  	buildpackBp "github.com/buildpack/libbuildpack/buildpack"
    37  	layersBp "github.com/buildpack/libbuildpack/layers"
    38  	loggerBp "github.com/buildpack/libbuildpack/logger"
    39  	"github.com/cloudfoundry/libcfbuildpack/buildpack"
    40  	"github.com/cloudfoundry/libcfbuildpack/helper"
    41  	"github.com/cloudfoundry/libcfbuildpack/layers"
    42  	"github.com/cloudfoundry/libcfbuildpack/logger"
    43  )
    44  
    45  const (
    46  	DefaultDstDir    = "packaged-cnb"
    47  	DefaultCacheBase = ".cnb-packager-cache"
    48  )
    49  
    50  var identityColor = color.New(color.FgBlue)
    51  
    52  type Packager struct {
    53  	buildpack       buildpack.Buildpack
    54  	layers          layers.Layers
    55  	logger          logger.Logger
    56  	outputDirectory string
    57  }
    58  
    59  func New(bpDir, outputDir, version, cacheDir string) (Packager, error) {
    60  	l, err := loggerBp.DefaultLogger("")
    61  	if err != nil {
    62  		return Packager{}, err
    63  	}
    64  
    65  	if err := insertTemplateVersion(bpDir, version); err != nil {
    66  		return Packager{}, err
    67  	}
    68  
    69  	specBP, err := buildpackBp.New(bpDir, l)
    70  	if err != nil {
    71  		return Packager{}, err
    72  	}
    73  
    74  	log := logger.Logger{Logger: l}
    75  	b := buildpack.NewBuildpack(specBP, log)
    76  
    77  	depCache, err := filepath.Abs(filepath.Join(cacheDir, buildpack.CacheRoot))
    78  	if err != nil {
    79  		return Packager{}, err
    80  	}
    81  
    82  	return Packager{
    83  		b,
    84  		layers.NewLayers(layersBp.NewLayers(depCache, l), layersBp.NewLayers(depCache, l), b, log),
    85  		log,
    86  		outputDir,
    87  	}, nil
    88  }
    89  
    90  type pkgFile struct {
    91  	path        string
    92  	packagePath string
    93  }
    94  
    95  func (p Packager) Create(cache bool) error {
    96  	p.logger.Title(p.buildpack)
    97  
    98  	if err := p.prePackage(); err != nil {
    99  		return err
   100  	}
   101  
   102  	includedFiles, err := p.buildpack.IncludeFiles()
   103  	if err != nil {
   104  		return err
   105  	}
   106  
   107  	var allFiles []pkgFile
   108  	for _, i := range includedFiles {
   109  		path, err := filepath.Abs(filepath.Join(p.buildpack.Root, i))
   110  		if err != nil {
   111  			return err
   112  		}
   113  		f := pkgFile{
   114  			path:        path,
   115  			packagePath: i,
   116  		}
   117  		allFiles = append(allFiles, f)
   118  	}
   119  
   120  	if cache {
   121  		dependencyFiles, err := p.cacheDependencies()
   122  		if err != nil {
   123  			return err
   124  		}
   125  		allFiles = append(allFiles, dependencyFiles...)
   126  	}
   127  
   128  	return p.createPackage(allFiles)
   129  }
   130  
   131  func insertTemplateVersion(bpDir, version string) error {
   132  	bpTomlPath := filepath.Join(bpDir, "buildpack.toml")
   133  	v := struct {
   134  		Version string
   135  	}{
   136  		Version: version,
   137  	}
   138  
   139  	template, err := templ.ParseFiles(bpTomlPath)
   140  	if err != nil {
   141  		return err
   142  	}
   143  
   144  	file, err := os.OpenFile(bpTomlPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
   145  	if err != nil {
   146  		log.Fatal(fmt.Errorf("failed to open buildpack.toml : %s", err))
   147  	}
   148  
   149  	return template.Execute(file, v)
   150  }
   151  
   152  func (p Packager) cacheDependencies() ([]pkgFile, error) {
   153  	var files []pkgFile
   154  
   155  	deps, err := p.buildpack.Dependencies()
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  
   160  	for _, dep := range deps {
   161  		p.logger.Header("Caching %s", p.prettyIdentity(dep))
   162  
   163  		layer := p.layers.DownloadLayer(dep)
   164  
   165  		a, err := layer.Artifact()
   166  		if err != nil {
   167  			return nil, err
   168  		}
   169  
   170  		f := pkgFile{
   171  			path:        a,
   172  			packagePath: filepath.Join(buildpack.CacheRoot, dep.SHA256, filepath.Base(a)),
   173  		}
   174  
   175  		metaF := pkgFile{
   176  			path:        layer.Metadata,
   177  			packagePath: filepath.Join(buildpack.CacheRoot, dep.SHA256+".toml"),
   178  		}
   179  
   180  		files = append(files, f, metaF)
   181  	}
   182  
   183  	return files, nil
   184  }
   185  
   186  func (Packager) prettyIdentity(v logger.Identifiable) string {
   187  	if v == nil {
   188  		return ""
   189  	}
   190  
   191  	name, description := v.Identity()
   192  
   193  	if description == "" {
   194  		return identityColor.Sprint(name)
   195  	}
   196  
   197  	return identityColor.Sprintf("%s %s", name, description)
   198  }
   199  
   200  func (p Packager) Archive() error {
   201  	defer os.RemoveAll(p.outputDirectory)
   202  	fileName := filepath.Base(p.outputDirectory)
   203  	tarFile := filepath.Join(filepath.Dir(p.outputDirectory), fileName+".tgz")
   204  
   205  	file, err := os.Create(tarFile)
   206  	if err != nil {
   207  		return err
   208  	}
   209  	defer file.Close()
   210  
   211  	gw := gzip.NewWriter(file)
   212  	defer gw.Close()
   213  	tw := tar.NewWriter(gw)
   214  	defer tw.Close()
   215  
   216  	filepath.Walk(p.outputDirectory, func(path string, info os.FileInfo, err error) error {
   217  		return p.addTarFile(tw, info, path)
   218  	})
   219  
   220  	return nil
   221  }
   222  
   223  func (p Packager) addTarFile(tw *tar.Writer, info os.FileInfo, path string) error {
   224  	if !info.Mode().IsRegular() && !info.Mode().IsDir() {
   225  		return nil
   226  	}
   227  
   228  	if header, err := tar.FileInfoHeader(info, path); err == nil {
   229  		header.Name = stripBaseDirectory(p.outputDirectory, path)
   230  
   231  		if header.Name == "" {
   232  			return nil
   233  		}
   234  
   235  		if err := tw.WriteHeader(header); err != nil {
   236  			return err
   237  		}
   238  
   239  		if info.Mode().IsRegular() {
   240  			file, err := os.Open(path)
   241  			if err != nil {
   242  				return err
   243  			}
   244  			defer file.Close()
   245  
   246  			if _, err := io.Copy(tw, file); err != nil {
   247  				return err
   248  			}
   249  		}
   250  	}
   251  	return nil
   252  }
   253  
   254  func (p Packager) createPackage(files []pkgFile) error {
   255  	if len(files) == 0 {
   256  		return errors.New("no files included")
   257  	}
   258  
   259  	p.logger.Header("Creating package in %s", p.outputDirectory)
   260  
   261  	for _, file := range files {
   262  		p.logger.Body("Adding %s", file.packagePath)
   263  		outputDir := filepath.Dir(filepath.Join(p.outputDirectory, file.packagePath))
   264  		if err := os.MkdirAll(outputDir, os.ModePerm); err != nil {
   265  			return err
   266  		}
   267  		if err := helper.CopyFile(file.path, filepath.Join(p.outputDirectory, file.packagePath)); err != nil {
   268  			return err
   269  		}
   270  	}
   271  	return nil
   272  }
   273  
   274  func (p Packager) prePackage() error {
   275  	pp, ok := p.buildpack.PrePackage()
   276  	if !ok {
   277  		return nil
   278  	}
   279  
   280  	cmd := exec.Command(pp)
   281  	cmd.Stdout = os.Stdout
   282  	cmd.Stderr = os.Stderr
   283  	cmd.Dir = p.buildpack.Root
   284  
   285  	p.logger.Header("Pre-Package with %s", strings.Join(cmd.Args, " "))
   286  
   287  	return cmd.Run()
   288  }
   289  
   290  func stripBaseDirectory(base, path string) string {
   291  	return strings.TrimPrefix(strings.Replace(path, base, "", -1), string(filepath.Separator))
   292  }
   293  
   294  func (p Packager) Summary() (string, error) {
   295  	var out string
   296  	if err := p.depsSummary(&out); err != nil {
   297  		return "", err
   298  	}
   299  
   300  	p.defaultsSummary(&out)
   301  	p.stacksSummary(&out)
   302  
   303  	return out, nil
   304  }
   305  
   306  func (p Packager) depsSummary(out *string) error {
   307  
   308  	type depKey struct {
   309  		Idx     int
   310  		ID      string
   311  		Version string
   312  	}
   313  
   314  	bpMetadata := p.buildpack.Metadata
   315  	deps, ok := bpMetadata["dependencies"].([]map[string]interface{})
   316  	if !ok || len(deps) == 0 {
   317  		return nil
   318  	}
   319  
   320  	*out = "\nPackaged binaries:\n\n"
   321  	*out += "| name | version | stacks |\n|-|-|-|\n"
   322  
   323  	depMap := map[depKey]buildpack.Stacks{}
   324  	for _, d := range deps {
   325  		dep, err := buildpack.NewDependency(d)
   326  		if err != nil {
   327  			return err
   328  		}
   329  		depKey := depKey{
   330  			ID:      dep.ID,
   331  			Version: dep.Version.Version.String(),
   332  		}
   333  		if _, ok := depMap[depKey]; !ok {
   334  			depMap[depKey] = dep.Stacks
   335  		} else {
   336  			depMap[depKey] = append(depMap[depKey], dep.Stacks...)
   337  		}
   338  	}
   339  	depKeyArray := make([]depKey, 0)
   340  	for key, _ := range depMap {
   341  		depKeyArray = append(depKeyArray, key)
   342  	}
   343  
   344  	sort.SliceStable(depKeyArray, func(i, j int) bool {
   345  		alph := strings.Compare(depKeyArray[i].ID, depKeyArray[j].ID)
   346  		if alph < 0 {
   347  			return true
   348  		} else if alph == 0 {
   349  			versionI, err := semver.NewVersion(depKeyArray[i].Version)
   350  			if err != nil {
   351  				return false
   352  			}
   353  			versionJ, err := semver.NewVersion(depKeyArray[j].Version)
   354  			if err != nil {
   355  				return false
   356  			}
   357  			return versionI.GreaterThan(versionJ)
   358  		}
   359  		return false
   360  	})
   361  
   362  	for _, dKey := range depKeyArray {
   363  		stacks := depMap[dKey]
   364  		stackStringArray := []string{}
   365  		for _, stack := range stacks {
   366  			stackStringArray = append(stackStringArray, string(stack))
   367  		}
   368  		*out += fmt.Sprintf("| %s | %s | %s |\n", dKey.ID, dKey.Version, strings.Join(stackStringArray, ", "))
   369  	}
   370  
   371  	return nil
   372  }
   373  
   374  func (p Packager) defaultsSummary(out *string) {
   375  	bpMetadata := p.buildpack.Metadata
   376  	defaults, ok := bpMetadata[buildpack.DefaultVersions].(map[string]interface{})
   377  	if !ok {
   378  		return
   379  	}
   380  
   381  	if len(defaults) > 0 {
   382  		*out += "\nDefault binary versions:\n\n"
   383  		*out += "| name | version |\n|-|-|\n"
   384  		for name, version := range defaults {
   385  			*out += fmt.Sprintf("| %s | %s |\n", name, version)
   386  		}
   387  	}
   388  }
   389  
   390  func (p Packager) stacksSummary(out *string) {
   391  	if len(p.buildpack.Stacks) < 1 {
   392  		return
   393  	}
   394  
   395  	*out += `
   396  Supported stacks:
   397  
   398  | name |
   399  |-|
   400  `
   401  	for _, stack := range p.buildpack.Stacks {
   402  		*out += fmt.Sprintf("| %s |\n", stack.ID)
   403  	}
   404  }