Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

0.12 Rebase Validation and Label Sync #1056

Merged
merged 4 commits into from
Apr 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions platform/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package platform

import (
"encoding/json"
"fmt"
"os"

"github.com/buildpacks/lifecycle/internal/fsutil"
Expand Down Expand Up @@ -104,6 +105,35 @@ func (t *TargetMetadata) IsSatisfiedBy(o *buildpack.TargetMetadata) bool {
return true
}

// IsValidRebaseTargetFor treats optional fields (ArchVariant and Distribution fields) as wildcards if empty, returns true if all populated fields match
func (t *TargetMetadata) IsValidRebaseTargetFor(appTargetMetadata *TargetMetadata) bool {
if t.Arch != appTargetMetadata.Arch || t.OS != appTargetMetadata.OS {
return false
}
if t.ArchVariant != "" && appTargetMetadata.ArchVariant != "" && t.ArchVariant != appTargetMetadata.ArchVariant {
return false
}

if t.Distribution != nil && appTargetMetadata.Distribution != nil {
if t.Distribution.Name != appTargetMetadata.Distribution.Name {
return false
}
if t.Distribution.Version != appTargetMetadata.Distribution.Version {
return false
}
jabrown85 marked this conversation as resolved.
Show resolved Hide resolved
}
return true
}

func (t *TargetMetadata) String() string {
var distName, distVersion string
if t.Distribution != nil {
distName = t.Distribution.Name
distVersion = t.Distribution.Version
}
return fmt.Sprintf("OS: %s, Arch: %s, ArchVariant: %s, Distribution: (Name: %s, Version: %s)", t.OS, t.Arch, t.ArchVariant, distName, distVersion)
}

// PopulateTargetOSFromFileSystem populates the target metadata you pass in if the information is available
// returns a boolean indicating whether it populated any data.
func PopulateTargetOSFromFileSystem(d fsutil.Detector, tm *TargetMetadata, logger log.Logger) {
Expand Down
52 changes: 44 additions & 8 deletions rebaser.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func (r *Rebaser) Rebase(workingImage imgutil.Image, newBaseImage imgutil.Image,
return RebaseReport{}, fmt.Errorf("incompatible stack: '%s' is not compatible with '%s'", newBaseStackID, appStackID)
}

if err := r.validateRebaseable(workingImage); err != nil {
if err := r.validateRebaseable(workingImage, newBaseImage); err != nil {
return RebaseReport{}, err
}

Expand Down Expand Up @@ -86,7 +86,12 @@ func (r *Rebaser) Rebase(workingImage imgutil.Image, newBaseImage imgutil.Image,
return RebaseReport{}, errors.Wrap(err, "set app image metadata label")
}

hasPrefix := func(l string) bool { return strings.HasPrefix(l, "io.buildpacks.stack.") }
hasPrefix := func(l string) bool {
if r.PlatformAPI.AtLeast("0.12") {
return strings.HasPrefix(l, "io.buildpacks.stack.") || strings.HasPrefix(l, "io.buildpacks.base.")
}
return strings.HasPrefix(l, "io.buildpacks.stack.")
}
if err := image.SyncLabels(newBaseImage, workingImage, hasPrefix); err != nil {
return RebaseReport{}, errors.Wrap(err, "set stack labels")
}
Expand Down Expand Up @@ -128,14 +133,45 @@ func validateMixins(appImg, newBaseImg imgutil.Image) error {
return nil
}

func (r *Rebaser) validateRebaseable(appImg imgutil.Image) error {
if r.PlatformAPI.AtLeast("0.12") {
rebaseable, err := appImg.Label(platform.RebaseableLabel)
func (r *Rebaser) validateRebaseable(appImg imgutil.Image, newBaseImg imgutil.Image) error {
if r.PlatformAPI.LessThan("0.12") {
return nil
}

// skip validation if the previous image was built before 0.12
appPlatformAPI, err := appImg.Env(platform.EnvPlatformAPI)
if err != nil {
return errors.Wrap(err, "get app image platform API")
}

// if the image doesn't have the platform API set, treat it as if it was built before 0.12 and skip additional validation
if appPlatformAPI == "" || api.MustParse(appPlatformAPI).LessThan("0.12") {
return nil
}

rebaseable, err := appImg.Label(platform.RebaseableLabel)
if err != nil {
return errors.Wrap(err, "get app image rebaseable label")
}
if !r.Force && rebaseable == "false" {
return fmt.Errorf("app image is not marked as rebaseable")
}

// check the OS, architecture, and variant values
// if they are not the same, the image cannot be rebased unless the force flag is set
if !r.Force {
appTarget, err := platform.GetTargetFromImage(appImg)
if err != nil {
return errors.Wrap(err, "get app image rebaseable label")
return errors.Wrap(err, "get app image target")
}
if !r.Force && rebaseable == "false" {
return fmt.Errorf("app image is not marked as rebaseable")

newBaseTarget, err := platform.GetTargetFromImage(newBaseImg)
if err != nil {
return errors.Wrap(err, "get new base image target")
}

if !newBaseTarget.IsValidRebaseTargetFor(appTarget) {
return fmt.Errorf("invalid base image target: '%s' is not equal to '%s'", newBaseTarget, appTarget)
}
}
return nil
Expand Down
130 changes: 130 additions & 0 deletions rebaser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ func testRebaser(t *testing.T, when spec.G, it spec.S) {
)
h.AssertNil(t, fakePreviousImage.SetLabel(platform.StackIDLabel, "io.buildpacks.stacks.bionic"))

h.AssertNil(t, fakeAppImage.SetEnv(platform.EnvPlatformAPI, api.Platform.Latest().String()))

additionalNames = []string{"some-repo/app-image:foo", "some-repo/app-image:bar"}

rebaser = &lifecycle.Rebaser{
Expand Down Expand Up @@ -168,6 +170,44 @@ func testRebaser(t *testing.T, when spec.G, it spec.S) {
})
})

when("image has io.buildpacks.base.* labels", func() {
var tests = []struct {
label string
appImageValue string
runImageValue string
want string
}{
{"io.buildpacks.base.homepage", "v1", "v2", "v2"},
{"io.buildpacks.stack.added", "", "new", "new"},
{"io.buildpacks.base.removed", "old", "", ""},
}

it.Before(func() {
for _, l := range tests {
if l.runImageValue != "" {
h.AssertNil(t, fakeNewBaseImage.SetLabel(l.label, l.runImageValue))
}
if l.appImageValue != "" {
h.AssertNil(t, fakeAppImage.SetLabel(l.label, l.appImageValue))
}
}
})

it("syncs matching labels", func() {
_, err := rebaser.Rebase(fakeAppImage, fakeNewBaseImage, fakeAppImage.Name(), additionalNames)
h.AssertNil(t, err)

for _, test := range tests {
test := test
t.Run(test.label, func(t *testing.T) {
actual, err := fakeAppImage.Label(test.label)
h.AssertNil(t, err)
h.AssertEq(t, test.want, actual)
})
}
})
})

when("image has a digest identifier", func() {
var fakeRemoteDigest = "sha256:c27a27006b74a056bed5d9edcebc394783880abe8691a8c87c78b7cffa6fa5ad"

Expand Down Expand Up @@ -388,6 +428,96 @@ func testRebaser(t *testing.T, when spec.G, it spec.S) {
})

when("app image and run image are based on different stacks", func() {
when("platform API >= 0.12", func() {
it.Before(func() {
rebaser.PlatformAPI = api.MustParse("0.12")
})

when("previous image was built on unknown platform API", func() {
it.Before(func() {
h.AssertNil(t, fakeAppImage.SetEnv(platform.EnvPlatformAPI, ""))
})

it("allows rebase with missing labels", func() {
h.AssertNil(t, fakeAppImage.SetOS(""))
h.AssertNil(t, fakeNewBaseImage.SetOS("linux"))
_, err := rebaser.Rebase(fakeAppImage, fakeNewBaseImage, fakeAppImage.Name(), additionalNames)
h.AssertNil(t, err)
h.AssertEq(t, fakeAppImage.Base(), "some-repo/new-base-image")
})

it("allows rebase with mismatched variants", func() {
h.AssertNil(t, fakeAppImage.SetVariant("variant1"))
h.AssertNil(t, fakeNewBaseImage.SetVariant("variant2"))
_, err := rebaser.Rebase(fakeAppImage, fakeNewBaseImage, fakeAppImage.Name(), additionalNames)
h.AssertNil(t, err)
h.AssertEq(t, fakeAppImage.Base(), "some-repo/new-base-image")
})
})

when("previous image was built on older platform API", func() {
it.Before(func() {
h.AssertNil(t, fakeAppImage.SetEnv(platform.EnvPlatformAPI, "0.11"))
})

it("allows rebase with missing labels", func() {
h.AssertNil(t, fakeAppImage.SetOS(""))
h.AssertNil(t, fakeNewBaseImage.SetOS("linux"))
_, err := rebaser.Rebase(fakeAppImage, fakeNewBaseImage, fakeAppImage.Name(), additionalNames)
h.AssertNil(t, err)
h.AssertEq(t, fakeAppImage.Base(), "some-repo/new-base-image")
})

it("allows rebase with mismatched variants", func() {
h.AssertNil(t, fakeAppImage.SetVariant("variant1"))
h.AssertNil(t, fakeNewBaseImage.SetVariant("variant2"))
_, err := rebaser.Rebase(fakeAppImage, fakeNewBaseImage, fakeAppImage.Name(), additionalNames)
h.AssertNil(t, err)
h.AssertEq(t, fakeAppImage.Base(), "some-repo/new-base-image")
})
})

it("returns an error and prevents the rebase from taking place when the os are different", func() {
h.AssertNil(t, fakeAppImage.SetOS("linux"))
h.AssertNil(t, fakeNewBaseImage.SetOS("notlinux"))

_, err := rebaser.Rebase(fakeAppImage, fakeNewBaseImage, fakeAppImage.Name(), additionalNames)
h.AssertError(t, err, "invalid base image target: 'OS: notlinux, Arch: amd64, ArchVariant: , Distribution: (Name: , Version: )' is not equal to 'OS: linux, Arch: amd64, ArchVariant: , Distribution: (Name: , Version: )'")
})

it("returns an error and prevents the rebase from taking place when the architecture are different", func() {
h.AssertNil(t, fakeAppImage.SetArchitecture("amd64"))
h.AssertNil(t, fakeNewBaseImage.SetArchitecture("arm64"))

_, err := rebaser.Rebase(fakeAppImage, fakeNewBaseImage, fakeAppImage.Name(), additionalNames)
h.AssertError(t, err, "invalid base image target: 'OS: linux, Arch: arm64, ArchVariant: , Distribution: (Name: , Version: )' is not equal to 'OS: linux, Arch: amd64, ArchVariant: , Distribution: (Name: , Version: )'")
})

it("returns an error and prevents the rebase from taking place when the architecture variant are different", func() {
h.AssertNil(t, fakeAppImage.SetVariant("variant1"))
h.AssertNil(t, fakeNewBaseImage.SetVariant("variant2"))

_, err := rebaser.Rebase(fakeAppImage, fakeNewBaseImage, fakeAppImage.Name(), additionalNames)
h.AssertError(t, err, "invalid base image target: 'OS: linux, Arch: amd64, ArchVariant: variant2, Distribution: (Name: , Version: )' is not equal to 'OS: linux, Arch: amd64, ArchVariant: variant1, Distribution: (Name: , Version: )'")
})

it("returns an error and prevents the rebase from taking place when the io.buildpacks.distribution.name are different", func() {
h.AssertNil(t, fakeAppImage.SetLabel("io.buildpacks.distribution.name", "distro1"))
h.AssertNil(t, fakeNewBaseImage.SetLabel("io.buildpacks.distribution.name", "distro2"))

_, err := rebaser.Rebase(fakeAppImage, fakeNewBaseImage, fakeAppImage.Name(), additionalNames)
h.AssertError(t, err, "invalid base image target: 'OS: linux, Arch: amd64, ArchVariant: , Distribution: (Name: distro2, Version: )' is not equal to 'OS: linux, Arch: amd64, ArchVariant: , Distribution: (Name: distro1, Version: )'")
})

it("returns an error and prevents the rebase from taking place when the io.buildpacks.distribution.version are different", func() {
h.AssertNil(t, fakeAppImage.SetLabel("io.buildpacks.distribution.version", "version1"))
h.AssertNil(t, fakeNewBaseImage.SetLabel("io.buildpacks.distribution.version", "version2"))

_, err := rebaser.Rebase(fakeAppImage, fakeNewBaseImage, fakeAppImage.Name(), additionalNames)
h.AssertError(t, err, "invalid base image target: 'OS: linux, Arch: amd64, ArchVariant: , Distribution: (Name: , Version: version2)' is not equal to 'OS: linux, Arch: amd64, ArchVariant: , Distribution: (Name: , Version: version1)'")
})
})

it("returns an error and prevents the rebase from taking place when the stacks are different", func() {
h.AssertNil(t, fakeAppImage.SetLabel(platform.StackIDLabel, "io.buildpacks.stacks.bionic"))
h.AssertNil(t, fakeNewBaseImage.SetLabel(platform.StackIDLabel, "io.buildpacks.stacks.cflinuxfs3"))
Expand Down