mirror of https://github.com/buildpacks/pack.git
298 lines
9.0 KiB
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)
|
|
}
|