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 }