1package llbsolver
2
3import (
4	"context"
5	"strings"
6	"time"
7
8	"github.com/moby/buildkit/cache"
9	"github.com/moby/buildkit/cache/remotecache"
10	"github.com/moby/buildkit/client"
11	controlgateway "github.com/moby/buildkit/control/gateway"
12	"github.com/moby/buildkit/exporter"
13	"github.com/moby/buildkit/frontend"
14	"github.com/moby/buildkit/frontend/gateway"
15	"github.com/moby/buildkit/identity"
16	"github.com/moby/buildkit/session"
17	"github.com/moby/buildkit/solver"
18	"github.com/moby/buildkit/util/entitlements"
19	"github.com/moby/buildkit/util/progress"
20	"github.com/moby/buildkit/worker"
21	digest "github.com/opencontainers/go-digest"
22	specs "github.com/opencontainers/image-spec/specs-go/v1"
23	"github.com/pkg/errors"
24)
25
26const keyEntitlements = "llb.entitlements"
27
28type ExporterRequest struct {
29	Exporter        exporter.ExporterInstance
30	CacheExporter   remotecache.Exporter
31	CacheExportMode solver.CacheExportMode
32}
33
34// ResolveWorkerFunc returns default worker for the temporary default non-distributed use cases
35type ResolveWorkerFunc func() (worker.Worker, error)
36
37type Solver struct {
38	workerController     *worker.Controller
39	solver               *solver.Solver
40	resolveWorker        ResolveWorkerFunc
41	frontends            map[string]frontend.Frontend
42	resolveCacheImporter remotecache.ResolveCacheImporterFunc
43	platforms            []specs.Platform
44	gatewayForwarder     *controlgateway.GatewayForwarder
45}
46
47func New(wc *worker.Controller, f map[string]frontend.Frontend, cache solver.CacheManager, resolveCI remotecache.ResolveCacheImporterFunc, gatewayForwarder *controlgateway.GatewayForwarder) (*Solver, error) {
48	s := &Solver{
49		workerController:     wc,
50		resolveWorker:        defaultResolver(wc),
51		frontends:            f,
52		resolveCacheImporter: resolveCI,
53		gatewayForwarder:     gatewayForwarder,
54	}
55
56	// executing is currently only allowed on default worker
57	w, err := wc.GetDefault()
58	if err != nil {
59		return nil, err
60	}
61	s.platforms = w.Platforms()
62
63	s.solver = solver.NewSolver(solver.SolverOpt{
64		ResolveOpFunc: s.resolver(),
65		DefaultCache:  cache,
66	})
67	return s, nil
68}
69
70func (s *Solver) resolver() solver.ResolveOpFunc {
71	return func(v solver.Vertex, b solver.Builder) (solver.Op, error) {
72		w, err := s.resolveWorker()
73		if err != nil {
74			return nil, err
75		}
76		return w.ResolveOp(v, s.Bridge(b))
77	}
78}
79
80func (s *Solver) Bridge(b solver.Builder) frontend.FrontendLLBBridge {
81	return &llbBridge{
82		builder:              b,
83		frontends:            s.frontends,
84		resolveWorker:        s.resolveWorker,
85		resolveCacheImporter: s.resolveCacheImporter,
86		cms:                  map[string]solver.CacheManager{},
87		platforms:            s.platforms,
88	}
89}
90
91func (s *Solver) Solve(ctx context.Context, id string, req frontend.SolveRequest, exp ExporterRequest, ent []entitlements.Entitlement) (*client.SolveResponse, error) {
92	j, err := s.solver.NewJob(id)
93	if err != nil {
94		return nil, err
95	}
96
97	defer j.Discard()
98
99	set, err := entitlements.WhiteList(ent, supportedEntitlements())
100	if err != nil {
101		return nil, err
102	}
103	j.SetValue(keyEntitlements, set)
104
105	j.SessionID = session.FromContext(ctx)
106
107	var res *frontend.Result
108	if s.gatewayForwarder != nil && req.Definition == nil && req.Frontend == "" {
109		fwd := gateway.NewBridgeForwarder(ctx, s.Bridge(j), s.workerController)
110		defer fwd.Discard()
111		if err := s.gatewayForwarder.RegisterBuild(ctx, id, fwd); err != nil {
112			return nil, err
113		}
114		defer s.gatewayForwarder.UnregisterBuild(ctx, id)
115
116		var err error
117		select {
118		case <-fwd.Done():
119			res, err = fwd.Result()
120		case <-ctx.Done():
121			err = ctx.Err()
122		}
123		if err != nil {
124			return nil, err
125		}
126	} else {
127		res, err = s.Bridge(j).Solve(ctx, req)
128		if err != nil {
129			return nil, err
130		}
131	}
132
133	defer func() {
134		res.EachRef(func(ref solver.CachedResult) error {
135			go ref.Release(context.TODO())
136			return nil
137		})
138	}()
139
140	var exporterResponse map[string]string
141	if exp := exp.Exporter; exp != nil {
142		inp := exporter.Source{
143			Metadata: res.Metadata,
144		}
145		if inp.Metadata == nil {
146			inp.Metadata = make(map[string][]byte)
147		}
148		if res := res.Ref; res != nil {
149			workerRef, ok := res.Sys().(*worker.WorkerRef)
150			if !ok {
151				return nil, errors.Errorf("invalid reference: %T", res.Sys())
152			}
153			inp.Ref = workerRef.ImmutableRef
154		}
155		if res.Refs != nil {
156			m := make(map[string]cache.ImmutableRef, len(res.Refs))
157			for k, res := range res.Refs {
158				if res == nil {
159					m[k] = nil
160				} else {
161					workerRef, ok := res.Sys().(*worker.WorkerRef)
162					if !ok {
163						return nil, errors.Errorf("invalid reference: %T", res.Sys())
164					}
165					m[k] = workerRef.ImmutableRef
166				}
167			}
168			inp.Refs = m
169		}
170
171		if err := inVertexContext(j.Context(ctx), exp.Name(), "", func(ctx context.Context) error {
172			exporterResponse, err = exp.Export(ctx, inp)
173			return err
174		}); err != nil {
175			return nil, err
176		}
177	}
178
179	if e := exp.CacheExporter; e != nil {
180		if err := inVertexContext(j.Context(ctx), "exporting cache", "", func(ctx context.Context) error {
181			prepareDone := oneOffProgress(ctx, "preparing build cache for export")
182			if err := res.EachRef(func(res solver.CachedResult) error {
183				// all keys have same export chain so exporting others is not needed
184				_, err := res.CacheKeys()[0].Exporter.ExportTo(ctx, e, solver.CacheExportOpt{
185					Convert: workerRefConverter,
186					Mode:    exp.CacheExportMode,
187				})
188				return err
189			}); err != nil {
190				return prepareDone(err)
191			}
192			prepareDone(nil)
193			return e.Finalize(ctx)
194		}); err != nil {
195			return nil, err
196		}
197	}
198
199	if exporterResponse == nil {
200		exporterResponse = make(map[string]string)
201	}
202
203	for k, v := range res.Metadata {
204		if strings.HasPrefix(k, "frontend.") {
205			exporterResponse[k] = string(v)
206		}
207	}
208
209	return &client.SolveResponse{
210		ExporterResponse: exporterResponse,
211	}, nil
212}
213
214func (s *Solver) Status(ctx context.Context, id string, statusChan chan *client.SolveStatus) error {
215	j, err := s.solver.Get(id)
216	if err != nil {
217		close(statusChan)
218		return err
219	}
220	return j.Status(ctx, statusChan)
221}
222
223func defaultResolver(wc *worker.Controller) ResolveWorkerFunc {
224	return func() (worker.Worker, error) {
225		return wc.GetDefault()
226	}
227}
228
229func oneOffProgress(ctx context.Context, id string) func(err error) error {
230	pw, _, _ := progress.FromContext(ctx)
231	now := time.Now()
232	st := progress.Status{
233		Started: &now,
234	}
235	pw.Write(id, st)
236	return func(err error) error {
237		// TODO: set error on status
238		now := time.Now()
239		st.Completed = &now
240		pw.Write(id, st)
241		pw.Close()
242		return err
243	}
244}
245
246func inVertexContext(ctx context.Context, name, id string, f func(ctx context.Context) error) error {
247	if id == "" {
248		id = identity.NewID()
249	}
250	v := client.Vertex{
251		Digest: digest.FromBytes([]byte(id)),
252		Name:   name,
253	}
254	pw, _, ctx := progress.FromContext(ctx, progress.WithMetadata("vertex", v.Digest))
255	notifyStarted(ctx, &v, false)
256	defer pw.Close()
257	err := f(ctx)
258	notifyCompleted(ctx, &v, err, false)
259	return err
260}
261
262func notifyStarted(ctx context.Context, v *client.Vertex, cached bool) {
263	pw, _, _ := progress.FromContext(ctx)
264	defer pw.Close()
265	now := time.Now()
266	v.Started = &now
267	v.Completed = nil
268	v.Cached = cached
269	pw.Write(v.Digest.String(), *v)
270}
271
272func notifyCompleted(ctx context.Context, v *client.Vertex, err error, cached bool) {
273	pw, _, _ := progress.FromContext(ctx)
274	defer pw.Close()
275	now := time.Now()
276	if v.Started == nil {
277		v.Started = &now
278	}
279	v.Completed = &now
280	v.Cached = cached
281	if err != nil {
282		v.Error = err.Error()
283	}
284	pw.Write(v.Digest.String(), *v)
285}
286
287var AllowNetworkHostUnstable = false // TODO: enable in constructor
288
289func supportedEntitlements() []entitlements.Entitlement {
290	out := []entitlements.Entitlement{} // nil means no filter
291	if AllowNetworkHostUnstable {
292		out = append(out, entitlements.EntitlementNetworkHost)
293	}
294	return out
295}
296
297func loadEntitlements(b solver.Builder) (entitlements.Set, error) {
298	var ent entitlements.Set = map[entitlements.Entitlement]struct{}{}
299	err := b.EachValue(context.TODO(), keyEntitlements, func(v interface{}) error {
300		set, ok := v.(entitlements.Set)
301		if !ok {
302			return errors.Errorf("invalid entitlements %T", v)
303		}
304		for k := range set {
305			ent[k] = struct{}{}
306		}
307		return nil
308	})
309	if err != nil {
310		return nil, err
311	}
312	return ent, nil
313}
314