Files
buildkit/solver/llbsolver/solver.go
Tonis Tiigi e05a89e0b8 improve stacks of cancels from defers
In this case the current stack trace points to the line
where the context was created. Instead the stack should be
captured when the defer is running so the return path to
the defer call is also part of the stack.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2024-11-19 18:40:00 -08:00

1156 lines
31 KiB
Go

package llbsolver
import (
"context"
"encoding/json"
"fmt"
"maps"
"os"
"strings"
"sync"
"time"
intoto "github.com/in-toto/in-toto-golang/in_toto"
slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
controlapi "github.com/moby/buildkit/api/services/control"
"github.com/moby/buildkit/cache"
cacheconfig "github.com/moby/buildkit/cache/config"
"github.com/moby/buildkit/cache/remotecache"
"github.com/moby/buildkit/client"
controlgateway "github.com/moby/buildkit/control/gateway"
"github.com/moby/buildkit/errdefs"
"github.com/moby/buildkit/executor/resources"
resourcestypes "github.com/moby/buildkit/executor/resources/types"
"github.com/moby/buildkit/exporter"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
"github.com/moby/buildkit/exporter/verifier"
"github.com/moby/buildkit/frontend"
"github.com/moby/buildkit/frontend/attestations"
"github.com/moby/buildkit/frontend/gateway"
"github.com/moby/buildkit/identity"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/solver"
"github.com/moby/buildkit/solver/llbsolver/provenance"
"github.com/moby/buildkit/solver/result"
spb "github.com/moby/buildkit/sourcepolicy/pb"
"github.com/moby/buildkit/util/bklog"
"github.com/moby/buildkit/util/compression"
"github.com/moby/buildkit/util/entitlements"
"github.com/moby/buildkit/util/leaseutil"
"github.com/moby/buildkit/util/progress"
"github.com/moby/buildkit/util/tracing"
"github.com/moby/buildkit/util/tracing/detect"
"github.com/moby/buildkit/worker"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
"google.golang.org/protobuf/types/known/timestamppb"
)
const (
keyEntitlements = "llb.entitlements"
keySourcePolicy = "llb.sourcepolicy"
)
type ExporterRequest struct {
Exporters []exporter.ExporterInstance
CacheExporters []RemoteCacheExporter
}
type RemoteCacheExporter struct {
remotecache.Exporter
solver.CacheExportMode
IgnoreError bool
}
// ResolveWorkerFunc returns default worker for the temporary default non-distributed use cases
type ResolveWorkerFunc func() (worker.Worker, error)
// Opt defines options for new Solver.
type Opt struct {
CacheManager solver.CacheManager
CacheResolvers map[string]remotecache.ResolveCacheImporterFunc
Entitlements []string
Frontends map[string]frontend.Frontend
GatewayForwarder *controlgateway.GatewayForwarder
SessionManager *session.Manager
WorkerController *worker.Controller
HistoryQueue *HistoryQueue
ResourceMonitor *resources.Monitor
}
type Solver struct {
workerController *worker.Controller
solver *solver.Solver
resolveWorker ResolveWorkerFunc
eachWorker func(func(worker.Worker) error) error
frontends map[string]frontend.Frontend
resolveCacheImporterFuncs map[string]remotecache.ResolveCacheImporterFunc
gatewayForwarder *controlgateway.GatewayForwarder
sm *session.Manager
entitlements []string
history *HistoryQueue
sysSampler *resources.Sampler[*resourcestypes.SysSample]
}
// Processor defines a processing function to be applied after solving, but
// before exporting
type Processor func(ctx context.Context, result *Result, s *Solver, j *solver.Job, usage *resources.SysSampler) (*Result, error)
func New(opt Opt) (*Solver, error) {
s := &Solver{
workerController: opt.WorkerController,
resolveWorker: defaultResolver(opt.WorkerController),
eachWorker: allWorkers(opt.WorkerController),
frontends: opt.Frontends,
resolveCacheImporterFuncs: opt.CacheResolvers,
gatewayForwarder: opt.GatewayForwarder,
sm: opt.SessionManager,
entitlements: opt.Entitlements,
history: opt.HistoryQueue,
}
sampler, err := resources.NewSysSampler()
if err != nil {
return nil, err
}
s.sysSampler = sampler
s.solver = solver.NewSolver(solver.SolverOpt{
ResolveOpFunc: s.resolver(),
DefaultCache: opt.CacheManager,
})
return s, nil
}
func (s *Solver) Close() error {
s.solver.Close()
if s.sysSampler != nil {
return s.sysSampler.Close()
}
return nil
}
func (s *Solver) resolver() solver.ResolveOpFunc {
return func(v solver.Vertex, b solver.Builder) (solver.Op, error) {
w, err := s.resolveWorker()
if err != nil {
return nil, err
}
return w.ResolveOp(v, s.Bridge(b), s.sm)
}
}
func (s *Solver) bridge(b solver.Builder) *provenanceBridge {
return &provenanceBridge{llbBridge: &llbBridge{
builder: b,
frontends: s.frontends,
resolveWorker: s.resolveWorker,
eachWorker: s.eachWorker,
resolveCacheImporterFuncs: s.resolveCacheImporterFuncs,
cms: map[string]solver.CacheManager{},
sm: s.sm,
}}
}
func (s *Solver) Bridge(b solver.Builder) frontend.FrontendLLBBridge {
return s.bridge(b)
}
func (s *Solver) recordBuildHistory(ctx context.Context, id string, req frontend.SolveRequest, exp ExporterRequest, j *solver.Job, usage *resources.SysSampler) (func(context.Context, *Result, []exporter.DescriptorReference, error) error, error) {
stopTrace, err := detect.Recorder.Record(ctx)
if err != nil {
return nil, errdefs.Internal(err)
}
rec := &controlapi.BuildHistoryRecord{
Ref: id,
Frontend: req.Frontend,
FrontendAttrs: req.FrontendOpt,
CreatedAt: timestamppb.Now(),
}
for _, e := range exp.Exporters {
rec.Exporters = append(rec.Exporters, &controlapi.Exporter{
Type: e.Type(),
Attrs: e.Attrs(),
})
}
if err := s.history.Update(ctx, &controlapi.BuildHistoryEvent{
Type: controlapi.BuildHistoryEventType_STARTED,
Record: rec,
}); err != nil {
if stopTrace != nil {
stopTrace()
}
return nil, errdefs.Internal(err)
}
return func(ctx context.Context, res *Result, descrefs []exporter.DescriptorReference, err error) error {
rec.CompletedAt = timestamppb.Now()
span, ctx := tracing.StartSpan(ctx, "create history record")
defer span.End()
j.CloseProgress()
if res != nil && len(res.Metadata) > 0 {
rec.ExporterResponse = map[string]string{}
for k, v := range res.Metadata {
rec.ExporterResponse[k] = string(v)
}
}
ctx, cancel := context.WithCancelCause(ctx)
ctx, _ = context.WithTimeoutCause(ctx, 300*time.Second, errors.WithStack(context.DeadlineExceeded))
defer func() { cancel(errors.WithStack(context.Canceled)) }()
var mu sync.Mutex
ch := make(chan *client.SolveStatus)
eg, ctx2 := errgroup.WithContext(ctx)
var releasers []func()
attrs := map[string]string{
"mode": "max",
"capture-usage": "true",
}
// infer builder-id from user input if available
if attests, err := attestations.Parse(rec.FrontendAttrs); err == nil {
if prvAttrs, ok := attests["provenance"]; ok {
if builderID, ok := prvAttrs["builder-id"]; ok {
attrs["builder-id"] = builderID
}
}
}
makeProvenance := func(name string, res solver.ResultProxy, cap *provenance.Capture) (*controlapi.Descriptor, func(), error) {
span, ctx := tracing.StartSpan(ctx, fmt.Sprintf("create %s history provenance", name))
defer span.End()
prc, err := NewProvenanceCreator(ctx2, cap, res, attrs, j, usage)
if err != nil {
return nil, nil, err
}
pr, err := prc.Predicate()
if err != nil {
return nil, nil, err
}
dt, err := json.MarshalIndent(pr, "", " ")
if err != nil {
return nil, nil, err
}
w, err := s.history.OpenBlobWriter(ctx, intoto.PayloadType)
if err != nil {
return nil, nil, err
}
defer func() {
if w != nil {
w.Discard()
}
}()
if _, err := w.Write(dt); err != nil {
return nil, nil, err
}
desc, release, err := w.Commit(ctx2)
if err != nil {
return nil, nil, err
}
w = nil
return &controlapi.Descriptor{
Digest: string(desc.Digest),
Size: desc.Size,
MediaType: desc.MediaType,
Annotations: map[string]string{
"in-toto.io/predicate-type": slsa02.PredicateSLSAProvenance,
},
}, release, nil
}
if res != nil {
if res.Ref != nil {
eg.Go(func() error {
desc, release, err := makeProvenance("default", res.Ref, res.Provenance.Ref)
if err != nil {
return err
}
mu.Lock()
releasers = append(releasers, release)
if rec.Result == nil {
rec.Result = &controlapi.BuildResultInfo{}
}
rec.Result.Attestations = append(rec.Result.Attestations, desc)
mu.Unlock()
return nil
})
}
for k, r := range res.Refs {
if r == nil {
continue
}
k, r := k, r
cp := res.Provenance.Refs[k]
eg.Go(func() error {
desc, release, err := makeProvenance(k, r, cp)
if err != nil {
return err
}
mu.Lock()
releasers = append(releasers, release)
if rec.Results == nil {
rec.Results = make(map[string]*controlapi.BuildResultInfo)
}
if rec.Results[k] == nil {
rec.Results[k] = &controlapi.BuildResultInfo{}
}
rec.Results[k].Attestations = append(rec.Results[k].Attestations, desc)
mu.Unlock()
return nil
})
}
}
eg.Go(func() error {
st, releaseStatus, err := s.history.ImportStatus(ctx2, ch)
if err != nil {
return err
}
mu.Lock()
releasers = append(releasers, releaseStatus)
rec.Logs = &controlapi.Descriptor{
Digest: string(st.Descriptor.Digest),
Size: st.Descriptor.Size,
MediaType: st.Descriptor.MediaType,
}
rec.NumCachedSteps = int32(st.NumCachedSteps)
rec.NumCompletedSteps = int32(st.NumCompletedSteps)
rec.NumTotalSteps = int32(st.NumTotalSteps)
rec.NumWarnings = int32(st.NumWarnings)
mu.Unlock()
return nil
})
eg.Go(func() error {
return j.Status(ctx2, ch)
})
setDeprecated := true
for i, descref := range descrefs {
i, descref := i, descref
if descref == nil {
continue
}
deprecate := setDeprecated
setDeprecated = false
eg.Go(func() error {
mu.Lock()
desc := descref.Descriptor()
controlDesc := &controlapi.Descriptor{
Digest: string(desc.Digest),
Size: desc.Size,
MediaType: desc.MediaType,
Annotations: desc.Annotations,
}
if rec.Result == nil {
rec.Result = &controlapi.BuildResultInfo{}
}
if rec.Result.Results == nil {
rec.Result.Results = make(map[int64]*controlapi.Descriptor)
}
if deprecate {
// write the first available descriptor to the deprecated
// field for legacy clients
rec.Result.ResultDeprecated = controlDesc
}
rec.Result.Results[int64(i)] = controlDesc
mu.Unlock()
return nil
})
}
if err1 := eg.Wait(); err == nil {
// any error from exporting history record is internal
err = errdefs.Internal(err1)
}
defer func() {
for _, f := range releasers {
f()
}
}()
if err != nil {
status, desc, release, err1 := s.history.ImportError(ctx, err)
if err1 != nil {
// don't replace the build error with this import error
bklog.G(ctx).Errorf("failed to import error to build record: %+v", err1)
} else {
releasers = append(releasers, release)
}
rec.ExternalError = desc
rec.Error = status
}
ready, done := s.history.AcquireFinalizer(rec.Ref)
if err1 := s.history.Update(ctx, &controlapi.BuildHistoryEvent{
Type: controlapi.BuildHistoryEventType_COMPLETE,
Record: rec,
}); err1 != nil {
if err == nil {
err = errdefs.Internal(err1)
}
}
if stopTrace == nil {
bklog.G(ctx).Warn("no trace recorder found, skipping")
done()
return err
}
go func() {
defer done()
// if there is no finalizer request then stop tracing after 3 seconds
select {
case <-time.After(3 * time.Second):
case <-ready:
}
spans := stopTrace()
if len(spans) == 0 {
return
}
if err := func() error {
w, err := s.history.OpenBlobWriter(context.TODO(), "application/vnd.buildkit.otlp.json.v0")
if err != nil {
return err
}
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
for _, sp := range spans {
if err := enc.Encode(sp); err != nil {
return err
}
}
desc, release, err := w.Commit(context.TODO())
if err != nil {
return err
}
defer release()
if err := s.history.UpdateRef(context.TODO(), id, func(rec *controlapi.BuildHistoryRecord) error {
rec.Trace = &controlapi.Descriptor{
Digest: string(desc.Digest),
MediaType: desc.MediaType,
Size: desc.Size,
}
return nil
}); err != nil {
return err
}
return nil
}(); err != nil {
bklog.G(ctx).Errorf("failed to save trace for %s: %+v", id, err)
}
}()
return err
}, nil
}
func (s *Solver) Solve(ctx context.Context, id string, sessionID string, req frontend.SolveRequest, exp ExporterRequest, ent []entitlements.Entitlement, post []Processor, internal bool, srcPol *spb.Policy) (_ *client.SolveResponse, err error) {
j, err := s.solver.NewJob(id)
if err != nil {
return nil, err
}
defer j.Discard()
var usage *resources.Sub[*resourcestypes.SysSample]
if s.sysSampler != nil {
usage = s.sysSampler.Record()
defer usage.Close(false)
}
var res *frontend.Result
var resProv *Result
var descrefs []exporter.DescriptorReference
var releasers []func()
defer func() {
for _, f := range releasers {
f()
}
for _, descref := range descrefs {
if descref != nil {
descref.Release()
}
}
}()
if internal {
defer j.CloseProgress()
}
set, err := entitlements.WhiteList(ent, supportedEntitlements(s.entitlements))
if err != nil {
return nil, err
}
j.SetValue(keyEntitlements, set)
if srcPol != nil {
if err := validateSourcePolicy(srcPol); err != nil {
return nil, err
}
j.SetValue(keySourcePolicy, srcPol)
}
j.SessionID = sessionID
br := s.bridge(j)
var fwd gateway.LLBBridgeForwarder
if s.gatewayForwarder != nil && req.Definition == nil && req.Frontend == "" {
fwd = gateway.NewBridgeForwarder(ctx, br, br, s.workerController.Infos(), req.FrontendInputs, sessionID, s.sm)
defer fwd.Discard()
// Register build before calling s.recordBuildHistory, because
// s.recordBuildHistory can block for several seconds on
// LeaseManager calls, and there is a fixed 3s timeout in
// GatewayForwarder on build registration.
if err := s.gatewayForwarder.RegisterBuild(ctx, id, fwd); err != nil {
return nil, err
}
defer s.gatewayForwarder.UnregisterBuild(context.WithoutCancel(ctx), id)
}
if !internal {
rec, err1 := s.recordBuildHistory(ctx, id, req, exp, j, usage)
if err1 != nil {
defer j.CloseProgress()
return nil, err1
}
defer func() {
err = rec(context.WithoutCancel(ctx), resProv, descrefs, err)
}()
}
if fwd != nil {
var err error
select {
case <-fwd.Done():
res, err = fwd.Result()
case <-ctx.Done():
err = context.Cause(ctx)
}
if err != nil {
return nil, err
}
} else {
res, err = br.Solve(ctx, req, sessionID)
if err != nil {
return nil, err
}
}
if res == nil {
res = &frontend.Result{}
}
if err := verifier.CaptureFrontendOpts(req.FrontendOpt, res); err != nil {
return nil, err
}
releasers = append(releasers, func() {
res.EachRef(func(ref solver.ResultProxy) error {
go ref.Release(context.TODO())
return nil
})
})
eg, ctx2 := errgroup.WithContext(ctx)
res.EachRef(func(ref solver.ResultProxy) error {
eg.Go(func() error {
_, err := ref.Result(ctx2)
return err
})
return nil
})
if err := eg.Wait(); err != nil {
return nil, err
}
resProv, err = addProvenanceToResult(res, br)
if err != nil {
return nil, err
}
for _, post := range post {
res2, err := post(ctx, resProv, s, j, usage)
if err != nil {
return nil, err
}
resProv = res2
}
res = resProv.Result
cached, err := result.ConvertResult(res, func(res solver.ResultProxy) (solver.CachedResult, error) {
return res.Result(ctx)
})
if err != nil {
return nil, err
}
inp, err := result.ConvertResult(cached, func(res solver.CachedResult) (cache.ImmutableRef, error) {
workerRef, ok := res.Sys().(*worker.WorkerRef)
if !ok {
return nil, errors.Errorf("invalid reference: %T", res.Sys())
}
return workerRef.ImmutableRef, nil
})
if err != nil {
return nil, err
}
// Functions that create new objects in containerd (eg. content blobs) need to have a lease to ensure
// that the object is not garbage collected immediately. This is protected by the indivual components,
// but because creating a lease is not cheap and requires a disk write, we create a single lease here
// early and let all the exporters, cache export and provenance creation use the same one.
lm, err := s.leaseManager()
if err != nil {
return nil, err
}
ctx, done, err := leaseutil.WithLease(ctx, lm, leaseutil.MakeTemporary)
if err != nil {
return nil, err
}
releasers = append(releasers, func() {
done(context.WithoutCancel(ctx))
})
cacheExporters, inlineCacheExporter := splitCacheExporters(exp.CacheExporters)
var exporterResponse map[string]string
exporterResponse, descrefs, err = s.runExporters(ctx, exp.Exporters, inlineCacheExporter, j, cached, inp)
if err != nil {
return nil, err
}
cacheExporterResponse, err := runCacheExporters(ctx, cacheExporters, j, cached, inp)
if err != nil {
return nil, err
}
if exporterResponse == nil {
exporterResponse = make(map[string]string)
}
for k, v := range res.Metadata {
if strings.HasPrefix(k, "frontend.") {
exporterResponse[k] = string(v)
}
}
for k, v := range cacheExporterResponse {
if strings.HasPrefix(k, "cache.") {
exporterResponse[k] = v
}
}
return &client.SolveResponse{
ExporterResponse: exporterResponse,
}, nil
}
func validateSourcePolicy(pol *spb.Policy) error {
for _, r := range pol.Rules {
if r == nil {
return errors.New("invalid nil rule in policy")
}
if r.Selector == nil {
return errors.New("invalid nil selector in policy")
}
for _, c := range r.Selector.Constraints {
if c == nil {
return errors.New("invalid nil constraint in policy")
}
}
}
return nil
}
func runCacheExporters(ctx context.Context, exporters []RemoteCacheExporter, j *solver.Job, cached *result.Result[solver.CachedResult], inp *result.Result[cache.ImmutableRef]) (map[string]string, error) {
eg, ctx := errgroup.WithContext(ctx)
g := session.NewGroup(j.SessionID)
var cacheExporterResponse map[string]string
resps := make([]map[string]string, len(exporters))
for i, exp := range exporters {
i, exp := i, exp
eg.Go(func() (err error) {
id := fmt.Sprint(j.SessionID, "-cache-", i)
err = inBuilderContext(ctx, j, exp.Exporter.Name(), id, func(ctx context.Context, _ session.Group) error {
prepareDone := progress.OneOff(ctx, "preparing build cache for export")
if err := result.EachRef(cached, inp, func(res solver.CachedResult, ref cache.ImmutableRef) error {
ctx = withDescHandlerCacheOpts(ctx, ref)
// Configure compression
compressionConfig := exp.Config().Compression
// all keys have same export chain so exporting others is not needed
_, err = res.CacheKeys()[0].Exporter.ExportTo(ctx, exp, solver.CacheExportOpt{
ResolveRemotes: workerRefResolver(cacheconfig.RefConfig{Compression: compressionConfig}, false, g),
Mode: exp.CacheExportMode,
Session: g,
CompressionOpt: &compressionConfig,
})
return err
}); err != nil {
return prepareDone(err)
}
resps[i], err = exp.Finalize(ctx)
return prepareDone(err)
})
if exp.IgnoreError {
err = nil
}
return err
})
}
if err := eg.Wait(); err != nil {
return nil, err
}
// TODO: separate these out, and return multiple cache exporter responses
// to the client
for _, resp := range resps {
if cacheExporterResponse == nil {
cacheExporterResponse = make(map[string]string)
}
maps.Copy(cacheExporterResponse, resp)
}
return cacheExporterResponse, nil
}
func runInlineCacheExporter(ctx context.Context, e exporter.ExporterInstance, inlineExporter inlineCacheExporter, j *solver.Job, cached *result.Result[solver.CachedResult]) (*result.Result[*exptypes.InlineCacheEntry], error) {
if inlineExporter == nil {
return nil, nil
}
done := progress.OneOff(ctx, "preparing layers for inline cache")
res, err := result.ConvertResult(cached, func(res solver.CachedResult) (*exptypes.InlineCacheEntry, error) {
dtic, err := inlineCache(ctx, inlineExporter, res, e.Config().Compression(), session.NewGroup(j.SessionID))
if err != nil {
return nil, err
}
if dtic == nil {
return nil, nil
}
return &exptypes.InlineCacheEntry{Data: dtic}, nil
})
return res, done(err)
}
func (s *Solver) runExporters(ctx context.Context, exporters []exporter.ExporterInstance, inlineCacheExporter inlineCacheExporter, job *solver.Job, cached *result.Result[solver.CachedResult], inp *result.Result[cache.ImmutableRef]) (exporterResponse map[string]string, descrefs []exporter.DescriptorReference, err error) {
warnings, err := verifier.CheckInvalidPlatforms(ctx, inp)
if err != nil {
return nil, nil, err
}
eg, ctx := errgroup.WithContext(ctx)
resps := make([]map[string]string, len(exporters))
descs := make([]exporter.DescriptorReference, len(exporters))
for i, exp := range exporters {
i, exp := i, exp
eg.Go(func() error {
id := fmt.Sprint(job.SessionID, "-export-", i)
return inBuilderContext(ctx, job, exp.Name(), id, func(ctx context.Context, _ session.Group) error {
span, ctx := tracing.StartSpan(ctx, exp.Name())
defer span.End()
if i == 0 && len(warnings) > 0 {
pw, _, _ := progress.NewFromContext(ctx)
for _, w := range warnings {
pw.Write(identity.NewID(), w)
}
if err := pw.Close(); err != nil {
return err
}
}
inlineCache := exptypes.InlineCache(func(ctx context.Context) (*result.Result[*exptypes.InlineCacheEntry], error) {
return runInlineCacheExporter(ctx, exp, inlineCacheExporter, job, cached)
})
resps[i], descs[i], err = exp.Export(ctx, inp, inlineCache, job.SessionID)
if err != nil {
return err
}
return nil
})
})
}
if err := eg.Wait(); err != nil {
return nil, nil, err
}
if len(exporters) == 0 && len(warnings) > 0 {
err := inBuilderContext(ctx, job, "Verifying build result", identity.NewID(), func(ctx context.Context, _ session.Group) error {
pw, _, _ := progress.NewFromContext(ctx)
for _, w := range warnings {
pw.Write(identity.NewID(), w)
}
return pw.Close()
})
if err != nil {
return nil, nil, err
}
}
// TODO: separate these out, and return multiple exporter responses to the
// client
for _, resp := range resps {
for k, v := range resp {
if exporterResponse == nil {
exporterResponse = make(map[string]string)
}
exporterResponse[k] = v
}
}
return exporterResponse, descs, nil
}
func (s *Solver) leaseManager() (*leaseutil.Manager, error) {
w, err := defaultResolver(s.workerController)()
if err != nil {
return nil, err
}
return w.LeaseManager(), nil
}
func splitCacheExporters(exporters []RemoteCacheExporter) (rest []RemoteCacheExporter, inline inlineCacheExporter) {
rest = make([]RemoteCacheExporter, 0, len(exporters))
for _, exp := range exporters {
if ic, ok := asInlineCache(exp.Exporter); ok {
inline = ic
continue
}
rest = append(rest, exp)
}
return rest, inline
}
func addProvenanceToResult(res *frontend.Result, br *provenanceBridge) (*Result, error) {
if res == nil {
return nil, nil
}
reqs, err := br.requests(res)
if err != nil {
return nil, err
}
out := &Result{
Result: res,
Provenance: &provenance.Result{},
}
if res.Ref != nil {
cp, err := getProvenance(res.Ref, reqs.ref.bridge, "", reqs)
if err != nil {
return nil, err
}
out.Provenance.Ref = cp
if res.Metadata == nil {
res.Metadata = map[string][]byte{}
}
}
if len(res.Refs) != 0 {
out.Provenance.Refs = make(map[string]*provenance.Capture, len(res.Refs))
}
for k, ref := range res.Refs {
if ref == nil {
continue
}
cp, err := getProvenance(ref, reqs.refs[k].bridge, k, reqs)
if err != nil {
return nil, err
}
out.Provenance.Refs[k] = cp
if res.Metadata == nil {
res.Metadata = map[string][]byte{}
}
}
if len(res.Attestations) != 0 {
out.Provenance.Attestations = make(map[string][]result.Attestation[*provenance.Capture], len(res.Attestations))
}
for k, as := range res.Attestations {
for i, a := range as {
a2, err := result.ConvertAttestation(&a, func(r solver.ResultProxy) (*provenance.Capture, error) {
return getProvenance(r, reqs.atts[k][i].bridge, k, reqs)
})
if err != nil {
return nil, err
}
out.Provenance.Attestations[k] = append(out.Provenance.Attestations[k], *a2)
}
}
return out, nil
}
func getRefProvenance(ref solver.ResultProxy, br *provenanceBridge) (*provenance.Capture, error) {
if ref == nil {
return nil, nil
}
p := ref.Provenance()
if p == nil {
return nil, nil
}
pr, ok := p.(*provenance.Capture)
if !ok {
return nil, errors.Errorf("invalid provenance type %T", p)
}
if br.req != nil {
if pr == nil {
return nil, errors.Errorf("missing provenance for %s", ref.ID())
}
pr.Frontend = br.req.Frontend
pr.Args = provenance.FilterArgs(br.req.FrontendOpt)
// TODO: should also save some output options like compression
if len(br.req.FrontendInputs) > 0 {
pr.IncompleteMaterials = true // not implemented
}
}
return pr, nil
}
func getProvenance(ref solver.ResultProxy, br *provenanceBridge, id string, reqs *resultRequests) (*provenance.Capture, error) {
pr, err := getRefProvenance(ref, br)
if err != nil {
return nil, err
}
if pr == nil {
return nil, nil
}
visited := reqs.allRes()
visited[ref.ID()] = struct{}{}
// provenance for all the refs not directly in the result needs to be captured as well
if err := br.eachRef(func(r solver.ResultProxy) error {
if _, ok := visited[r.ID()]; ok {
return nil
}
visited[r.ID()] = struct{}{}
pr2, err := getRefProvenance(r, br)
if err != nil {
return err
}
return pr.Merge(pr2)
}); err != nil {
return nil, err
}
imgs := br.allImages()
if id != "" {
imgs = reqs.filterImagePlatforms(id, imgs)
}
for _, img := range imgs {
pr.AddImage(img)
}
if err := pr.OptimizeImageSources(); err != nil {
return nil, err
}
pr.Sort()
return pr, nil
}
type inlineCacheExporter interface {
solver.CacheExporterTarget
ExportForLayers(context.Context, []digest.Digest) ([]byte, error)
}
func asInlineCache(e remotecache.Exporter) (inlineCacheExporter, bool) {
ie, ok := e.(inlineCacheExporter)
return ie, ok
}
func inlineCache(ctx context.Context, ie inlineCacheExporter, res solver.CachedResult, compressionopt compression.Config, g session.Group) ([]byte, error) {
workerRef, ok := res.Sys().(*worker.WorkerRef)
if !ok {
return nil, errors.Errorf("invalid reference: %T", res.Sys())
}
remotes, err := workerRef.GetRemotes(ctx, true, cacheconfig.RefConfig{Compression: compressionopt}, false, g)
if err != nil || len(remotes) == 0 {
return nil, nil
}
remote := remotes[0]
digests := make([]digest.Digest, 0, len(remote.Descriptors))
for _, desc := range remote.Descriptors {
digests = append(digests, desc.Digest)
}
ctx = withDescHandlerCacheOpts(ctx, workerRef.ImmutableRef)
refCfg := cacheconfig.RefConfig{Compression: compressionopt}
if _, err := res.CacheKeys()[0].Exporter.ExportTo(ctx, ie, solver.CacheExportOpt{
ResolveRemotes: workerRefResolver(refCfg, true, g), // load as many compression blobs as possible
Mode: solver.CacheExportModeMin,
Session: g,
CompressionOpt: &compressionopt, // cache possible compression variants
}); err != nil {
return nil, err
}
return ie.ExportForLayers(ctx, digests)
}
func withDescHandlerCacheOpts(ctx context.Context, ref cache.ImmutableRef) context.Context {
return solver.WithCacheOptGetter(ctx, func(includeAncestors bool, keys ...interface{}) map[interface{}]interface{} {
vals := make(map[interface{}]interface{})
for _, k := range keys {
if key, ok := k.(cache.DescHandlerKey); ok {
if handler := ref.DescHandler(digest.Digest(key)); handler != nil {
vals[k] = handler
}
}
}
return vals
})
}
func (s *Solver) Status(ctx context.Context, id string, statusChan chan *client.SolveStatus) error {
if err := s.history.Status(ctx, id, statusChan); err != nil {
if !errors.Is(err, os.ErrNotExist) {
close(statusChan)
return err
}
} else {
close(statusChan)
return nil
}
j, err := s.solver.Get(id)
if err != nil {
close(statusChan)
return err
}
return j.Status(ctx, statusChan)
}
func defaultResolver(wc *worker.Controller) ResolveWorkerFunc {
return func() (worker.Worker, error) {
return wc.GetDefault()
}
}
func allWorkers(wc *worker.Controller) func(func(w worker.Worker) error) error {
return func(f func(worker.Worker) error) error {
all, err := wc.List()
if err != nil {
return err
}
for _, w := range all {
if err := f(w); err != nil {
return err
}
}
return nil
}
}
func inBuilderContext(ctx context.Context, b solver.Builder, name, id string, f func(ctx context.Context, g session.Group) error) error {
if id == "" {
id = name
}
v := client.Vertex{
Digest: digest.FromBytes([]byte(id)),
Name: name,
}
return b.InContext(ctx, func(ctx context.Context, g session.Group) error {
pw, _, ctx := progress.NewFromContext(ctx, progress.WithMetadata("vertex", v.Digest))
notifyCompleted := notifyStarted(ctx, &v)
defer pw.Close()
err := f(ctx, g)
notifyCompleted(err)
return err
})
}
func notifyStarted(ctx context.Context, v *client.Vertex) func(err error) {
pw, _, _ := progress.NewFromContext(ctx)
start := time.Now()
v.Started = &start
v.Completed = nil
id := identity.NewID()
pw.Write(id, *v)
return func(err error) {
defer pw.Close()
stop := time.Now()
v.Completed = &stop
v.Cached = false
if err != nil {
v.Error = err.Error()
}
pw.Write(id, *v)
}
}
func supportedEntitlements(ents []string) []entitlements.Entitlement {
out := []entitlements.Entitlement{} // nil means no filter
for _, e := range ents {
if e == string(entitlements.EntitlementNetworkHost) {
out = append(out, entitlements.EntitlementNetworkHost)
}
if e == string(entitlements.EntitlementSecurityInsecure) {
out = append(out, entitlements.EntitlementSecurityInsecure)
}
}
return out
}
func loadEntitlements(b solver.Builder) (entitlements.Set, error) {
var ent entitlements.Set = map[entitlements.Entitlement]struct{}{}
err := b.EachValue(context.TODO(), keyEntitlements, func(v interface{}) error {
set, ok := v.(entitlements.Set)
if !ok {
return errors.Errorf("invalid entitlements %T", v)
}
for k := range set {
ent[k] = struct{}{}
}
return nil
})
if err != nil {
return nil, err
}
return ent, nil
}
func loadSourcePolicy(b solver.Builder) (*spb.Policy, error) {
var srcPol spb.Policy
err := b.EachValue(context.TODO(), keySourcePolicy, func(v interface{}) error {
x, ok := v.(*spb.Policy)
if !ok {
return errors.Errorf("invalid source policy %T", v)
}
for _, f := range x.Rules {
if f == nil {
return errors.Errorf("invalid nil policy rule")
}
srcPol.Rules = append(srcPol.Rules, f.CloneVT())
}
srcPol.Version = x.Version
return nil
})
if err != nil {
return nil, err
}
return &srcPol, nil
}