github.com/paketo-buildpacks/libpak/v2@v2.0.0-alpha.3.0.20231023030503-8365f81de65a/buildmodule.go (about)

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