lifecycle/phase/builder.go

270 lines
7.9 KiB
Go

package phase
import (
"fmt"
"io"
"os"
"path/filepath"
"sort"
"github.com/pkg/errors"
"github.com/buildpacks/lifecycle/api"
"github.com/buildpacks/lifecycle/buildpack"
"github.com/buildpacks/lifecycle/env"
"github.com/buildpacks/lifecycle/internal/encoding"
"github.com/buildpacks/lifecycle/internal/fsutil"
"github.com/buildpacks/lifecycle/launch"
"github.com/buildpacks/lifecycle/layers"
"github.com/buildpacks/lifecycle/log"
"github.com/buildpacks/lifecycle/platform"
"github.com/buildpacks/lifecycle/platform/files"
)
type Platform interface {
API() *api.Version
}
// BuildEnv encapsulates modifications that the lifecycle can make to buildpacks' build environment.
//
//go:generate mockgen -package testmock -destination testmock/build_env.go github.com/buildpacks/lifecycle/phase BuildEnv
type BuildEnv interface {
AddRootDir(baseDir string) error
AddEnvDir(envDir string, defaultAction env.ActionType) error
WithOverrides(platformDir string, baseConfigDir string) ([]string, error)
List() []string
}
type Builder struct {
AppDir string
BuildConfigDir string
LayersDir string
PlatformDir string
ExecEnv string
BuildExecutor buildpack.BuildExecutor
DirStore DirStore
Group buildpack.Group
Logger log.Logger
Out, Err io.Writer
Plan files.Plan
PlatformAPI *api.Version
AnalyzeMD files.Analyzed
}
func (b *Builder) Build() (*files.BuildMetadata, error) {
defer log.NewMeasurement("Builder", b.Logger)()
// ensure layers SBOM directory is removed
if err := os.RemoveAll(filepath.Join(b.LayersDir, "sbom")); err != nil {
return nil, errors.Wrap(err, "cleaning layers SBOM directory")
}
var (
bomFiles []buildpack.BOMFile
buildBOM []buildpack.BOMEntry
labels []buildpack.Label
launchBOM []buildpack.BOMEntry
slices []layers.Slice
)
processMap := newProcessMap()
inputs := b.getBuildInputs()
filteredPlan := b.Plan
for _, bp := range b.Group.Group {
b.Logger.Debugf("Running build for buildpack %s", bp)
b.Logger.Debug("Looking up buildpack")
bpTOML, err := b.DirStore.LookupBp(bp.ID, bp.Version)
if err != nil {
return nil, err
}
b.Logger.Debug("Finding plan")
inputs.Plan = filteredPlan.Find(buildpack.KindBuildpack, bp.ID)
br, err := b.BuildExecutor.Build(*bpTOML, inputs, b.Logger)
if err != nil {
return nil, err
}
b.Logger.Debug("Updating buildpack processes")
bomFiles = append(bomFiles, br.BOMFiles...)
buildBOM = append(buildBOM, br.BuildBOM...)
filteredPlan = filteredPlan.Filter(br.MetRequires)
labels = append(labels, br.Labels...)
launchBOM = append(launchBOM, br.LaunchBOM...)
slices = append(slices, br.Slices...)
b.Logger.Debug("Updating process list")
warning := processMap.add(br.Processes)
if warning != "" {
b.Logger.Warn(warning)
}
b.Logger.Debugf("Finished running build for buildpack %s", bp)
}
if b.PlatformAPI.AtLeast("0.8") {
b.Logger.Debug("Copying SBOM files")
if err := b.copySBOMFiles(inputs.LayersDir, bomFiles); err != nil {
return nil, err
}
}
if b.PlatformAPI.AtLeast("0.9") {
b.Logger.Debug("Creating SBOM files for legacy BOM")
if err := encoding.WriteJSON(filepath.Join(b.LayersDir, "sbom", "launch", "sbom.legacy.json"), launchBOM); err != nil {
return nil, errors.Wrap(err, "encoding launch bom")
}
if err := encoding.WriteJSON(filepath.Join(b.LayersDir, "sbom", "build", "sbom.legacy.json"), buildBOM); err != nil {
return nil, errors.Wrap(err, "encoding build bom")
}
launchBOM = []buildpack.BOMEntry{}
}
b.Logger.Debug("Listing processes")
procList := processMap.list(b.PlatformAPI)
// Don't redundantly print `extension = true` and `optional = true` in metadata.toml and metadata label
for i, ext := range b.Group.GroupExtensions {
b.Group.GroupExtensions[i] = ext.NoExtension().NoOpt()
}
return &files.BuildMetadata{
BOM: launchBOM,
Buildpacks: b.Group.Group,
Extensions: b.Group.GroupExtensions,
Labels: labels,
Processes: procList,
Slices: slices,
BuildpackDefaultProcessType: processMap.defaultType,
}, nil
}
func (b *Builder) getBuildInputs() buildpack.BuildInputs {
return buildpack.BuildInputs{
AppDir: b.AppDir,
BuildConfigDir: b.BuildConfigDir,
LayersDir: b.LayersDir,
PlatformDir: b.PlatformDir,
Env: env.NewBuildEnv(os.Environ()),
TargetEnv: platform.EnvVarsFor(&fsutil.DefaultDetector{}, b.AnalyzeMD.RunImageTarget(), b.Logger),
ExecEnv: b.ExecEnv,
Out: b.Out,
Err: b.Err,
}
}
// copySBOMFiles() copies any BOM files written by buildpacks during the Build() process
// to their appropriate locations, in preparation for its final application layer.
// This function handles both BOMs that are associated with a layer directory and BOMs that are not
// associated with a layer directory, since "bomFile.LayerName" will be "" in the latter case.
//
// Before:
// /layers
// └── buildpack.id
//
// ├── A
// │ └── ...
// ├── A.sbom.cdx.json
// └── launch.sbom.cdx.json
//
// After:
// /layers
// └── sbom
//
// └── launch
// └── buildpack.id
// ├── A
// │ └── sbom.cdx.json
// └── sbom.cdx.json
func (b *Builder) copySBOMFiles(layersDir string, bomFiles []buildpack.BOMFile) error {
var (
buildSBOMDir = filepath.Join(layersDir, "sbom", "build")
cacheSBOMDir = filepath.Join(layersDir, "sbom", "cache")
launchSBOMDir = filepath.Join(layersDir, "sbom", "launch")
copyBOMFileTo = func(bomFile buildpack.BOMFile, sbomDir string) error {
targetDir := filepath.Join(sbomDir, launch.EscapeID(bomFile.BuildpackID), bomFile.LayerName)
err := os.MkdirAll(targetDir, os.ModePerm)
if err != nil {
return err
}
name, err := bomFile.Name()
if err != nil {
return err
}
return fsutil.Copy(bomFile.Path, filepath.Join(targetDir, name))
}
)
for _, bomFile := range bomFiles {
switch bomFile.LayerType {
case buildpack.LayerTypeBuild:
if err := copyBOMFileTo(bomFile, buildSBOMDir); err != nil {
return err
}
case buildpack.LayerTypeCache:
if err := copyBOMFileTo(bomFile, cacheSBOMDir); err != nil {
return err
}
case buildpack.LayerTypeLaunch:
if err := copyBOMFileTo(bomFile, launchSBOMDir); err != nil {
return err
}
}
}
return nil
}
type processMap struct {
typeToProcess map[string]launch.Process
defaultType string
}
func newProcessMap() processMap {
return processMap{
typeToProcess: make(map[string]launch.Process),
defaultType: "",
}
}
// This function adds the processes from listToAdd to processMap
// it sets m.defaultType to the last default process
// if a non-default process overrides a default process, it returns a warning and unset m.defaultType
func (m *processMap) add(listToAdd []launch.Process) string {
warning := ""
for _, procToAdd := range listToAdd {
if procToAdd.Default {
m.defaultType = procToAdd.Type
warning = ""
} else if procToAdd.Type == m.defaultType {
// non-default process overrides a default process
m.defaultType = ""
warning = fmt.Sprintf("Warning: redefining the following default process type with a process not marked as default: %s\n", procToAdd.Type)
}
m.typeToProcess[procToAdd.Type] = procToAdd
}
return warning
}
// list returns a sorted array of processes.
// The array is sorted based on the process types.
// The list is sorted for reproducibility.
func (m processMap) list(platformAPI *api.Version) []launch.Process {
var keys []string
for proc := range m.typeToProcess {
keys = append(keys, proc)
}
sort.Strings(keys)
result := []launch.Process{}
for _, key := range keys {
result = append(result, m.typeToProcess[key].NoDefault().WithPlatformAPI(platformAPI)) // we set the default to false so it won't be part of metadata.toml
}
return result
}