From add3e0501ffc78fb9b32a1b8ddf18c94729a360e Mon Sep 17 00:00:00 2001 From: Johannes Dillmann Date: Fri, 31 Jan 2025 11:48:45 +0100 Subject: [PATCH] Dynamically determine valid lifecycle Signed-off-by: Johannes Dillmann --- go.mod | 2 +- go.sum | 4 +-- pkg/client/create_builder.go | 38 +++++++++++++++++----------- pkg/client/create_builder_test.go | 41 +++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 5637c0241..c809d16e4 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/Masterminds/semver v1.5.0 github.com/Microsoft/go-winio v0.6.2 github.com/apex/log v1.9.0 - github.com/buildpacks/imgutil v0.0.0-20240605145725-186f89b2d168 + github.com/buildpacks/imgutil v0.0.0-20250224200932-4dcbf829e753 github.com/buildpacks/lifecycle v0.20.4 github.com/docker/cli v27.5.1+incompatible github.com/docker/docker v27.5.1+incompatible diff --git a/go.sum b/go.sum index 7f854e12e..6218989c2 100644 --- a/go.sum +++ b/go.sum @@ -91,8 +91,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/buildpacks/imgutil v0.0.0-20240605145725-186f89b2d168 h1:yVYVi1V7x1bXklOx9lpbTfteyzQKGZC/wkl+IlaVRlU= -github.com/buildpacks/imgutil v0.0.0-20240605145725-186f89b2d168/go.mod h1:n2R6VRuWsAX3cyHCp/u0Z4WJcixny0gYg075J39owrk= +github.com/buildpacks/imgutil v0.0.0-20250224200932-4dcbf829e753 h1:wfR3MA91PSFuKlaT07lx8BU8x918GSHmz6phaSbXJ6g= +github.com/buildpacks/imgutil v0.0.0-20250224200932-4dcbf829e753/go.mod h1:n2R6VRuWsAX3cyHCp/u0Z4WJcixny0gYg075J39owrk= github.com/buildpacks/lifecycle v0.20.4 h1:VVVTrd9y1LHY3adchh6oktw0wKQuYsWLq3/g23TLaGQ= github.com/buildpacks/lifecycle v0.20.4/go.mod h1:ZsExeEhN+6Qws7iDHJl6PV6zsHycgK/RmDKnRgKQTH0= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= diff --git a/pkg/client/create_builder.go b/pkg/client/create_builder.go index 6a3e1dbac..cf20cb045 100644 --- a/pkg/client/create_builder.go +++ b/pkg/client/create_builder.go @@ -16,6 +16,7 @@ import ( pubbldr "github.com/buildpacks/pack/builder" "github.com/buildpacks/pack/internal/builder" + "github.com/buildpacks/pack/internal/config" "github.com/buildpacks/pack/internal/paths" "github.com/buildpacks/pack/internal/style" "github.com/buildpacks/pack/pkg/buildpack" @@ -284,14 +285,20 @@ func (c *Client) fetchLifecycle(ctx context.Context, config pubbldr.LifecycleCon return nil, errors.Wrapf(err, "%s must be a valid semver", style.Symbol("lifecycle.version")) } - uri = c.uriFromLifecycleVersion(*v, os, architecture) + uri, err = c.uriFromLifecycleVersion(*v, os, architecture) + if err != nil { + return nil, errors.Wrap(err, "determine lifecycle") + } case config.URI != "": uri, err = paths.FilePathToURI(config.URI, relativeBaseDir) if err != nil { - return nil, err + return nil, errors.Wrap(err, "determine lifecycle") } default: - uri = c.uriFromLifecycleVersion(*semver.MustParse(builder.DefaultLifecycleVersion), os, architecture) + uri, err = c.uriFromLifecycleVersion(*semver.MustParse(builder.DefaultLifecycleVersion), os, architecture) + if err != nil { + return nil, errors.Wrap(err, "determine lifecycle") + } } blob, err := c.downloader.Download(ctx, uri) @@ -434,19 +441,22 @@ func validateModule(kind string, module buildpack.BuildModule, source, expectedI return nil } -func (c *Client) uriFromLifecycleVersion(version semver.Version, os string, architecture string) string { - arch := "x86-64" +func (c *Client) uriFromLifecycleVersion(version semver.Version, os string, architecture string) (string, error) { + image, _ := c.indexFactory.FetchIndex(config.DefaultLifecycleImageRepo, imgutil.FromBaseIndex(config.DefaultLifecycleImageRepo)) + manifest, _ := image.IndexManifest() - if os == "windows" { - return fmt.Sprintf("https://github.com/buildpacks/lifecycle/releases/download/v%s/lifecycle-v%s+windows.%s.tgz", version.String(), version.String(), arch) + for _, m := range manifest.Manifests { + if m.Platform.OS == os && m.Platform.Architecture == architecture { + return lifecycleDownloadURL(version, os, architecture), nil + } } - if builder.SupportedLinuxArchitecture(architecture) { - arch = architecture - } else { - // FIXME: this should probably be an error case in the future, see https://github.com/buildpacks/pack/issues/2163 - c.logger.Warnf("failed to find a lifecycle binary for requested architecture %s, defaulting to %s", style.Symbol(architecture), style.Symbol(arch)) - } + return "", fmt.Errorf("could not determine lifecyle, unsupported os/arch: %s/%s", os, architecture) +} - return fmt.Sprintf("https://github.com/buildpacks/lifecycle/releases/download/v%s/lifecycle-v%s+linux.%s.tgz", version.String(), version.String(), arch) +func lifecycleDownloadURL(version semver.Version, os, architecture string) string { + if architecture == "amd64" { + architecture = "x86-64" + } + return fmt.Sprintf("https://github.com/buildpacks/lifecycle/releases/download/v%s/lifecycle-v%s+%s.%s.tgz", version.String(), version.String(), os, architecture) } diff --git a/pkg/client/create_builder_test.go b/pkg/client/create_builder_test.go index 4fa47d647..3a4204733 100644 --- a/pkg/client/create_builder_test.go +++ b/pkg/client/create_builder_test.go @@ -13,6 +13,7 @@ import ( "github.com/buildpacks/lifecycle/api" "github.com/docker/docker/api/types/system" "github.com/golang/mock/gomock" + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/heroku/color" "github.com/pkg/errors" "github.com/sclevine/spec" @@ -49,10 +50,12 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { mockBuildpackDownloader *testmocks.MockBuildpackDownloader mockImageFactory *testmocks.MockImageFactory mockImageFetcher *testmocks.MockImageFetcher + mockIndexFactory *testmocks.MockIndexFactory mockDockerClient *testmocks.MockCommonAPIClient fakeBuildImage *fakes.Image fakeRunImage *fakes.Image fakeRunImageMirror *fakes.Image + fakeLifecycleImage *fakes.ImageIndex opts client.CreateBuilderOptions subject *client.Client logger logging.Logger @@ -68,6 +71,10 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", gomock.Any()).Return(fakeBuildImage, nil) } + var prepareIndexFetcherWithLifecycleImage = func() { + mockIndexFactory.EXPECT().FetchIndex(gomock.Any(), gomock.Any()).Return(fakeLifecycleImage, nil) + } + var createBuildpack = func(descriptor dist.BuildpackDescriptor) buildpack.BuildModule { buildpack, err := ifakes.NewFakeBuildpack(descriptor, 0644) h.AssertNil(t, err) @@ -89,6 +96,7 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { mockDownloader = testmocks.NewMockBlobDownloader(mockController) mockImageFetcher = testmocks.NewMockImageFetcher(mockController) mockImageFactory = testmocks.NewMockImageFactory(mockController) + mockIndexFactory = testmocks.NewMockIndexFactory(mockController) mockDockerClient = testmocks.NewMockCommonAPIClient(mockController) mockBuildpackDownloader = testmocks.NewMockBuildpackDownloader(mockController) @@ -101,6 +109,29 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { fakeRunImage = fakes.NewImage("some/run-image", "", nil) h.AssertNil(t, fakeRunImage.SetLabel("io.buildpacks.stack.id", "some.stack.id")) + fakeLifecycleImage = &fakes.ImageIndex{ + Manifests: []v1.Descriptor{ + { + Platform: &v1.Platform{ + OS: "linux", + Architecture: "amd64", + }, + }, + { + Platform: &v1.Platform{ + OS: "linux", + Architecture: "arm64", + }, + }, + { + Platform: &v1.Platform{ + OS: "windows", + Architecture: "amd64", + }, + }, + }, + } + fakeRunImageMirror = fakes.NewImage("localhost:5000/some/run-image", "", nil) h.AssertNil(t, fakeRunImageMirror.SetLabel("io.buildpacks.stack.id", "some.stack.id")) @@ -124,6 +155,7 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { client.WithDownloader(mockDownloader), client.WithImageFactory(mockImageFactory), client.WithFetcher(mockImageFetcher), + client.WithIndexFactory(mockIndexFactory), client.WithDockerClient(mockDockerClient), client.WithBuildpackDownloader(mockBuildpackDownloader), ) @@ -452,6 +484,7 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { client.WithDownloader(mockDownloader), client.WithImageFactory(mockImageFactory), client.WithFetcher(mockImageFetcher), + client.WithIndexFactory(mockIndexFactory), client.WithExperimental(true), ) h.AssertNil(t, err) @@ -516,6 +549,7 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { it("should download from predetermined uri", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() + prepareIndexFetcherWithLifecycleImage() opts.Config.Lifecycle.URI = "" opts.Config.Lifecycle.Version = "3.4.5" @@ -533,6 +567,7 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { it("should download from predetermined uri for arm64", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() + prepareIndexFetcherWithLifecycleImage() opts.Config.Lifecycle.URI = "" opts.Config.Lifecycle.Version = "3.4.5" h.AssertNil(t, fakeBuildImage.SetArchitecture("arm64")) @@ -557,12 +592,14 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { client.WithDownloader(mockDownloader), client.WithImageFactory(mockImageFactory), client.WithFetcher(mockImageFetcher), + client.WithIndexFactory(mockIndexFactory), client.WithExperimental(true), ) h.AssertNil(t, err) prepareFetcherWithBuildImage() prepareFetcherWithRunImages() + prepareIndexFetcherWithLifecycleImage() opts.Config.Lifecycle.URI = "" opts.Config.Lifecycle.Version = "3.4.5" h.AssertNil(t, fakeBuildImage.SetOS("windows")) @@ -584,6 +621,7 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { it("should download default lifecycle", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() + prepareIndexFetcherWithLifecycleImage() opts.Config.Lifecycle.URI = "" opts.Config.Lifecycle.Version = "" @@ -605,6 +643,7 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { it("should download default lifecycle on arm64", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() + prepareIndexFetcherWithLifecycleImage() opts.Config.Lifecycle.URI = "" opts.Config.Lifecycle.Version = "" h.AssertNil(t, fakeBuildImage.SetArchitecture("arm64")) @@ -633,12 +672,14 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { client.WithDownloader(mockDownloader), client.WithImageFactory(mockImageFactory), client.WithFetcher(mockImageFetcher), + client.WithIndexFactory(mockIndexFactory), client.WithExperimental(true), ) h.AssertNil(t, err) prepareFetcherWithBuildImage() prepareFetcherWithRunImages() + prepareIndexFetcherWithLifecycleImage() opts.Config.Lifecycle.URI = "" opts.Config.Lifecycle.Version = "" h.AssertNil(t, fakeBuildImage.SetOS("windows"))