lifecycle/buildpack/detect.go

202 lines
6.4 KiB
Go

package buildpack
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"syscall"
"github.com/BurntSushi/toml"
"github.com/buildpacks/lifecycle/api"
"github.com/buildpacks/lifecycle/log"
)
const (
// EnvBuildPlanPath is the absolute path of the build plan; a different copy is provided for each buildpack
EnvBuildPlanPath = "CNB_BUILD_PLAN_PATH"
// EnvBuildpackDir is the absolute path of the buildpack root directory (read-only)
EnvBuildpackDir = "CNB_BUILDPACK_DIR"
// EnvExtensionDir is the absolute path of the extension root directory (read-only)
EnvExtensionDir = "CNB_EXTENSION_DIR"
// EnvPlatformDir is the absolute path of the platform directory (read-only); a single copy is provided for all buildpacks
EnvPlatformDir = "CNB_PLATFORM_DIR"
// EnvExecEnv is the target execution environment. Standard values include "production", "test", and "development".
EnvExecEnv = "CNB_EXEC_ENV"
)
type DetectInputs struct {
AppDir string
BuildConfigDir string
PlatformDir string
Env BuildEnv
TargetEnv []string
ExecEnv string
}
type DetectOutputs struct {
BuildPlan
Output []byte `toml:"-"`
Code int `toml:"-"`
Err error `toml:"-"`
}
// DetectExecutor executes a single buildpack or image extension's `./bin/detect` binary,
// providing inputs as defined in the Buildpack Interface Specification,
// and processing outputs for the platform.
// For image extensions (where `./bin/detect` is optional), pre-populated outputs are processed here.
//
//go:generate mockgen -package testmock -destination ../phase/testmock/detect_executor.go github.com/buildpacks/lifecycle/buildpack DetectExecutor
type DetectExecutor interface {
Detect(d Descriptor, inputs DetectInputs, logger log.Logger) DetectOutputs
}
type DefaultDetectExecutor struct{}
func (e *DefaultDetectExecutor) Detect(d Descriptor, inputs DetectInputs, logger log.Logger) DetectOutputs {
switch descriptor := d.(type) {
case *BpDescriptor:
return detectBp(*descriptor, inputs, logger)
case *ExtDescriptor:
return detectExt(*descriptor, inputs, logger)
default:
return DetectOutputs{Code: -1, Err: fmt.Errorf("unknown descriptor type: %t", descriptor)}
}
}
func detectBp(d BpDescriptor, inputs DetectInputs, _ log.Logger) DetectOutputs {
planDir, planPath, err := processBuildpackPaths()
defer os.RemoveAll(planDir)
if err != nil {
return DetectOutputs{Code: -1, Err: err}
}
result := runDetect(&d, inputs, planPath, EnvBuildpackDir)
if result.Code != 0 {
return result
}
backupOut := result.Output
if _, err := toml.DecodeFile(planPath, &result); err != nil {
return DetectOutputs{Code: -1, Err: err, Output: backupOut}
}
if result.hasDoublySpecifiedVersions() || result.Or.hasDoublySpecifiedVersions() {
result.Err = fmt.Errorf(`buildpack %s has a "version" key and a "metadata.version" which cannot be specified together. "metadata.version" should be used instead`, d.Buildpack.ID)
result.Code = -1
}
if result.hasTopLevelVersions() || result.Or.hasTopLevelVersions() {
result.Err = fmt.Errorf(`buildpack %s has a "version" key which is not supported. "metadata.version" should be used instead`, d.Buildpack.ID)
result.Code = -1
}
return result
}
func detectExt(d ExtDescriptor, inputs DetectInputs, logger log.Logger) DetectOutputs {
planDir, planPath, err := processBuildpackPaths()
defer os.RemoveAll(planDir)
if err != nil {
return DetectOutputs{Code: -1, Err: err}
}
var result DetectOutputs
_, err = os.Stat(filepath.Join(d.WithRootDir, "bin", "detect"))
if os.IsNotExist(err) {
// treat extension root directory as pre-populated output directory
planPath = filepath.Join(d.WithRootDir, "detect", "plan.toml")
if _, err := toml.DecodeFile(planPath, &result); err != nil && !os.IsNotExist(err) {
return DetectOutputs{Code: -1, Err: err}
}
} else {
result = runDetect(&d, inputs, planPath, EnvExtensionDir)
if result.Code != 0 {
return result
}
backupOut := result.Output
if _, err := toml.DecodeFile(planPath, &result); err != nil {
return DetectOutputs{Code: -1, Err: err, Output: backupOut}
}
}
if result.hasDoublySpecifiedVersions() || result.Or.hasDoublySpecifiedVersions() {
result.Err = fmt.Errorf(`extension %s has a "version" key and a "metadata.version" which cannot be specified together. "metadata.version" should be used instead`, d.Extension.ID)
result.Code = -1
}
if result.hasTopLevelVersions() || result.Or.hasTopLevelVersions() {
result.Err = fmt.Errorf(`extension %s has a "version" key which is not supported. "metadata.version" should be used instead`, d.Extension.ID)
result.Code = -1
}
if result.hasRequires() || result.Or.hasRequires() {
result.Err = fmt.Errorf(`extension %s outputs "requires" which is not allowed`, d.Extension.ID)
result.Code = -1
}
return result
}
func processBuildpackPaths() (string, string, error) {
planDir, err := os.MkdirTemp("", "plan.")
if err != nil {
return "", "", err
}
planPath := filepath.Join(planDir, "plan.toml")
if err = os.WriteFile(planPath, nil, 0600); err != nil {
return "", "", err
}
return planDir, planPath, nil
}
type detectable interface {
API() string
ClearEnv() bool
RootDir() string
}
func runDetect(d detectable, inputs DetectInputs, planPath string, envRootDirKey string) DetectOutputs {
out := &bytes.Buffer{}
cmd := exec.Command(
filepath.Join(d.RootDir(), "bin", "detect"),
inputs.PlatformDir,
planPath,
) // #nosec G204
cmd.Dir = inputs.AppDir
cmd.Stdout = out
cmd.Stderr = out
var err error
if d.ClearEnv() {
cmd.Env, err = inputs.Env.WithOverrides("", inputs.BuildConfigDir)
} else {
cmd.Env, err = inputs.Env.WithOverrides(inputs.PlatformDir, inputs.BuildConfigDir)
}
if err != nil {
return DetectOutputs{Code: -1, Err: err}
}
cmd.Env = append(cmd.Env, envRootDirKey+"="+d.RootDir())
if api.MustParse(d.API()).AtLeast("0.8") {
cmd.Env = append(
cmd.Env,
EnvPlatformDir+"="+inputs.PlatformDir,
EnvBuildPlanPath+"="+planPath,
)
}
if api.MustParse(d.API()).AtLeast("0.10") {
cmd.Env = append(cmd.Env, inputs.TargetEnv...)
}
if api.MustParse(d.API()).AtLeast("0.12") && inputs.ExecEnv != "" {
cmd.Env = append(cmd.Env, EnvExecEnv+"="+inputs.ExecEnv)
}
if err := cmd.Run(); err != nil {
if err, ok := err.(*exec.ExitError); ok {
if status, ok := err.Sys().(syscall.WaitStatus); ok {
return DetectOutputs{Code: status.ExitStatus(), Output: out.Bytes()}
}
}
return DetectOutputs{Code: -1, Err: err, Output: out.Bytes()}
}
return DetectOutputs{Code: 0, Err: nil, Output: out.Bytes()}
}