Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetch lifecycle binaries from lifecycle image #2007

Merged
merged 2 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
}
}

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
}

Check warning on line 250 in internal/build/lifecycle_execution.go

View check run for this annotation

Codecov / codecov/patch

internal/build/lifecycle_execution.go#L249-L250

Added lines #L249 - L250 were not covered by tests
if ephemeralRunImage, err = l.opts.FetchRunImageWithLifecycleLayer(currentRunImage); err != nil {
return err
}
}
Expand Down Expand Up @@ -269,7 +276,7 @@
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 @@
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 @@
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 @@
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 @@
"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 @@
}
}

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 @@
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

Check warning on line 590 in pkg/client/build.go

View check run for this annotation

Codecov / codecov/patch

pkg/client/build.go#L561-L590

Added lines #L561 - L590 were not covered by tests
}()
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

Check warning on line 603 in pkg/client/build.go

View check run for this annotation

Codecov / codecov/patch

pkg/client/build.go#L592-L603

Added lines #L592 - L603 were not covered by tests
}
if err != nil {
return nil, err
}
if header.Name != wantName {
continue

Check warning on line 609 in pkg/client/build.go

View check run for this annotation

Codecov / codecov/patch

pkg/client/build.go#L605-L609

Added lines #L605 - L609 were not covered by tests
}
return header, nil

Check warning on line 611 in pkg/client/build.go

View check run for this annotation

Codecov / codecov/patch

pkg/client/build.go#L611

Added line #L611 was not covered by tests
}
return nil, fmt.Errorf("failed to find header with name: %s", wantName)

Check warning on line 613 in pkg/client/build.go

View check run for this annotation

Codecov / codecov/patch

pkg/client/build.go#L613

Added line #L613 was not covered by tests
}
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

Check warning on line 636 in pkg/client/build.go

View check run for this annotation

Codecov / codecov/patch

pkg/client/build.go#L615-L636

Added lines #L615 - L636 were not covered by tests
}()
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

Check warning on line 664 in pkg/client/build.go

View check run for this annotation

Codecov / codecov/patch

pkg/client/build.go#L638-L664

Added lines #L638 - L664 were not covered by tests
}()
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
}

Check warning on line 678 in pkg/client/build.go

View check run for this annotation

Codecov / codecov/patch

pkg/client/build.go#L666-L678

Added lines #L666 - L678 were not covered by tests
// it's weird that this doesn't match lifecycleLayerTar
return hex.EncodeToString(hasher.Sum(nil)), nil

Check warning on line 680 in pkg/client/build.go

View check run for this annotation

Codecov / codecov/patch

pkg/client/build.go#L680

Added line #L680 was not covered by tests
}()
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

Check warning on line 691 in pkg/client/build.go

View check run for this annotation

Codecov / codecov/patch

pkg/client/build.go#L682-L691

Added lines #L682 - L691 were not covered by tests
}

if err = c.lifecycleExecutor.Execute(ctx, lifecycleOpts); err != nil {
return fmt.Errorf("executing lifecycle: %w", err)
}
Expand Down
Loading