lifecycle/launch/launch.go

206 lines
5.6 KiB
Go

package launch
import (
"encoding/json"
"fmt"
"path"
"path/filepath"
"strings"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/buildpacks/lifecycle/api"
)
// Process represents a process to launch at runtime.
type Process struct {
Type string `toml:"type" json:"type"`
Command RawCommand `toml:"command" json:"command"`
Args []string `toml:"args" json:"args"`
Direct bool `toml:"direct" json:"direct"`
Default bool `toml:"default,omitempty" json:"default,omitempty"`
BuildpackID string `toml:"buildpack-id" json:"buildpackID"`
WorkingDirectory string `toml:"working-dir,omitempty" json:"working-dir,omitempty"`
ExecEnv []string `toml:"exec-env,omitempty" json:"exec-env,omitempty"`
PlatformAPI *api.Version `toml:"-" json:"-"`
}
func (p Process) NoDefault() Process {
p.Default = false
return p
}
func (p Process) WithPlatformAPI(platformAPI *api.Version) Process {
// set on the process itself
p.PlatformAPI = platformAPI
// set on the command as well, this is needed when we serialize the command
p.Command.PlatformAPI = platformAPI
// for platform versions < 0.10
// we only support a single command
// push any extra entries into the args so they aren't lost
if p.PlatformAPI.LessThan("0.10") {
p.Args = append(p.Command.Entries[1:], p.Args[0:]...)
p.Command.Entries = []string{p.Command.Entries[0]}
}
return p
}
type RawCommand struct {
Entries []string
PlatformAPI *api.Version
}
func NewRawCommand(entries []string) RawCommand {
return RawCommand{Entries: entries}
}
func (c RawCommand) WithPlatformAPI(api *api.Version) RawCommand {
c.PlatformAPI = api
return c
}
func (c RawCommand) MarshalTOML() ([]byte, error) {
if c.PlatformAPI == nil {
return nil, fmt.Errorf("missing PlatformAPI while encoding RawCommand")
}
if c.PlatformAPI.AtLeast("0.10") {
buffer := &strings.Builder{}
// turn array into toml array
buffer.WriteString("[")
for i, entry := range c.Entries {
if i != 0 {
buffer.WriteString(", ")
}
escaped, err := json.Marshal(entry) // properly escape special characters in single string
if err != nil {
return nil, err
}
buffer.WriteString(string(escaped))
}
buffer.WriteString("]")
return []byte(buffer.String()), nil
}
return json.Marshal(c.Entries[0]) // properly escape special characters in single string
}
func (c RawCommand) MarshalJSON() ([]byte, error) {
if c.PlatformAPI == nil {
return nil, fmt.Errorf("missing PlatformAPI while encoding RawCommand")
}
if c.PlatformAPI.AtLeast("0.10") {
return json.Marshal(c.Entries)
}
return json.Marshal(c.Entries[0])
}
// UnmarshalTOML implements toml.Unmarshaler and is needed because we read metadata.toml
// this method will attempt to parse the command in either string or array format
func (c *RawCommand) UnmarshalTOML(data interface{}) error {
var entries []string
// the raw value is either "the-command" or ["the-command", "arg1", "arg2"]
// the latter is exposed as []interface{} by toml library and needs conversion
switch v := data.(type) {
case string:
entries = []string{v}
case []interface{}:
s := make([]string, len(v))
for i, el := range v {
s[i] = fmt.Sprint(el)
}
entries = s
default:
return fmt.Errorf("unknown command type %T with data %v", data, data)
}
*c = NewRawCommand(entries)
return nil
}
// UnmarshalJSON implements json.Unmarshaler and is provided to help library consumers who need to read the build metadata label
// this method will attempt to parse the command in either string or array format
func (c *RawCommand) UnmarshalJSON(data []byte) error {
var entries []string
strData := string(data)
// first try to decode array
err := json.NewDecoder(strings.NewReader(strData)).Decode(&entries)
if err != nil {
// then try string
var s string
err = json.NewDecoder(strings.NewReader(strData)).Decode(&s)
if err != nil {
return err
}
entries = []string{s}
}
*c = NewRawCommand(entries)
return nil
}
// ProcessPath returns the absolute path to the symlink for a given process type
func ProcessPath(pType string) string {
return filepath.Join(ProcessDir, pType+exe)
}
type Metadata struct {
Processes []Process `toml:"processes" json:"processes"`
Buildpacks []Buildpack `toml:"buildpacks" json:"buildpacks"`
}
// Matches is used by goMock to compare two Metadata objects in tests
// when matching expected calls to methods containing Metadata objects
func (m Metadata) Matches(x interface{}) bool {
metadatax, ok := x.(Metadata)
if !ok {
return false
}
// don't compare Processes directly, we will compare those individually next
if s := cmp.Diff(metadatax, m, cmpopts.IgnoreFields(Metadata{}, "Processes")); s != "" {
return false
}
// we need to ignore the PlatformAPI field because it isn't always set where these are used
// and trying to compare it will cause a panic
for i, p := range m.Processes {
if s := cmp.Diff(metadatax.Processes[i], p,
cmpopts.IgnoreFields(Process{}, "PlatformAPI"),
cmpopts.IgnoreFields(RawCommand{}, "PlatformAPI")); s != "" {
return false
}
}
return true
}
func (m Metadata) String() string {
return fmt.Sprintf("%+v %+v", m.Processes, m.Buildpacks)
}
func (m Metadata) FindProcessType(pType string) (Process, bool) {
for _, p := range m.Processes {
if p.Type == pType {
return p, true
}
}
return Process{}, false
}
type Buildpack struct {
API string `toml:"api"`
ID string `toml:"id"`
}
func EscapeID(id string) string {
return strings.ReplaceAll(id, "/", "_")
}
func GetMetadataFilePath(layersDir string) string {
return path.Join(layersDir, "config", "metadata.toml")
}