lifecycle/phase/rebaser.go

238 lines
7.4 KiB
Go

package phase
import (
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/buildpacks/imgutil"
"github.com/pkg/errors"
"github.com/buildpacks/lifecycle/api"
"github.com/buildpacks/lifecycle/image"
"github.com/buildpacks/lifecycle/internal/encoding"
"github.com/buildpacks/lifecycle/internal/str"
"github.com/buildpacks/lifecycle/log"
"github.com/buildpacks/lifecycle/platform"
"github.com/buildpacks/lifecycle/platform/files"
)
var (
msgProvideForceToOverride = "please provide -force to override"
msgAppImageNotMarkedRebasable = "app image is not marked as rebasable"
msgRunImageMDNotContainsName = "rebase app image: new base image '%s' not found in existing run image metadata: %s"
msgUnableToSatisfyTargetConstraints = "unable to satisfy target os/arch constraints; new run image: %s, old run image: %s"
)
type Rebaser struct {
Logger log.Logger
PlatformAPI *api.Version
Force bool
}
// Rebase changes the underlying base image for an application image.
func (r *Rebaser) Rebase(workingImage imgutil.Image, newBaseImage imgutil.Image, outputImageRef string, additionalNames []string) (files.RebaseReport, error) {
defer log.NewMeasurement("Rebaser", r.Logger)()
appPlatformAPI, err := workingImage.Env(platform.EnvPlatformAPI)
if err != nil {
return files.RebaseReport{}, fmt.Errorf("failed to get app image platform API: %w", err)
}
// perform platform API-specific validations
if appPlatformAPI == "" || api.MustParse(appPlatformAPI).LessThan("0.12") {
if err = validateStackID(workingImage, newBaseImage); err != nil {
return files.RebaseReport{}, err
}
if err = validateMixins(workingImage, newBaseImage); err != nil {
return files.RebaseReport{}, err
}
} else {
if err = r.validateTarget(workingImage, newBaseImage); err != nil {
return files.RebaseReport{}, err
}
}
// get existing metadata label
var origMetadata files.LayersMetadataCompat
if err = image.DecodeLabel(workingImage, platform.LifecycleMetadataLabel, &origMetadata); err != nil {
return files.RebaseReport{}, fmt.Errorf("get image metadata: %w", err)
}
// rebase
if err = workingImage.Rebase(origMetadata.RunImage.TopLayer, newBaseImage); err != nil {
return files.RebaseReport{}, fmt.Errorf("rebase app image: %w", err)
}
// update metadata label
origMetadata.RunImage.TopLayer, err = newBaseImage.TopLayer()
if err != nil {
return files.RebaseReport{}, fmt.Errorf("get rebase run image top layer SHA: %w", err)
}
identifier, err := newBaseImage.Identifier()
if err != nil {
return files.RebaseReport{}, fmt.Errorf("get run image id or digest: %w", err)
}
origMetadata.RunImage.Reference = identifier.String()
if r.PlatformAPI.AtLeast("0.12") {
// update stack and runImage if needed
if !containsName(origMetadata, newBaseImage.Name()) {
var existingRunImageMD string
switch {
case origMetadata.RunImage.Image != "":
existingRunImageMD = encoding.ToJSONMaybe(origMetadata.RunImage)
case origMetadata.Stack != nil:
existingRunImageMD = encoding.ToJSONMaybe(origMetadata.Stack.RunImage)
default:
existingRunImageMD = "not found"
}
if r.Force {
r.Logger.Warnf(
msgRunImageMDNotContainsName,
newBaseImage.Name(),
existingRunImageMD,
)
// update original metadata
origMetadata.RunImage.Image = newBaseImage.Name()
origMetadata.RunImage.Mirrors = []string{}
newStackMD := origMetadata.RunImage.ToStack()
origMetadata.Stack = &newStackMD
} else {
return files.RebaseReport{}, fmt.Errorf(
msgRunImageMDNotContainsName+"; "+msgProvideForceToOverride,
newBaseImage.Name(),
existingRunImageMD,
)
}
}
}
// set metadata label
data, err := json.Marshal(origMetadata)
if err != nil {
return files.RebaseReport{}, fmt.Errorf("marshall metadata: %w", err)
}
if err := workingImage.SetLabel(platform.LifecycleMetadataLabel, string(data)); err != nil {
return files.RebaseReport{}, fmt.Errorf("set app image metadata label: %w", err)
}
// update other labels
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 files.RebaseReport{}, fmt.Errorf("set stack labels: %w", err)
}
// save
report := files.RebaseReport{}
report.Image, err = saveImageAs(workingImage, outputImageRef, additionalNames, r.Logger)
if err != nil {
return files.RebaseReport{}, err
}
return report, err
}
func containsName(origMetadata files.LayersMetadataCompat, newBaseName string) bool {
if origMetadata.RunImage.Contains(newBaseName) {
return true
}
if origMetadata.Stack == nil {
return false
}
return origMetadata.Stack.RunImage.Contains(newBaseName)
}
func validateStackID(appImg, newBaseImage imgutil.Image) error {
appStackID, err := appImg.Label(platform.StackIDLabel)
if err != nil {
return fmt.Errorf("get app image stack: %w", err)
}
newBaseStackID, err := newBaseImage.Label(platform.StackIDLabel)
if err != nil {
return fmt.Errorf("get new base image stack: %w", err)
}
if appStackID == "" {
return errors.New("stack not defined on app image")
}
if newBaseStackID == "" {
return errors.New("stack not defined on new base image")
}
if appStackID != newBaseStackID {
return fmt.Errorf("incompatible stack: '%s' is not compatible with '%s'", newBaseStackID, appStackID)
}
return nil
}
func validateMixins(appImg, newBaseImg imgutil.Image) error {
var appImageMixins []string
var newBaseImageMixins []string
if err := image.DecodeLabel(appImg, platform.MixinsLabel, &appImageMixins); err != nil {
return fmt.Errorf("get app image mixins: %w", err)
}
if err := image.DecodeLabel(newBaseImg, platform.MixinsLabel, &newBaseImageMixins); err != nil {
return fmt.Errorf("get run image mixins: %w", err)
}
appImageMixins = removeStagePrefixes(appImageMixins)
newBaseImageMixins = removeStagePrefixes(newBaseImageMixins)
_, missing, _ := str.Compare(newBaseImageMixins, appImageMixins)
if len(missing) > 0 {
sort.Strings(missing)
return fmt.Errorf("missing required mixin(s): %s", strings.Join(missing, ", "))
}
return nil
}
func (r *Rebaser) validateTarget(appImg imgutil.Image, newBaseImg imgutil.Image) error {
rebasable, err := appImg.Label(platform.RebasableLabel)
if err != nil {
return fmt.Errorf("get app image rebasable label: %w", err)
}
if rebasable == "false" {
if !r.Force {
return errors.New(msgAppImageNotMarkedRebasable + "; " + msgProvideForceToOverride)
}
r.Logger.Warn(msgAppImageNotMarkedRebasable)
}
// check the OS, architecture, and variant values
// if they are not the same, the image cannot be rebased unless the force flag is set
appTarget, err := platform.GetTargetMetadata(appImg)
if err != nil {
return fmt.Errorf("get app image target: %w", err)
}
newBaseTarget, err := platform.GetTargetMetadata(newBaseImg)
if err != nil {
return fmt.Errorf("get new base image target: %w", err)
}
if !platform.TargetSatisfiedForRebase(*newBaseTarget, *appTarget) {
if !r.Force {
return fmt.Errorf(
msgUnableToSatisfyTargetConstraints+"; "+msgProvideForceToOverride,
encoding.ToJSONMaybe(newBaseTarget),
encoding.ToJSONMaybe(appTarget),
)
}
r.Logger.Warnf(
msgUnableToSatisfyTargetConstraints,
encoding.ToJSONMaybe(newBaseTarget),
encoding.ToJSONMaybe(appTarget),
)
}
return nil
}