From bc08e37fdfe3ed1ce71a760acb7ba1caea5be6f4 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 --- pkg/client/create_builder.go | 38 +++++++++++++++++----------- pkg/client/create_builder_test.go | 41 +++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/pkg/client/create_builder.go b/pkg/client/create_builder.go index 6a3e1dbac9..cf20cb045d 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 4fa47d6474..3a42047338 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"))