github.com/paketo-buildpacks/libpak@v1.70.0/layer.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 libpak
    18  
    19  import (
    20  	"fmt"
    21  	"io/fs"
    22  	"os"
    23  	"path/filepath"
    24  	"reflect"
    25  	"time"
    26  
    27  	"github.com/BurntSushi/toml"
    28  	"github.com/heroku/color"
    29  
    30  	"github.com/buildpacks/libcnb"
    31  
    32  	"github.com/paketo-buildpacks/libpak/internal"
    33  	"github.com/paketo-buildpacks/libpak/sbom"
    34  	"github.com/paketo-buildpacks/libpak/sherpa"
    35  
    36  	"github.com/paketo-buildpacks/libpak/bard"
    37  )
    38  
    39  // LayerContributor is a helper for implementing a libcnb.LayerContributor in order to get consistent logging and
    40  // avoidance.
    41  type LayerContributor struct {
    42  
    43  	// ExpectedMetadata is the metadata to compare against any existing layer metadata.
    44  	ExpectedMetadata interface{}
    45  
    46  	// Logger is the logger to use.
    47  	Logger bard.Logger
    48  
    49  	// Name is the user readable name of the contribution.
    50  	Name string
    51  
    52  	// ExpectedTypes indicates the types that should be set on the layer.
    53  	ExpectedTypes libcnb.LayerTypes
    54  }
    55  
    56  // NewLayerContributor creates a new instance.
    57  func NewLayerContributor(name string, expectedMetadata interface{}, expectedTypes libcnb.LayerTypes) LayerContributor {
    58  	return LayerContributor{
    59  		ExpectedMetadata: expectedMetadata,
    60  		Name:             name,
    61  		ExpectedTypes:    expectedTypes,
    62  	}
    63  }
    64  
    65  // LayerFunc is a callback function that is invoked when a layer needs to be contributed.
    66  type LayerFunc func() (libcnb.Layer, error)
    67  
    68  // Contribute is the function to call when implementing your libcnb.LayerContributor.
    69  func (l *LayerContributor) Contribute(layer libcnb.Layer, f LayerFunc) (libcnb.Layer, error) {
    70  	layerRestored, err := l.checkIfLayerRestored(layer)
    71  	if err != nil {
    72  		return libcnb.Layer{}, fmt.Errorf("unable to check metadata\n%w", err)
    73  	}
    74  
    75  	expected, cached, err := l.checkIfMetadataMatches(layer)
    76  	if err != nil {
    77  		return libcnb.Layer{}, fmt.Errorf("unable to check metadata\n%w", err)
    78  	}
    79  
    80  	if cached && layerRestored {
    81  		l.Logger.Headerf("%s: %s cached layer", color.BlueString(l.Name), color.GreenString("Reusing"))
    82  		layer.LayerTypes = l.ExpectedTypes
    83  		return layer, nil
    84  	}
    85  
    86  	if !layerRestored {
    87  		l.Logger.Headerf("%s: %s cached layer", color.BlueString(l.Name), color.RedString("Reloading"))
    88  	} else {
    89  		l.Logger.Headerf("%s: %s to layer", color.BlueString(l.Name), color.YellowString("Contributing"))
    90  	}
    91  
    92  	err = l.reset(layer)
    93  	if err != nil {
    94  		return libcnb.Layer{}, fmt.Errorf("unable to reset\n%w", err)
    95  	}
    96  
    97  	layer, err = f()
    98  	if err != nil {
    99  		return libcnb.Layer{}, err
   100  	}
   101  
   102  	layer.LayerTypes = l.ExpectedTypes
   103  	layer.Metadata = expected
   104  
   105  	return layer, nil
   106  }
   107  
   108  func (l *LayerContributor) checkIfMetadataMatches(layer libcnb.Layer) (map[string]interface{}, bool, error) {
   109  	raw, err := internal.Marshal(l.ExpectedMetadata)
   110  	if err != nil {
   111  		return map[string]interface{}{}, false, fmt.Errorf("unable to encode metadata\n%w", err)
   112  	}
   113  
   114  	expected := map[string]interface{}{}
   115  	if err := toml.Unmarshal(raw, &expected); err != nil {
   116  		return map[string]interface{}{}, false, fmt.Errorf("unable to decode metadata\n%w", err)
   117  	}
   118  
   119  	l.Logger.Debugf("Expected metadata: %+v", expected)
   120  	l.Logger.Debugf("Actual metadata: %+v", layer.Metadata)
   121  
   122  	match, err := l.Equals(expected, layer.Metadata)
   123  	if err != nil {
   124  		return map[string]interface{}{}, false, fmt.Errorf("unable to compare metadata\n%w", err)
   125  	}
   126  	return expected, match, nil
   127  }
   128  
   129  func (l *LayerContributor) Equals(expectedM map[string]interface{}, layerM map[string]interface{}) (bool, error) {
   130  	// TODO Do we want the Equals method to modify the underlying maps? Else we need to make a copy here.
   131  
   132  	if err := l.normalizeDependencyDeprecationDate(expectedM); err != nil {
   133  		return false, fmt.Errorf("%w (expected layer)", err)
   134  	}
   135  
   136  	if err := l.normalizeDependencyDeprecationDate(layerM); err != nil {
   137  		return false, fmt.Errorf("%w (actual layer)", err)
   138  	}
   139  
   140  	return reflect.DeepEqual(expectedM, layerM), nil
   141  }
   142  
   143  // normalizeDependencyDeprecationDate makes sure the dependency deprecation date is represented as a time.Time object
   144  // in the map whenever it exists.
   145  func (l *LayerContributor) normalizeDependencyDeprecationDate(input map[string]interface{}) error {
   146  	if dep, ok := input["dependency"].(map[string]interface{}); ok {
   147  		for k, v := range dep {
   148  			if k == "deprecation_date" {
   149  				if err := l.replaceDeprecationDate(dep, v); err != nil {
   150  					return err
   151  				}
   152  				break
   153  			}
   154  		}
   155  	} else if depr_date, ok := input["deprecation_date"]; ok {
   156  		if err := l.replaceDeprecationDate(input, depr_date); err != nil {
   157  			return err
   158  		}
   159  	}
   160  
   161  	return nil
   162  }
   163  
   164  func (l *LayerContributor) replaceDeprecationDate(metadata map[string]interface{}, value interface{}) error {
   165  	deprecationDate, err := l.parseDeprecationDate(value)
   166  	if err != nil {
   167  		return err
   168  	}
   169  	metadata["deprecation_date"] = deprecationDate
   170  	return nil
   171  }
   172  
   173  // parseDeprecationDate accepts both string and time.Time as input, and returns
   174  // a truncated time.Time value.
   175  func (l *LayerContributor) parseDeprecationDate(v interface{}) (deprecationDate time.Time, err error) {
   176  	switch vDate := v.(type) {
   177  	case time.Time:
   178  		deprecationDate = vDate
   179  	case string:
   180  		deprecationDate, err = time.Parse(time.RFC3339, vDate)
   181  		if err != nil {
   182  			return time.Time{}, fmt.Errorf("unable to parse deprecation_date %s", vDate)
   183  		}
   184  	default:
   185  		return time.Time{}, fmt.Errorf("unexpected type %T for deprecation_date %v", v, v)
   186  	}
   187  
   188  	deprecationDate = deprecationDate.Truncate(time.Second).In(time.UTC)
   189  	return
   190  }
   191  
   192  func (l *LayerContributor) checkIfLayerRestored(layer libcnb.Layer) (bool, error) {
   193  	layerTOML := fmt.Sprintf("%s.toml", layer.Path)
   194  	tomlExists, err := sherpa.FileExists(layerTOML)
   195  	if err != nil {
   196  		return false, fmt.Errorf("unable to check if layer toml exists %s\n%w", layerTOML, err)
   197  	}
   198  
   199  	layerDirExists, err := sherpa.DirExists(layer.Path)
   200  	if err != nil {
   201  		return false, fmt.Errorf("unable to check if layer directory exists %s\n%w", layer.Path, err)
   202  	}
   203  
   204  	var dirContents []fs.DirEntry
   205  	if layerDirExists {
   206  		dirContents, err = os.ReadDir(layer.Path)
   207  		if err != nil {
   208  			return false, fmt.Errorf("unable to read directory %s\n%w", layer.Path, err)
   209  		}
   210  	}
   211  
   212  	l.Logger.Debugf("Check If Layer Restored -> tomlExists: %s, layerDirExists: %s, dirContents: %s, cache: %s, build: %s",
   213  		tomlExists, layerDirExists, dirContents, l.ExpectedTypes.Cache, l.ExpectedTypes.Build)
   214  	return !(tomlExists && (!layerDirExists || len(dirContents) == 0) && (l.ExpectedTypes.Cache || l.ExpectedTypes.Build)), nil
   215  }
   216  
   217  func (l *LayerContributor) reset(layer libcnb.Layer) error {
   218  	if err := os.RemoveAll(layer.Path); err != nil {
   219  		return fmt.Errorf("unable to remove existing layer directory %s\n%w", layer.Path, err)
   220  	}
   221  
   222  	if err := os.MkdirAll(layer.Path, 0755); err != nil {
   223  		return fmt.Errorf("unable to create layer directory %s\n%w", layer.Path, err)
   224  	}
   225  
   226  	return nil
   227  }
   228  
   229  // DependencyLayerContributor is a helper for implementing a libcnb.LayerContributor for a BuildpackDependency in order
   230  // to get consistent logging and avoidance.
   231  type DependencyLayerContributor struct {
   232  
   233  	// Dependency is the dependency being contributed.
   234  	Dependency BuildpackDependency
   235  
   236  	// DependencyCache is the cache to use to get the dependency.
   237  	DependencyCache DependencyCache
   238  
   239  	// ExpectedTypes indicates the types that should be set on the layer.
   240  	ExpectedTypes libcnb.LayerTypes
   241  
   242  	// ExpectedMetadata contains metadata describing the expected layer
   243  	ExpectedMetadata interface{}
   244  
   245  	// Logger is the logger to use.
   246  	Logger bard.Logger
   247  
   248  	// RequestModifierFuncs is an optional Request Modifier to use when downloading the dependency.
   249  	RequestModifierFuncs []RequestModifierFunc
   250  }
   251  
   252  // NewDependencyLayer returns a new DependencyLayerContributor for the given BuildpackDependency and a BOMEntry describing the layer contents.
   253  //
   254  // Deprecated: this method uses `libcnb.BOMEntry` which has been deprecated upstream, a future version will drop
   255  // support for `libcnb.BOMEntry` which will change this method signature. Use NewDependencyLayerContributor instead.
   256  func NewDependencyLayer(dependency BuildpackDependency, cache DependencyCache, types libcnb.LayerTypes) (DependencyLayerContributor, libcnb.BOMEntry) {
   257  	dlc := NewDependencyLayerContributor(dependency, cache, types)
   258  
   259  	entry := dependency.AsBOMEntry()
   260  	entry.Metadata["layer"] = dlc.LayerName()
   261  
   262  	if types.Launch {
   263  		entry.Launch = true
   264  	}
   265  	if !(types.Launch && !types.Cache && !types.Build) {
   266  		// launch-only layers are the only layers NOT guaranteed to be present in the build environment
   267  		entry.Build = true
   268  	}
   269  
   270  	return dlc, entry
   271  }
   272  
   273  // NewDependencyLayerContributor returns a new DependencyLayerContributor for the given BuildpackDependency
   274  func NewDependencyLayerContributor(dependency BuildpackDependency, cache DependencyCache, types libcnb.LayerTypes) DependencyLayerContributor {
   275  	return DependencyLayerContributor{
   276  		Dependency:       dependency,
   277  		ExpectedMetadata: dependency,
   278  		DependencyCache:  cache,
   279  		ExpectedTypes:    types,
   280  	}
   281  }
   282  
   283  // DependencyLayerFunc is a callback function that is invoked when a dependency needs to be contributed.
   284  type DependencyLayerFunc func(artifact *os.File) (libcnb.Layer, error)
   285  
   286  // Contribute is the function to call whe implementing your libcnb.LayerContributor.
   287  func (d *DependencyLayerContributor) Contribute(layer libcnb.Layer, f DependencyLayerFunc) (libcnb.Layer, error) {
   288  	lc := NewLayerContributor(d.Name(), d.ExpectedMetadata, d.ExpectedTypes)
   289  	lc.Logger = d.Logger
   290  
   291  	return lc.Contribute(layer, func() (libcnb.Layer, error) {
   292  		artifact, err := d.DependencyCache.Artifact(d.Dependency, d.RequestModifierFuncs...)
   293  		if err != nil {
   294  			d.Logger.Debugf("fetching dependency %s failed\n%w", d.Dependency.Name, err)
   295  			return libcnb.Layer{}, fmt.Errorf("unable to get dependency %s. see DEBUG log level", d.Dependency.Name)
   296  		}
   297  		defer artifact.Close()
   298  
   299  		sbomArtifact, err := d.Dependency.AsSyftArtifact()
   300  		if err != nil {
   301  			return libcnb.Layer{}, fmt.Errorf("unable to get SBOM artifact %s\n%w", d.Dependency.ID, err)
   302  		}
   303  
   304  		sbomPath := layer.SBOMPath(libcnb.SyftJSON)
   305  		dep := sbom.NewSyftDependency(layer.Path, []sbom.SyftArtifact{sbomArtifact})
   306  		d.Logger.Debugf("Writing Syft SBOM at %s: %+v", sbomPath, dep)
   307  		if err := dep.WriteTo(sbomPath); err != nil {
   308  			return libcnb.Layer{}, fmt.Errorf("unable to write SBOM\n%w", err)
   309  		}
   310  
   311  		return f(artifact)
   312  	})
   313  }
   314  
   315  // LayerName returns the conventional name of the layer for this contributor
   316  func (d *DependencyLayerContributor) LayerName() string {
   317  	return d.Dependency.ID
   318  }
   319  
   320  // Name returns the human readable name of the layer
   321  func (d *DependencyLayerContributor) Name() string {
   322  	return fmt.Sprintf("%s %s", d.Dependency.Name, d.Dependency.Version)
   323  }
   324  
   325  // HelperLayerContributor is a helper for implementing a libcnb.LayerContributor for a buildpack helper application in
   326  // order to get consistent logging and avoidance.
   327  type HelperLayerContributor struct {
   328  
   329  	// Path is the path to the helper application.
   330  	Path string
   331  
   332  	// BuildpackInfo describes the buildpack that provides the helper
   333  	BuildpackInfo libcnb.BuildpackInfo
   334  
   335  	// Logger is the logger to use.
   336  	Logger bard.Logger
   337  
   338  	// Names are the names of the helpers to create
   339  	Names []string
   340  }
   341  
   342  // NewHelperLayer returns a new HelperLayerContributor and a BOMEntry describing the layer contents.
   343  //
   344  // Deprecated: this method uses `libcnb.BOMEntry` which has been deprecated upstream, a future version will drop
   345  // support for `libcnb.BOMEntry` which will change this method signature. Use NewHelperLayerContributor instead.
   346  func NewHelperLayer(buildpack libcnb.Buildpack, names ...string) (HelperLayerContributor, libcnb.BOMEntry) {
   347  	hl := NewHelperLayerContributor(buildpack, names...)
   348  
   349  	return hl, libcnb.BOMEntry{
   350  		Name: "helper",
   351  		Metadata: map[string]interface{}{
   352  			"layer":   hl.Name(),
   353  			"names":   names,
   354  			"version": buildpack.Info.Version,
   355  		},
   356  		Launch: true,
   357  	}
   358  }
   359  
   360  // NewHelperLayerContributor returns a new HelperLayerContributor
   361  func NewHelperLayerContributor(buildpack libcnb.Buildpack, names ...string) HelperLayerContributor {
   362  	return HelperLayerContributor{
   363  		Path:          filepath.Join(buildpack.Path, "bin", "helper"),
   364  		Names:         names,
   365  		BuildpackInfo: buildpack.Info,
   366  	}
   367  }
   368  
   369  // Name returns the conventional name of the layer for this contributor
   370  func (h HelperLayerContributor) Name() string {
   371  	return filepath.Base(h.Path)
   372  }
   373  
   374  // Contribute is the function to call whe implementing your libcnb.LayerContributor.
   375  func (h HelperLayerContributor) Contribute(layer libcnb.Layer) (libcnb.Layer, error) {
   376  	expected := map[string]interface{}{"buildpackInfo": h.BuildpackInfo, "helperNames": h.Names}
   377  	lc := NewLayerContributor("Launch Helper", expected, libcnb.LayerTypes{
   378  		Launch: true,
   379  	})
   380  
   381  	lc.Logger = h.Logger
   382  
   383  	return lc.Contribute(layer, func() (libcnb.Layer, error) {
   384  		in, err := os.Open(h.Path)
   385  		if err != nil {
   386  			return libcnb.Layer{}, fmt.Errorf("unable to open %s\n%w", h.Path, err)
   387  		}
   388  		defer in.Close()
   389  
   390  		out := filepath.Join(layer.Path, "helper")
   391  		if err := sherpa.CopyFile(in, out); err != nil {
   392  			return libcnb.Layer{}, fmt.Errorf("unable to copy %s to %s", h.Path, out)
   393  		}
   394  
   395  		for _, n := range h.Names {
   396  			link := layer.Exec.FilePath(n)
   397  			h.Logger.Bodyf("Creating %s", link)
   398  
   399  			f := filepath.Dir(link)
   400  			if err := os.MkdirAll(f, 0755); err != nil {
   401  				return libcnb.Layer{}, fmt.Errorf("unable to create %s\n%w", f, err)
   402  			}
   403  
   404  			if err := os.Symlink(out, link); err != nil {
   405  				return libcnb.Layer{}, fmt.Errorf("unable to link %s to %s\n%w", out, link, err)
   406  			}
   407  		}
   408  
   409  		sbomArtifact, err := h.AsSyftArtifact()
   410  		if err != nil {
   411  			return libcnb.Layer{}, fmt.Errorf("unable to get SBOM artifact for helper\n%w", err)
   412  		}
   413  
   414  		sbomPath := layer.SBOMPath(libcnb.SyftJSON)
   415  		dep := sbom.NewSyftDependency(layer.Path, []sbom.SyftArtifact{sbomArtifact})
   416  		h.Logger.Debugf("Writing Syft SBOM at %s: %+v", sbomPath, dep)
   417  		if err := dep.WriteTo(sbomPath); err != nil {
   418  			return libcnb.Layer{}, fmt.Errorf("unable to write SBOM\n%w", err)
   419  		}
   420  
   421  		return layer, nil
   422  	})
   423  }
   424  
   425  func (h HelperLayerContributor) AsSyftArtifact() (sbom.SyftArtifact, error) {
   426  	licenses := []string{}
   427  	for _, license := range h.BuildpackInfo.Licenses {
   428  		licenses = append(licenses, license.Type)
   429  	}
   430  
   431  	locations := []sbom.SyftLocation{}
   432  	cpes := []string{}
   433  	for _, name := range h.Names {
   434  		locations = append(locations, sbom.SyftLocation{Path: name})
   435  		cpes = append(cpes, fmt.Sprintf("cpe:2.3:a:%s:%s:%s:*:*:*:*:*:*:*",
   436  			h.BuildpackInfo.ID, name, h.BuildpackInfo.Version))
   437  	}
   438  
   439  	artifact := sbom.SyftArtifact{
   440  		Name:      "helper",
   441  		Version:   h.BuildpackInfo.Version,
   442  		Type:      "UnknownPackage",
   443  		FoundBy:   "libpak",
   444  		Licenses:  licenses,
   445  		Locations: locations,
   446  		CPEs:      cpes,
   447  		PURL:      fmt.Sprintf("pkg:generic/%s@%s", h.BuildpackInfo.ID, h.BuildpackInfo.Version),
   448  	}
   449  	var err error
   450  	artifact.ID, err = artifact.Hash()
   451  	if err != nil {
   452  		return sbom.SyftArtifact{}, fmt.Errorf("unable to generate hash\n%w", err)
   453  	}
   454  
   455  	return artifact, nil
   456  }