Skip to content

Commit

Permalink
if lifecycle image is supplied, consider its apis
Browse files Browse the repository at this point in the history
if we're injecting a custom lifecycle image into an untrusted builder,
when we select the platform api version we'll consider the intersection
between the builder, the lifecycle, and this instance of pack
when choosing which version of the api to speak.

because this is a rare case, the default is that there is no lifecycle
api list, which is treated as no constraints.

Signed-off-by: Joe Kimmel <jkimmel@vmware.com>
  • Loading branch information
joe-kimmel-vmw committed Jan 23, 2023
1 parent 6c23a08 commit aa80e11
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 3 deletions.
43 changes: 40 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,44 @@ 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 {
//first convert string versions into version versions if needed:
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
34 changes: 34 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,16 @@ 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 +447,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 aa80e11

Please sign in to comment.