Skip to content

Commit e52a3e0

Browse files
author
Natalie Arellano
committed
Fetch lifecycle binaries from lifecycle image instead of restorer container
Hopefully fixes missing file issues on Linux Signed-off-by: Natalie Arellano <narellano@vmware.com>
1 parent e5d4417 commit e52a3e0

5 files changed

Lines changed: 192 additions & 65 deletions

File tree

internal/build/lifecycle_execution.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -239,9 +239,16 @@ func (l *LifecycleExecution) Run(ctx context.Context, phaseFactoryCreator PhaseF
239239
}
240240
}
241241

242+
var (
243+
ephemeralRunImage string
244+
err error
245+
)
242246
currentRunImage := l.runImageAfterExtensions()
243-
if currentRunImage != "" && currentRunImage != l.opts.RunImage {
244-
if err := l.opts.FetchRunImage(currentRunImage); err != nil {
247+
if l.runImageChanged() || l.hasExtensionsForRun() {
248+
if currentRunImage == "" { // sanity check
249+
return nil
250+
}
251+
if ephemeralRunImage, err = l.opts.FetchRunImageWithLifecycleLayer(currentRunImage); err != nil {
245252
return err
246253
}
247254
}
@@ -269,7 +276,7 @@ func (l *LifecycleExecution) Run(ctx context.Context, phaseFactoryCreator PhaseF
269276
if l.platformAPI.AtLeast("0.12") && l.hasExtensionsForRun() {
270277
group.Go(func() error {
271278
l.logger.Info(style.Step("EXTENDING (RUN)"))
272-
return l.ExtendRun(ctx, kanikoCache, phaseFactory)
279+
return l.ExtendRun(ctx, kanikoCache, phaseFactory, ephemeralRunImage)
273280
})
274281
}
275282

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

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

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

internal/build/lifecycle_execution_test.go

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) {
124124
calledWithArgAtCall: make(map[int]string),
125125
}
126126
withFakeFetchRunImageFunc := func(opts *build.LifecycleOptions) {
127-
opts.FetchRunImage = newFakeFetchRunImageFunc(&fakeFetcher)
127+
opts.FetchRunImageWithLifecycleLayer = newFakeFetchRunImageFunc(&fakeFetcher)
128128
}
129129
lifecycleOps = append(lifecycleOps, fakes.WithBuilder(fakeBuilder), withFakeFetchRunImageFunc)
130130

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

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

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

1993-
when("extensions change the run image", func() {
1994-
extensionsRunImage = "some-new-run-image"
1995-
providedOrderExt = dist.Order{dist.OrderEntry{Group: []dist.ModuleRef{ /* don't care */ }}}
1996-
1997-
it("runs the phase with the new run image", func() {
1998-
h.AssertEq(t, configProvider.ContainerConfig().Image, "some-new-run-image")
1999-
})
2000-
})
2001-
20021993
it("configures the phase with the expected arguments", func() {
20031994
h.AssertSliceContainsInOrder(t, configProvider.ContainerConfig().Entrypoint, "") // the run image may have an entrypoint configured, override it
20041995
h.AssertSliceContainsInOrder(t, configProvider.ContainerConfig().Cmd, "-log-level", "debug")
@@ -2008,7 +1999,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) {
20081999

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

20132004
h.AssertSliceContains(t, configProvider.HostConfig().Binds, expectedBinds...)
20142005
})
@@ -2453,9 +2444,9 @@ func newFakeImageCache() *fakes.FakeCache {
24532444
return c
24542445
}
24552446

2456-
func newFakeFetchRunImageFunc(f *fakeImageFetcher) func(name string) error {
2457-
return func(name string) error {
2458-
return f.fetchRunImage(name)
2447+
func newFakeFetchRunImageFunc(f *fakeImageFetcher) func(name string) (string, error) {
2448+
return func(name string) (string, error) {
2449+
return fmt.Sprintf("ephemeral-%s", name), f.fetchRunImage(name)
24592450
}
24602451
}
24612452

internal/build/lifecycle_executor.go

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -66,40 +66,40 @@ type Termui interface {
6666
}
6767

6868
type LifecycleOptions struct {
69-
AppPath string
70-
Image name.Reference
71-
Builder Builder
72-
BuilderImage string // differs from Builder.Name() and Builder.Image().Name() in that it includes the registry context
73-
LifecycleImage string
74-
LifecycleApis []string // optional - populated only if custom lifecycle image is downloaded, from that lifecycle's container's Labels.
75-
RunImage string
76-
FetchRunImage func(name string) error
77-
ProjectMetadata files.ProjectMetadata
78-
ClearCache bool
79-
Publish bool
80-
TrustBuilder bool
81-
UseCreator bool
82-
Interactive bool
83-
Layout bool
84-
Termui Termui
85-
DockerHost string
86-
Cache cache.CacheOpts
87-
CacheImage string
88-
HTTPProxy string
89-
HTTPSProxy string
90-
NoProxy string
91-
Network string
92-
AdditionalTags []string
93-
Volumes []string
94-
DefaultProcessType string
95-
FileFilter func(string) bool
96-
Workspace string
97-
GID int
98-
PreviousImage string
99-
ReportDestinationDir string
100-
SBOMDestinationDir string
101-
CreationTime *time.Time
102-
Keychain authn.Keychain
69+
AppPath string
70+
Image name.Reference
71+
Builder Builder
72+
BuilderImage string // differs from Builder.Name() and Builder.Image().Name() in that it includes the registry context
73+
LifecycleImage string
74+
LifecycleApis []string // optional - populated only if custom lifecycle image is downloaded, from that lifecycle's container's Labels.
75+
RunImage string
76+
FetchRunImageWithLifecycleLayer func(name string) (string, error)
77+
ProjectMetadata files.ProjectMetadata
78+
ClearCache bool
79+
Publish bool
80+
TrustBuilder bool
81+
UseCreator bool
82+
Interactive bool
83+
Layout bool
84+
Termui Termui
85+
DockerHost string
86+
Cache cache.CacheOpts
87+
CacheImage string
88+
HTTPProxy string
89+
HTTPSProxy string
90+
NoProxy string
91+
Network string
92+
AdditionalTags []string
93+
Volumes []string
94+
DefaultProcessType string
95+
FileFilter func(string) bool
96+
Workspace string
97+
GID int
98+
PreviousImage string
99+
ReportDestinationDir string
100+
SBOMDestinationDir string
101+
CreationTime *time.Time
102+
Keychain authn.Keychain
103103
}
104104

105105
func NewLifecycleExecutor(logger logging.Logger, docker DockerClient) *LifecycleExecutor {

internal/build/phase_config_provider.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ func NewPhaseConfigProvider(name string, lifecycleExec *LifecycleExecution, ops
6666
provider.ctrConf.Entrypoint = []string{""} // override entrypoint in case it is set
6767
provider.ctrConf.Cmd = append([]string{"/cnb/lifecycle/" + name}, provider.ctrConf.Cmd...)
6868

69-
lifecycleExec.logger.Debugf("Running the %s on OS %s with:", style.Symbol(provider.Name()), style.Symbol(provider.os))
69+
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))
7070
lifecycleExec.logger.Debug("Container Settings:")
7171
lifecycleExec.logger.Debugf(" Args: %s", style.Symbol(strings.Join(provider.ctrConf.Cmd, " ")))
7272
lifecycleExec.logger.Debugf(" System Envs: %s", style.Symbol(strings.Join(sanitized(provider.ctrConf.Env), " ")))

pkg/client/build.go

Lines changed: 139 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
package client
22

33
import (
4+
"archive/tar"
45
"context"
56
"crypto/rand"
7+
"crypto/sha256"
8+
"encoding/hex"
69
"encoding/json"
710
"fmt"
11+
"io"
812
"os"
913
"path/filepath"
1014
"runtime"
1115
"sort"
1216
"strings"
1317
"time"
1418

15-
"github.com/buildpacks/pack/buildpackage"
16-
1719
"github.com/Masterminds/semver"
1820
"github.com/buildpacks/imgutil"
1921
"github.com/buildpacks/imgutil/layout"
@@ -26,6 +28,7 @@ import (
2628
"github.com/pkg/errors"
2729
ignore "github.com/sabhiram/go-gitignore"
2830

31+
"github.com/buildpacks/pack/buildpackage"
2932
"github.com/buildpacks/pack/internal/build"
3033
"github.com/buildpacks/pack/internal/builder"
3134
internalConfig "github.com/buildpacks/pack/internal/config"
@@ -509,18 +512,13 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
509512
}
510513
}
511514

512-
fetchRunImage := func(name string) error {
513-
_, err := c.imageFetcher.Fetch(ctx, name, fetchOptions)
514-
return err
515-
}
516515
lifecycleOpts := build.LifecycleOptions{
517516
AppPath: appPath,
518517
Image: imageRef,
519518
Builder: ephemeralBuilder,
520519
BuilderImage: builderRef.Name(),
521520
LifecycleImage: ephemeralBuilder.Name(),
522521
RunImage: runImageName,
523-
FetchRunImage: fetchRunImage,
524522
ProjectMetadata: projectMetadata,
525523
ClearCache: opts.ClearCache,
526524
Publish: opts.Publish,
@@ -559,6 +557,140 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
559557
return errors.Errorf("Lifecycle %s does not have an associated lifecycle image. Builder must be trusted.", lifecycleVersion.String())
560558
}
561559

560+
lifecycleOpts.FetchRunImageWithLifecycleLayer = func(runImageName string) (string, error) {
561+
ephemeralRunImageName := fmt.Sprintf("pack.local/run-image/%x:latest", randString(10))
562+
runImage, err := c.imageFetcher.Fetch(ctx, runImageName, fetchOptions)
563+
if err != nil {
564+
return "", err
565+
}
566+
ephemeralRunImage, err := local.NewImage(ephemeralRunImageName, c.docker, local.FromBaseImage(runImage.Name()))
567+
if err != nil {
568+
return "", err
569+
}
570+
tmpDir, err := os.MkdirTemp("", "extend-run-image-scratch") // we need to write to disk because manifest.json is last in the tar
571+
if err != nil {
572+
return "", err
573+
}
574+
defer os.RemoveAll(tmpDir)
575+
lifecycleImageTar, err := func() (string, error) {
576+
lifecycleImageTar := filepath.Join(tmpDir, "lifecycle-image.tar")
577+
lifecycleImageReader, err := c.docker.ImageSave(context.Background(), []string{lifecycleOpts.LifecycleImage}) // this is fast because the lifecycle image is based on distroless static
578+
if err != nil {
579+
return "", err
580+
}
581+
defer lifecycleImageReader.Close()
582+
lifecycleImageWriter, err := os.Create(lifecycleImageTar)
583+
if err != nil {
584+
return "", err
585+
}
586+
defer lifecycleImageWriter.Close()
587+
if _, err = io.Copy(lifecycleImageWriter, lifecycleImageReader); err != nil {
588+
return "", err
589+
}
590+
return lifecycleImageTar, nil
591+
}()
592+
if err != nil {
593+
return "", err
594+
}
595+
advanceTarToEntryWithName := func(tarReader *tar.Reader, wantName string) (*tar.Header, error) {
596+
var (
597+
header *tar.Header
598+
err error
599+
)
600+
for {
601+
header, err = tarReader.Next()
602+
if err == io.EOF {
603+
break
604+
}
605+
if err != nil {
606+
return nil, err
607+
}
608+
if header.Name != wantName {
609+
continue
610+
}
611+
return header, nil
612+
}
613+
return nil, fmt.Errorf("failed to find header with name: %s", wantName)
614+
}
615+
lifecycleLayerName, err := func() (string, error) {
616+
lifecycleImageReader, err := os.Open(lifecycleImageTar)
617+
if err != nil {
618+
return "", err
619+
}
620+
defer lifecycleImageReader.Close()
621+
tarReader := tar.NewReader(lifecycleImageReader)
622+
if _, err = advanceTarToEntryWithName(tarReader, "manifest.json"); err != nil {
623+
return "", err
624+
}
625+
type descriptor struct {
626+
Layers []string
627+
}
628+
type manifestJSON []descriptor
629+
var manifestContents manifestJSON
630+
if err = json.NewDecoder(tarReader).Decode(&manifestContents); err != nil {
631+
return "", err
632+
}
633+
if len(manifestContents) < 1 {
634+
return "", errors.New("missing manifest entries")
635+
}
636+
return manifestContents[0].Layers[len(manifestContents[0].Layers)-1], nil // we can assume the lifecycle layer is the last in the tar
637+
}()
638+
if err != nil {
639+
return "", err
640+
}
641+
if lifecycleLayerName == "" {
642+
return "", errors.New("failed to find lifecycle layer")
643+
}
644+
lifecycleLayerTar, err := func() (string, error) {
645+
lifecycleImageReader, err := os.Open(lifecycleImageTar)
646+
if err != nil {
647+
return "", err
648+
}
649+
defer lifecycleImageReader.Close()
650+
tarReader := tar.NewReader(lifecycleImageReader)
651+
var header *tar.Header
652+
if header, err = advanceTarToEntryWithName(tarReader, lifecycleLayerName); err != nil {
653+
return "", err
654+
}
655+
lifecycleLayerTar := filepath.Join(filepath.Dir(lifecycleImageTar), filepath.Dir(lifecycleLayerName)+".tar")
656+
lifecycleLayerWriter, err := os.OpenFile(lifecycleLayerTar, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
657+
if err != nil {
658+
return "", err
659+
}
660+
defer lifecycleLayerWriter.Close()
661+
if _, err = io.Copy(lifecycleLayerWriter, tarReader); err != nil {
662+
return "", err
663+
}
664+
return lifecycleLayerTar, nil
665+
}()
666+
if err != nil {
667+
return "", err
668+
}
669+
diffID, err := func() (string, error) {
670+
lifecycleLayerReader, err := os.Open(lifecycleLayerTar)
671+
if err != nil {
672+
return "", err
673+
}
674+
defer lifecycleLayerReader.Close()
675+
hasher := sha256.New()
676+
if _, err = io.Copy(hasher, lifecycleLayerReader); err != nil {
677+
return "", err
678+
}
679+
// it's weird that this doesn't match lifecycleLayerTar
680+
return hex.EncodeToString(hasher.Sum(nil)), nil
681+
}()
682+
if err != nil {
683+
return "", err
684+
}
685+
if err = ephemeralRunImage.AddLayerWithDiffID(lifecycleLayerTar, "sha256:"+diffID); err != nil {
686+
return "", err
687+
}
688+
if err = ephemeralRunImage.Save(); err != nil {
689+
return "", err
690+
}
691+
return ephemeralRunImageName, nil
692+
}
693+
562694
if err = c.lifecycleExecutor.Execute(ctx, lifecycleOpts); err != nil {
563695
return fmt.Errorf("executing lifecycle: %w", err)
564696
}

0 commit comments

Comments
 (0)