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 }