pack/pkg/client/build.go

1826 lines
59 KiB
Go

package client
import (
"archive/tar"
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/Masterminds/semver"
"github.com/buildpacks/imgutil"
"github.com/buildpacks/imgutil/layout"
"github.com/buildpacks/imgutil/local"
"github.com/buildpacks/imgutil/remote"
"github.com/buildpacks/lifecycle/platform/files"
"github.com/chainguard-dev/kaniko/pkg/util/proc"
"github.com/google/go-containerregistry/pkg/name"
"github.com/moby/moby/client"
"github.com/pkg/errors"
ignore "github.com/sabhiram/go-gitignore"
"github.com/buildpacks/pack/buildpackage"
"github.com/buildpacks/pack/internal/build"
"github.com/buildpacks/pack/internal/builder"
internalConfig "github.com/buildpacks/pack/internal/config"
"github.com/buildpacks/pack/internal/layer"
pname "github.com/buildpacks/pack/internal/name"
"github.com/buildpacks/pack/internal/paths"
"github.com/buildpacks/pack/internal/stack"
"github.com/buildpacks/pack/internal/stringset"
"github.com/buildpacks/pack/internal/style"
"github.com/buildpacks/pack/internal/termui"
"github.com/buildpacks/pack/pkg/archive"
"github.com/buildpacks/pack/pkg/buildpack"
"github.com/buildpacks/pack/pkg/cache"
"github.com/buildpacks/pack/pkg/dist"
"github.com/buildpacks/pack/pkg/image"
"github.com/buildpacks/pack/pkg/logging"
projectTypes "github.com/buildpacks/pack/pkg/project/types"
v02 "github.com/buildpacks/pack/pkg/project/v02"
)
const (
minLifecycleVersionSupportingCreator = "0.7.4"
prevLifecycleVersionSupportingImage = "0.6.1"
minLifecycleVersionSupportingImage = "0.7.5"
minLifecycleVersionSupportingCreatorWithExtensions = "0.19.0"
)
var RunningInContainer = func() bool {
return proc.GetContainerRuntime(0, 0) != proc.RuntimeNotFound
}
// LifecycleExecutor executes the lifecycle which satisfies the Cloud Native Buildpacks Lifecycle specification.
// Implementations of the Lifecycle must execute the following phases by calling the
// phase-specific lifecycle binary in order:
//
// Detection: /cnb/lifecycle/detector
// Analysis: /cnb/lifecycle/analyzer
// Cache Restoration: /cnb/lifecycle/restorer
// Build: /cnb/lifecycle/builder
// Export: /cnb/lifecycle/exporter
//
// or invoke the single creator binary:
//
// Creator: /cnb/lifecycle/creator
type LifecycleExecutor interface {
// Execute is responsible for invoking each of these binaries
// with the desired configuration.
Execute(ctx context.Context, opts build.LifecycleOptions) error
}
type IsTrustedBuilder func(string) bool
// BuildOptions defines configuration settings for a Build.
type BuildOptions struct {
// The base directory to use to resolve relative assets
RelativeBaseDir string
// required. Name of output image.
Image string
// required. Builder image name.
Builder string
// Name of the buildpack registry. Used to
// add buildpacks to a build.
Registry string
// AppPath is the path to application bits.
// If unset it defaults to current working directory.
AppPath string
// Specify the run image the Image will be
// built atop.
RunImage string
// Address of docker daemon exposed to build container
// e.g. tcp://example.com:1234, unix:///run/user/1000/podman/podman.sock
DockerHost string
// the target environment the OCI image is expected to be run in, i.e. production, test, development.
CNBExecutionEnv string
// Used to determine a run-image mirror if Run Image is empty.
// Used in combination with Builder metadata to determine to the 'best' mirror.
// 'best' is defined as:
// - if Publish is true, the best mirror matches registry we are publishing to.
// - if Publish is false, the best mirror matches a registry specified in Image.
// - otherwise if both of the above did not match, use mirror specified in
// the builder metadata
AdditionalMirrors map[string][]string
// User provided environment variables to the buildpacks.
// Buildpacks may both read and overwrite these values.
Env map[string]string
// Used to configure various cache available options
Cache cache.CacheOpts
// Option only valid if Publish is true
// Create an additional image that contains cache=true layers and push it to the registry.
CacheImage string
// Option passed directly to the lifecycle.
// If true, publishes Image directly to a registry.
// Assumes Image contains a valid registry with credentials
// provided by the docker client.
Publish bool
// Clear the build cache from previous builds.
ClearCache bool
// Launch a terminal UI to depict the build process
Interactive bool
// Disable System Buildpacks present in the builder
DisableSystemBuildpacks bool
// List of buildpack images or archives to add to a builder.
// These buildpacks may overwrite those on the builder if they
// share both an ID and Version with a buildpack on the builder.
Buildpacks []string
// List of extension images or archives to add to a builder.
// These extensions may overwrite those on the builder if they
// share both an ID and Version with an extension on the builder.
Extensions []string
// Additional image tags to push to, each will contain contents identical to Image
AdditionalTags []string
// Configure the proxy environment variables,
// These variables will only be set in the build image
// and will not be used if proxy env vars are already set.
ProxyConfig *ProxyConfig
// Configure network and volume mounts for the build containers.
ContainerConfig ContainerConfig
// Process type that will be used when setting container start command.
DefaultProcessType string
// Platform is the desired platform to build on (e.g., linux/amd64)
Platform string
// Strategy for updating local images before a build.
PullPolicy image.PullPolicy
// ProjectDescriptorBaseDir is the base directory to find relative resources referenced by the ProjectDescriptor
ProjectDescriptorBaseDir string
// ProjectDescriptor describes the project and any configuration specific to the project
ProjectDescriptor projectTypes.Descriptor
// List of buildpack images or archives to add to a builder.
// these buildpacks will be prepended to the builder's order
PreBuildpacks []string
// List of buildpack images or archives to add to a builder.
// these buildpacks will be appended to the builder's order
PostBuildpacks []string
// The lifecycle image that will be used for the analysis, restore and export phases
// when using an untrusted builder.
LifecycleImage string
// The location at which to mount the AppDir in the build image.
Workspace string
// User's group id used to build the image
GroupID int
// User's user id used to build the image
UserID int
// A previous image to set to a particular tag reference, digest reference, or (when performing a daemon build) image ID;
PreviousImage string
// TrustBuilder when true optimizes builds by running
// all lifecycle phases in a single container.
// This places registry credentials on the builder's build image.
// Only trust builders from reputable sources. The optimized
// build happens only when both builder and buildpacks are
// trusted
TrustBuilder IsTrustedBuilder
// TrustExtraBuildpacks when true optimizes builds by running
// all lifecycle phases in a single container. The optimized
// build happens only when both builder and buildpacks are
// trusted
TrustExtraBuildpacks bool
// Directory to output any SBOM artifacts
SBOMDestinationDir string
// Directory to output the report.toml metadata artifact
ReportDestinationDir string
// Desired create time in the output image config
CreationTime *time.Time
// Configuration to export to OCI layout format
LayoutConfig *LayoutConfig
// Enable user namespace isolation for the build containers
EnableUsernsHost bool
InsecureRegistries []string
}
func (b *BuildOptions) Layout() bool {
if b.LayoutConfig != nil {
return b.LayoutConfig.Enable()
}
return false
}
// ProxyConfig specifies proxy setting to be set as environment variables in a container.
type ProxyConfig struct {
HTTPProxy string // Used to set HTTP_PROXY env var.
HTTPSProxy string // Used to set HTTPS_PROXY env var.
NoProxy string // Used to set NO_PROXY env var.
}
// ContainerConfig is additional configuration of the docker container that all build steps
// occur within.
type ContainerConfig struct {
// Configure network settings of the build containers.
// The value of Network is handed directly to the docker client.
// For valid values of this field see:
// https://docs.docker.com/network/#network-drivers
Network string
// Volumes are accessible during both detect build phases
// should have the form: /path/in/host:/path/in/container.
// For more about volume mounts, and their permissions see:
// https://docs.docker.com/storage/volumes/
//
// It is strongly recommended you do not override any of the
// paths with volume mounts at the following locations:
// - /cnb
// - /layers
// - anything below /cnb/**
Volumes []string
}
type LayoutConfig struct {
// Application image reference provided by the user
InputImage InputImageReference
// Previous image reference provided by the user
PreviousInputImage InputImageReference
// Local root path to save the run-image in OCI layout format
LayoutRepoDir string
// Configure the OCI layout fetch mode to avoid saving layers on disk
Sparse bool
}
func (l *LayoutConfig) Enable() bool {
return l.InputImage.Layout()
}
type layoutPathConfig struct {
hostImagePath string
hostPreviousImagePath string
hostRunImagePath string
targetImagePath string
targetPreviousImagePath string
targetRunImagePath string
}
// Build configures settings for the build container(s) and lifecycle.
// It then invokes the lifecycle to build an app image.
// If any configuration is deemed invalid, or if any lifecycle phases fail,
// an error will be returned and no image produced.
func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
var pathsConfig layoutPathConfig
if RunningInContainer() && (opts.PullPolicy != image.PullAlways) {
c.logger.Warnf("Detected pack is running in a container; if using a shared docker host, failing to pull build inputs from a remote registry is insecure - " +
"other tenants may have compromised build inputs stored in the daemon." +
"This configuration is insecure and may become unsupported in the future." +
"Re-run with '--pull-policy=always' to silence this warning.")
}
if !opts.Publish && usesContainerdStorage(c.docker) {
c.logger.Warnf("Exporting to docker daemon (building without --publish) and daemon uses containerd storage; performance may be significantly degraded.\n" +
"For more information, see https://github.com/buildpacks/pack/issues/2272.")
}
imageRef, err := c.parseReference(opts)
if err != nil {
return errors.Wrapf(err, "invalid image name '%s'", opts.Image)
}
imgRegistry := imageRef.Context().RegistryStr()
imageName := imageRef.Name()
if opts.Layout() {
pathsConfig, err = c.processLayoutPath(opts.LayoutConfig.InputImage, opts.LayoutConfig.PreviousInputImage)
if err != nil {
if opts.LayoutConfig.PreviousInputImage != nil {
return errors.Wrapf(err, "invalid layout paths image name '%s' or previous-image name '%s'", opts.LayoutConfig.InputImage.Name(),
opts.LayoutConfig.PreviousInputImage.Name())
}
return errors.Wrapf(err, "invalid layout paths image name '%s'", opts.LayoutConfig.InputImage.Name())
}
}
appPath, err := c.processAppPath(opts.AppPath)
if err != nil {
return errors.Wrapf(err, "invalid app path '%s'", opts.AppPath)
}
proxyConfig := c.processProxyConfig(opts.ProxyConfig)
builderRef, err := c.processBuilderName(opts.Builder)
if err != nil {
return errors.Wrapf(err, "invalid builder '%s'", opts.Builder)
}
requestedTarget := func() *dist.Target {
if opts.Platform == "" {
return nil
}
parts := strings.Split(opts.Platform, "/")
switch len(parts) {
case 0:
return nil
case 1:
return &dist.Target{OS: parts[0]}
case 2:
return &dist.Target{OS: parts[0], Arch: parts[1]}
default:
return &dist.Target{OS: parts[0], Arch: parts[1], ArchVariant: parts[2]}
}
}()
rawBuilderImage, err := c.imageFetcher.Fetch(
ctx,
builderRef.Name(),
image.FetchOptions{
Daemon: true,
Target: requestedTarget,
PullPolicy: opts.PullPolicy,
InsecureRegistries: opts.InsecureRegistries,
},
)
if err != nil {
return errors.Wrapf(err, "failed to fetch builder image '%s'", builderRef.Name())
}
var targetToUse *dist.Target
if requestedTarget != nil {
targetToUse = requestedTarget
} else {
targetToUse, err = getTargetFromBuilder(rawBuilderImage)
if err != nil {
return err
}
}
bldr, err := c.getBuilder(rawBuilderImage)
if err != nil {
return errors.Wrapf(err, "invalid builder %s", style.Symbol(opts.Builder))
}
fetchOptions := image.FetchOptions{
Daemon: !opts.Publish,
PullPolicy: opts.PullPolicy,
Target: targetToUse,
InsecureRegistries: opts.InsecureRegistries,
}
runImageName := c.resolveRunImage(opts.RunImage, imgRegistry, builderRef.Context().RegistryStr(), bldr.DefaultRunImage(), opts.AdditionalMirrors, opts.Publish, fetchOptions)
if opts.Layout() {
targetRunImagePath, err := layout.ParseRefToPath(runImageName)
if err != nil {
return err
}
hostRunImagePath := filepath.Join(opts.LayoutConfig.LayoutRepoDir, targetRunImagePath)
targetRunImagePath = filepath.Join(paths.RootDir, "layout-repo", targetRunImagePath)
fetchOptions.LayoutOption = image.LayoutOption{
Path: hostRunImagePath,
Sparse: opts.LayoutConfig.Sparse,
}
fetchOptions.Daemon = false
pathsConfig.targetRunImagePath = targetRunImagePath
pathsConfig.hostRunImagePath = hostRunImagePath
}
runImage, warnings, err := c.validateRunImage(ctx, runImageName, fetchOptions, bldr.StackID)
if err != nil {
return errors.Wrapf(err, "invalid run-image '%s'", runImageName)
}
for _, warning := range warnings {
c.logger.Warn(warning)
}
var runMixins []string
if _, err := dist.GetLabel(runImage, stack.MixinsLabel, &runMixins); err != nil {
return err
}
fetchedBPs, nInlineBPs, order, err := c.processBuildpacks(ctx, bldr.Buildpacks(), bldr.Order(), bldr.StackID, opts, targetToUse)
if err != nil {
return err
}
fetchedExs, orderExtensions, err := c.processExtensions(ctx, bldr.Extensions(), opts, targetToUse)
if err != nil {
return err
}
system, err := c.processSystem(bldr.System(), fetchedBPs, opts.DisableSystemBuildpacks)
if err != nil {
return err
}
// Default mode: if the TrustBuilder option is not set, trust the known trusted builders.
if opts.TrustBuilder == nil {
opts.TrustBuilder = builder.IsKnownTrustedBuilder
}
// Ensure the builder's platform APIs are supported
var builderPlatformAPIs builder.APISet
builderPlatformAPIs = append(builderPlatformAPIs, bldr.LifecycleDescriptor().APIs.Platform.Deprecated...)
builderPlatformAPIs = append(builderPlatformAPIs, bldr.LifecycleDescriptor().APIs.Platform.Supported...)
if !supportsPlatformAPI(builderPlatformAPIs) {
c.logger.Debugf("pack %s supports Platform API(s): %s", c.version, strings.Join(build.SupportedPlatformAPIVersions.AsStrings(), ", "))
c.logger.Debugf("Builder %s supports Platform API(s): %s", style.Symbol(opts.Builder), strings.Join(builderPlatformAPIs.AsStrings(), ", "))
return errors.Errorf("Builder %s is incompatible with this version of pack", style.Symbol(opts.Builder))
}
// Get the platform API version to use
lifecycleVersion := bldr.LifecycleDescriptor().Info.Version
useCreator := supportsCreator(lifecycleVersion) && opts.TrustBuilder(opts.Builder)
hasAdditionalBuildpacks := func() bool {
return len(fetchedBPs) != nInlineBPs
}()
hasExtensions := func() bool {
return len(fetchedExs) != 0
}()
if hasExtensions {
c.logger.Warnf("Builder is trusted but additional modules were added; using the untrusted (5 phases) build flow")
useCreator = false
}
if hasAdditionalBuildpacks && !opts.TrustExtraBuildpacks {
c.logger.Warnf("Builder is trusted but additional modules were added; using the untrusted (5 phases) build flow")
useCreator = false
}
var (
lifecycleOptsLifecycleImage string
lifecycleAPIs []string
)
if !(useCreator) {
// fetch the lifecycle image
if supportsLifecycleImage(lifecycleVersion) {
lifecycleImageName := opts.LifecycleImage
if lifecycleImageName == "" {
lifecycleImageName = fmt.Sprintf("%s:%s", internalConfig.DefaultLifecycleImageRepo, lifecycleVersion.String())
}
lifecycleImage, err := c.imageFetcher.FetchForPlatform(
ctx,
lifecycleImageName,
image.FetchOptions{
Daemon: true,
PullPolicy: opts.PullPolicy,
Target: targetToUse,
InsecureRegistries: opts.InsecureRegistries,
},
)
if err != nil {
return fmt.Errorf("fetching lifecycle image: %w", err)
}
// if lifecyle container os isn't windows, use ephemeral lifecycle to add /workspace with correct ownership
imageOS, err := lifecycleImage.OS()
if err != nil {
return errors.Wrap(err, "getting lifecycle image OS")
}
if imageOS != "windows" {
// obtain uid/gid from builder to use when extending lifecycle image
uid, gid, err := userAndGroupIDs(rawBuilderImage)
if err != nil {
return fmt.Errorf("obtaining build uid/gid from builder image: %w", err)
}
c.logger.Debugf("Creating ephemeral lifecycle from %s with uid %d and gid %d. With workspace dir %s", lifecycleImage.Name(), uid, gid, opts.Workspace)
// extend lifecycle image with mountpoints, and use it instead of current lifecycle image
lifecycleImage, err = c.createEphemeralLifecycle(lifecycleImage, opts.Workspace, uid, gid)
if err != nil {
return err
}
c.logger.Debugf("Selecting ephemeral lifecycle image %s for build", lifecycleImage.Name())
// cleanup the extended lifecycle image when done
defer c.docker.ImageRemove(context.Background(), lifecycleImage.Name(), client.ImageRemoveOptions{Force: true})
}
lifecycleOptsLifecycleImage = lifecycleImage.Name()
labels, err := lifecycleImage.Labels()
if err != nil {
return fmt.Errorf("reading labels of lifecycle image: %w", err)
}
lifecycleAPIs, err = extractSupportedLifecycleApis(labels)
if err != nil {
return fmt.Errorf("reading api versions of lifecycle image: %w", err)
}
}
}
usingPlatformAPI, err := build.FindLatestSupported(append(
bldr.LifecycleDescriptor().APIs.Platform.Deprecated,
bldr.LifecycleDescriptor().APIs.Platform.Supported...),
lifecycleAPIs)
if err != nil {
return fmt.Errorf("finding latest supported Platform API: %w", err)
}
if usingPlatformAPI.LessThan("0.12") {
if err = c.validateMixins(fetchedBPs, bldr, runImageName, runMixins); err != nil {
return fmt.Errorf("validating stack mixins: %w", err)
}
}
buildEnvs := map[string]string{}
for _, envVar := range opts.ProjectDescriptor.Build.Env {
buildEnvs[envVar.Name] = envVar.Value
}
for k, v := range opts.Env {
buildEnvs[k] = v
}
origBuilderName := rawBuilderImage.Name()
ephemeralBuilder, err := c.createEphemeralBuilder(
rawBuilderImage,
buildEnvs,
order,
fetchedBPs,
orderExtensions,
fetchedExs,
usingPlatformAPI.LessThan("0.12"),
opts.RunImage,
system,
opts.DisableSystemBuildpacks,
)
if err != nil {
return err
}
defer func() {
if ephemeralBuilder.Name() == origBuilderName {
return
}
_, _ = c.docker.ImageRemove(context.Background(), ephemeralBuilder.Name(), client.ImageRemoveOptions{Force: true})
}()
if len(bldr.OrderExtensions()) > 0 || len(ephemeralBuilder.OrderExtensions()) > 0 {
if targetToUse.OS == "windows" {
return fmt.Errorf("builder contains image extensions which are not supported for Windows builds")
}
if opts.PullPolicy != image.PullAlways {
return fmt.Errorf("pull policy must be 'always' when builder contains image extensions")
}
}
if opts.Layout() {
opts.ContainerConfig.Volumes = appendLayoutVolumes(opts.ContainerConfig.Volumes, pathsConfig)
}
processedVolumes, warnings, err := processVolumes(targetToUse.OS, opts.ContainerConfig.Volumes)
if err != nil {
return err
}
for _, warning := range warnings {
c.logger.Warn(warning)
}
fileFilter, err := getFileFilter(opts.ProjectDescriptor)
if err != nil {
return err
}
runImageName, err = pname.TranslateRegistry(runImageName, c.registryMirrors, c.logger)
if err != nil {
return err
}
projectMetadata := files.ProjectMetadata{}
if c.experimental {
version := opts.ProjectDescriptor.Project.Version
sourceURL := opts.ProjectDescriptor.Project.SourceURL
if version != "" || sourceURL != "" {
projectMetadata.Source = &files.ProjectSource{
Type: "project",
Version: map[string]interface{}{"declared": version},
Metadata: map[string]interface{}{"url": sourceURL},
}
} else {
projectMetadata.Source = v02.GitMetadata(opts.AppPath)
}
}
lifecycleOpts := build.LifecycleOptions{
AppPath: appPath,
Image: imageRef,
Builder: ephemeralBuilder,
BuilderImage: builderRef.Name(),
LifecycleImage: ephemeralBuilder.Name(),
RunImage: runImageName,
ProjectMetadata: projectMetadata,
ClearCache: opts.ClearCache,
Publish: opts.Publish,
TrustBuilder: opts.TrustBuilder(opts.Builder),
UseCreator: useCreator,
UseCreatorWithExtensions: supportsCreatorWithExtensions(lifecycleVersion),
DockerHost: opts.DockerHost,
Cache: opts.Cache,
CacheImage: opts.CacheImage,
HTTPProxy: proxyConfig.HTTPProxy,
HTTPSProxy: proxyConfig.HTTPSProxy,
NoProxy: proxyConfig.NoProxy,
Network: opts.ContainerConfig.Network,
AdditionalTags: opts.AdditionalTags,
Volumes: processedVolumes,
DefaultProcessType: opts.DefaultProcessType,
FileFilter: fileFilter,
Workspace: opts.Workspace,
GID: opts.GroupID,
UID: opts.UserID,
PreviousImage: opts.PreviousImage,
Interactive: opts.Interactive,
Termui: termui.NewTermui(imageName, ephemeralBuilder, runImageName),
ReportDestinationDir: opts.ReportDestinationDir,
SBOMDestinationDir: opts.SBOMDestinationDir,
CreationTime: opts.CreationTime,
Layout: opts.Layout(),
Keychain: c.keychain,
EnableUsernsHost: opts.EnableUsernsHost,
ExecutionEnvironment: opts.CNBExecutionEnv,
InsecureRegistries: opts.InsecureRegistries,
}
switch {
case useCreator:
lifecycleOpts.UseCreator = true
case supportsLifecycleImage(lifecycleVersion):
lifecycleOpts.LifecycleImage = lifecycleOptsLifecycleImage
lifecycleOpts.LifecycleApis = lifecycleAPIs
case !opts.TrustBuilder(opts.Builder):
return errors.Errorf("Lifecycle %s does not have an associated lifecycle image. Builder must be trusted.", lifecycleVersion.String())
}
lifecycleOpts.FetchRunImageWithLifecycleLayer = func(runImageName string) (string, error) {
ephemeralRunImageName := fmt.Sprintf("pack.local/run-image/%x:latest", randString(10))
runImage, err := c.imageFetcher.Fetch(ctx, runImageName, fetchOptions)
if err != nil {
return "", err
}
ephemeralRunImage, err := local.NewImage(ephemeralRunImageName, c.docker, local.FromBaseImage(runImage.Name()))
if err != nil {
return "", err
}
tmpDir, err := os.MkdirTemp("", "extend-run-image-scratch") // we need to write to disk because manifest.json is last in the tar
if err != nil {
return "", err
}
defer os.RemoveAll(tmpDir)
lifecycleImageTar, err := func() (string, error) {
lifecycleImageTar := filepath.Join(tmpDir, "lifecycle-image.tar")
lifecycleImageReader, err := c.docker.ImageSave(context.Background(), []string{lifecycleOpts.LifecycleImage}) // this is fast because the lifecycle image is based on distroless static
if err != nil {
return "", err
}
defer lifecycleImageReader.Close()
lifecycleImageWriter, err := os.Create(lifecycleImageTar)
if err != nil {
return "", err
}
defer lifecycleImageWriter.Close()
if _, err = io.Copy(lifecycleImageWriter, lifecycleImageReader); err != nil {
return "", err
}
return lifecycleImageTar, nil
}()
if err != nil {
return "", err
}
advanceTarToEntryWithName := func(tarReader *tar.Reader, wantName string) (*tar.Header, error) {
var (
header *tar.Header
err error
)
for {
header, err = tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if header.Name != wantName {
continue
}
return header, nil
}
return nil, fmt.Errorf("failed to find header with name: %s", wantName)
}
lifecycleLayerName, err := func() (string, error) {
lifecycleImageReader, err := os.Open(lifecycleImageTar)
if err != nil {
return "", err
}
defer lifecycleImageReader.Close()
tarReader := tar.NewReader(lifecycleImageReader)
if _, err = advanceTarToEntryWithName(tarReader, "manifest.json"); err != nil {
return "", err
}
type descriptor struct {
Layers []string
}
type manifestJSON []descriptor
var manifestContents manifestJSON
if err = json.NewDecoder(tarReader).Decode(&manifestContents); err != nil {
return "", err
}
if len(manifestContents) < 1 {
return "", errors.New("missing manifest entries")
}
// we can assume the lifecycle layer is the last in the tar, except if the lifecycle has been extended as an ephemeral lifecycle
layerOffset := 1
if strings.Contains(lifecycleOpts.LifecycleImage, "pack.local/lifecycle") {
layerOffset = 2
}
if (len(manifestContents[0].Layers) - layerOffset) < 0 {
return "", errors.New("Lifecycle image did not contain expected layer count")
}
return manifestContents[0].Layers[len(manifestContents[0].Layers)-layerOffset], nil
}()
if err != nil {
return "", err
}
if lifecycleLayerName == "" {
return "", errors.New("failed to find lifecycle layer")
}
lifecycleLayerTar, err := func() (string, error) {
lifecycleImageReader, err := os.Open(lifecycleImageTar)
if err != nil {
return "", err
}
defer lifecycleImageReader.Close()
tarReader := tar.NewReader(lifecycleImageReader)
var header *tar.Header
if header, err = advanceTarToEntryWithName(tarReader, lifecycleLayerName); err != nil {
return "", err
}
lifecycleLayerTar := filepath.Join(filepath.Dir(lifecycleImageTar), filepath.Dir(lifecycleLayerName)+".tar") // this will be either <s0m3d1g3st>/layer.tar (docker < 25.x) OR blobs/sha256.tar (docker 25.x and later OR containerd storage enabled)
if err = os.MkdirAll(filepath.Dir(lifecycleLayerTar), 0755); err != nil {
return "", err
}
lifecycleLayerWriter, err := os.OpenFile(lifecycleLayerTar, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
if err != nil {
return "", err
}
defer lifecycleLayerWriter.Close()
if _, err = io.Copy(lifecycleLayerWriter, tarReader); err != nil {
return "", err
}
return lifecycleLayerTar, nil
}()
if err != nil {
return "", err
}
diffID, err := func() (string, error) {
lifecycleLayerReader, err := os.Open(lifecycleLayerTar)
if err != nil {
return "", err
}
defer lifecycleLayerReader.Close()
hasher := sha256.New()
if _, err = io.Copy(hasher, lifecycleLayerReader); err != nil {
return "", err
}
// it's weird that this doesn't match lifecycleLayerTar
return hex.EncodeToString(hasher.Sum(nil)), nil
}()
if err != nil {
return "", err
}
if err = ephemeralRunImage.AddLayerWithDiffID(lifecycleLayerTar, "sha256:"+diffID); err != nil {
return "", err
}
if err = ephemeralRunImage.Save(); err != nil {
return "", err
}
return ephemeralRunImageName, nil
}
if err = c.lifecycleExecutor.Execute(ctx, lifecycleOpts); err != nil {
return fmt.Errorf("executing lifecycle: %w", err)
}
return c.logImageNameAndSha(ctx, opts.Publish, imageRef, opts.InsecureRegistries)
}
func usesContainerdStorage(docker DockerClient) bool {
result, err := docker.Info(context.Background(), client.InfoOptions{})
if err != nil {
return false
}
for _, driverStatus := range result.Info.DriverStatus {
if driverStatus[0] == "driver-type" && driverStatus[1] == "io.containerd.snapshotter.v1" {
return true
}
}
return false
}
func getTargetFromBuilder(builderImage imgutil.Image) (*dist.Target, error) {
builderOS, err := builderImage.OS()
if err != nil {
return nil, fmt.Errorf("failed to get builder OS: %w", err)
}
builderArch, err := builderImage.Architecture()
if err != nil {
return nil, fmt.Errorf("failed to get builder architecture: %w", err)
}
builderArchVariant, err := builderImage.Variant()
if err != nil {
return nil, fmt.Errorf("failed to get builder architecture variant: %w", err)
}
return &dist.Target{
OS: builderOS,
Arch: builderArch,
ArchVariant: builderArchVariant,
}, nil
}
func extractSupportedLifecycleApis(labels map[string]string) ([]string, error) {
// sample contents of labels:
// {io.buildpacks.builder.metadata:\"{\"lifecycle\":{\"version\":\"0.15.3\"},\"api\":{\"buildpack\":\"0.2\",\"platform\":\"0.3\"}}",
// io.buildpacks.lifecycle.apis":"{\"buildpack\":{\"deprecated\":[],\"supported\":[\"0.2\",\"0.3\",\"0.4\",\"0.5\",\"0.6\",\"0.7\",\"0.8\",\"0.9\"]},\"platform\":{\"deprecated\":[],\"supported\":[\"0.3\",\"0.4\",\"0.5\",\"0.6\",\"0.7\",\"0.8\",\"0.9\",\"0.10\"]}}\",\"io.buildpacks.lifecycle.version\":\"0.15.3\"}")
// This struct is defined in lifecycle-repository/tools/image/main.go#Descriptor -- we could consider moving it from the main package to an importable location.
var bpPlatformAPI struct {
Platform struct {
Deprecated []string
Supported []string
}
}
if len(labels["io.buildpacks.lifecycle.apis"]) > 0 {
err := json.Unmarshal([]byte(labels["io.buildpacks.lifecycle.apis"]), &bpPlatformAPI)
if err != nil {
return nil, err
}
return append(bpPlatformAPI.Platform.Deprecated, bpPlatformAPI.Platform.Supported...), nil
}
return []string{}, nil
}
func getFileFilter(descriptor projectTypes.Descriptor) (func(string) bool, error) {
if len(descriptor.Build.Exclude) > 0 {
excludes := ignore.CompileIgnoreLines(descriptor.Build.Exclude...)
return func(fileName string) bool {
return !excludes.MatchesPath(fileName)
}, nil
}
if len(descriptor.Build.Include) > 0 {
includes := ignore.CompileIgnoreLines(descriptor.Build.Include...)
return includes.MatchesPath, nil
}
return nil, nil
}
func supportsCreator(lifecycleVersion *builder.Version) bool {
// Technically the creator is supported as of platform API version 0.3 (lifecycle version 0.7.0+) but earlier versions
// have bugs that make using the creator problematic.
return !lifecycleVersion.LessThan(semver.MustParse(minLifecycleVersionSupportingCreator))
}
func supportsCreatorWithExtensions(lifecycleVersion *builder.Version) bool {
return !lifecycleVersion.LessThan(semver.MustParse(minLifecycleVersionSupportingCreatorWithExtensions))
}
func supportsLifecycleImage(lifecycleVersion *builder.Version) bool {
return lifecycleVersion.Equal(builder.VersionMustParse(prevLifecycleVersionSupportingImage)) ||
!lifecycleVersion.LessThan(semver.MustParse(minLifecycleVersionSupportingImage))
}
// supportsPlatformAPI determines whether pack can build using the builder based on the builder's supported Platform API versions.
func supportsPlatformAPI(builderPlatformAPIs builder.APISet) bool {
for _, packSupportedAPI := range build.SupportedPlatformAPIVersions {
for _, builderSupportedAPI := range builderPlatformAPIs {
supportsPlatform := packSupportedAPI.Compare(builderSupportedAPI) == 0
if supportsPlatform {
return true
}
}
}
return false
}
func (c *Client) processBuilderName(builderName string) (name.Reference, error) {
if builderName == "" {
return nil, errors.New("builder is a required parameter if the client has no default builder")
}
return name.ParseReference(builderName, name.WeakValidation)
}
func (c *Client) getBuilder(img imgutil.Image) (*builder.Builder, error) {
bldr, err := builder.FromImage(img)
if err != nil {
return nil, err
}
if bldr.Stack().RunImage.Image == "" && len(bldr.RunImages()) == 0 {
return nil, errors.New("builder metadata is missing run-image")
}
lifecycleDescriptor := bldr.LifecycleDescriptor()
if lifecycleDescriptor.Info.Version == nil {
return nil, errors.New("lifecycle version must be specified in builder")
}
if len(lifecycleDescriptor.APIs.Buildpack.Supported) == 0 {
return nil, errors.New("supported Lifecycle Buildpack APIs not specified")
}
if len(lifecycleDescriptor.APIs.Platform.Supported) == 0 {
return nil, errors.New("supported Lifecycle Platform APIs not specified")
}
return bldr, nil
}
func (c *Client) validateRunImage(context context.Context, name string, opts image.FetchOptions, expectedStack string) (runImage imgutil.Image, warnings []string, err error) {
if name == "" {
return nil, nil, errors.New("run image must be specified")
}
img, err := c.imageFetcher.Fetch(context, name, opts)
if err != nil {
return nil, nil, err
}
stackID, err := img.Label("io.buildpacks.stack.id")
if err != nil {
return nil, nil, err
}
if stackID != expectedStack {
warnings = append(warnings, "deprecated usage of stack")
}
return img, warnings, err
}
func (c *Client) validateMixins(additionalBuildpacks []buildpack.BuildModule, bldr *builder.Builder, runImageName string, runMixins []string) error {
if err := stack.ValidateMixins(bldr.Image().Name(), bldr.Mixins(), runImageName, runMixins); err != nil {
return err
}
bps, err := allBuildpacks(bldr.Image(), additionalBuildpacks)
if err != nil {
return err
}
mixins := assembleAvailableMixins(bldr.Mixins(), runMixins)
for _, bp := range bps {
if err := bp.EnsureStackSupport(bldr.StackID, mixins, true); err != nil {
return err
}
}
return nil
}
// assembleAvailableMixins returns the set of mixins that are common between the two provided sets, plus build-only mixins and run-only mixins.
func assembleAvailableMixins(buildMixins, runMixins []string) []string {
// NOTE: We cannot simply union the two mixin sets, as this could introduce a mixin that is only present on one stack
// image but not the other. A buildpack that happens to require the mixin would fail to run properly, even though validation
// would pass.
//
// For example:
//
// Incorrect:
// Run image mixins: [A, B]
// Build image mixins: [A]
// Merged: [A, B]
// Buildpack requires: [A, B]
// Match? Yes
//
// Correct:
// Run image mixins: [A, B]
// Build image mixins: [A]
// Merged: [A]
// Buildpack requires: [A, B]
// Match? No
buildOnly := stack.FindStageMixins(buildMixins, "build")
runOnly := stack.FindStageMixins(runMixins, "run")
_, _, common := stringset.Compare(buildMixins, runMixins)
return append(common, append(buildOnly, runOnly...)...)
}
// allBuildpacks aggregates all buildpacks declared on the image with additional buildpacks passed in. They are sorted
// by ID then Version.
func allBuildpacks(builderImage imgutil.Image, additionalBuildpacks []buildpack.BuildModule) ([]buildpack.Descriptor, error) {
var all []buildpack.Descriptor
var bpLayers dist.ModuleLayers
if _, err := dist.GetLabel(builderImage, dist.BuildpackLayersLabel, &bpLayers); err != nil {
return nil, err
}
for id, bps := range bpLayers {
for ver, bp := range bps {
desc := dist.BuildpackDescriptor{
WithInfo: dist.ModuleInfo{
ID: id,
Version: ver,
},
WithStacks: bp.Stacks,
WithTargets: bp.Targets,
WithOrder: bp.Order,
}
all = append(all, &desc)
}
}
for _, bp := range additionalBuildpacks {
all = append(all, bp.Descriptor())
}
sort.Slice(all, func(i, j int) bool {
if all[i].Info().ID != all[j].Info().ID {
return all[i].Info().ID < all[j].Info().ID
}
return all[i].Info().Version < all[j].Info().Version
})
return all, nil
}
func (c *Client) processAppPath(appPath string) (string, error) {
var (
resolvedAppPath string
err error
)
if appPath == "" {
if appPath, err = os.Getwd(); err != nil {
return "", errors.Wrap(err, "get working dir")
}
}
if resolvedAppPath, err = filepath.EvalSymlinks(appPath); err != nil {
return "", errors.Wrap(err, "evaluate symlink")
}
if resolvedAppPath, err = filepath.Abs(resolvedAppPath); err != nil {
return "", errors.Wrap(err, "resolve absolute path")
}
fi, err := os.Stat(resolvedAppPath)
if err != nil {
return "", errors.Wrap(err, "stat file")
}
if !fi.IsDir() {
isZip, err := archive.IsZip(filepath.Clean(resolvedAppPath))
if err != nil {
return "", errors.Wrap(err, "check zip")
}
if !isZip {
return "", errors.New("app path must be a directory or zip")
}
}
return resolvedAppPath, nil
}
// processLayoutPath given an image reference and a previous image reference this method calculates the
// local full path and the expected path in the lifecycle container for both images provides. Those values
// can be used to mount the correct volumes
func (c *Client) processLayoutPath(inputImageRef, previousImageRef InputImageReference) (layoutPathConfig, error) {
var (
hostImagePath, hostPreviousImagePath, targetImagePath, targetPreviousImagePath string
err error
)
hostImagePath, err = fullImagePath(inputImageRef, true)
if err != nil {
return layoutPathConfig{}, err
}
targetImagePath, err = layout.ParseRefToPath(inputImageRef.Name())
if err != nil {
return layoutPathConfig{}, err
}
targetImagePath = filepath.Join(paths.RootDir, "layout-repo", targetImagePath)
c.logger.Debugf("local image path %s will be mounted into the container at path %s", hostImagePath, targetImagePath)
if previousImageRef != nil && previousImageRef.Name() != "" {
hostPreviousImagePath, err = fullImagePath(previousImageRef, false)
if err != nil {
return layoutPathConfig{}, err
}
targetPreviousImagePath, err = layout.ParseRefToPath(previousImageRef.Name())
if err != nil {
return layoutPathConfig{}, err
}
targetPreviousImagePath = filepath.Join(paths.RootDir, "layout-repo", targetPreviousImagePath)
c.logger.Debugf("local previous image path %s will be mounted into the container at path %s", hostPreviousImagePath, targetPreviousImagePath)
}
return layoutPathConfig{
hostImagePath: hostImagePath,
targetImagePath: targetImagePath,
hostPreviousImagePath: hostPreviousImagePath,
targetPreviousImagePath: targetPreviousImagePath,
}, nil
}
func (c *Client) parseReference(opts BuildOptions) (name.Reference, error) {
if !opts.Layout() {
return c.parseTagReference(opts.Image)
}
base := filepath.Base(opts.Image)
return c.parseTagReference(base)
}
func (c *Client) processProxyConfig(config *ProxyConfig) ProxyConfig {
var (
httpProxy, httpsProxy, noProxy string
ok bool
)
if config != nil {
return *config
}
if httpProxy, ok = os.LookupEnv("HTTP_PROXY"); !ok {
httpProxy = os.Getenv("http_proxy")
}
if httpsProxy, ok = os.LookupEnv("HTTPS_PROXY"); !ok {
httpsProxy = os.Getenv("https_proxy")
}
if noProxy, ok = os.LookupEnv("NO_PROXY"); !ok {
noProxy = os.Getenv("no_proxy")
}
return ProxyConfig{
HTTPProxy: httpProxy,
HTTPSProxy: httpsProxy,
NoProxy: noProxy,
}
}
// processBuildpacks computes an order group based on the existing builder order and declared buildpacks. Additionally,
// it returns buildpacks that should be added to the builder.
//
// Visual examples:
//
// BUILDER ORDER
// ----------
// - group:
// - A
// - B
// - group:
// - A
//
// WITH DECLARED: "from=builder", X
// ----------
// - group:
// - A
// - B
// - X
// - group:
// - A
// - X
//
// WITH DECLARED: X, "from=builder", Y
// ----------
// - group:
// - X
// - A
// - B
// - Y
// - group:
// - X
// - A
// - Y
//
// WITH DECLARED: X
// ----------
// - group:
// - X
//
// WITH DECLARED: A
// ----------
// - group:
// - A
func (c *Client) processBuildpacks(ctx context.Context, builderBPs []dist.ModuleInfo, builderOrder dist.Order, stackID string, opts BuildOptions, targetToUse *dist.Target) (fetchedBPs []buildpack.BuildModule, nInlineBPs int, order dist.Order, err error) {
relativeBaseDir := opts.RelativeBaseDir
declaredBPs := opts.Buildpacks
// Buildpacks from --buildpack override buildpacks from project descriptor
if len(declaredBPs) == 0 && len(opts.ProjectDescriptor.Build.Buildpacks) != 0 {
relativeBaseDir = opts.ProjectDescriptorBaseDir
for _, bp := range opts.ProjectDescriptor.Build.Buildpacks {
buildpackLocator, isInline, err := getBuildpackLocator(bp, stackID)
if err != nil {
return nil, 0, nil, err
}
if isInline {
nInlineBPs++
}
declaredBPs = append(declaredBPs, buildpackLocator)
}
}
order = dist.Order{{Group: []dist.ModuleRef{}}}
for _, bp := range declaredBPs {
locatorType, err := buildpack.GetLocatorType(bp, relativeBaseDir, builderBPs)
if err != nil {
return nil, 0, nil, err
}
switch locatorType {
case buildpack.FromBuilderLocator:
switch {
case len(order) == 0 || len(order[0].Group) == 0:
order = builderOrder
case len(order) > 1:
// This should only ever be possible if they are using from=builder twice which we don't allow
return nil, 0, nil, errors.New("buildpacks from builder can only be defined once")
default:
newOrder := dist.Order{}
groupToAdd := order[0].Group
for _, bOrderEntry := range builderOrder {
newEntry := dist.OrderEntry{Group: append(groupToAdd, bOrderEntry.Group...)}
newOrder = append(newOrder, newEntry)
}
order = newOrder
}
default:
newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderBPs, opts, buildpack.KindBuildpack, targetToUse)
if err != nil {
return fetchedBPs, 0, order, err
}
fetchedBPs = append(fetchedBPs, newFetchedBPs...)
order = appendBuildpackToOrder(order, *moduleInfo)
}
}
if (len(order) == 0 || len(order[0].Group) == 0) && len(builderOrder) > 0 {
preBuildpacks := opts.PreBuildpacks
postBuildpacks := opts.PostBuildpacks
// Pre-buildpacks from --pre-buildpack override pre-buildpacks from project descriptor
if len(preBuildpacks) == 0 && len(opts.ProjectDescriptor.Build.Pre.Buildpacks) > 0 {
for _, bp := range opts.ProjectDescriptor.Build.Pre.Buildpacks {
buildpackLocator, isInline, err := getBuildpackLocator(bp, stackID)
if err != nil {
return nil, 0, nil, errors.Wrap(err, "get pre-buildpack locator")
}
if isInline {
nInlineBPs++
}
preBuildpacks = append(preBuildpacks, buildpackLocator)
}
}
// Post-buildpacks from --post-buildpack override post-buildpacks from project descriptor
if len(postBuildpacks) == 0 && len(opts.ProjectDescriptor.Build.Post.Buildpacks) > 0 {
for _, bp := range opts.ProjectDescriptor.Build.Post.Buildpacks {
buildpackLocator, isInline, err := getBuildpackLocator(bp, stackID)
if err != nil {
return nil, 0, nil, errors.Wrap(err, "get post-buildpack locator")
}
if isInline {
nInlineBPs++
}
postBuildpacks = append(postBuildpacks, buildpackLocator)
}
}
if len(preBuildpacks) > 0 || len(postBuildpacks) > 0 {
order = builderOrder
for _, bp := range preBuildpacks {
newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderBPs, opts, buildpack.KindBuildpack, targetToUse)
if err != nil {
return fetchedBPs, 0, order, err
}
fetchedBPs = append(fetchedBPs, newFetchedBPs...)
order = prependBuildpackToOrder(order, *moduleInfo)
}
for _, bp := range postBuildpacks {
newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderBPs, opts, buildpack.KindBuildpack, targetToUse)
if err != nil {
return fetchedBPs, 0, order, err
}
fetchedBPs = append(fetchedBPs, newFetchedBPs...)
order = appendBuildpackToOrder(order, *moduleInfo)
}
}
}
return fetchedBPs, nInlineBPs, order, nil
}
func (c *Client) fetchBuildpack(ctx context.Context, bp string, relativeBaseDir string, builderBPs []dist.ModuleInfo, opts BuildOptions, kind string, targetToUse *dist.Target) ([]buildpack.BuildModule, *dist.ModuleInfo, error) {
pullPolicy := opts.PullPolicy
publish := opts.Publish
registry := opts.Registry
locatorType, err := buildpack.GetLocatorType(bp, relativeBaseDir, builderBPs)
if err != nil {
return nil, nil, err
}
fetchedBPs := []buildpack.BuildModule{}
var moduleInfo *dist.ModuleInfo
switch locatorType {
case buildpack.IDLocator:
id, version := buildpack.ParseIDLocator(bp)
moduleInfo = &dist.ModuleInfo{
ID: id,
Version: version,
}
default:
downloadOptions := buildpack.DownloadOptions{
RegistryName: registry,
Target: targetToUse,
RelativeBaseDir: relativeBaseDir,
Daemon: !publish,
PullPolicy: pullPolicy,
}
if kind == buildpack.KindExtension {
downloadOptions.ModuleKind = kind
}
mainBP, depBPs, err := c.buildpackDownloader.Download(ctx, bp, downloadOptions)
if err != nil {
return nil, nil, errors.Wrap(err, "downloading buildpack")
}
fetchedBPs = append(append(fetchedBPs, mainBP), depBPs...)
mainBPInfo := mainBP.Descriptor().Info()
moduleInfo = &mainBPInfo
packageCfgPath := filepath.Join(bp, "package.toml")
_, err = os.Stat(packageCfgPath)
if err == nil {
fetchedDeps, err := c.fetchBuildpackDependencies(ctx, bp, packageCfgPath, downloadOptions)
if err != nil {
return nil, nil, errors.Wrapf(err, "fetching package.toml dependencies (path=%s)", style.Symbol(packageCfgPath))
}
fetchedBPs = append(fetchedBPs, fetchedDeps...)
}
}
return fetchedBPs, moduleInfo, nil
}
func (c *Client) fetchBuildpackDependencies(ctx context.Context, bp string, packageCfgPath string, downloadOptions buildpack.DownloadOptions) ([]buildpack.BuildModule, error) {
packageReader := buildpackage.NewConfigReader()
packageCfg, err := packageReader.Read(packageCfgPath)
if err == nil {
fetchedBPs := []buildpack.BuildModule{}
for _, dep := range packageCfg.Dependencies {
mainBP, deps, err := c.buildpackDownloader.Download(ctx, dep.URI, buildpack.DownloadOptions{
RegistryName: downloadOptions.RegistryName,
Target: downloadOptions.Target,
Daemon: downloadOptions.Daemon,
PullPolicy: downloadOptions.PullPolicy,
RelativeBaseDir: filepath.Join(bp, packageCfg.Buildpack.URI),
})
if err != nil {
return nil, errors.Wrapf(err, "fetching dependencies (uri=%s,image=%s)", style.Symbol(dep.URI), style.Symbol(dep.ImageName))
}
fetchedBPs = append(append(fetchedBPs, mainBP), deps...)
}
return fetchedBPs, nil
}
return nil, err
}
func getBuildpackLocator(bp projectTypes.Buildpack, stackID string) (locator string, isInline bool, err error) {
switch {
case bp.ID != "" && bp.Script.Inline != "" && bp.URI == "":
if bp.Script.API == "" {
return "", false, errors.New("Missing API version for inline buildpack")
}
pathToInlineBuildpack, err := createInlineBuildpack(bp, stackID)
if err != nil {
return "", false, errors.Wrap(err, "Could not create temporary inline buildpack")
}
return pathToInlineBuildpack, true, nil
case bp.URI != "":
return bp.URI, false, nil
case bp.ID != "" && bp.Version != "":
return fmt.Sprintf("%s@%s", bp.ID, bp.Version), false, nil
case bp.ID != "" && bp.Version == "":
return bp.ID, false, nil
default:
return "", false, errors.New("Invalid buildpack definition")
}
}
func appendBuildpackToOrder(order dist.Order, bpInfo dist.ModuleInfo) (newOrder dist.Order) {
for _, orderEntry := range order {
newEntry := orderEntry
newEntry.Group = append(newEntry.Group, dist.ModuleRef{
ModuleInfo: bpInfo,
Optional: false,
})
newOrder = append(newOrder, newEntry)
}
return newOrder
}
func prependBuildpackToOrder(order dist.Order, bpInfo dist.ModuleInfo) (newOrder dist.Order) {
for _, orderEntry := range order {
newEntry := orderEntry
newGroup := []dist.ModuleRef{{
ModuleInfo: bpInfo,
Optional: false,
}}
newEntry.Group = append(newGroup, newEntry.Group...)
newOrder = append(newOrder, newEntry)
}
return newOrder
}
func (c *Client) processExtensions(ctx context.Context, builderExs []dist.ModuleInfo, opts BuildOptions, targetToUse *dist.Target) (fetchedExs []buildpack.BuildModule, orderExtensions dist.Order, err error) {
relativeBaseDir := opts.RelativeBaseDir
declaredExs := opts.Extensions
orderExtensions = dist.Order{{Group: []dist.ModuleRef{}}}
for _, ex := range declaredExs {
locatorType, err := buildpack.GetLocatorType(ex, relativeBaseDir, builderExs)
if err != nil {
return nil, nil, err
}
switch locatorType {
case buildpack.RegistryLocator:
return nil, nil, errors.New("RegistryLocator type is not valid for extensions")
case buildpack.FromBuilderLocator:
return nil, nil, errors.New("from builder is not supported for extensions")
default:
newFetchedExs, moduleInfo, err := c.fetchBuildpack(ctx, ex, relativeBaseDir, builderExs, opts, buildpack.KindExtension, targetToUse)
if err != nil {
return fetchedExs, orderExtensions, err
}
fetchedExs = append(fetchedExs, newFetchedExs...)
orderExtensions = prependBuildpackToOrder(orderExtensions, *moduleInfo)
}
}
return fetchedExs, orderExtensions, nil
}
func userAndGroupIDs(img imgutil.Image) (int, int, error) {
sUID, err := img.Env(builder.EnvUID)
if err != nil {
return 0, 0, errors.Wrap(err, "reading builder env variables")
} else if sUID == "" {
return 0, 0, fmt.Errorf("image %s missing required env var %s", style.Symbol(img.Name()), style.Symbol(builder.EnvUID))
}
sGID, err := img.Env(builder.EnvGID)
if err != nil {
return 0, 0, errors.Wrap(err, "reading builder env variables")
} else if sGID == "" {
return 0, 0, fmt.Errorf("image %s missing required env var %s", style.Symbol(img.Name()), style.Symbol(builder.EnvGID))
}
var uid, gid int
uid, err = strconv.Atoi(sUID)
if err != nil {
return 0, 0, fmt.Errorf("failed to parse %s, value %s should be an integer", style.Symbol(builder.EnvUID), style.Symbol(sUID))
}
gid, err = strconv.Atoi(sGID)
if err != nil {
return 0, 0, fmt.Errorf("failed to parse %s, value %s should be an integer", style.Symbol(builder.EnvGID), style.Symbol(sGID))
}
return uid, gid, nil
}
func workspacePathForOS(os, workspace string) string {
if workspace == "" {
workspace = "workspace"
}
if os == "windows" {
// note we don't use ephemeral lifecycle when os is windows..
return "c:\\" + workspace
}
return "/" + workspace
}
func (c *Client) addUserMountpoints(lifecycleImage imgutil.Image, dest string, workspace string, uid int, gid int) (string, error) {
// today only workspace needs to be added, easy to add future dirs if required.
imageOS, err := lifecycleImage.OS()
if err != nil {
return "", errors.Wrap(err, "getting image OS")
}
layerWriterFactory, err := layer.NewWriterFactory(imageOS)
if err != nil {
return "", err
}
workspace = workspacePathForOS(imageOS, workspace)
fh, err := os.Create(filepath.Join(dest, "dirs.tar"))
if err != nil {
return "", err
}
defer fh.Close()
lw := layerWriterFactory.NewWriter(fh)
defer lw.Close()
for _, path := range []string{workspace} {
if err := lw.WriteHeader(&tar.Header{
Typeflag: tar.TypeDir,
Name: path,
Mode: 0755,
ModTime: archive.NormalizedDateTime,
Uid: uid,
Gid: gid,
}); err != nil {
return "", errors.Wrapf(err, "creating %s mountpoint dir in layer", style.Symbol(path))
}
}
return fh.Name(), nil
}
func (c *Client) createEphemeralLifecycle(lifecycleImage imgutil.Image, workspace string, uid int, gid int) (imgutil.Image, error) {
lifecycleImage.Rename(fmt.Sprintf("pack.local/lifecycle/%x:latest", randString(10)))
tmpDir, err := os.MkdirTemp("", "create-lifecycle-scratch")
if err != nil {
return nil, err
}
defer os.RemoveAll(tmpDir)
dirsTar, err := c.addUserMountpoints(lifecycleImage, tmpDir, workspace, uid, gid)
if err != nil {
return nil, err
}
if err := lifecycleImage.AddLayer(dirsTar); err != nil {
return nil, errors.Wrap(err, "adding mountpoint dirs layer")
}
err = lifecycleImage.Save()
if err != nil {
return nil, err
}
return lifecycleImage, nil
}
func (c *Client) createEphemeralBuilder(
rawBuilderImage imgutil.Image,
env map[string]string,
order dist.Order,
buildpacks []buildpack.BuildModule,
orderExtensions dist.Order,
extensions []buildpack.BuildModule,
validateMixins bool,
runImage string,
system dist.System,
disableSystem bool,
) (*builder.Builder, error) {
if !ephemeralBuilderNeeded(env, order, buildpacks, orderExtensions, extensions, runImage) && !disableSystem {
return builder.New(rawBuilderImage, rawBuilderImage.Name(), builder.WithoutSave())
}
origBuilderName := rawBuilderImage.Name()
bldr, err := builder.New(rawBuilderImage, fmt.Sprintf("pack.local/builder/%x:latest", randString(10)), builder.WithRunImage(runImage))
if err != nil {
return nil, errors.Wrapf(err, "invalid builder %s", style.Symbol(origBuilderName))
}
bldr.SetEnv(env)
for _, bp := range buildpacks {
bpInfo := bp.Descriptor().Info()
c.logger.Debugf("Adding buildpack %s version %s to builder", style.Symbol(bpInfo.ID), style.Symbol(bpInfo.Version))
bldr.AddBuildpack(bp)
}
if len(order) > 0 && len(order[0].Group) > 0 {
c.logger.Debug("Setting custom order")
bldr.SetOrder(order)
}
for _, ex := range extensions {
exInfo := ex.Descriptor().Info()
c.logger.Debugf("Adding extension %s version %s to builder", style.Symbol(exInfo.ID), style.Symbol(exInfo.Version))
bldr.AddExtension(ex)
}
if len(orderExtensions) > 0 && len(orderExtensions[0].Group) > 0 {
c.logger.Debug("Setting custom order for extensions")
bldr.SetOrderExtensions(orderExtensions)
}
bldr.SetValidateMixins(validateMixins)
bldr.SetSystem(system)
if err := bldr.Save(c.logger, builder.CreatorMetadata{Version: c.version}); err != nil {
return nil, err
}
return bldr, nil
}
func ephemeralBuilderNeeded(
env map[string]string,
order dist.Order,
buildpacks []buildpack.BuildModule,
orderExtensions dist.Order,
extensions []buildpack.BuildModule,
runImage string,
) bool {
if len(env) > 0 {
return true
}
if len(order) > 0 && len(order[0].Group) > 0 {
return true
}
if len(buildpacks) > 0 {
return true
}
if len(orderExtensions) > 0 && len(orderExtensions[0].Group) > 0 {
return true
}
if len(extensions) > 0 {
return true
}
if runImage != "" {
return true
}
return false
}
// Returns a string iwith lowercase a-z, of length n
func randString(n int) string {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
panic(err)
}
for i := range b {
b[i] = 'a' + (b[i] % 26)
}
return string(b)
}
func (c *Client) logImageNameAndSha(ctx context.Context, publish bool, imageRef name.Reference, insecureRegistries []string) error {
// The image name and sha are printed in the lifecycle logs, and there is no need to print it again, unless output is suppressed.
if !logging.IsQuiet(c.logger) {
return nil
}
img, err := c.imageFetcher.Fetch(ctx, imageRef.Name(), image.FetchOptions{Daemon: !publish, PullPolicy: image.PullNever, InsecureRegistries: insecureRegistries})
if err != nil {
return fmt.Errorf("fetching built image: %w", err)
}
id, err := img.Identifier()
if err != nil {
return fmt.Errorf("reading image sha: %w", err)
}
// Remove tag, if it exists, from the image name
imgName := strings.TrimSuffix(imageRef.String(), imageRef.Identifier())
imgNameAndSha := fmt.Sprintf("%s@%s\n", imgName, parseDigestFromImageID(id))
// Access the logger's Writer directly to bypass ReportSuccessfulQuietBuild mode
_, err = c.logger.Writer().Write([]byte(imgNameAndSha))
return err
}
func parseDigestFromImageID(id imgutil.Identifier) string {
var digest string
switch v := id.(type) {
case local.IDIdentifier:
digest = v.String()
case remote.DigestIdentifier:
digest = v.Digest.DigestStr()
}
digest = strings.TrimPrefix(digest, "sha256:")
return fmt.Sprintf("sha256:%s", digest)
}
func createInlineBuildpack(bp projectTypes.Buildpack, stackID string) (string, error) {
pathToInlineBuilpack, err := os.MkdirTemp("", "inline-cnb")
if err != nil {
return pathToInlineBuilpack, err
}
if bp.Version == "" {
bp.Version = "0.0.0"
}
if err = createBuildpackTOML(pathToInlineBuilpack, bp.ID, bp.Version, bp.Script.API, []dist.Stack{{ID: stackID}}, []dist.Target{}, nil); err != nil {
return pathToInlineBuilpack, err
}
shell := bp.Script.Shell
if shell == "" {
shell = "/bin/sh"
}
binBuild := fmt.Sprintf(`#!%s
%s
`, shell, bp.Script.Inline)
binDetect := fmt.Sprintf(`#!%s
exit 0
`, shell)
if err = createBinScript(pathToInlineBuilpack, "build", binBuild, nil); err != nil {
return pathToInlineBuilpack, err
}
if err = createBinScript(pathToInlineBuilpack, "build.bat", bp.Script.Inline, nil); err != nil {
return pathToInlineBuilpack, err
}
if err = createBinScript(pathToInlineBuilpack, "detect", binDetect, nil); err != nil {
return pathToInlineBuilpack, err
}
if err = createBinScript(pathToInlineBuilpack, "detect.bat", bp.Script.Inline, nil); err != nil {
return pathToInlineBuilpack, err
}
return pathToInlineBuilpack, nil
}
// fullImagePath parses the inputImageReference provided by the user and creates the directory
// structure if create value is true
func fullImagePath(inputImageRef InputImageReference, create bool) (string, error) {
imagePath, err := inputImageRef.FullName()
if err != nil {
return "", errors.Wrapf(err, "evaluating image %s destination path", inputImageRef.Name())
}
if create {
if err := os.MkdirAll(imagePath, os.ModePerm); err != nil {
return "", errors.Wrapf(err, "creating %s layout application destination", imagePath)
}
}
return imagePath, nil
}
// appendLayoutVolumes mount host volume into the build container, in the form '<host path>:<target path>[:<options>]'
// the volumes mounted are:
// - The path where the user wants the image to be exported in OCI layout format
// - The previous image path if it exits
// - The run-image path
func appendLayoutVolumes(volumes []string, config layoutPathConfig) []string {
if config.hostPreviousImagePath != "" {
volumes = append(volumes, readOnlyVolume(config.hostPreviousImagePath, config.targetPreviousImagePath),
readOnlyVolume(config.hostRunImagePath, config.targetRunImagePath),
writableVolume(config.hostImagePath, config.targetImagePath))
} else {
volumes = append(volumes, readOnlyVolume(config.hostRunImagePath, config.targetRunImagePath),
writableVolume(config.hostImagePath, config.targetImagePath))
}
return volumes
}
func writableVolume(hostPath, targetPath string) string {
tp := targetPath
if !filepath.IsAbs(targetPath) {
tp = filepath.Join(string(filepath.Separator), targetPath)
}
return fmt.Sprintf("%s:%s:rw", hostPath, tp)
}
func readOnlyVolume(hostPath, targetPath string) string {
tp := targetPath
if !filepath.IsAbs(targetPath) {
tp = filepath.Join(string(filepath.Separator), targetPath)
}
return fmt.Sprintf("%s:%s", hostPath, tp)
}