Skip to content

Commit

Permalink
Merge pull request #1607 from joe-kimmel-vmw/lifecyle-api-versions-check
Browse files Browse the repository at this point in the history
if lifecycle image is supplied, consider its apis
  • Loading branch information
jkutner authored Jan 31, 2023
2 parents 2e7e9b6 + be5a709 commit 66fa019
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 3 deletions.
42 changes: 39 additions & 3 deletions internal/build/lifecycle_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ type LifecycleExecution struct {
}

func NewLifecycleExecution(logger logging.Logger, docker client.CommonAPIClient, opts LifecycleOptions) (*LifecycleExecution, error) {
latestSupportedPlatformAPI, err := findLatestSupported(append(
latestSupportedPlatformAPI, err := FindLatestSupported(append(
opts.Builder.LifecycleDescriptor().APIs.Platform.Deprecated,
opts.Builder.LifecycleDescriptor().APIs.Platform.Supported...,
))
), opts.LifecycleApis)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -69,7 +69,43 @@ func NewLifecycleExecution(logger logging.Logger, docker client.CommonAPIClient,
return exec, nil
}

func findLatestSupported(apis []*api.Version) (*api.Version, error) {
// intersection of two sorted lists of api versions
func apiIntersection(apisA, apisB []*api.Version) []*api.Version {
bind := 0
aind := 0
apis := []*api.Version{}
for ; aind < len(apisA); aind++ {
for ; bind < len(apisB) && apisA[aind].Compare(apisB[bind]) > 0; bind++ {
}
if bind == len(apisB) {
break
}
if apisA[aind].Equal(apisB[bind]) {
apis = append(apis, apisA[aind])
}
}
return apis
}

// public for unit test purposes but cmon you probably don't want to actually call this.
func FindLatestSupported(builderapis []*api.Version, lifecycleapis []string) (*api.Version, error) {
var apis []*api.Version
// if a custom lifecycle image was used we need to take an intersection of its supported apis with the builder's supported apis.
// generally no custom lifecycle is used, which will be indicated by the lifecycleapis list being empty in the struct.
if len(lifecycleapis) > 0 {
lcapis := []*api.Version{}
for _, ver := range lifecycleapis {
v, err := api.NewVersion(ver)
if err != nil {
return nil, fmt.Errorf("unable to parse lifecycle api version %s (%v)", ver, err)
}
lcapis = append(lcapis, v)
}
apis = apiIntersection(lcapis, builderapis)
} else {
apis = builderapis
}

for i := len(SupportedPlatformAPIVersions) - 1; i >= 0; i-- {
for _, version := range apis {
if SupportedPlatformAPIVersions[i].Equal(version) {
Expand Down
41 changes: 41 additions & 0 deletions internal/build/lifecycle_execution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,47 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) {
})
})

when("FindLatestSupported", func() {
it("chooses a shared version", func() {
version, err := build.FindLatestSupported([]*api.Version{api.MustParse("0.6"), api.MustParse("0.7"), api.MustParse("0.8")}, []string{"0.7"})
h.AssertNil(t, err)
h.AssertEq(t, version, api.MustParse("0.7"))
})

it("chooses a shared version, highest builder supported version", func() {
version, err := build.FindLatestSupported([]*api.Version{api.MustParse("0.4"), api.MustParse("0.5"), api.MustParse("0.7")}, []string{"0.7", "0.8"})
h.AssertNil(t, err)
h.AssertEq(t, version, api.MustParse("0.7"))
})

it("chooses a shared version, lowest builder supported version", func() {
version, err := build.FindLatestSupported([]*api.Version{api.MustParse("0.4"), api.MustParse("0.5"), api.MustParse("0.7")}, []string{"0.1", "0.2", "0.4"})
h.AssertNil(t, err)
h.AssertEq(t, version, api.MustParse("0.4"))
})

it("Interprets empty lifecycle versions list as lack of constraints", func() {
version, err := build.FindLatestSupported([]*api.Version{api.MustParse("0.6"), api.MustParse("0.7")}, []string{})
h.AssertNil(t, err)
h.AssertEq(t, version, api.MustParse("0.7"))
})

it("errors with no shared version, builder has no versions supported for some reason", func() {
_, err := build.FindLatestSupported([]*api.Version{}, []string{"0.7"})
h.AssertNotNil(t, err)
})

it("errors with no shared version, builder less than lifecycle", func() {
_, err := build.FindLatestSupported([]*api.Version{api.MustParse("0.4"), api.MustParse("0.5")}, []string{"0.7", "0.8"})
h.AssertNotNil(t, err)
})

it("errors with no shared version, builder greater than lifecycle", func() {
_, err := build.FindLatestSupported([]*api.Version{api.MustParse("0.8"), api.MustParse("0.9")}, []string{"0.6", "0.7"})
h.AssertNotNil(t, err)
})
})

when("Run", func() {
var (
imageName name.Tag
Expand Down
1 change: 1 addition & 0 deletions internal/build/lifecycle_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type LifecycleOptions struct {
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
ProjectMetadata platform.ProjectMetadata
ClearCache bool
Expand Down
33 changes: 33 additions & 0 deletions pkg/client/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package client
import (
"context"
"crypto/rand"
"encoding/json"
"fmt"
"os"
"path/filepath"
Expand All @@ -15,6 +16,7 @@ import (
"github.com/buildpacks/imgutil/local"
"github.com/buildpacks/imgutil/remote"
"github.com/buildpacks/lifecycle/platform"

"github.com/docker/docker/api/types"
"github.com/docker/docker/volume/mounts"
"github.com/google/go-containerregistry/pkg/name"
Expand Down Expand Up @@ -423,6 +425,15 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
}

lifecycleOpts.LifecycleImage = lifecycleImage.Name()
labels, err := lifecycleImage.Labels()
if err != nil {
return errors.Wrap(err, "reading labels of lifecycle image")
}

lifecycleOpts.LifecycleApis, err = extractSupportedLifecycleApis(labels)
if err != nil {
return errors.Wrap(err, "reading api versions of lifecycle image")
}
} else {
return errors.Errorf("Lifecycle %s does not have an associated lifecycle image. Builder must be trusted.", lifecycleVersion.String())
}
Expand All @@ -435,6 +446,28 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
return c.logImageNameAndSha(ctx, opts.Publish, imageRef)
}

func extractSupportedLifecycleApis(labels map[string]string) ([]string, error) {
// sample contents of labels:
// {io.buildpacks.builder.metadata:\"{\"lifecycle\":{\"version\":\"0.15.3\"},\"api\":{\"buildpack\":\"0.2\",\"platform\":\"0.3\"}}",
// io.buildpacks.lifecycle.apis":"{\"buildpack\":{\"deprecated\":[],\"supported\":[\"0.2\",\"0.3\",\"0.4\",\"0.5\",\"0.6\",\"0.7\",\"0.8\",\"0.9\"]},\"platform\":{\"deprecated\":[],\"supported\":[\"0.3\",\"0.4\",\"0.5\",\"0.6\",\"0.7\",\"0.8\",\"0.9\",\"0.10\"]}}\",\"io.buildpacks.lifecycle.version\":\"0.15.3\"}")

// This struct is defined in lifecycle-repository/tools/image/main.go#Descriptor -- we could consider moving it from the main package to an importable location.
var bpPlatformApi struct {
Platform struct {
Deprecated []string
Supported []string
}
}
if len(labels["io.buildpacks.lifecycle.apis"]) > 0 {
err := json.Unmarshal([]byte(labels["io.buildpacks.lifecycle.apis"]), &bpPlatformApi)
if err != nil {
return nil, err
}
return append(bpPlatformApi.Platform.Deprecated, bpPlatformApi.Platform.Supported...), nil
}
return []string{}, nil
}

func getFileFilter(descriptor projectTypes.Descriptor) (func(string) bool, error) {
if len(descriptor.Build.Exclude) > 0 {
excludes := ignore.CompileIgnoreLines(descriptor.Build.Exclude...)
Expand Down
14 changes: 14 additions & 0 deletions pkg/client/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1702,6 +1702,20 @@ func testBuild(t *testing.T, when spec.G, it spec.S) {
h.AssertEq(t, args.PullPolicy, image.PullAlways)
h.AssertEq(t, args.Platform, "linux/amd64")
})
it("uses the api versions of the lifecycle image", func() {
h.AssertTrue(t, true)
})
it("parses the versions correctly", func() {
fakeLifecycleImage.SetLabel("io.buildpacks.lifecycle.apis", "{\"platform\":{\"deprecated\":[\"0.1\"],\"supported\":[\"0.2\",\"0.3\",\"0.4\",\"0.5\"]}}")

h.AssertNil(t, subject.Build(context.TODO(), BuildOptions{
Image: "some/app",
Builder: defaultBuilderName,
Publish: true,
TrustBuilder: func(string) bool { return false },
}))
h.AssertSliceContainsInOrder(t, fakeLifecycle.Opts.LifecycleApis, "0.1", "0.2", "0.3", "0.4", "0.5")
})
})

when("lifecycle image is not available", func() {
Expand Down

0 comments on commit 66fa019

Please sign in to comment.