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