Skip to content

Commit

Permalink
Fetch lifecycle binaries from lifecycle image instead of restorer con…
Browse files Browse the repository at this point in the history
…tainer

Hopefully fixes missing file issues on Linux

Signed-off-by: Natalie Arellano <[email protected]>
  • Loading branch information
natalieparellano committed Dec 13, 2023
1 parent e5d4417 commit 4d8de33
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 69 deletions.
20 changes: 12 additions & 8 deletions internal/build/lifecycle_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,16 @@ func (l *LifecycleExecution) Run(ctx context.Context, phaseFactoryCreator PhaseF
}
}

var (
ephemeralRunImage string
err error
)
currentRunImage := l.runImageAfterExtensions()
if currentRunImage != "" && currentRunImage != l.opts.RunImage {
if err := l.opts.FetchRunImage(currentRunImage); err != nil {
if l.runImageChanged() || l.hasExtensionsForRun() {
if currentRunImage == "" { // sanity check
return nil
}
if ephemeralRunImage, err = l.opts.FetchRunImageWithLifecycleLayer(currentRunImage); err != nil {
return err
}
}
Expand Down Expand Up @@ -269,7 +276,7 @@ func (l *LifecycleExecution) Run(ctx context.Context, phaseFactoryCreator PhaseF
if l.platformAPI.AtLeast("0.12") && l.hasExtensionsForRun() {
group.Go(func() error {
l.logger.Info(style.Step("EXTENDING (RUN)"))
return l.ExtendRun(ctx, kanikoCache, phaseFactory)
return l.ExtendRun(ctx, kanikoCache, phaseFactory, ephemeralRunImage)
})
}

Expand Down Expand Up @@ -518,8 +525,6 @@ func (l *LifecycleExecution) Restore(ctx context.Context, buildCache Cache, kani
l.withLogLevel()...,
),
WithNetwork(l.opts.Network),
If(l.hasExtensionsForRun(), WithPostContainerRunOperations(
CopyOutToMaybe(l.mountPaths.cnbDir(), l.tmpDir))), // FIXME: this is hacky; we should get the lifecycle binaries from the lifecycle image
cacheBindOp,
dockerOp,
flagsOp,
Expand Down Expand Up @@ -712,7 +717,7 @@ func (l *LifecycleExecution) ExtendBuild(ctx context.Context, kanikoCache Cache,
return extend.Run(ctx)
}

func (l *LifecycleExecution) ExtendRun(ctx context.Context, kanikoCache Cache, phaseFactory PhaseFactory) error {
func (l *LifecycleExecution) ExtendRun(ctx context.Context, kanikoCache Cache, phaseFactory PhaseFactory, runImageName string) error {
flags := []string{"-app", l.mountPaths.appDir(), "-kind", "run"}

configProvider := NewPhaseConfigProvider(
Expand All @@ -725,8 +730,7 @@ func (l *LifecycleExecution) ExtendRun(ctx context.Context, kanikoCache Cache, p
WithFlags(flags...),
WithNetwork(l.opts.Network),
WithRoot(),
WithImage(l.runImageAfterExtensions()),
WithBinds(fmt.Sprintf("%s:%s", filepath.Join(l.tmpDir, "cnb"), l.mountPaths.cnbDir())),
WithImage(runImageName),
WithBinds(fmt.Sprintf("%s:%s", kanikoCache.Name(), l.mountPaths.kanikoCacheDir())),
)

Expand Down
21 changes: 6 additions & 15 deletions internal/build/lifecycle_execution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) {
calledWithArgAtCall: make(map[int]string),
}
withFakeFetchRunImageFunc := func(opts *build.LifecycleOptions) {
opts.FetchRunImage = newFakeFetchRunImageFunc(&fakeFetcher)
opts.FetchRunImageWithLifecycleLayer = newFakeFetchRunImageFunc(&fakeFetcher)
}
lifecycleOps = append(lifecycleOps, fakes.WithBuilder(fakeBuilder), withFakeFetchRunImageFunc)

Expand Down Expand Up @@ -1971,7 +1971,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) {

when("#ExtendRun", func() {
it.Before(func() {
err := lifecycle.ExtendRun(context.Background(), fakeKanikoCache, fakePhaseFactory)
err := lifecycle.ExtendRun(context.Background(), fakeKanikoCache, fakePhaseFactory, "some-run-image")
h.AssertNil(t, err)

lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1
Expand All @@ -1990,15 +1990,6 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) {
h.AssertEq(t, configProvider.ContainerConfig().Image, "some-run-image")
})

when("extensions change the run image", func() {
extensionsRunImage = "some-new-run-image"
providedOrderExt = dist.Order{dist.OrderEntry{Group: []dist.ModuleRef{ /* don't care */ }}}

it("runs the phase with the new run image", func() {
h.AssertEq(t, configProvider.ContainerConfig().Image, "some-new-run-image")
})
})

it("configures the phase with the expected arguments", func() {
h.AssertSliceContainsInOrder(t, configProvider.ContainerConfig().Entrypoint, "") // the run image may have an entrypoint configured, override it
h.AssertSliceContainsInOrder(t, configProvider.ContainerConfig().Cmd, "-log-level", "debug")
Expand All @@ -2008,7 +1999,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) {

it("configures the phase with binds", func() {
expectedBinds := providedVolumes
expectedBinds = append(expectedBinds, "some-kaniko-cache:/kaniko", fmt.Sprintf("%s:/cnb", filepath.Join(tmpDir, "cnb")))
expectedBinds = append(expectedBinds, "some-kaniko-cache:/kaniko")

h.AssertSliceContains(t, configProvider.HostConfig().Binds, expectedBinds...)
})
Expand Down Expand Up @@ -2453,9 +2444,9 @@ func newFakeImageCache() *fakes.FakeCache {
return c
}

func newFakeFetchRunImageFunc(f *fakeImageFetcher) func(name string) error {
return func(name string) error {
return f.fetchRunImage(name)
func newFakeFetchRunImageFunc(f *fakeImageFetcher) func(name string) (string, error) {
return func(name string) (string, error) {
return fmt.Sprintf("ephemeral-%s", name), f.fetchRunImage(name)
}
}

Expand Down
68 changes: 34 additions & 34 deletions internal/build/lifecycle_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,40 +66,40 @@ type Termui interface {
}

type LifecycleOptions struct {
AppPath string
Image name.Reference
Builder Builder
BuilderImage string // differs from Builder.Name() and Builder.Image().Name() in that it includes the registry context
LifecycleImage string
LifecycleApis []string // optional - populated only if custom lifecycle image is downloaded, from that lifecycle's container's Labels.
RunImage string
FetchRunImage func(name string) error
ProjectMetadata files.ProjectMetadata
ClearCache bool
Publish bool
TrustBuilder bool
UseCreator bool
Interactive bool
Layout bool
Termui Termui
DockerHost string
Cache cache.CacheOpts
CacheImage string
HTTPProxy string
HTTPSProxy string
NoProxy string
Network string
AdditionalTags []string
Volumes []string
DefaultProcessType string
FileFilter func(string) bool
Workspace string
GID int
PreviousImage string
ReportDestinationDir string
SBOMDestinationDir string
CreationTime *time.Time
Keychain authn.Keychain
AppPath string
Image name.Reference
Builder Builder
BuilderImage string // differs from Builder.Name() and Builder.Image().Name() in that it includes the registry context
LifecycleImage string
LifecycleApis []string // optional - populated only if custom lifecycle image is downloaded, from that lifecycle's container's Labels.
RunImage string
FetchRunImageWithLifecycleLayer func(name string) (string, error)
ProjectMetadata files.ProjectMetadata
ClearCache bool
Publish bool
TrustBuilder bool
UseCreator bool
Interactive bool
Layout bool
Termui Termui
DockerHost string
Cache cache.CacheOpts
CacheImage string
HTTPProxy string
HTTPSProxy string
NoProxy string
Network string
AdditionalTags []string
Volumes []string
DefaultProcessType string
FileFilter func(string) bool
Workspace string
GID int
PreviousImage string
ReportDestinationDir string
SBOMDestinationDir string
CreationTime *time.Time
Keychain authn.Keychain
}

func NewLifecycleExecutor(logger logging.Logger, docker DockerClient) *LifecycleExecutor {
Expand Down
4 changes: 0 additions & 4 deletions internal/build/mount_paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@ func (m mountPaths) join(parts ...string) string {
return strings.Join(parts, m.separator)
}

func (m mountPaths) cnbDir() string {
return m.join(m.volume, "cnb")
}

func (m mountPaths) layersDir() string {
return m.join(m.volume, "layers")
}
Expand Down
2 changes: 1 addition & 1 deletion internal/build/phase_config_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func NewPhaseConfigProvider(name string, lifecycleExec *LifecycleExecution, ops
provider.ctrConf.Entrypoint = []string{""} // override entrypoint in case it is set
provider.ctrConf.Cmd = append([]string{"/cnb/lifecycle/" + name}, provider.ctrConf.Cmd...)

lifecycleExec.logger.Debugf("Running the %s on OS %s with:", style.Symbol(provider.Name()), style.Symbol(provider.os))
lifecycleExec.logger.Debugf("Running the %s on OS %s from image %s with:", style.Symbol(provider.Name()), style.Symbol(provider.os), style.Symbol(provider.ctrConf.Image))
lifecycleExec.logger.Debug("Container Settings:")
lifecycleExec.logger.Debugf(" Args: %s", style.Symbol(strings.Join(provider.ctrConf.Cmd, " ")))
lifecycleExec.logger.Debugf(" System Envs: %s", style.Symbol(strings.Join(sanitized(provider.ctrConf.Env), " ")))
Expand Down
146 changes: 139 additions & 7 deletions pkg/client/build.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
package client

import (
"archive/tar"
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"time"

"github.com/buildpacks/pack/buildpackage"

"github.com/Masterminds/semver"
"github.com/buildpacks/imgutil"
"github.com/buildpacks/imgutil/layout"
Expand All @@ -26,6 +28,7 @@ import (
"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"
Expand Down Expand Up @@ -509,18 +512,13 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
}
}

fetchRunImage := func(name string) error {
_, err := c.imageFetcher.Fetch(ctx, name, fetchOptions)
return err
}
lifecycleOpts := build.LifecycleOptions{
AppPath: appPath,
Image: imageRef,
Builder: ephemeralBuilder,
BuilderImage: builderRef.Name(),
LifecycleImage: ephemeralBuilder.Name(),
RunImage: runImageName,
FetchRunImage: fetchRunImage,
ProjectMetadata: projectMetadata,
ClearCache: opts.ClearCache,
Publish: opts.Publish,
Expand Down Expand Up @@ -559,6 +557,140 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
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")
}
return manifestContents[0].Layers[len(manifestContents[0].Layers)-1], nil // we can assume the lifecycle layer is the last in the tar
}()
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")
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)
}
Expand Down

0 comments on commit 4d8de33

Please sign in to comment.