github.com/paketo-buildpacks/libpak@v1.70.0/buildpack.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  	"net/url"
    22  	"os"
    23  	"reflect"
    24  	"runtime"
    25  	"sort"
    26  	"strconv"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/Masterminds/semver/v3"
    31  	"github.com/buildpacks/libcnb"
    32  	"github.com/heroku/color"
    33  
    34  	"github.com/paketo-buildpacks/libpak/bard"
    35  	"github.com/paketo-buildpacks/libpak/sbom"
    36  )
    37  
    38  // BuildpackConfiguration represents a build or launch configuration parameter.
    39  type BuildpackConfiguration struct {
    40  
    41  	// Build indicates whether the configuration is for build-time.  Optional.
    42  	Build bool `toml:"build"`
    43  
    44  	// Default is the default value of the configuration parameter.  Optional.
    45  	Default string `toml:"default"`
    46  
    47  	// Description is the description of the configuration parameter.
    48  	Description string `toml:"description"`
    49  
    50  	// Launch indicates whether the configuration is for launch-time.  Optional.
    51  	Launch bool `toml:"launch"`
    52  
    53  	// Name is the environment variable name of the configuration parameter.
    54  	Name string `toml:"name"`
    55  }
    56  
    57  // BuildpackDependencyLicense represents a license that a BuildpackDependency is distributed under.  At least one of
    58  // Name or URI MUST be specified.
    59  type BuildpackDependencyLicense struct {
    60  
    61  	// Type is the type of the license.  This is typically the SPDX short identifier.
    62  	Type string `toml:"type"`
    63  
    64  	// URI is the location where the license can be found.
    65  	URI string `toml:"uri"`
    66  }
    67  
    68  // BuildpackDependency describes a dependency known to the buildpack.
    69  type BuildpackDependency struct {
    70  	// ID is the dependency ID.
    71  	ID string `toml:"id"`
    72  
    73  	// Name is the dependency name.
    74  	Name string `toml:"name"`
    75  
    76  	// Version is the dependency version.
    77  	Version string `toml:"version"`
    78  
    79  	// URI is the dependency URI.
    80  	URI string `toml:"uri"`
    81  
    82  	// SHA256 is the hash of the dependency.
    83  	SHA256 string `toml:"sha256"`
    84  
    85  	// Stacks are the stacks the dependency is compatible with.
    86  	Stacks []string `toml:"stacks"`
    87  
    88  	// Licenses are the licenses the dependency is distributed under.
    89  	Licenses []BuildpackDependencyLicense `toml:"licenses"`
    90  
    91  	// CPEs are the Common Platform Enumeration identifiers for the dependency
    92  	CPEs []string `toml:"cpes"`
    93  
    94  	// PURL is the package URL that identifies the dependency
    95  	PURL string `toml:"purl"`
    96  
    97  	// DeprecationDate is the time when the dependency is deprecated
    98  	DeprecationDate time.Time `toml:"deprecation_date"`
    99  }
   100  
   101  // Equals compares the 2 structs if they are equal. This is very simiar to reflect.DeepEqual
   102  // except that properties that will not work (e.g. DeprecationDate) are ignored.
   103  func (b1 BuildpackDependency) Equals(b2 BuildpackDependency) bool {
   104  	b1.DeprecationDate = b1.DeprecationDate.Truncate(time.Second).In(time.UTC)
   105  	b2.DeprecationDate = b2.DeprecationDate.Truncate(time.Second).In(time.UTC)
   106  
   107  	if len(b1.CPEs) == 0 {
   108  		b1.CPEs = nil
   109  	}
   110  	if len(b2.CPEs) == 0 {
   111  		b2.CPEs = nil
   112  	}
   113  
   114  	return reflect.DeepEqual(b1, b2)
   115  }
   116  
   117  // AsBOMEntry renders a bill of materials entry describing the dependency.
   118  //
   119  // Deprecated: as of Buildpacks RFC 95, use `BuildpackDependency.AsSyftArtifact` instead
   120  func (b BuildpackDependency) AsBOMEntry() libcnb.BOMEntry {
   121  	return libcnb.BOMEntry{
   122  		Name: b.ID,
   123  		Metadata: map[string]interface{}{
   124  			"name":     b.Name,
   125  			"version":  b.Version,
   126  			"uri":      b.URI,
   127  			"sha256":   b.SHA256,
   128  			"stacks":   b.Stacks,
   129  			"licenses": b.Licenses,
   130  		},
   131  	}
   132  }
   133  
   134  // AsSyftArtifact renders a bill of materials entry describing the dependency as Syft.
   135  func (b BuildpackDependency) AsSyftArtifact() (sbom.SyftArtifact, error) {
   136  	licenses := []string{}
   137  	for _, license := range b.Licenses {
   138  		licenses = append(licenses, license.Type)
   139  	}
   140  
   141  	sbomArtifact := sbom.SyftArtifact{
   142  		Name:      b.Name,
   143  		Version:   b.Version,
   144  		Type:      "UnknownPackage",
   145  		FoundBy:   "libpak",
   146  		Licenses:  licenses,
   147  		Locations: []sbom.SyftLocation{{Path: "buildpack.toml"}},
   148  		CPEs:      b.CPEs,
   149  		PURL:      b.PURL,
   150  	}
   151  
   152  	var err error
   153  	sbomArtifact.ID, err = sbomArtifact.Hash()
   154  	if err != nil {
   155  		return sbom.SyftArtifact{}, fmt.Errorf("unable to generate hash\n%w", err)
   156  	}
   157  
   158  	return sbomArtifact, nil
   159  }
   160  
   161  func (b BuildpackDependency) IsDeprecated() bool {
   162  	deprecationDate := b.DeprecationDate.UTC()
   163  	now := time.Now().UTC()
   164  	return deprecationDate.Equal(now) || deprecationDate.Before(now)
   165  }
   166  
   167  func (b BuildpackDependency) IsSoonDeprecated() bool {
   168  	deprecationDate := b.DeprecationDate.UTC()
   169  	now := time.Now().UTC()
   170  	return deprecationDate.Add(-30*24*time.Hour).Before(now) && deprecationDate.After(now)
   171  }
   172  
   173  // BuildpackMetadata is an extension to libcnb.Buildpack's metadata with opinions.
   174  type BuildpackMetadata struct {
   175  
   176  	// Configurations are environment variables that can be used at build time to configure the buildpack and launch
   177  	// time to configure the application.
   178  	Configurations []BuildpackConfiguration
   179  
   180  	// Dependencies are the dependencies known to the buildpack.
   181  	Dependencies []BuildpackDependency
   182  
   183  	// IncludeFiles describes the files to include in the package.
   184  	IncludeFiles []string
   185  
   186  	// PrePackage describes a command to invoke before packaging.
   187  	PrePackage string
   188  }
   189  
   190  // NewBuildpackMetadata creates a new instance of BuildpackMetadata from the contents of libcnb.Buildpack.Metadata
   191  func NewBuildpackMetadata(metadata map[string]interface{}) (BuildpackMetadata, error) {
   192  	m := BuildpackMetadata{}
   193  
   194  	if v, ok := metadata["configurations"]; ok {
   195  		for _, v := range v.([]map[string]interface{}) {
   196  			var c BuildpackConfiguration
   197  
   198  			if v, ok := v["build"].(bool); ok {
   199  				c.Build = v
   200  			}
   201  
   202  			if v, ok := v["default"].(string); ok {
   203  				c.Default = v
   204  			}
   205  
   206  			if v, ok := v["description"].(string); ok {
   207  				c.Description = v
   208  			}
   209  
   210  			if v, ok := v["launch"].(bool); ok {
   211  				c.Launch = v
   212  			}
   213  
   214  			if v, ok := v["name"].(string); ok {
   215  				c.Name = v
   216  			}
   217  
   218  			m.Configurations = append(m.Configurations, c)
   219  		}
   220  	}
   221  
   222  	if v, ok := metadata["dependencies"]; ok {
   223  		for _, v := range v.([]map[string]interface{}) {
   224  			var d BuildpackDependency
   225  
   226  			if v, ok := v["id"].(string); ok {
   227  				d.ID = v
   228  			}
   229  
   230  			if v, ok := v["name"].(string); ok {
   231  				d.Name = v
   232  			}
   233  
   234  			if v, ok := v["version"].(string); ok {
   235  				d.Version = v
   236  			}
   237  
   238  			if v, ok := v["uri"].(string); ok {
   239  				d.URI = v
   240  			}
   241  
   242  			if v, ok := v["sha256"].(string); ok {
   243  				d.SHA256 = v
   244  			}
   245  
   246  			if v, ok := v["stacks"].([]interface{}); ok {
   247  				for _, v := range v {
   248  					d.Stacks = append(d.Stacks, v.(string))
   249  				}
   250  			}
   251  
   252  			if v, ok := v["licenses"].([]map[string]interface{}); ok {
   253  				for _, v := range v {
   254  					var l BuildpackDependencyLicense
   255  
   256  					if v, ok := v["type"].(string); ok {
   257  						l.Type = v
   258  					}
   259  
   260  					if v, ok := v["uri"].(string); ok {
   261  						l.URI = v
   262  					}
   263  
   264  					d.Licenses = append(d.Licenses, l)
   265  				}
   266  			}
   267  
   268  			if v, ok := v["cpes"].([]interface{}); ok {
   269  				for _, v := range v {
   270  					d.CPEs = append(d.CPEs, v.(string))
   271  				}
   272  			}
   273  
   274  			if v, ok := v["purl"].(string); ok {
   275  				d.PURL = v
   276  			}
   277  
   278  			if v, ok := v["deprecation_date"].(string); ok {
   279  				deprecationDate, err := time.Parse(time.RFC3339, v)
   280  
   281  				if err != nil {
   282  					return BuildpackMetadata{}, fmt.Errorf("unable to parse deprecation date\n%w", err)
   283  				}
   284  
   285  				d.DeprecationDate = deprecationDate
   286  			}
   287  
   288  			m.Dependencies = append(m.Dependencies, d)
   289  		}
   290  	}
   291  
   292  	if v, ok := metadata["include-files"].([]interface{}); ok {
   293  		for _, v := range v {
   294  			m.IncludeFiles = append(m.IncludeFiles, v.(string))
   295  		}
   296  	}
   297  
   298  	if v, ok := metadata["pre-package"].(string); ok {
   299  		m.PrePackage = v
   300  	}
   301  
   302  	return m, nil
   303  }
   304  
   305  // ConfigurationResolver provides functionality for resolving a configuration value.
   306  type ConfigurationResolver struct {
   307  
   308  	// Configurations are the configurations to resolve against
   309  	Configurations []BuildpackConfiguration
   310  }
   311  
   312  type configurationEntry struct {
   313  	Name        string
   314  	Description string
   315  	Value       string
   316  }
   317  
   318  func (c configurationEntry) String(nameLength int, valueLength int) string {
   319  	sb := strings.Builder{}
   320  
   321  	sb.WriteString("$")
   322  	sb.WriteString(c.Name)
   323  	for i := 0; i < nameLength-len(c.Name); i++ {
   324  		sb.WriteString(" ")
   325  	}
   326  
   327  	sb.WriteString("  ")
   328  	sb.WriteString(c.Value)
   329  	for i := 0; i < valueLength-len(c.Value); i++ {
   330  		sb.WriteString(" ")
   331  	}
   332  
   333  	if valueLength > 0 {
   334  		sb.WriteString("  ")
   335  	}
   336  
   337  	sb.WriteString(c.Description)
   338  
   339  	return sb.String()
   340  }
   341  
   342  // NewConfigurationResolver creates a new instance from buildpack metadata.  Logs configuration options to the body
   343  // level int the form 'Set $Name to configure $Description[. Default <i>$Default</i>.]'.
   344  func NewConfigurationResolver(buildpack libcnb.Buildpack, logger *bard.Logger) (ConfigurationResolver, error) {
   345  	md, err := NewBuildpackMetadata(buildpack.Metadata)
   346  	if err != nil {
   347  		return ConfigurationResolver{}, fmt.Errorf("unable to unmarshal buildpack metadata\n%w", err)
   348  	}
   349  
   350  	cr := ConfigurationResolver{Configurations: md.Configurations}
   351  
   352  	if logger == nil {
   353  		return cr, nil
   354  	}
   355  
   356  	var (
   357  		build   []configurationEntry
   358  		launch  []configurationEntry
   359  		unknown []configurationEntry
   360  
   361  		nameLength  int
   362  		valueLength int
   363  	)
   364  
   365  	sort.Slice(md.Configurations, func(i, j int) bool {
   366  		return md.Configurations[i].Name < md.Configurations[j].Name
   367  	})
   368  
   369  	for _, c := range md.Configurations {
   370  		s, _ := cr.Resolve(c.Name)
   371  
   372  		e := configurationEntry{
   373  			Name:        c.Name,
   374  			Description: c.Description,
   375  			Value:       s,
   376  		}
   377  
   378  		if l := len(e.Name); l > nameLength {
   379  			nameLength = l
   380  		}
   381  
   382  		if l := len(e.Value); l > valueLength {
   383  			valueLength = l
   384  		}
   385  
   386  		if c.Build {
   387  			build = append(build, e)
   388  		}
   389  
   390  		if c.Launch {
   391  			launch = append(launch, e)
   392  		}
   393  
   394  		if !c.Build && !c.Launch {
   395  			unknown = append(unknown, e)
   396  		}
   397  	}
   398  
   399  	f := color.New(color.Faint)
   400  
   401  	if len(build) > 0 {
   402  		logger.Header(f.Sprint("Build Configuration:"))
   403  		for _, e := range build {
   404  			logger.Body(e.String(nameLength, valueLength))
   405  		}
   406  	}
   407  
   408  	if len(launch) > 0 {
   409  		logger.Header(f.Sprint("Launch Configuration:"))
   410  		for _, e := range launch {
   411  			logger.Body(e.String(nameLength, valueLength))
   412  		}
   413  	}
   414  
   415  	if len(unknown) > 0 {
   416  		logger.Header(f.Sprint("Unknown Configuration:"))
   417  		for _, e := range unknown {
   418  			logger.Body(e.String(nameLength, valueLength))
   419  		}
   420  	}
   421  
   422  	return cr, nil
   423  }
   424  
   425  // Resolve resolves the value for a configuration option, returning the default value and false if it was not set.
   426  func (c *ConfigurationResolver) Resolve(name string) (string, bool) {
   427  	if v, ok := os.LookupEnv(name); ok {
   428  		return v, ok
   429  	}
   430  
   431  	for _, c := range c.Configurations {
   432  		if c.Name == name {
   433  			return c.Default, false
   434  		}
   435  	}
   436  
   437  	return "", false
   438  }
   439  
   440  // ResolveBool resolves a boolean value for a configuration option. Returns true for 1, t, T, TRUE, true, True. Returns
   441  // false for all other values or unset.
   442  func (c *ConfigurationResolver) ResolveBool(name string) bool {
   443  	s, _ := c.Resolve(name)
   444  	t, err := strconv.ParseBool(s)
   445  	if err != nil {
   446  		return false
   447  	}
   448  
   449  	return t
   450  }
   451  
   452  // DependencyResolver provides functionality for resolving a dependency given a collection of constraints.
   453  type DependencyResolver struct {
   454  
   455  	// Dependencies are the dependencies to resolve against.
   456  	Dependencies []BuildpackDependency
   457  
   458  	// StackID is the stack id of the build.
   459  	StackID string
   460  
   461  	// Logger is the logger used to write to the console.
   462  	Logger *bard.Logger
   463  }
   464  
   465  // NewDependencyResolver creates a new instance from the buildpack metadata and stack id.
   466  func NewDependencyResolver(context libcnb.BuildContext) (DependencyResolver, error) {
   467  	md, err := NewBuildpackMetadata(context.Buildpack.Metadata)
   468  	if err != nil {
   469  		return DependencyResolver{}, fmt.Errorf("unable to unmarshal buildpack metadata\n%w", err)
   470  	}
   471  
   472  	return DependencyResolver{Dependencies: md.Dependencies, StackID: context.StackID}, nil
   473  }
   474  
   475  // NoValidDependenciesError is returned when the resolver cannot find any valid dependencies given the constraints.
   476  type NoValidDependenciesError struct {
   477  	// Message is the error message
   478  	Message string
   479  }
   480  
   481  func (n NoValidDependenciesError) Error() string {
   482  	return n.Message
   483  }
   484  
   485  // IsNoValidDependencies indicates whether an error is a NoValidDependenciesError.
   486  func IsNoValidDependencies(err error) bool {
   487  	_, ok := err.(NoValidDependenciesError)
   488  	return ok
   489  }
   490  
   491  // Resolve returns the latest version of a dependency within the collection of Dependencies.  The candidate set is first
   492  // filtered by the constraints, then the remaining candidates are sorted for the latest result by semver semantics.
   493  // Version can contain wildcards and defaults to "*" if not specified.
   494  func (d *DependencyResolver) Resolve(id string, version string) (BuildpackDependency, error) {
   495  	if version == "" {
   496  		version = "*"
   497  	}
   498  
   499  	vc, err := semver.NewConstraint(version)
   500  	if err != nil {
   501  		return BuildpackDependency{}, fmt.Errorf("invalid constraint %s\n%w", vc, err)
   502  	}
   503  
   504  	var candidates []BuildpackDependency
   505  	for _, c := range d.Dependencies {
   506  		v, err := semver.NewVersion(c.Version)
   507  		if err != nil {
   508  			return BuildpackDependency{}, fmt.Errorf("unable to parse version %s\n%w", c.Version, err)
   509  		}
   510  
   511  		// filter out deps that do not match the current running architecture
   512  		arch, err := archFromPURL(c.PURL)
   513  		if err != nil {
   514  			return BuildpackDependency{}, fmt.Errorf("unable to compare arch\n%w", err)
   515  		}
   516  		if arch != archFromSystem() {
   517  			continue
   518  		}
   519  
   520  		if c.ID == id && vc.Check(v) && d.contains(c.Stacks, d.StackID) {
   521  			candidates = append(candidates, c)
   522  		}
   523  	}
   524  
   525  	if len(candidates) == 0 {
   526  		return BuildpackDependency{}, NoValidDependenciesError{
   527  			Message: fmt.Sprintf("no valid dependencies for %s, %s, and %s in %s",
   528  				id, version, d.StackID, DependenciesFormatter(d.Dependencies)),
   529  		}
   530  	}
   531  
   532  	sort.Slice(candidates, func(i int, j int) bool {
   533  		a, _ := semver.NewVersion(candidates[i].Version)
   534  		b, _ := semver.NewVersion(candidates[j].Version)
   535  
   536  		return a.GreaterThan(b)
   537  	})
   538  
   539  	candidate := candidates[0]
   540  
   541  	if (candidate.DeprecationDate != time.Time{}) {
   542  		d.printDependencyDeprecation(candidate)
   543  	}
   544  
   545  	return candidate, nil
   546  }
   547  
   548  func archFromPURL(rawPURL string) (string, error) {
   549  	if len(strings.TrimSpace(rawPURL)) == 0 {
   550  		return "amd64", nil
   551  	}
   552  
   553  	purl, err := url.Parse(rawPURL)
   554  	if err != nil {
   555  		return "", fmt.Errorf("unable to parse PURL\n%w", err)
   556  	}
   557  
   558  	queryParams := purl.Query()
   559  	if arch, ok := queryParams["arch"]; ok {
   560  		return arch[0], nil
   561  	}
   562  
   563  	return archFromSystem(), nil
   564  }
   565  
   566  func archFromSystem() string {
   567  	archFromEnv, ok := os.LookupEnv("BP_ARCH")
   568  	if !ok {
   569  		archFromEnv = runtime.GOARCH
   570  	}
   571  
   572  	return archFromEnv
   573  }
   574  
   575  func (DependencyResolver) contains(candidates []string, value string) bool {
   576  	if len(candidates) == 0 {
   577  		return true
   578  	}
   579  
   580  	for _, c := range candidates {
   581  		if c == value || c == "*" {
   582  			return true
   583  		}
   584  	}
   585  
   586  	return false
   587  }
   588  
   589  func (d *DependencyResolver) printDependencyDeprecation(dependency BuildpackDependency) {
   590  	if d.Logger == nil {
   591  		return
   592  	}
   593  
   594  	f := color.New(color.FgYellow)
   595  
   596  	if dependency.IsDeprecated() {
   597  		d.Logger.Header(f.Sprint("Deprecation Notice:"))
   598  		d.Logger.Body(f.Sprintf("Version %s of %s is deprecated.", dependency.Version, dependency.Name))
   599  		d.Logger.Body(f.Sprintf("Migrate your application to a supported version of %s.", dependency.Name))
   600  	} else if dependency.IsSoonDeprecated() {
   601  		d.Logger.Header(f.Sprint("Deprecation Notice:"))
   602  		d.Logger.Body(f.Sprintf("Version %s of %s will be deprecated after %s.", dependency.Version, dependency.Name, dependency.DeprecationDate.Format("2006-01-02")))
   603  		d.Logger.Body(f.Sprintf("Migrate your application to a supported version of %s before this time.", dependency.Name))
   604  	}
   605  }