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 RuntimePlatforms(p []specs.Platform) LoadOpt { var defaultPlatform *pb.Platform pp := make([]specs.Platform, len(p)) for i := range p { pp[i] = platforms.Normalize(p[i]) } 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, } if _, ok := op.Op.(*pb.Op_Exec); ok { var found bool for _, pp := range pp { if pp.OS == op.Platform.OS && pp.Architecture == op.Platform.Architecture && pp.Variant == op.Platform.Variant { found = true break } } if !found { return errors.Errorf("runtime execution on platform %s not supported", platforms.Format(specs.Platform{OS: op.Platform.OS, Architecture: op.Platform.Architecture, Variant: op.Platform.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, ", ") }