github.com/paketo-buildpacks/libpak@v1.70.0/layer.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 "io/fs" 22 "os" 23 "path/filepath" 24 "reflect" 25 "time" 26 27 "github.com/BurntSushi/toml" 28 "github.com/heroku/color" 29 30 "github.com/buildpacks/libcnb" 31 32 "github.com/paketo-buildpacks/libpak/internal" 33 "github.com/paketo-buildpacks/libpak/sbom" 34 "github.com/paketo-buildpacks/libpak/sherpa" 35 36 "github.com/paketo-buildpacks/libpak/bard" 37 ) 38 39 // LayerContributor is a helper for implementing a libcnb.LayerContributor in order to get consistent logging and 40 // avoidance. 41 type LayerContributor struct { 42 43 // ExpectedMetadata is the metadata to compare against any existing layer metadata. 44 ExpectedMetadata interface{} 45 46 // Logger is the logger to use. 47 Logger bard.Logger 48 49 // Name is the user readable name of the contribution. 50 Name string 51 52 // ExpectedTypes indicates the types that should be set on the layer. 53 ExpectedTypes libcnb.LayerTypes 54 } 55 56 // NewLayerContributor creates a new instance. 57 func NewLayerContributor(name string, expectedMetadata interface{}, expectedTypes libcnb.LayerTypes) LayerContributor { 58 return LayerContributor{ 59 ExpectedMetadata: expectedMetadata, 60 Name: name, 61 ExpectedTypes: expectedTypes, 62 } 63 } 64 65 // LayerFunc is a callback function that is invoked when a layer needs to be contributed. 66 type LayerFunc func() (libcnb.Layer, error) 67 68 // Contribute is the function to call when implementing your libcnb.LayerContributor. 69 func (l *LayerContributor) Contribute(layer libcnb.Layer, f LayerFunc) (libcnb.Layer, error) { 70 layerRestored, err := l.checkIfLayerRestored(layer) 71 if err != nil { 72 return libcnb.Layer{}, fmt.Errorf("unable to check metadata\n%w", err) 73 } 74 75 expected, cached, err := l.checkIfMetadataMatches(layer) 76 if err != nil { 77 return libcnb.Layer{}, fmt.Errorf("unable to check metadata\n%w", err) 78 } 79 80 if cached && layerRestored { 81 l.Logger.Headerf("%s: %s cached layer", color.BlueString(l.Name), color.GreenString("Reusing")) 82 layer.LayerTypes = l.ExpectedTypes 83 return layer, nil 84 } 85 86 if !layerRestored { 87 l.Logger.Headerf("%s: %s cached layer", color.BlueString(l.Name), color.RedString("Reloading")) 88 } else { 89 l.Logger.Headerf("%s: %s to layer", color.BlueString(l.Name), color.YellowString("Contributing")) 90 } 91 92 err = l.reset(layer) 93 if err != nil { 94 return libcnb.Layer{}, fmt.Errorf("unable to reset\n%w", err) 95 } 96 97 layer, err = f() 98 if err != nil { 99 return libcnb.Layer{}, err 100 } 101 102 layer.LayerTypes = l.ExpectedTypes 103 layer.Metadata = expected 104 105 return layer, nil 106 } 107 108 func (l *LayerContributor) checkIfMetadataMatches(layer libcnb.Layer) (map[string]interface{}, bool, error) { 109 raw, err := internal.Marshal(l.ExpectedMetadata) 110 if err != nil { 111 return map[string]interface{}{}, false, fmt.Errorf("unable to encode metadata\n%w", err) 112 } 113 114 expected := map[string]interface{}{} 115 if err := toml.Unmarshal(raw, &expected); err != nil { 116 return map[string]interface{}{}, false, fmt.Errorf("unable to decode metadata\n%w", err) 117 } 118 119 l.Logger.Debugf("Expected metadata: %+v", expected) 120 l.Logger.Debugf("Actual metadata: %+v", layer.Metadata) 121 122 match, err := l.Equals(expected, layer.Metadata) 123 if err != nil { 124 return map[string]interface{}{}, false, fmt.Errorf("unable to compare metadata\n%w", err) 125 } 126 return expected, match, nil 127 } 128 129 func (l *LayerContributor) Equals(expectedM map[string]interface{}, layerM map[string]interface{}) (bool, error) { 130 // TODO Do we want the Equals method to modify the underlying maps? Else we need to make a copy here. 131 132 if err := l.normalizeDependencyDeprecationDate(expectedM); err != nil { 133 return false, fmt.Errorf("%w (expected layer)", err) 134 } 135 136 if err := l.normalizeDependencyDeprecationDate(layerM); err != nil { 137 return false, fmt.Errorf("%w (actual layer)", err) 138 } 139 140 return reflect.DeepEqual(expectedM, layerM), nil 141 } 142 143 // normalizeDependencyDeprecationDate makes sure the dependency deprecation date is represented as a time.Time object 144 // in the map whenever it exists. 145 func (l *LayerContributor) normalizeDependencyDeprecationDate(input map[string]interface{}) error { 146 if dep, ok := input["dependency"].(map[string]interface{}); ok { 147 for k, v := range dep { 148 if k == "deprecation_date" { 149 if err := l.replaceDeprecationDate(dep, v); err != nil { 150 return err 151 } 152 break 153 } 154 } 155 } else if depr_date, ok := input["deprecation_date"]; ok { 156 if err := l.replaceDeprecationDate(input, depr_date); err != nil { 157 return err 158 } 159 } 160 161 return nil 162 } 163 164 func (l *LayerContributor) replaceDeprecationDate(metadata map[string]interface{}, value interface{}) error { 165 deprecationDate, err := l.parseDeprecationDate(value) 166 if err != nil { 167 return err 168 } 169 metadata["deprecation_date"] = deprecationDate 170 return nil 171 } 172 173 // parseDeprecationDate accepts both string and time.Time as input, and returns 174 // a truncated time.Time value. 175 func (l *LayerContributor) parseDeprecationDate(v interface{}) (deprecationDate time.Time, err error) { 176 switch vDate := v.(type) { 177 case time.Time: 178 deprecationDate = vDate 179 case string: 180 deprecationDate, err = time.Parse(time.RFC3339, vDate) 181 if err != nil { 182 return time.Time{}, fmt.Errorf("unable to parse deprecation_date %s", vDate) 183 } 184 default: 185 return time.Time{}, fmt.Errorf("unexpected type %T for deprecation_date %v", v, v) 186 } 187 188 deprecationDate = deprecationDate.Truncate(time.Second).In(time.UTC) 189 return 190 } 191 192 func (l *LayerContributor) checkIfLayerRestored(layer libcnb.Layer) (bool, error) { 193 layerTOML := fmt.Sprintf("%s.toml", layer.Path) 194 tomlExists, err := sherpa.FileExists(layerTOML) 195 if err != nil { 196 return false, fmt.Errorf("unable to check if layer toml exists %s\n%w", layerTOML, err) 197 } 198 199 layerDirExists, err := sherpa.DirExists(layer.Path) 200 if err != nil { 201 return false, fmt.Errorf("unable to check if layer directory exists %s\n%w", layer.Path, err) 202 } 203 204 var dirContents []fs.DirEntry 205 if layerDirExists { 206 dirContents, err = os.ReadDir(layer.Path) 207 if err != nil { 208 return false, fmt.Errorf("unable to read directory %s\n%w", layer.Path, err) 209 } 210 } 211 212 l.Logger.Debugf("Check If Layer Restored -> tomlExists: %s, layerDirExists: %s, dirContents: %s, cache: %s, build: %s", 213 tomlExists, layerDirExists, dirContents, l.ExpectedTypes.Cache, l.ExpectedTypes.Build) 214 return !(tomlExists && (!layerDirExists || len(dirContents) == 0) && (l.ExpectedTypes.Cache || l.ExpectedTypes.Build)), nil 215 } 216 217 func (l *LayerContributor) reset(layer libcnb.Layer) error { 218 if err := os.RemoveAll(layer.Path); err != nil { 219 return fmt.Errorf("unable to remove existing layer directory %s\n%w", layer.Path, err) 220 } 221 222 if err := os.MkdirAll(layer.Path, 0755); err != nil { 223 return fmt.Errorf("unable to create layer directory %s\n%w", layer.Path, err) 224 } 225 226 return nil 227 } 228 229 // DependencyLayerContributor is a helper for implementing a libcnb.LayerContributor for a BuildpackDependency in order 230 // to get consistent logging and avoidance. 231 type DependencyLayerContributor struct { 232 233 // Dependency is the dependency being contributed. 234 Dependency BuildpackDependency 235 236 // DependencyCache is the cache to use to get the dependency. 237 DependencyCache DependencyCache 238 239 // ExpectedTypes indicates the types that should be set on the layer. 240 ExpectedTypes libcnb.LayerTypes 241 242 // ExpectedMetadata contains metadata describing the expected layer 243 ExpectedMetadata interface{} 244 245 // Logger is the logger to use. 246 Logger bard.Logger 247 248 // RequestModifierFuncs is an optional Request Modifier to use when downloading the dependency. 249 RequestModifierFuncs []RequestModifierFunc 250 } 251 252 // NewDependencyLayer returns a new DependencyLayerContributor for the given BuildpackDependency and a BOMEntry describing the layer contents. 253 // 254 // Deprecated: this method uses `libcnb.BOMEntry` which has been deprecated upstream, a future version will drop 255 // support for `libcnb.BOMEntry` which will change this method signature. Use NewDependencyLayerContributor instead. 256 func NewDependencyLayer(dependency BuildpackDependency, cache DependencyCache, types libcnb.LayerTypes) (DependencyLayerContributor, libcnb.BOMEntry) { 257 dlc := NewDependencyLayerContributor(dependency, cache, types) 258 259 entry := dependency.AsBOMEntry() 260 entry.Metadata["layer"] = dlc.LayerName() 261 262 if types.Launch { 263 entry.Launch = true 264 } 265 if !(types.Launch && !types.Cache && !types.Build) { 266 // launch-only layers are the only layers NOT guaranteed to be present in the build environment 267 entry.Build = true 268 } 269 270 return dlc, entry 271 } 272 273 // NewDependencyLayerContributor returns a new DependencyLayerContributor for the given BuildpackDependency 274 func NewDependencyLayerContributor(dependency BuildpackDependency, cache DependencyCache, types libcnb.LayerTypes) DependencyLayerContributor { 275 return DependencyLayerContributor{ 276 Dependency: dependency, 277 ExpectedMetadata: dependency, 278 DependencyCache: cache, 279 ExpectedTypes: types, 280 } 281 } 282 283 // DependencyLayerFunc is a callback function that is invoked when a dependency needs to be contributed. 284 type DependencyLayerFunc func(artifact *os.File) (libcnb.Layer, error) 285 286 // Contribute is the function to call whe implementing your libcnb.LayerContributor. 287 func (d *DependencyLayerContributor) Contribute(layer libcnb.Layer, f DependencyLayerFunc) (libcnb.Layer, error) { 288 lc := NewLayerContributor(d.Name(), d.ExpectedMetadata, d.ExpectedTypes) 289 lc.Logger = d.Logger 290 291 return lc.Contribute(layer, func() (libcnb.Layer, error) { 292 artifact, err := d.DependencyCache.Artifact(d.Dependency, d.RequestModifierFuncs...) 293 if err != nil { 294 d.Logger.Debugf("fetching dependency %s failed\n%w", d.Dependency.Name, err) 295 return libcnb.Layer{}, fmt.Errorf("unable to get dependency %s. see DEBUG log level", d.Dependency.Name) 296 } 297 defer artifact.Close() 298 299 sbomArtifact, err := d.Dependency.AsSyftArtifact() 300 if err != nil { 301 return libcnb.Layer{}, fmt.Errorf("unable to get SBOM artifact %s\n%w", d.Dependency.ID, err) 302 } 303 304 sbomPath := layer.SBOMPath(libcnb.SyftJSON) 305 dep := sbom.NewSyftDependency(layer.Path, []sbom.SyftArtifact{sbomArtifact}) 306 d.Logger.Debugf("Writing Syft SBOM at %s: %+v", sbomPath, dep) 307 if err := dep.WriteTo(sbomPath); err != nil { 308 return libcnb.Layer{}, fmt.Errorf("unable to write SBOM\n%w", err) 309 } 310 311 return f(artifact) 312 }) 313 } 314 315 // LayerName returns the conventional name of the layer for this contributor 316 func (d *DependencyLayerContributor) LayerName() string { 317 return d.Dependency.ID 318 } 319 320 // Name returns the human readable name of the layer 321 func (d *DependencyLayerContributor) Name() string { 322 return fmt.Sprintf("%s %s", d.Dependency.Name, d.Dependency.Version) 323 } 324 325 // HelperLayerContributor is a helper for implementing a libcnb.LayerContributor for a buildpack helper application in 326 // order to get consistent logging and avoidance. 327 type HelperLayerContributor struct { 328 329 // Path is the path to the helper application. 330 Path string 331 332 // BuildpackInfo describes the buildpack that provides the helper 333 BuildpackInfo libcnb.BuildpackInfo 334 335 // Logger is the logger to use. 336 Logger bard.Logger 337 338 // Names are the names of the helpers to create 339 Names []string 340 } 341 342 // NewHelperLayer returns a new HelperLayerContributor and a BOMEntry describing the layer contents. 343 // 344 // Deprecated: this method uses `libcnb.BOMEntry` which has been deprecated upstream, a future version will drop 345 // support for `libcnb.BOMEntry` which will change this method signature. Use NewHelperLayerContributor instead. 346 func NewHelperLayer(buildpack libcnb.Buildpack, names ...string) (HelperLayerContributor, libcnb.BOMEntry) { 347 hl := NewHelperLayerContributor(buildpack, names...) 348 349 return hl, libcnb.BOMEntry{ 350 Name: "helper", 351 Metadata: map[string]interface{}{ 352 "layer": hl.Name(), 353 "names": names, 354 "version": buildpack.Info.Version, 355 }, 356 Launch: true, 357 } 358 } 359 360 // NewHelperLayerContributor returns a new HelperLayerContributor 361 func NewHelperLayerContributor(buildpack libcnb.Buildpack, names ...string) HelperLayerContributor { 362 return HelperLayerContributor{ 363 Path: filepath.Join(buildpack.Path, "bin", "helper"), 364 Names: names, 365 BuildpackInfo: buildpack.Info, 366 } 367 } 368 369 // Name returns the conventional name of the layer for this contributor 370 func (h HelperLayerContributor) Name() string { 371 return filepath.Base(h.Path) 372 } 373 374 // Contribute is the function to call whe implementing your libcnb.LayerContributor. 375 func (h HelperLayerContributor) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { 376 expected := map[string]interface{}{"buildpackInfo": h.BuildpackInfo, "helperNames": h.Names} 377 lc := NewLayerContributor("Launch Helper", expected, libcnb.LayerTypes{ 378 Launch: true, 379 }) 380 381 lc.Logger = h.Logger 382 383 return lc.Contribute(layer, func() (libcnb.Layer, error) { 384 in, err := os.Open(h.Path) 385 if err != nil { 386 return libcnb.Layer{}, fmt.Errorf("unable to open %s\n%w", h.Path, err) 387 } 388 defer in.Close() 389 390 out := filepath.Join(layer.Path, "helper") 391 if err := sherpa.CopyFile(in, out); err != nil { 392 return libcnb.Layer{}, fmt.Errorf("unable to copy %s to %s", h.Path, out) 393 } 394 395 for _, n := range h.Names { 396 link := layer.Exec.FilePath(n) 397 h.Logger.Bodyf("Creating %s", link) 398 399 f := filepath.Dir(link) 400 if err := os.MkdirAll(f, 0755); err != nil { 401 return libcnb.Layer{}, fmt.Errorf("unable to create %s\n%w", f, err) 402 } 403 404 if err := os.Symlink(out, link); err != nil { 405 return libcnb.Layer{}, fmt.Errorf("unable to link %s to %s\n%w", out, link, err) 406 } 407 } 408 409 sbomArtifact, err := h.AsSyftArtifact() 410 if err != nil { 411 return libcnb.Layer{}, fmt.Errorf("unable to get SBOM artifact for helper\n%w", err) 412 } 413 414 sbomPath := layer.SBOMPath(libcnb.SyftJSON) 415 dep := sbom.NewSyftDependency(layer.Path, []sbom.SyftArtifact{sbomArtifact}) 416 h.Logger.Debugf("Writing Syft SBOM at %s: %+v", sbomPath, dep) 417 if err := dep.WriteTo(sbomPath); err != nil { 418 return libcnb.Layer{}, fmt.Errorf("unable to write SBOM\n%w", err) 419 } 420 421 return layer, nil 422 }) 423 } 424 425 func (h HelperLayerContributor) AsSyftArtifact() (sbom.SyftArtifact, error) { 426 licenses := []string{} 427 for _, license := range h.BuildpackInfo.Licenses { 428 licenses = append(licenses, license.Type) 429 } 430 431 locations := []sbom.SyftLocation{} 432 cpes := []string{} 433 for _, name := range h.Names { 434 locations = append(locations, sbom.SyftLocation{Path: name}) 435 cpes = append(cpes, fmt.Sprintf("cpe:2.3:a:%s:%s:%s:*:*:*:*:*:*:*", 436 h.BuildpackInfo.ID, name, h.BuildpackInfo.Version)) 437 } 438 439 artifact := sbom.SyftArtifact{ 440 Name: "helper", 441 Version: h.BuildpackInfo.Version, 442 Type: "UnknownPackage", 443 FoundBy: "libpak", 444 Licenses: licenses, 445 Locations: locations, 446 CPEs: cpes, 447 PURL: fmt.Sprintf("pkg:generic/%s@%s", h.BuildpackInfo.ID, h.BuildpackInfo.Version), 448 } 449 var err error 450 artifact.ID, err = artifact.Hash() 451 if err != nil { 452 return sbom.SyftArtifact{}, fmt.Errorf("unable to generate hash\n%w", err) 453 } 454 455 return artifact, nil 456 }