buildkit/solver/llbsolver/vertex.go

333 lines
8.1 KiB
Go

package llbsolver
import (
"fmt"
"strings"
"github.com/containerd/containerd/platforms"
"github.com/moby/buildkit/solver"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/source"
"github.com/moby/buildkit/util/entitlements"
digest "github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
type vertex struct {
sys interface{}
options solver.VertexOptions
inputs []solver.Edge
digest digest.Digest
name string
}
func (v *vertex) Digest() digest.Digest {
return v.digest
}
func (v *vertex) Sys() interface{} {
return v.sys
}
func (v *vertex) Options() solver.VertexOptions {
return v.options
}
func (v *vertex) Inputs() []solver.Edge {
return v.inputs
}
func (v *vertex) Name() string {
if name, ok := v.options.Description["llb.customname"]; ok {
return name
}
return v.name
}
type LoadOpt func(*pb.Op, *pb.OpMetadata, *solver.VertexOptions) error
func WithValidateCaps() LoadOpt {
cs := pb.Caps.CapSet(pb.Caps.All())
return func(_ *pb.Op, md *pb.OpMetadata, opt *solver.VertexOptions) error {
if md != nil {
for c := range md.Caps {
if err := cs.Supports(c); err != nil {
return err
}
}
}
return nil
}
}
func WithCacheSources(cms []solver.CacheManager) LoadOpt {
return func(_ *pb.Op, _ *pb.OpMetadata, opt *solver.VertexOptions) error {
opt.CacheSources = cms
return nil
}
}
func NormalizeRuntimePlatforms() LoadOpt {
var defaultPlatform *pb.Platform
return func(op *pb.Op, _ *pb.OpMetadata, opt *solver.VertexOptions) error {
if op.Platform == nil {
if defaultPlatform == nil {
p := platforms.DefaultSpec()
defaultPlatform = &pb.Platform{
OS: p.OS,
Architecture: p.Architecture,
Variant: p.Variant,
}
}
op.Platform = defaultPlatform
}
platform := specs.Platform{OS: op.Platform.OS, Architecture: op.Platform.Architecture, Variant: op.Platform.Variant}
normalizedPlatform := platforms.Normalize(platform)
op.Platform = &pb.Platform{
OS: normalizedPlatform.OS,
Architecture: normalizedPlatform.Architecture,
Variant: normalizedPlatform.Variant,
}
return nil
}
}
func ValidateEntitlements(ent entitlements.Set) LoadOpt {
return func(op *pb.Op, _ *pb.OpMetadata, opt *solver.VertexOptions) error {
switch op := op.Op.(type) {
case *pb.Op_Exec:
if op.Exec.Network == pb.NetMode_HOST {
if !ent.Allowed(entitlements.EntitlementNetworkHost) {
return errors.Errorf("%s is not allowed", entitlements.EntitlementNetworkHost)
}
}
if op.Exec.Security == pb.SecurityMode_INSECURE {
if !ent.Allowed(entitlements.EntitlementSecurityInsecure) {
return errors.Errorf("%s is not allowed", entitlements.EntitlementSecurityInsecure)
}
}
}
return nil
}
}
type detectPrunedCacheID struct {
ids map[string]struct{}
}
func (dpc *detectPrunedCacheID) Load(op *pb.Op, md *pb.OpMetadata, opt *solver.VertexOptions) error {
if md == nil || !md.IgnoreCache {
return nil
}
switch op := op.Op.(type) {
case *pb.Op_Exec:
for _, m := range op.Exec.GetMounts() {
if m.MountType == pb.MountType_CACHE {
if m.CacheOpt != nil {
id := m.CacheOpt.ID
if id == "" {
id = m.Dest
}
if dpc.ids == nil {
dpc.ids = map[string]struct{}{}
}
dpc.ids[id] = struct{}{}
}
}
}
}
return nil
}
func Load(def *pb.Definition, opts ...LoadOpt) (solver.Edge, error) {
return loadLLB(def, func(dgst digest.Digest, pbOp *pb.Op, load func(digest.Digest) (solver.Vertex, error)) (solver.Vertex, error) {
opMetadata := def.Metadata[dgst]
vtx, err := newVertex(dgst, pbOp, &opMetadata, load, opts...)
if err != nil {
return nil, err
}
return vtx, nil
})
}
func newVertex(dgst digest.Digest, op *pb.Op, opMeta *pb.OpMetadata, load func(digest.Digest) (solver.Vertex, error), opts ...LoadOpt) (*vertex, error) {
opt := solver.VertexOptions{}
if opMeta != nil {
opt.IgnoreCache = opMeta.IgnoreCache
opt.Description = opMeta.Description
if opMeta.ExportCache != nil {
opt.ExportCache = &opMeta.ExportCache.Value
}
}
for _, fn := range opts {
if err := fn(op, opMeta, &opt); err != nil {
return nil, err
}
}
vtx := &vertex{sys: op, options: opt, digest: dgst, name: llbOpName(op)}
for _, in := range op.Inputs {
sub, err := load(in.Digest)
if err != nil {
return nil, err
}
vtx.inputs = append(vtx.inputs, solver.Edge{Index: solver.Index(in.Index), Vertex: sub})
}
return vtx, nil
}
// loadLLB loads LLB.
// fn is executed sequentially.
func loadLLB(def *pb.Definition, fn func(digest.Digest, *pb.Op, func(digest.Digest) (solver.Vertex, error)) (solver.Vertex, error)) (solver.Edge, error) {
if len(def.Def) == 0 {
return solver.Edge{}, errors.New("invalid empty definition")
}
allOps := make(map[digest.Digest]*pb.Op)
var dgst digest.Digest
for _, dt := range def.Def {
var op pb.Op
if err := (&op).Unmarshal(dt); err != nil {
return solver.Edge{}, errors.Wrap(err, "failed to parse llb proto op")
}
dgst = digest.FromBytes(dt)
allOps[dgst] = &op
}
if len(allOps) < 2 {
return solver.Edge{}, errors.Errorf("invalid LLB with %d vertexes", len(allOps))
}
lastOp := allOps[dgst]
delete(allOps, dgst)
if len(lastOp.Inputs) == 0 {
return solver.Edge{}, errors.Errorf("invalid LLB with no inputs on last vertex")
}
dgst = lastOp.Inputs[0].Digest
cache := make(map[digest.Digest]solver.Vertex)
var rec func(dgst digest.Digest) (solver.Vertex, error)
rec = func(dgst digest.Digest) (solver.Vertex, error) {
if v, ok := cache[dgst]; ok {
return v, nil
}
op, ok := allOps[dgst]
if !ok {
return nil, errors.Errorf("invalid missing input digest %s", dgst)
}
if err := ValidateOp(op); err != nil {
return nil, err
}
v, err := fn(dgst, op, rec)
if err != nil {
return nil, err
}
cache[dgst] = v
return v, nil
}
v, err := rec(dgst)
if err != nil {
return solver.Edge{}, err
}
return solver.Edge{Vertex: v, Index: solver.Index(lastOp.Inputs[0].Index)}, nil
}
func llbOpName(op *pb.Op) string {
switch op := op.Op.(type) {
case *pb.Op_Source:
if id, err := source.FromLLB(op, nil); err == nil {
if id, ok := id.(*source.LocalIdentifier); ok {
if len(id.IncludePatterns) == 1 {
return op.Source.Identifier + " (" + id.IncludePatterns[0] + ")"
}
}
}
return op.Source.Identifier
case *pb.Op_Exec:
return strings.Join(op.Exec.Meta.Args, " ")
case *pb.Op_File:
return fileOpName(op.File.Actions)
case *pb.Op_Build:
return "build"
default:
return "unknown"
}
}
func ValidateOp(op *pb.Op) error {
if op == nil {
return errors.Errorf("invalid nil op")
}
switch op := op.Op.(type) {
case *pb.Op_Source:
if op.Source == nil {
return errors.Errorf("invalid nil source op")
}
case *pb.Op_Exec:
if op.Exec == nil {
return errors.Errorf("invalid nil exec op")
}
if op.Exec.Meta == nil {
return errors.Errorf("invalid exec op with no meta")
}
if len(op.Exec.Meta.Args) == 0 {
return errors.Errorf("invalid exec op with no args")
}
if len(op.Exec.Mounts) == 0 {
return errors.Errorf("invalid exec op with no mounts")
}
isRoot := false
for _, m := range op.Exec.Mounts {
if m.Dest == pb.RootMount {
isRoot = true
break
}
}
if !isRoot {
return errors.Errorf("invalid exec op with no rootfs")
}
case *pb.Op_File:
if op.File == nil {
return errors.Errorf("invalid nil file op")
}
if len(op.File.Actions) == 0 {
return errors.Errorf("invalid file op with no actions")
}
case *pb.Op_Build:
if op.Build == nil {
return errors.Errorf("invalid nil build op")
}
}
return nil
}
func fileOpName(actions []*pb.FileAction) string {
names := make([]string, 0, len(actions))
for _, action := range actions {
switch a := action.Action.(type) {
case *pb.FileAction_Mkdir:
names = append(names, fmt.Sprintf("mkdir %s", a.Mkdir.Path))
case *pb.FileAction_Mkfile:
names = append(names, fmt.Sprintf("mkfile %s", a.Mkfile.Path))
case *pb.FileAction_Rm:
names = append(names, fmt.Sprintf("rm %s", a.Rm.Path))
case *pb.FileAction_Copy:
names = append(names, fmt.Sprintf("copy %s %s", a.Copy.Src, a.Copy.Dest))
}
}
return strings.Join(names, ", ")
}