pack/pkg/client/create_builder.go

590 lines
17 KiB
Go

package client
import (
"archive/tar"
"context"
"fmt"
"io"
OS "os"
"path/filepath"
"sort"
"strings"
"github.com/buildpacks/pack/internal/name"
"github.com/Masterminds/semver"
"github.com/buildpacks/imgutil"
"github.com/pkg/errors"
"golang.org/x/text/cases"
"golang.org/x/text/language"
pubbldr "github.com/buildpacks/pack/builder"
"github.com/buildpacks/pack/internal/builder"
"github.com/buildpacks/pack/internal/paths"
"github.com/buildpacks/pack/internal/style"
"github.com/buildpacks/pack/pkg/buildpack"
"github.com/buildpacks/pack/pkg/dist"
"github.com/buildpacks/pack/pkg/image"
)
// CreateBuilderOptions is a configuration object used to change the behavior of
// CreateBuilder.
type CreateBuilderOptions struct {
// The base directory to use to resolve relative assets
RelativeBaseDir string
// Name of the builder.
BuilderName string
// BuildConfigEnv for Builder
BuildConfigEnv map[string]string
// Map of labels to add to the Buildpack
Labels map[string]string
// Configuration that defines the functionality a builder provides.
Config pubbldr.Config
// Skip building image locally, directly publish to a registry.
// Requires BuilderName to be a valid registry location.
Publish bool
// Append [os]-[arch] suffix to the image tag when publishing a multi-arch to a registry
// Requires Publish to be true
AppendImageNameSuffix bool
// Buildpack registry name. Defines where all registry buildpacks will be pulled from.
Registry string
// Strategy for updating images before a build.
PullPolicy image.PullPolicy
// List of modules to be flattened
Flatten buildpack.FlattenModuleInfos
// Target platforms to build builder images for
Targets []dist.Target
// Temporary directory to use for downloading lifecycle images.
TempDirectory string
// Additional image tags to push to, each will contain contents identical to Image
AdditionalTags []string
}
// CreateBuilder creates and saves a builder image to a registry with the provided options.
// If any configuration is invalid, it will error and exit without creating any images.
func (c *Client) CreateBuilder(ctx context.Context, opts CreateBuilderOptions) error {
targets, err := c.processBuilderCreateTargets(ctx, opts)
if err != nil {
return err
}
if len(targets) == 0 {
_, err = c.createBuilderTarget(ctx, opts, nil, false)
if err != nil {
return err
}
} else {
var digests []string
multiArch := len(targets) > 1 && opts.Publish
for _, target := range targets {
digest, err := c.createBuilderTarget(ctx, opts, &target, multiArch)
if err != nil {
return err
}
digests = append(digests, digest)
}
if multiArch && len(digests) > 1 {
return c.CreateManifest(ctx, CreateManifestOptions{
IndexRepoName: opts.BuilderName,
RepoNames: digests,
Publish: true,
})
}
}
return nil
}
func (c *Client) createBuilderTarget(ctx context.Context, opts CreateBuilderOptions, target *dist.Target, multiArch bool) (string, error) {
if err := c.validateConfig(ctx, opts, target); err != nil {
return "", err
}
bldr, err := c.createBaseBuilder(ctx, opts, target, multiArch)
if err != nil {
return "", errors.Wrap(err, "failed to create builder")
}
if err := c.addBuildpacksToBuilder(ctx, opts, bldr); err != nil {
return "", errors.Wrap(err, "failed to add buildpacks to builder")
}
if err := c.addExtensionsToBuilder(ctx, opts, bldr); err != nil {
return "", errors.Wrap(err, "failed to add extensions to builder")
}
bldr.SetOrder(opts.Config.Order)
bldr.SetOrderExtensions(opts.Config.OrderExtensions)
bldr.SetSystem(opts.Config.System)
if opts.Config.Stack.ID != "" {
bldr.SetStack(opts.Config.Stack)
}
bldr.SetRunImage(opts.Config.Run)
bldr.SetBuildConfigEnv(opts.BuildConfigEnv)
err = bldr.Save(c.logger, builder.CreatorMetadata{Version: c.version}, opts.AdditionalTags...)
if err != nil {
return "", err
}
if multiArch {
// We need to keep the identifier to create the image index
id, err := bldr.Image().Identifier()
if err != nil {
return "", errors.Wrapf(err, "determining image manifest digest")
}
return id.String(), nil
}
return "", nil
}
func (c *Client) validateConfig(ctx context.Context, opts CreateBuilderOptions, target *dist.Target) error {
if err := pubbldr.ValidateConfig(opts.Config); err != nil {
return errors.Wrap(err, "invalid builder config")
}
if err := c.validateRunImageConfig(ctx, opts, target); err != nil {
return errors.Wrap(err, "invalid run image config")
}
return nil
}
func (c *Client) validateRunImageConfig(ctx context.Context, opts CreateBuilderOptions, target *dist.Target) error {
var runImages []imgutil.Image
for _, r := range opts.Config.Run.Images {
for _, i := range append([]string{r.Image}, r.Mirrors...) {
if !opts.Publish {
img, err := c.imageFetcher.Fetch(ctx, i, image.FetchOptions{Daemon: true, PullPolicy: opts.PullPolicy, Target: target})
if err != nil {
if errors.Cause(err) != image.ErrNotFound {
return errors.Wrap(err, "failed to fetch image")
}
} else {
runImages = append(runImages, img)
continue
}
}
img, err := c.imageFetcher.Fetch(ctx, i, image.FetchOptions{Daemon: false, PullPolicy: opts.PullPolicy, Target: target})
if err != nil {
if errors.Cause(err) != image.ErrNotFound {
return errors.Wrap(err, "failed to fetch image")
}
c.logger.Warnf("run image %s is not accessible", style.Symbol(i))
} else {
runImages = append(runImages, img)
}
}
}
for _, img := range runImages {
if opts.Config.Stack.ID != "" {
stackID, err := img.Label("io.buildpacks.stack.id")
if err != nil {
return errors.Wrap(err, "failed to label image")
}
if stackID != opts.Config.Stack.ID {
return fmt.Errorf(
"stack %s from builder config is incompatible with stack %s from run image %s",
style.Symbol(opts.Config.Stack.ID),
style.Symbol(stackID),
style.Symbol(img.Name()),
)
}
}
}
return nil
}
func (c *Client) createBaseBuilder(ctx context.Context, opts CreateBuilderOptions, target *dist.Target, multiArch bool) (*builder.Builder, error) {
baseImage, err := c.imageFetcher.Fetch(ctx, opts.Config.Build.Image, image.FetchOptions{Daemon: !opts.Publish, PullPolicy: opts.PullPolicy, Target: target})
if err != nil {
return nil, errors.Wrap(err, "fetch build image")
}
c.logger.Debugf("Creating builder %s from build-image %s", style.Symbol(opts.BuilderName), style.Symbol(baseImage.Name()))
var builderOpts []builder.BuilderOption
if opts.Flatten != nil && len(opts.Flatten.FlattenModules()) > 0 {
builderOpts = append(builderOpts, builder.WithFlattened(opts.Flatten))
}
if len(opts.Labels) > 0 {
builderOpts = append(builderOpts, builder.WithLabels(opts.Labels))
}
builderName := opts.BuilderName
if multiArch && opts.AppendImageNameSuffix {
builderName, err = name.AppendSuffix(builderName, *target)
if err != nil {
return nil, errors.Wrap(err, "invalid image name")
}
}
bldr, err := builder.New(baseImage, builderName, builderOpts...)
if err != nil {
return nil, errors.Wrap(err, "invalid build-image")
}
architecture, err := baseImage.Architecture()
if err != nil {
return nil, errors.Wrap(err, "lookup image Architecture")
}
os, err := baseImage.OS()
if err != nil {
return nil, errors.Wrap(err, "lookup image OS")
}
if os == "windows" && !c.experimental {
return nil, NewExperimentError("Windows containers support is currently experimental.")
}
bldr.SetDescription(opts.Config.Description)
if opts.Config.Stack.ID != "" && bldr.StackID != opts.Config.Stack.ID {
return nil, fmt.Errorf(
"stack %s from builder config is incompatible with stack %s from build image",
style.Symbol(opts.Config.Stack.ID),
style.Symbol(bldr.StackID),
)
}
lifecycle, err := c.fetchLifecycle(ctx, opts, os, architecture)
if err != nil {
return nil, errors.Wrap(err, "fetch lifecycle")
}
// Validate lifecycle version for image extensions
if err := c.validateLifecycleVersion(opts.Config, lifecycle); err != nil {
return nil, err
}
bldr.SetLifecycle(lifecycle)
bldr.SetBuildConfigEnv(opts.BuildConfigEnv)
return bldr, nil
}
func (c *Client) fetchLifecycle(ctx context.Context, opts CreateBuilderOptions, os string, architecture string) (builder.Lifecycle, error) {
config := opts.Config.Lifecycle
if config.Version != "" && config.URI != "" {
return nil, errors.Errorf(
"%s can only declare %s or %s, not both",
style.Symbol("lifecycle"), style.Symbol("version"), style.Symbol("uri"),
)
}
var uri string
var err error
switch {
case buildpack.HasDockerLocator(config.URI):
uri, err = c.uriFromLifecycleImage(ctx, opts.TempDirectory, config)
if err != nil {
return nil, errors.Wrap(err, "Could not parse uri from lifecycle image")
}
case config.Version != "":
v, err := semver.NewVersion(config.Version)
if err != nil {
return nil, errors.Wrapf(err, "%s must be a valid semver", style.Symbol("lifecycle.version"))
}
uri = c.uriFromLifecycleVersion(*v, os, architecture)
case config.URI != "":
uri, err = paths.FilePathToURI(config.URI, opts.RelativeBaseDir)
if err != nil {
return nil, err
}
default:
uri = c.uriFromLifecycleVersion(*semver.MustParse(builder.DefaultLifecycleVersion), os, architecture)
}
blob, err := c.downloader.Download(ctx, uri)
if err != nil {
return nil, errors.Wrap(err, "downloading lifecycle")
}
lifecycle, err := builder.NewLifecycle(blob)
if err != nil {
return nil, errors.Wrap(err, "invalid lifecycle")
}
return lifecycle, nil
}
func (c *Client) addBuildpacksToBuilder(ctx context.Context, opts CreateBuilderOptions, bldr *builder.Builder) error {
for _, b := range opts.Config.Buildpacks {
if err := c.addConfig(ctx, buildpack.KindBuildpack, b, opts, bldr); err != nil {
return err
}
}
return nil
}
func (c *Client) addExtensionsToBuilder(ctx context.Context, opts CreateBuilderOptions, bldr *builder.Builder) error {
for _, e := range opts.Config.Extensions {
if err := c.addConfig(ctx, buildpack.KindExtension, e, opts, bldr); err != nil {
return err
}
}
return nil
}
func (c *Client) addConfig(ctx context.Context, kind string, config pubbldr.ModuleConfig, opts CreateBuilderOptions, bldr *builder.Builder) error {
c.logger.Debugf("Looking up %s %s", kind, style.Symbol(config.DisplayString()))
builderOS, err := bldr.Image().OS()
if err != nil {
return errors.Wrapf(err, "getting builder OS")
}
builderArch, err := bldr.Image().Architecture()
if err != nil {
return errors.Wrapf(err, "getting builder architecture")
}
target := &dist.Target{OS: builderOS, Arch: builderArch}
c.logger.Debugf("Downloading buildpack for platform: %s", target.ValuesAsPlatform())
mainBP, depBPs, err := c.buildpackDownloader.Download(ctx, config.URI, buildpack.DownloadOptions{
Daemon: !opts.Publish,
ImageName: config.ImageName,
ModuleKind: kind,
PullPolicy: opts.PullPolicy,
RegistryName: opts.Registry,
RelativeBaseDir: opts.RelativeBaseDir,
Target: target,
})
if err != nil {
return errors.Wrapf(err, "downloading %s", kind)
}
err = validateModule(kind, mainBP, config.URI, config.ID, config.Version)
if err != nil {
return errors.Wrapf(err, "invalid %s", kind)
}
bpDesc := mainBP.Descriptor()
for _, deprecatedAPI := range bldr.LifecycleDescriptor().APIs.Buildpack.Deprecated {
if deprecatedAPI.Equal(bpDesc.API()) {
c.logger.Warnf(
"%s %s is using deprecated Buildpacks API version %s",
cases.Title(language.AmericanEnglish).String(kind),
style.Symbol(bpDesc.Info().FullName()),
style.Symbol(bpDesc.API().String()),
)
break
}
}
// Fixes 1453
sort.Slice(depBPs, func(i, j int) bool {
compareID := strings.Compare(depBPs[i].Descriptor().Info().ID, depBPs[j].Descriptor().Info().ID)
if compareID == 0 {
return strings.Compare(depBPs[i].Descriptor().Info().Version, depBPs[j].Descriptor().Info().Version) <= 0
}
return compareID < 0
})
switch kind {
case buildpack.KindBuildpack:
bldr.AddBuildpacks(mainBP, depBPs)
case buildpack.KindExtension:
// Extensions can't be composite
bldr.AddExtension(mainBP)
default:
return fmt.Errorf("unknown module kind: %s", kind)
}
return nil
}
func (c *Client) processBuilderCreateTargets(ctx context.Context, opts CreateBuilderOptions) ([]dist.Target, error) {
var targets []dist.Target
if len(opts.Targets) > 0 {
if opts.Publish {
targets = opts.Targets
} else {
// find a target that matches the daemon
daemonTarget, err := c.daemonTarget(ctx, opts.Targets)
if err != nil {
return targets, err
}
targets = append(targets, daemonTarget)
}
}
return targets, nil
}
func validateModule(kind string, module buildpack.BuildModule, source, expectedID, expectedVersion string) error {
info := module.Descriptor().Info()
if expectedID != "" && info.ID != expectedID {
return fmt.Errorf(
"%s from URI %s has ID %s which does not match ID %s from builder config",
kind,
style.Symbol(source),
style.Symbol(info.ID),
style.Symbol(expectedID),
)
}
if expectedVersion != "" && info.Version != expectedVersion {
return fmt.Errorf(
"%s from URI %s has version %s which does not match version %s from builder config",
kind,
style.Symbol(source),
style.Symbol(info.Version),
style.Symbol(expectedVersion),
)
}
return nil
}
func (c *Client) uriFromLifecycleVersion(version semver.Version, os string, architecture string) string {
arch := "x86-64"
if os == "windows" {
return fmt.Sprintf("https://github.com/buildpacks/lifecycle/releases/download/v%s/lifecycle-v%s+windows.%s.tgz", version.String(), version.String(), arch)
}
if builder.SupportedLinuxArchitecture(architecture) {
arch = architecture
} else {
// FIXME: this should probably be an error case in the future, see https://github.com/buildpacks/pack/issues/2163
c.logger.Warnf("failed to find a lifecycle binary for requested architecture %s, defaulting to %s", style.Symbol(architecture), style.Symbol(arch))
}
return fmt.Sprintf("https://github.com/buildpacks/lifecycle/releases/download/v%s/lifecycle-v%s+linux.%s.tgz", version.String(), version.String(), arch)
}
func stripTopDirAndWrite(layerReader io.ReadCloser, outputPath string) (*OS.File, error) {
file, err := OS.Create(outputPath)
if err != nil {
return nil, err
}
tarWriter := tar.NewWriter(file)
tarReader := tar.NewReader(layerReader)
tarReader.Next()
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
pathSep := string(OS.PathSeparator)
cnbPrefix := fmt.Sprintf("%scnb%s", pathSep, pathSep)
newHeader := *header
newHeader.Name = strings.TrimPrefix(header.Name, cnbPrefix)
if err := tarWriter.WriteHeader(&newHeader); err != nil {
return nil, err
}
if _, err := io.Copy(tarWriter, tarReader); err != nil {
return nil, err
}
}
return file, nil
}
func (c *Client) uriFromLifecycleImage(ctx context.Context, basePath string, config pubbldr.LifecycleConfig) (uri string, err error) {
var lifecycleImage imgutil.Image
imageName := buildpack.ParsePackageLocator(config.URI)
c.logger.Debugf("Downloading lifecycle image: %s", style.Symbol(imageName))
lifecycleImage, err = c.imageFetcher.Fetch(ctx, imageName, image.FetchOptions{Daemon: false})
if err != nil {
return "", err
}
lifecyclePath := filepath.Join(basePath, "lifecycle.tar")
underlyingImage := lifecycleImage.UnderlyingImage()
if underlyingImage == nil {
return "", errors.New("lifecycle image has no underlying image")
}
layers, err := underlyingImage.Layers()
if err != nil {
return "", err
}
if len(layers) == 0 {
return "", errors.New("lifecycle image has no layers")
}
// Assume the last layer has the lifecycle
lifecycleLayer := layers[len(layers)-1]
layerReader, err := lifecycleLayer.Uncompressed()
if err != nil {
return "", err
}
defer layerReader.Close()
file, err := stripTopDirAndWrite(layerReader, lifecyclePath)
if err != nil {
return "", err
}
defer file.Close()
uri, err = paths.FilePathToURI(lifecyclePath, "")
if err != nil {
return "", err
}
return uri, err
}
func hasExtensions(builderConfig pubbldr.Config) bool {
return len(builderConfig.Extensions) > 0 || len(builderConfig.OrderExtensions) > 0
}
func (c *Client) validateLifecycleVersion(builderConfig pubbldr.Config, lifecycle builder.Lifecycle) error {
if !hasExtensions(builderConfig) {
return nil
}
descriptor := lifecycle.Descriptor()
// Extensions are stable starting from Platform API 0.13
// Check the latest supported Platform API version
if len(descriptor.APIs.Platform.Supported) == 0 {
// No Platform API information available, skip validation
return nil
}
platformAPI := descriptor.APIs.Platform.Supported.Latest()
if platformAPI.LessThan("0.13") {
if !c.experimental {
return errors.Errorf(
"builder config contains image extensions, but the lifecycle Platform API version (%s) is older than 0.13; "+
"support for image extensions with Platform API < 0.13 is currently experimental",
platformAPI.String(),
)
}
}
return nil
}