github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/pkg/buildpack/buildpack.go (about) 1 package buildpack 2 3 import ( 4 "archive/tar" 5 "fmt" 6 "io" 7 "os" 8 "path" 9 "path/filepath" 10 "strings" 11 12 "github.com/BurntSushi/toml" 13 "github.com/buildpacks/lifecycle/api" 14 "github.com/pkg/errors" 15 16 "github.com/buildpacks/pack/internal/style" 17 "github.com/buildpacks/pack/pkg/archive" 18 "github.com/buildpacks/pack/pkg/dist" 19 ) 20 21 const ( 22 KindBuildpack = "buildpack" 23 KindExtension = "extension" 24 ) 25 26 //go:generate mockgen -package testmocks -destination ../testmocks/mock_build_module.go github.com/buildpacks/pack/pkg/buildpack BuildModule 27 type BuildModule interface { 28 // Open returns a reader to a tar with contents structured as per the distribution spec 29 // (currently '/cnb/buildpacks/{ID}/{version}/*', all entries with a zeroed-out 30 // timestamp and root UID/GID). 31 Open() (io.ReadCloser, error) 32 Descriptor() Descriptor 33 } 34 35 type Descriptor interface { 36 API() *api.Version 37 EnsureStackSupport(stackID string, providedMixins []string, validateRunStageMixins bool) error 38 EnsureTargetSupport(os, arch, distroName, distroVersion string) error 39 EscapedID() string 40 Info() dist.ModuleInfo 41 Kind() string 42 Order() dist.Order 43 Stacks() []dist.Stack 44 Targets() []dist.Target 45 } 46 47 type Blob interface { 48 // Open returns a io.ReadCloser for the contents of the Blob in tar format. 49 Open() (io.ReadCloser, error) 50 } 51 52 type buildModule struct { 53 descriptor Descriptor 54 Blob `toml:"-"` 55 } 56 57 func (b *buildModule) Descriptor() Descriptor { 58 return b.descriptor 59 } 60 61 // FromBlob constructs a buildpack or extension from a blob. It is assumed that the buildpack 62 // contents are structured as per the distribution spec (currently '/cnb/buildpacks/{ID}/{version}/*' or 63 // '/cnb/extensions/{ID}/{version}/*'). 64 func FromBlob(descriptor Descriptor, blob Blob) BuildModule { 65 return &buildModule{ 66 Blob: blob, 67 descriptor: descriptor, 68 } 69 } 70 71 // FromBuildpackRootBlob constructs a buildpack from a blob. It is assumed that the buildpack contents reside at the 72 // root of the blob. The constructed buildpack contents will be structured as per the distribution spec (currently 73 // a tar with contents under '/cnb/buildpacks/{ID}/{version}/*'). 74 func FromBuildpackRootBlob(blob Blob, layerWriterFactory archive.TarWriterFactory, logger Logger) (BuildModule, error) { 75 descriptor := dist.BuildpackDescriptor{} 76 descriptor.WithAPI = api.MustParse(dist.AssumedBuildpackAPIVersion) 77 undecodedKeys, err := readDescriptor(KindBuildpack, &descriptor, blob) 78 if err != nil { 79 return nil, err 80 } 81 if len(undecodedKeys) > 0 { 82 logger.Warnf("Ignoring unexpected key(s) in descriptor for buildpack %s: %s", descriptor.EscapedID(), strings.Join(undecodedKeys, ",")) 83 } 84 if err := detectPlatformSpecificValues(&descriptor, blob); err != nil { 85 return nil, err 86 } 87 if err := validateBuildpackDescriptor(descriptor); err != nil { 88 return nil, err 89 } 90 return buildpackFrom(&descriptor, blob, layerWriterFactory) 91 } 92 93 // FromExtensionRootBlob constructs an extension from a blob. It is assumed that the extension contents reside at the 94 // root of the blob. The constructed extension contents will be structured as per the distribution spec (currently 95 // a tar with contents under '/cnb/extensions/{ID}/{version}/*'). 96 func FromExtensionRootBlob(blob Blob, layerWriterFactory archive.TarWriterFactory, logger Logger) (BuildModule, error) { 97 descriptor := dist.ExtensionDescriptor{} 98 descriptor.WithAPI = api.MustParse(dist.AssumedBuildpackAPIVersion) 99 undecodedKeys, err := readDescriptor(KindExtension, &descriptor, blob) 100 if err != nil { 101 return nil, err 102 } 103 if len(undecodedKeys) > 0 { 104 logger.Warnf("Ignoring unexpected key(s) in descriptor for extension %s: %s", descriptor.EscapedID(), strings.Join(undecodedKeys, ",")) 105 } 106 if err := validateExtensionDescriptor(descriptor); err != nil { 107 return nil, err 108 } 109 return buildpackFrom(&descriptor, blob, layerWriterFactory) 110 } 111 112 func readDescriptor(kind string, descriptor interface{}, blob Blob) (undecodedKeys []string, err error) { 113 rc, err := blob.Open() 114 if err != nil { 115 return undecodedKeys, errors.Wrapf(err, "open %s", kind) 116 } 117 defer rc.Close() 118 119 descriptorFile := kind + ".toml" 120 121 _, buf, err := archive.ReadTarEntry(rc, descriptorFile) 122 if err != nil { 123 return undecodedKeys, errors.Wrapf(err, "reading %s", descriptorFile) 124 } 125 126 md, err := toml.Decode(string(buf), descriptor) 127 if err != nil { 128 return undecodedKeys, errors.Wrapf(err, "decoding %s", descriptorFile) 129 } 130 131 undecoded := md.Undecoded() 132 for _, k := range undecoded { 133 undecodedKeys = append(undecodedKeys, k.String()) 134 } 135 136 return undecodedKeys, nil 137 } 138 139 func detectPlatformSpecificValues(descriptor *dist.BuildpackDescriptor, blob Blob) error { 140 if val, err := hasFile(blob, path.Join("bin", "build")); val { 141 descriptor.WithLinuxBuild = true 142 } else if err != nil { 143 return err 144 } 145 if val, err := hasFile(blob, path.Join("bin", "build.bat")); val { 146 descriptor.WithWindowsBuild = true 147 } else if err != nil { 148 return err 149 } 150 if val, err := hasFile(blob, path.Join("bin", "build.exe")); val { 151 descriptor.WithWindowsBuild = true 152 } else if err != nil { 153 return err 154 } 155 return nil 156 } 157 158 func hasFile(blob Blob, file string) (bool, error) { 159 rc, err := blob.Open() 160 if err != nil { 161 return false, errors.Wrapf(err, "open %s", "buildpack bin/") 162 } 163 defer rc.Close() 164 _, _, err = archive.ReadTarEntry(rc, file) 165 return err == nil, nil 166 } 167 168 func buildpackFrom(descriptor Descriptor, blob Blob, layerWriterFactory archive.TarWriterFactory) (BuildModule, error) { 169 return &buildModule{ 170 descriptor: descriptor, 171 Blob: &distBlob{ 172 openFn: func() io.ReadCloser { 173 return archive.GenerateTarWithWriter( 174 func(tw archive.TarWriter) error { 175 return toDistTar(tw, descriptor, blob) 176 }, 177 layerWriterFactory, 178 ) 179 }, 180 }, 181 }, nil 182 } 183 184 type distBlob struct { 185 openFn func() io.ReadCloser 186 } 187 188 func (b *distBlob) Open() (io.ReadCloser, error) { 189 return b.openFn(), nil 190 } 191 192 func toDistTar(tw archive.TarWriter, descriptor Descriptor, blob Blob) error { 193 ts := archive.NormalizedDateTime 194 195 parentDir := dist.BuildpacksDir 196 if descriptor.Kind() == KindExtension { 197 parentDir = dist.ExtensionsDir 198 } 199 200 if err := tw.WriteHeader(&tar.Header{ 201 Typeflag: tar.TypeDir, 202 Name: path.Join(parentDir, descriptor.EscapedID()), 203 Mode: 0755, 204 ModTime: ts, 205 }); err != nil { 206 return errors.Wrapf(err, "writing %s id dir header", descriptor.Kind()) 207 } 208 209 baseTarDir := path.Join(parentDir, descriptor.EscapedID(), descriptor.Info().Version) 210 if err := tw.WriteHeader(&tar.Header{ 211 Typeflag: tar.TypeDir, 212 Name: baseTarDir, 213 Mode: 0755, 214 ModTime: ts, 215 }); err != nil { 216 return errors.Wrapf(err, "writing %s version dir header", descriptor.Kind()) 217 } 218 219 rc, err := blob.Open() 220 if err != nil { 221 return errors.Wrapf(err, "reading %s blob", descriptor.Kind()) 222 } 223 defer rc.Close() 224 225 tr := tar.NewReader(rc) 226 for { 227 header, err := tr.Next() 228 if err == io.EOF { 229 break 230 } 231 if err != nil { 232 return errors.Wrap(err, "failed to get next tar entry") 233 } 234 235 archive.NormalizeHeader(header, true) 236 header.Name = path.Clean(header.Name) 237 if header.Name == "." || header.Name == "/" { 238 continue 239 } 240 241 header.Mode = calcFileMode(header) 242 header.Name = path.Join(baseTarDir, header.Name) 243 244 if header.Typeflag == tar.TypeLink { 245 header.Linkname = path.Join(baseTarDir, path.Clean(header.Linkname)) 246 } 247 err = tw.WriteHeader(header) 248 if err != nil { 249 return errors.Wrapf(err, "failed to write header for '%s'", header.Name) 250 } 251 252 _, err = io.Copy(tw, tr) 253 if err != nil { 254 return errors.Wrapf(err, "failed to write contents to '%s'", header.Name) 255 } 256 } 257 258 return nil 259 } 260 261 func calcFileMode(header *tar.Header) int64 { 262 switch { 263 case header.Typeflag == tar.TypeDir: 264 return 0755 265 case nameOneOf(header.Name, 266 path.Join("bin", "build"), 267 path.Join("bin", "detect"), 268 path.Join("bin", "generate"), 269 ): 270 return 0755 271 case anyExecBit(header.Mode): 272 return 0755 273 } 274 275 return 0644 276 } 277 278 func nameOneOf(name string, paths ...string) bool { 279 for _, p := range paths { 280 if name == p { 281 return true 282 } 283 } 284 return false 285 } 286 287 func anyExecBit(mode int64) bool { 288 return mode&0111 != 0 289 } 290 291 func validateBuildpackDescriptor(bpd dist.BuildpackDescriptor) error { 292 if bpd.Info().ID == "" { 293 return errors.Errorf("%s is required", style.Symbol("buildpack.id")) 294 } 295 296 if bpd.Info().Version == "" { 297 return errors.Errorf("%s is required", style.Symbol("buildpack.version")) 298 } 299 300 if len(bpd.Order()) >= 1 && (len(bpd.Stacks()) >= 1 || len(bpd.Targets()) >= 1) { 301 return errors.Errorf( 302 "buildpack %s: cannot have both %s/%s and an %s defined", 303 style.Symbol(bpd.Info().FullName()), 304 style.Symbol("targets"), 305 style.Symbol("stacks"), 306 style.Symbol("order"), 307 ) 308 } 309 310 return nil 311 } 312 313 func validateExtensionDescriptor(extd dist.ExtensionDescriptor) error { 314 if extd.Info().ID == "" { 315 return errors.Errorf("%s is required", style.Symbol("extension.id")) 316 } 317 318 if extd.Info().Version == "" { 319 return errors.Errorf("%s is required", style.Symbol("extension.version")) 320 } 321 322 return nil 323 } 324 325 func ToLayerTar(dest string, module BuildModule) (string, error) { 326 descriptor := module.Descriptor() 327 modReader, err := module.Open() 328 if err != nil { 329 return "", errors.Wrap(err, "opening blob") 330 } 331 defer modReader.Close() 332 333 layerTar := filepath.Join(dest, fmt.Sprintf("%s.%s.tar", descriptor.EscapedID(), descriptor.Info().Version)) 334 fh, err := os.Create(layerTar) 335 if err != nil { 336 return "", errors.Wrap(err, "create file for tar") 337 } 338 defer fh.Close() 339 340 if _, err := io.Copy(fh, modReader); err != nil { 341 return "", errors.Wrap(err, "writing blob to tar") 342 } 343 344 return layerTar, nil 345 } 346 347 func ToNLayerTar(dest string, module BuildModule) ([]ModuleTar, error) { 348 modReader, err := module.Open() 349 if err != nil { 350 return nil, errors.Wrap(err, "opening blob") 351 } 352 defer modReader.Close() 353 354 tarCollection := newModuleTarCollection(dest) 355 tr := tar.NewReader(modReader) 356 357 var ( 358 header *tar.Header 359 forWindows bool 360 ) 361 362 for { 363 header, err = tr.Next() 364 if err != nil { 365 if err == io.EOF { 366 return handleEmptyModule(dest, module) 367 } 368 return nil, err 369 } 370 if _, err := sanitizePath(header.Name); err != nil { 371 return nil, err 372 } 373 if header.Name == "Files" { 374 forWindows = true 375 } 376 if strings.Contains(header.Name, `/cnb/buildpacks/`) || strings.Contains(header.Name, `\cnb\buildpacks\`) { 377 // Only for Windows, the first four headers are: 378 // - Files 379 // - Hives 380 // - Files/cnb 381 // - Files/cnb/buildpacks 382 // Skip over these until we find "Files/cnb/buildpacks/<buildpack-id>": 383 break 384 } 385 } 386 // The header should look like "/cnb/buildpacks/<buildpack-id>" 387 // The version should be blank because the first header is missing <buildpack-version>. 388 origID, origVersion := parseBpIDAndVersion(header) 389 if origVersion != "" { 390 return nil, fmt.Errorf("first header '%s' contained unexpected version", header.Name) 391 } 392 393 if err := toNLayerTar(origID, origVersion, header, tr, tarCollection, forWindows); err != nil { 394 return nil, err 395 } 396 397 errs := tarCollection.close() 398 if len(errs) > 0 { 399 return nil, errors.New("closing files") 400 } 401 402 return tarCollection.moduleTars(), nil 403 } 404 405 func toNLayerTar(origID, origVersion string, firstHeader *tar.Header, tr *tar.Reader, tc *moduleTarCollection, forWindows bool) error { 406 toWrite := []*tar.Header{firstHeader} 407 if origVersion == "" { 408 // the first header only contains the id - e.g., /cnb/buildpacks/<buildpack-id>, 409 // read the next header to get the version 410 secondHeader, err := tr.Next() 411 if err != nil { 412 return fmt.Errorf("getting second header: %w; first header was %s", err, firstHeader.Name) 413 } 414 if _, err := sanitizePath(secondHeader.Name); err != nil { 415 return err 416 } 417 nextID, nextVersion := parseBpIDAndVersion(secondHeader) 418 if nextID != origID || nextVersion == "" { 419 return fmt.Errorf("second header '%s' contained unexpected id or missing version", secondHeader.Name) 420 } 421 origVersion = nextVersion 422 toWrite = append(toWrite, secondHeader) 423 } else { 424 // the first header contains id and version - e.g., /cnb/buildpacks/<buildpack-id>/<buildpack-version>, 425 // we need to write the parent header - e.g., /cnb/buildpacks/<buildpack-id> 426 realFirstHeader := *firstHeader 427 realFirstHeader.Name = filepath.ToSlash(filepath.Dir(firstHeader.Name)) 428 toWrite = append([]*tar.Header{&realFirstHeader}, toWrite...) 429 } 430 if forWindows { 431 toWrite = append(windowsPreamble(), toWrite...) 432 } 433 mt, err := tc.get(origID, origVersion) 434 if err != nil { 435 return fmt.Errorf("getting module from collection: %w", err) 436 } 437 for _, h := range toWrite { 438 if err := mt.writer.WriteHeader(h); err != nil { 439 return fmt.Errorf("failed to write header '%s': %w", h.Name, err) 440 } 441 } 442 // write the rest of the package 443 var header *tar.Header 444 for { 445 header, err = tr.Next() 446 if err != nil { 447 if err == io.EOF { 448 return nil 449 } 450 return fmt.Errorf("getting next header: %w", err) 451 } 452 if _, err := sanitizePath(header.Name); err != nil { 453 return err 454 } 455 nextID, nextVersion := parseBpIDAndVersion(header) 456 if nextID != origID || nextVersion != origVersion { 457 // we found a new module, recurse 458 return toNLayerTar(nextID, nextVersion, header, tr, tc, forWindows) 459 } 460 461 err = mt.writer.WriteHeader(header) 462 if err != nil { 463 return fmt.Errorf("failed to write header for '%s': %w", header.Name, err) 464 } 465 466 _, err = io.Copy(mt.writer, tr) 467 if err != nil { 468 return errors.Wrapf(err, "failed to write contents to '%s'", header.Name) 469 } 470 } 471 } 472 473 func sanitizePath(path string) (string, error) { 474 if strings.Contains(path, "..") { 475 return "", fmt.Errorf("path %s contains unexpected special elements", path) 476 } 477 return path, nil 478 } 479 480 func windowsPreamble() []*tar.Header { 481 return []*tar.Header{ 482 { 483 Name: "Files", 484 Typeflag: tar.TypeDir, 485 }, 486 { 487 Name: "Hives", 488 Typeflag: tar.TypeDir, 489 }, 490 { 491 Name: "Files/cnb", 492 Typeflag: tar.TypeDir, 493 }, 494 { 495 Name: "Files/cnb/buildpacks", 496 Typeflag: tar.TypeDir, 497 }, 498 } 499 } 500 501 func parseBpIDAndVersion(hdr *tar.Header) (id, version string) { 502 // splitting "/cnb/buildpacks/{ID}/{version}/*" returns 503 // [0] = "" -> first element is empty or "Files" in windows 504 // [1] = "cnb" 505 // [2] = "buildpacks" 506 // [3] = "{ID}" 507 // [4] = "{version}" 508 // ... 509 parts := strings.Split(strings.ReplaceAll(filepath.Clean(hdr.Name), `\`, `/`), `/`) 510 size := len(parts) 511 switch { 512 case size < 4: 513 // error 514 case size == 4: 515 id = parts[3] 516 case size >= 5: 517 id = parts[3] 518 version = parts[4] 519 } 520 return id, version 521 } 522 523 func handleEmptyModule(dest string, module BuildModule) ([]ModuleTar, error) { 524 tarFile, err := ToLayerTar(dest, module) 525 if err != nil { 526 return nil, err 527 } 528 layerTar := &moduleTar{ 529 info: module.Descriptor().Info(), 530 path: tarFile, 531 } 532 return []ModuleTar{layerTar}, nil 533 } 534 535 // Set returns a set of the given string slice. 536 func Set(exclude []string) map[string]struct{} { 537 type void struct{} 538 var member void 539 var excludedModules = make(map[string]struct{}) 540 for _, fullName := range exclude { 541 excludedModules[fullName] = member 542 } 543 return excludedModules 544 } 545 546 type ModuleTar interface { 547 Info() dist.ModuleInfo 548 Path() string 549 } 550 551 type moduleTar struct { 552 info dist.ModuleInfo 553 path string 554 writer archive.TarWriter 555 } 556 557 func (t *moduleTar) Info() dist.ModuleInfo { 558 return t.info 559 } 560 561 func (t *moduleTar) Path() string { 562 return t.path 563 } 564 565 func newModuleTar(dest, id, version string) (moduleTar, error) { 566 layerTar := filepath.Join(dest, fmt.Sprintf("%s.%s.tar", id, version)) 567 fh, err := os.Create(layerTar) 568 if err != nil { 569 return moduleTar{}, errors.Wrapf(err, "creating file at path %s", layerTar) 570 } 571 return moduleTar{ 572 info: dist.ModuleInfo{ 573 ID: id, 574 Version: version, 575 }, 576 path: layerTar, 577 writer: tar.NewWriter(fh), 578 }, nil 579 } 580 581 type moduleTarCollection struct { 582 rootPath string 583 modules map[string]moduleTar 584 } 585 586 func newModuleTarCollection(rootPath string) *moduleTarCollection { 587 return &moduleTarCollection{ 588 rootPath: rootPath, 589 modules: map[string]moduleTar{}, 590 } 591 } 592 593 func (m *moduleTarCollection) get(id, version string) (moduleTar, error) { 594 key := fmt.Sprintf("%s@%s", id, version) 595 if _, ok := m.modules[key]; !ok { 596 module, err := newModuleTar(m.rootPath, id, version) 597 if err != nil { 598 return moduleTar{}, err 599 } 600 m.modules[key] = module 601 } 602 return m.modules[key], nil 603 } 604 605 func (m *moduleTarCollection) moduleTars() []ModuleTar { 606 var modulesTar []ModuleTar 607 for _, v := range m.modules { 608 v := v 609 vv := &v 610 modulesTar = append(modulesTar, vv) 611 } 612 return modulesTar 613 } 614 615 func (m *moduleTarCollection) close() []error { 616 var errors []error 617 for _, v := range m.modules { 618 err := v.writer.Close() 619 if err != nil { 620 errors = append(errors, err) 621 } 622 } 623 return errors 624 }