github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/pkg/buildpack/builder.go (about) 1 package buildpack 2 3 import ( 4 "archive/tar" 5 "compress/gzip" 6 "fmt" 7 "io" 8 "os" 9 "path/filepath" 10 "strconv" 11 12 "github.com/buildpacks/imgutil" 13 "github.com/buildpacks/imgutil/layer" 14 v1 "github.com/google/go-containerregistry/pkg/v1" 15 "github.com/google/go-containerregistry/pkg/v1/empty" 16 "github.com/google/go-containerregistry/pkg/v1/layout" 17 "github.com/google/go-containerregistry/pkg/v1/mutate" 18 "github.com/google/go-containerregistry/pkg/v1/tarball" 19 "github.com/pkg/errors" 20 21 "github.com/buildpacks/pack/pkg/logging" 22 23 "github.com/buildpacks/pack/internal/stack" 24 "github.com/buildpacks/pack/internal/style" 25 "github.com/buildpacks/pack/pkg/archive" 26 "github.com/buildpacks/pack/pkg/dist" 27 ) 28 29 type ImageFactory interface { 30 NewImage(repoName string, local bool, imageOS string) (imgutil.Image, error) 31 } 32 33 type WorkableImage interface { 34 SetLabel(string, string) error 35 AddLayerWithDiffID(path, diffID string) error 36 } 37 38 type layoutImage struct { 39 v1.Image 40 } 41 42 type toAdd struct { 43 tarPath string 44 diffID string 45 module BuildModule 46 } 47 48 func (i *layoutImage) SetLabel(key string, val string) error { 49 configFile, err := i.ConfigFile() 50 if err != nil { 51 return err 52 } 53 config := *configFile.Config.DeepCopy() 54 if config.Labels == nil { 55 config.Labels = map[string]string{} 56 } 57 config.Labels[key] = val 58 i.Image, err = mutate.Config(i.Image, config) 59 return err 60 } 61 62 func (i *layoutImage) AddLayerWithDiffID(path, _ string) error { 63 tarLayer, err := tarball.LayerFromFile(path, tarball.WithCompressionLevel(gzip.DefaultCompression)) 64 if err != nil { 65 return err 66 } 67 i.Image, err = mutate.AppendLayers(i.Image, tarLayer) 68 if err != nil { 69 return errors.Wrap(err, "add layer") 70 } 71 return nil 72 } 73 74 type PackageBuilderOption func(*options) error 75 76 type options struct { 77 flatten bool 78 exclude []string 79 logger logging.Logger 80 factory archive.TarWriterFactory 81 } 82 83 type PackageBuilder struct { 84 buildpack BuildModule 85 extension BuildModule 86 logger logging.Logger 87 layerWriterFactory archive.TarWriterFactory 88 dependencies ManagedCollection 89 imageFactory ImageFactory 90 flattenAllBuildpacks bool 91 flattenExcludeBuildpacks []string 92 } 93 94 // TODO: Rename to PackageBuilder 95 func NewBuilder(imageFactory ImageFactory, ops ...PackageBuilderOption) *PackageBuilder { 96 opts := &options{} 97 for _, op := range ops { 98 if err := op(opts); err != nil { 99 return nil 100 } 101 } 102 moduleManager := NewManagedCollectionV1(opts.flatten) 103 return &PackageBuilder{ 104 imageFactory: imageFactory, 105 dependencies: moduleManager, 106 flattenAllBuildpacks: opts.flatten, 107 flattenExcludeBuildpacks: opts.exclude, 108 logger: opts.logger, 109 layerWriterFactory: opts.factory, 110 } 111 } 112 113 func FlattenAll() PackageBuilderOption { 114 return func(o *options) error { 115 o.flatten = true 116 return nil 117 } 118 } 119 120 func DoNotFlatten(exclude []string) PackageBuilderOption { 121 return func(o *options) error { 122 o.flatten = true 123 o.exclude = exclude 124 return nil 125 } 126 } 127 128 func WithLogger(logger logging.Logger) PackageBuilderOption { 129 return func(o *options) error { 130 o.logger = logger 131 return nil 132 } 133 } 134 135 func WithLayerWriterFactory(factory archive.TarWriterFactory) PackageBuilderOption { 136 return func(o *options) error { 137 o.factory = factory 138 return nil 139 } 140 } 141 142 func (b *PackageBuilder) SetBuildpack(buildpack BuildModule) { 143 b.buildpack = buildpack 144 } 145 func (b *PackageBuilder) SetExtension(extension BuildModule) { 146 b.extension = extension 147 } 148 149 func (b *PackageBuilder) AddDependency(buildpack BuildModule) { 150 b.dependencies.AddModules(buildpack) 151 } 152 153 func (b *PackageBuilder) AddDependencies(main BuildModule, dependencies []BuildModule) { 154 b.dependencies.AddModules(main, dependencies...) 155 } 156 157 func (b *PackageBuilder) ShouldFlatten(module BuildModule) bool { 158 return b.flattenAllBuildpacks || (b.dependencies.ShouldFlatten(module)) 159 } 160 161 func (b *PackageBuilder) FlattenedModules() [][]BuildModule { 162 return b.dependencies.FlattenedModules() 163 } 164 165 func (b *PackageBuilder) AllModules() []BuildModule { 166 all := b.dependencies.ExplodedModules() 167 for _, modules := range b.dependencies.FlattenedModules() { 168 all = append(all, modules...) 169 } 170 return all 171 } 172 173 func (b *PackageBuilder) finalizeImage(image WorkableImage, tmpDir string) error { 174 if err := dist.SetLabel(image, MetadataLabel, &Metadata{ 175 ModuleInfo: b.buildpack.Descriptor().Info(), 176 Stacks: b.resolvedStacks(), 177 }); err != nil { 178 return err 179 } 180 181 collectionToAdd := map[string]toAdd{} 182 var individualBuildModules []BuildModule 183 184 // Let's create the tarball for each flatten module 185 if len(b.FlattenedModules()) > 0 { 186 buildModuleWriter := NewBuildModuleWriter(b.logger, b.layerWriterFactory) 187 excludedModules := Set(b.flattenExcludeBuildpacks) 188 189 var ( 190 finalTarPath string 191 err error 192 ) 193 for i, additionalModules := range b.FlattenedModules() { 194 modFlattenTmpDir := filepath.Join(tmpDir, fmt.Sprintf("buildpack-%s-flatten", strconv.Itoa(i))) 195 if err := os.MkdirAll(modFlattenTmpDir, os.ModePerm); err != nil { 196 return errors.Wrap(err, "creating flatten temp dir") 197 } 198 199 if b.flattenAllBuildpacks { 200 // include the buildpack itself 201 additionalModules = append(additionalModules, b.buildpack) 202 } 203 finalTarPath, individualBuildModules, err = buildModuleWriter.NToLayerTar(modFlattenTmpDir, fmt.Sprintf("buildpack-flatten-%s", strconv.Itoa(i)), additionalModules, excludedModules) 204 if err != nil { 205 return errors.Wrapf(err, "adding layer %s", finalTarPath) 206 } 207 208 diffID, err := dist.LayerDiffID(finalTarPath) 209 if err != nil { 210 return errors.Wrapf(err, "calculating diffID for layer %s", finalTarPath) 211 } 212 213 for _, module := range additionalModules { 214 collectionToAdd[module.Descriptor().Info().FullName()] = toAdd{ 215 tarPath: finalTarPath, 216 diffID: diffID.String(), 217 module: module, 218 } 219 } 220 } 221 } 222 223 if !b.flattenAllBuildpacks || len(b.FlattenedModules()) == 0 { 224 individualBuildModules = append(individualBuildModules, b.buildpack) 225 } 226 227 // Let's create the tarball for each individual module 228 for _, bp := range append(b.dependencies.ExplodedModules(), individualBuildModules...) { 229 bpLayerTar, err := ToLayerTar(tmpDir, bp) 230 if err != nil { 231 return err 232 } 233 234 diffID, err := dist.LayerDiffID(bpLayerTar) 235 if err != nil { 236 return errors.Wrapf(err, 237 "getting content hashes for buildpack %s", 238 style.Symbol(bp.Descriptor().Info().FullName()), 239 ) 240 } 241 collectionToAdd[bp.Descriptor().Info().FullName()] = toAdd{ 242 tarPath: bpLayerTar, 243 diffID: diffID.String(), 244 module: bp, 245 } 246 } 247 248 bpLayers := dist.ModuleLayers{} 249 diffIDAdded := map[string]string{} 250 251 for key := range collectionToAdd { 252 module := collectionToAdd[key] 253 bp := module.module 254 addLayer := true 255 if b.ShouldFlatten(bp) { 256 if _, ok := diffIDAdded[module.diffID]; !ok { 257 diffIDAdded[module.diffID] = module.tarPath 258 } else { 259 addLayer = false 260 } 261 } 262 if addLayer { 263 if err := image.AddLayerWithDiffID(module.tarPath, module.diffID); err != nil { 264 return errors.Wrapf(err, "adding layer tar for buildpack %s", style.Symbol(bp.Descriptor().Info().FullName())) 265 } 266 } 267 268 dist.AddToLayersMD(bpLayers, bp.Descriptor(), module.diffID) 269 } 270 271 if err := dist.SetLabel(image, dist.BuildpackLayersLabel, bpLayers); err != nil { 272 return err 273 } 274 275 return nil 276 } 277 278 func (b *PackageBuilder) finalizeExtensionImage(image WorkableImage, tmpDir string) error { 279 if err := dist.SetLabel(image, MetadataLabel, &Metadata{ 280 ModuleInfo: b.extension.Descriptor().Info(), 281 }); err != nil { 282 return err 283 } 284 285 exLayers := dist.ModuleLayers{} 286 exLayerTar, err := ToLayerTar(tmpDir, b.extension) 287 if err != nil { 288 return err 289 } 290 291 diffID, err := dist.LayerDiffID(exLayerTar) 292 if err != nil { 293 return errors.Wrapf(err, 294 "getting content hashes for extension %s", 295 style.Symbol(b.extension.Descriptor().Info().FullName()), 296 ) 297 } 298 299 if err := image.AddLayerWithDiffID(exLayerTar, diffID.String()); err != nil { 300 return errors.Wrapf(err, "adding layer tar for extension %s", style.Symbol(b.extension.Descriptor().Info().FullName())) 301 } 302 303 dist.AddToLayersMD(exLayers, b.extension.Descriptor(), diffID.String()) 304 305 if err := dist.SetLabel(image, dist.ExtensionLayersLabel, exLayers); err != nil { 306 return err 307 } 308 309 return nil 310 } 311 312 func (b *PackageBuilder) validate() error { 313 if b.buildpack == nil && b.extension == nil { 314 return errors.New("buildpack or extension must be set") 315 } 316 317 // we don't need to validate extensions because there are no order or stacks in extensions 318 if b.buildpack != nil && b.extension == nil { 319 if err := validateBuildpacks(b.buildpack, b.AllModules()); err != nil { 320 return err 321 } 322 323 if len(b.resolvedStacks()) == 0 { 324 return errors.Errorf("no compatible stacks among provided buildpacks") 325 } 326 } 327 328 return nil 329 } 330 331 func (b *PackageBuilder) resolvedStacks() []dist.Stack { 332 stacks := b.buildpack.Descriptor().Stacks() 333 if len(stacks) == 0 && len(b.buildpack.Descriptor().Order()) == 0 { 334 // For non-meta-buildpacks using targets, not stacks: assume any stack 335 stacks = append(stacks, dist.Stack{ID: "*"}) 336 } 337 for _, bp := range b.AllModules() { 338 bpd := bp.Descriptor() 339 bpdStacks := bp.Descriptor().Stacks() 340 if len(bpdStacks) == 0 && len(bpd.Order()) == 0 { 341 // For non-meta-buildpacks using targets, not stacks: assume any stack 342 bpdStacks = append(bpdStacks, dist.Stack{ID: "*"}) 343 } 344 345 if len(stacks) == 0 { 346 stacks = bpdStacks 347 } else if len(bpdStacks) > 0 { // skip over "meta-buildpacks" 348 stacks = stack.MergeCompatible(stacks, bpdStacks) 349 } 350 } 351 352 return stacks 353 } 354 355 func (b *PackageBuilder) SaveAsFile(path, imageOS string, labels map[string]string) error { 356 if err := b.validate(); err != nil { 357 return err 358 } 359 360 layoutImage, err := newLayoutImage(imageOS) 361 if err != nil { 362 return errors.Wrap(err, "creating layout image") 363 } 364 365 for labelKey, labelValue := range labels { 366 err = layoutImage.SetLabel(labelKey, labelValue) 367 if err != nil { 368 return errors.Wrapf(err, "adding label %s=%s", labelKey, labelValue) 369 } 370 } 371 372 tempDirName := "" 373 if b.buildpack != nil { 374 tempDirName = "package-buildpack" 375 } else if b.extension != nil { 376 tempDirName = "extension-buildpack" 377 } 378 379 tmpDir, err := os.MkdirTemp("", tempDirName) 380 if err != nil { 381 return err 382 } 383 defer os.RemoveAll(tmpDir) 384 385 if b.buildpack != nil { 386 if err := b.finalizeImage(layoutImage, tmpDir); err != nil { 387 return err 388 } 389 } else if b.extension != nil { 390 if err := b.finalizeExtensionImage(layoutImage, tmpDir); err != nil { 391 return err 392 } 393 } 394 layoutDir, err := os.MkdirTemp(tmpDir, "oci-layout") 395 if err != nil { 396 return errors.Wrap(err, "creating oci-layout temp dir") 397 } 398 399 p, err := layout.Write(layoutDir, empty.Index) 400 if err != nil { 401 return errors.Wrap(err, "writing index") 402 } 403 404 if err := p.AppendImage(layoutImage); err != nil { 405 return errors.Wrap(err, "writing layout") 406 } 407 408 outputFile, err := os.Create(path) 409 if err != nil { 410 return errors.Wrap(err, "creating output file") 411 } 412 defer outputFile.Close() 413 414 tw := tar.NewWriter(outputFile) 415 defer tw.Close() 416 417 return archive.WriteDirToTar(tw, layoutDir, "/", 0, 0, 0755, true, false, nil) 418 } 419 420 func newLayoutImage(imageOS string) (*layoutImage, error) { 421 i := empty.Image 422 423 configFile, err := i.ConfigFile() 424 if err != nil { 425 return nil, err 426 } 427 428 configFile.OS = imageOS 429 i, err = mutate.ConfigFile(i, configFile) 430 if err != nil { 431 return nil, err 432 } 433 434 if imageOS == "windows" { 435 opener := func() (io.ReadCloser, error) { 436 reader, err := layer.WindowsBaseLayer() 437 return io.NopCloser(reader), err 438 } 439 440 baseLayer, err := tarball.LayerFromOpener(opener, tarball.WithCompressionLevel(gzip.DefaultCompression)) 441 if err != nil { 442 return nil, err 443 } 444 445 i, err = mutate.AppendLayers(i, baseLayer) 446 if err != nil { 447 return nil, err 448 } 449 } 450 451 return &layoutImage{Image: i}, nil 452 } 453 454 func (b *PackageBuilder) SaveAsImage(repoName string, publish bool, imageOS string, labels map[string]string) (imgutil.Image, error) { 455 if err := b.validate(); err != nil { 456 return nil, err 457 } 458 459 image, err := b.imageFactory.NewImage(repoName, !publish, imageOS) 460 if err != nil { 461 return nil, errors.Wrapf(err, "creating image") 462 } 463 464 for labelKey, labelValue := range labels { 465 err = image.SetLabel(labelKey, labelValue) 466 if err != nil { 467 return nil, errors.Wrapf(err, "adding label %s=%s", labelKey, labelValue) 468 } 469 } 470 471 tempDirName := "" 472 if b.buildpack != nil { 473 tempDirName = "package-buildpack" 474 } else if b.extension != nil { 475 tempDirName = "extension-buildpack" 476 } 477 478 tmpDir, err := os.MkdirTemp("", tempDirName) 479 if err != nil { 480 return nil, err 481 } 482 defer os.RemoveAll(tmpDir) 483 if b.buildpack != nil { 484 if err := b.finalizeImage(image, tmpDir); err != nil { 485 return nil, err 486 } 487 } else if b.extension != nil { 488 if err := b.finalizeExtensionImage(image, tmpDir); err != nil { 489 return nil, err 490 } 491 } 492 493 if err := image.Save(); err != nil { 494 return nil, err 495 } 496 497 return image, nil 498 } 499 500 func validateBuildpacks(mainBP BuildModule, depBPs []BuildModule) error { 501 depsWithRefs := map[string][]dist.ModuleInfo{} 502 503 for _, bp := range depBPs { 504 depsWithRefs[bp.Descriptor().Info().FullName()] = nil 505 } 506 507 for _, bp := range append([]BuildModule{mainBP}, depBPs...) { // List of everything 508 bpd := bp.Descriptor() 509 for _, orderEntry := range bpd.Order() { 510 for _, groupEntry := range orderEntry.Group { 511 bpFullName, err := groupEntry.ModuleInfo.FullNameWithVersion() 512 if err != nil { 513 return errors.Wrapf( 514 err, 515 "buildpack %s must specify a version when referencing buildpack %s", 516 style.Symbol(bpd.Info().FullName()), 517 style.Symbol(bpFullName), 518 ) 519 } 520 if _, ok := depsWithRefs[bpFullName]; !ok { 521 return errors.Errorf( 522 "buildpack %s references buildpack %s which is not present", 523 style.Symbol(bpd.Info().FullName()), 524 style.Symbol(bpFullName), 525 ) 526 } 527 528 depsWithRefs[bpFullName] = append(depsWithRefs[bpFullName], bpd.Info()) 529 } 530 } 531 } 532 533 for bp, refs := range depsWithRefs { 534 if len(refs) == 0 { 535 return errors.Errorf( 536 "buildpack %s is not used by buildpack %s", 537 style.Symbol(bp), 538 style.Symbol(mainBP.Descriptor().Info().FullName()), 539 ) 540 } 541 } 542 543 return nil 544 }