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 }