pack/pkg/client/package_buildpack.go

298 lines
9.0 KiB
Go

package client
import (
"context"
"fmt"
"path/filepath"
"github.com/moby/moby/client"
"github.com/pkg/errors"
"github.com/buildpacks/pack/internal/name"
pubbldpkg "github.com/buildpacks/pack/buildpackage"
"github.com/buildpacks/pack/internal/layer"
"github.com/buildpacks/pack/internal/paths"
"github.com/buildpacks/pack/internal/style"
"github.com/buildpacks/pack/pkg/blob"
"github.com/buildpacks/pack/pkg/buildpack"
"github.com/buildpacks/pack/pkg/dist"
"github.com/buildpacks/pack/pkg/image"
)
const (
// Packaging indicator that format of inputs/outputs will be an OCI image on the registry.
FormatImage = "image"
// Packaging indicator that format of output will be a file on the host filesystem.
FormatFile = "file"
// CNBExtension is the file extension for a cloud native buildpack tar archive
CNBExtension = ".cnb"
)
// PackageBuildpackOptions is a configuration object used to define
// the behavior of PackageBuildpack.
type PackageBuildpackOptions struct {
// The base director to resolve relative assest from
RelativeBaseDir string
// The name of the output buildpack artifact.
Name string
// Type of output format, The options are the either the const FormatImage, or FormatFile.
Format string
// Defines the Buildpacks configuration.
Config pubbldpkg.Config
// Push resulting builder image up to a registry
// specified in the Name variable.
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
// Strategy for updating images before packaging.
PullPolicy image.PullPolicy
// Name of the buildpack registry. Used to
// add buildpacks to a package.
Registry string
// Flatten layers
Flatten bool
// List of buildpack images to exclude from being flattened.
FlattenExclude []string
// Map of labels to add to the Buildpack
Labels map[string]string
// Target platforms to build packages for
Targets []dist.Target
// Additional image tags to push to, each will contain contents identical to Image
AdditionalTags []string
}
// PackageBuildpack packages buildpack(s) into either an image or file.
func (c *Client) PackageBuildpack(ctx context.Context, opts PackageBuildpackOptions) error {
if opts.Format == "" {
opts.Format = FormatImage
}
targets, err := c.processPackageBuildpackTargets(ctx, opts)
if err != nil {
return err
}
multiArch := len(targets) > 1 && (opts.Publish || opts.Format == FormatFile)
var digests []string
targets = dist.ExpandTargetsDistributions(targets...)
for _, target := range targets {
digest, err := c.packageBuildpackTarget(ctx, opts, target, multiArch)
if err != nil {
return err
}
digests = append(digests, digest)
}
if opts.Publish && len(digests) > 1 {
// Image Index must be created only when we pushed to registry
return c.CreateManifest(ctx, CreateManifestOptions{
IndexRepoName: opts.Name,
RepoNames: digests,
Publish: true,
})
}
return nil
}
func (c *Client) packageBuildpackTarget(ctx context.Context, opts PackageBuildpackOptions, target dist.Target, multiArch bool) (string, error) {
var digest string
if target.OS == "windows" && !c.experimental {
return "", NewExperimentError("Windows buildpackage support is currently experimental.")
}
err := c.validateOSPlatform(ctx, target.OS, opts.Publish, opts.Format)
if err != nil {
return digest, err
}
writerFactory, err := layer.NewWriterFactory(target.OS)
if err != nil {
return digest, errors.Wrap(err, "creating layer writer factory")
}
var packageBuilderOpts []buildpack.PackageBuilderOption
if opts.Flatten {
packageBuilderOpts = append(packageBuilderOpts, buildpack.DoNotFlatten(opts.FlattenExclude),
buildpack.WithLayerWriterFactory(writerFactory), buildpack.WithLogger(c.logger))
}
packageBuilder := buildpack.NewBuilder(c.imageFactory, packageBuilderOpts...)
bpURI := opts.Config.Buildpack.URI
if bpURI == "" {
return digest, errors.New("buildpack URI must be provided")
}
if ok, platformRootFolder := buildpack.PlatformRootFolder(bpURI, target); ok {
bpURI = platformRootFolder
}
mainBlob, err := c.downloadBuildpackFromURI(ctx, bpURI, opts.RelativeBaseDir)
if err != nil {
return digest, err
}
bp, err := buildpack.FromBuildpackRootBlob(mainBlob, writerFactory, c.logger)
if err != nil {
return digest, errors.Wrapf(err, "creating buildpack from %s", style.Symbol(bpURI))
}
packageBuilder.SetBuildpack(bp)
platform := target.ValuesAsPlatform()
for _, dep := range opts.Config.Dependencies {
if multiArch {
locatorType, err := buildpack.GetLocatorType(dep.URI, opts.RelativeBaseDir, []dist.ModuleInfo{})
if err != nil {
return digest, err
}
if locatorType == buildpack.URILocator {
// When building a composite multi-platform buildpack all the dependencies must be pushed to a registry
return digest, errors.New(fmt.Sprintf("uri %s is not allowed when creating a composite multi-platform buildpack; push your dependencies to a registry and use 'docker://<image>' instead", style.Symbol(dep.URI)))
}
}
c.logger.Debugf("Downloading buildpack dependency for platform %s", platform)
mainBP, deps, err := c.buildpackDownloader.Download(ctx, dep.URI, buildpack.DownloadOptions{
RegistryName: opts.Registry,
RelativeBaseDir: opts.RelativeBaseDir,
ImageName: dep.ImageName,
Daemon: !opts.Publish,
PullPolicy: opts.PullPolicy,
Target: &target,
})
if err != nil {
return digest, errors.Wrapf(err, "packaging dependencies (uri=%s,image=%s)", style.Symbol(dep.URI), style.Symbol(dep.ImageName))
}
packageBuilder.AddDependencies(mainBP, deps)
}
switch opts.Format {
case FormatFile:
name := opts.Name
if multiArch {
extension := filepath.Ext(name)
origFileName := name[:len(name)-len(filepath.Ext(name))]
if target.Arch != "" {
name = fmt.Sprintf("%s-%s-%s%s", origFileName, target.OS, target.Arch, extension)
} else {
name = fmt.Sprintf("%s-%s%s", origFileName, target.OS, extension)
}
}
err = packageBuilder.SaveAsFile(name, target, opts.Labels)
if err != nil {
return digest, err
}
case FormatImage:
packageName := opts.Name
if multiArch && opts.AppendImageNameSuffix {
packageName, err = name.AppendSuffix(packageName, target)
if err != nil {
return "", errors.Wrap(err, "invalid image name")
}
}
img, err := packageBuilder.SaveAsImage(packageName, opts.Publish, target, opts.Labels, opts.AdditionalTags...)
if err != nil {
return digest, errors.Wrapf(err, "saving image")
}
if multiArch {
// We need to keep the identifier to create the image index
id, err := img.Identifier()
if err != nil {
return digest, errors.Wrapf(err, "determining image manifest digest")
}
digest = id.String()
}
default:
return digest, errors.Errorf("unknown format: %s", style.Symbol(opts.Format))
}
return digest, nil
}
func (c *Client) downloadBuildpackFromURI(ctx context.Context, uri, relativeBaseDir string) (blob.Blob, error) {
absPath, err := paths.FilePathToURI(uri, relativeBaseDir)
if err != nil {
return nil, errors.Wrapf(err, "making absolute: %s", style.Symbol(uri))
}
uri = absPath
c.logger.Debugf("Downloading buildpack from URI: %s", style.Symbol(uri))
blob, err := c.downloader.Download(ctx, uri)
if err != nil {
return nil, errors.Wrapf(err, "downloading buildpack from %s", style.Symbol(uri))
}
return blob, nil
}
func (c *Client) processPackageBuildpackTargets(ctx context.Context, opts PackageBuildpackOptions) ([]dist.Target, error) {
var targets []dist.Target
if len(opts.Targets) > 0 {
// when exporting to the daemon, we need to select just one target
if !opts.Publish && opts.Format == FormatImage {
daemonTarget, err := c.daemonTarget(ctx, opts.Targets)
if err != nil {
return targets, err
}
targets = append(targets, daemonTarget)
} else {
targets = opts.Targets
}
} else {
targets = append(targets, dist.Target{OS: opts.Config.Platform.OS})
}
return targets, nil
}
func (c *Client) validateOSPlatform(ctx context.Context, os string, publish bool, format string) error {
if publish || format == FormatFile {
return nil
}
result, err := c.docker.Info(ctx, client.InfoOptions{})
if err != nil {
return err
}
if result.Info.OSType != os {
return errors.Errorf("invalid %s specified: DOCKER_OS is %s", style.Symbol("platform.os"), style.Symbol(result.Info.OSType))
}
return nil
}
// daemonTarget returns a target that matches with the given daemon os/arch
func (c *Client) daemonTarget(ctx context.Context, targets []dist.Target) (dist.Target, error) {
serverResult, err := c.docker.ServerVersion(ctx, client.ServerVersionOptions{})
if err != nil {
return dist.Target{}, err
}
for _, t := range targets {
if t.Arch != "" && t.OS == serverResult.Os && t.Arch == serverResult.Arch {
return t, nil
} else if t.Arch == "" && t.OS == serverResult.Os {
return t, nil
}
}
return dist.Target{}, errors.Errorf("could not find a target that matches daemon os=%s and architecture=%s", serverResult.Os, serverResult.Arch)
}