package builder import ( "context" "strings" "sync" "github.com/containerd/platforms" "github.com/moby/buildkit/client/llb" "github.com/moby/buildkit/client/llb/sourceresolver" "github.com/moby/buildkit/frontend" "github.com/moby/buildkit/frontend/attestations/sbom" "github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb" "github.com/moby/buildkit/frontend/dockerfile/linter" "github.com/moby/buildkit/frontend/dockerfile/parser" "github.com/moby/buildkit/frontend/dockerui" "github.com/moby/buildkit/frontend/gateway/client" gwpb "github.com/moby/buildkit/frontend/gateway/pb" "github.com/moby/buildkit/frontend/subrequests/lint" "github.com/moby/buildkit/frontend/subrequests/outline" "github.com/moby/buildkit/frontend/subrequests/targets" "github.com/moby/buildkit/solver/errdefs" "github.com/moby/buildkit/solver/pb" "github.com/moby/buildkit/solver/result" dockerspec "github.com/moby/docker-image-spec/specs-go/v1" ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" ) const ( // Don't forget to update frontend documentation if you add // a new build-arg: frontend/dockerfile/docs/reference.md keySyntaxArg = "build-arg:BUILDKIT_SYNTAX" ) func Build(ctx context.Context, c client.Client) (_ *client.Result, err error) { c = &withResolveCache{Client: c} bc, err := dockerui.NewClient(c) if err != nil { return nil, err } opts := bc.BuildOpts().Opts allowForward, capsError := validateCaps(opts["frontend.caps"]) if !allowForward && capsError != nil { return nil, capsError } src, err := bc.ReadEntrypoint(ctx, "Dockerfile") if err != nil { return nil, err } if _, ok := opts["cmdline"]; !ok { if cmdline, ok := opts[keySyntaxArg]; ok { p := strings.SplitN(strings.TrimSpace(cmdline), " ", 2) res, err := forwardGateway(ctx, c, p[0], cmdline) if err != nil && len(errdefs.Sources(err)) == 0 { return nil, errors.Wrapf(err, "failed with %s = %s", keySyntaxArg, cmdline) } return res, err } else if ref, cmdline, loc, ok := parser.DetectSyntax(src.Data); ok { res, err := forwardGateway(ctx, c, ref, cmdline) if err != nil && len(errdefs.Sources(err)) == 0 { return nil, wrapSource(err, src.SourceMap, loc) } return res, err } } if capsError != nil { return nil, capsError } convertOpt := dockerfile2llb.ConvertOpt{ Config: bc.Config, Client: bc, SourceMap: src.SourceMap, MetaResolver: c, Warn: func(rulename, description, url, msg string, location []parser.Range) { startLine := 0 if len(location) > 0 { startLine = location[0].Start.Line } msg = linter.LintFormatShort(rulename, msg, startLine) src.Warn(ctx, msg, warnOpts(location, [][]byte{[]byte(description)}, url)) }, } if res, ok, err := bc.HandleSubrequest(ctx, dockerui.RequestHandler{ Outline: func(ctx context.Context) (*outline.Outline, error) { return dockerfile2llb.Dockerfile2Outline(ctx, src.Data, convertOpt) }, ListTargets: func(ctx context.Context) (*targets.List, error) { return dockerfile2llb.ListTargets(ctx, src.Data) }, Lint: func(ctx context.Context) (*lint.LintResults, error) { return dockerfile2llb.DockerfileLint(ctx, src.Data, convertOpt) }, }); err != nil { return nil, err } else if ok { return res, nil } defer func() { var el *parser.LocationError if errors.As(err, &el) { for _, l := range el.Locations { err = wrapSource(err, src.SourceMap, l) } } }() var scanner sbom.Scanner if bc.SBOM != nil { // TODO: scanner should pass policy scanner, err = sbom.CreateSBOMScanner(ctx, c, bc.SBOM.Generator, sourceresolver.Opt{ ImageOpt: &sourceresolver.ResolveImageOpt{ ResolveMode: opts["image-resolve-mode"], }, }, bc.SBOM.Parameters) if err != nil { return nil, err } } scanTargets := sync.Map{} rb, err := bc.Build(ctx, func(ctx context.Context, platform *ocispecs.Platform, idx int) (client.Reference, *dockerspec.DockerOCIImage, *dockerspec.DockerOCIImage, error) { opt := convertOpt opt.TargetPlatform = platform if idx != 0 { opt.Warn = nil } st, img, baseImg, scanTarget, err := dockerfile2llb.Dockerfile2LLB(ctx, src.Data, opt) if err != nil { return nil, nil, nil, err } def, err := st.Marshal(ctx) if err != nil { return nil, nil, nil, errors.Wrapf(err, "failed to marshal LLB definition") } r, err := c.Solve(ctx, client.SolveRequest{ Definition: def.ToPB(), CacheImports: bc.CacheImports, }) if err != nil { return nil, nil, nil, err } ref, err := r.SingleRef() if err != nil { return nil, nil, nil, err } var p ocispecs.Platform if platform != nil { p = *platform } else { p = platforms.DefaultSpec() } scanTargets.Store(platforms.FormatAll(platforms.Normalize(p)), scanTarget) return ref, img, baseImg, nil }) if err != nil { return nil, err } if scanner != nil { if err := rb.EachPlatform(ctx, func(ctx context.Context, id string, p ocispecs.Platform) error { v, ok := scanTargets.Load(id) if !ok { return errors.Errorf("no scan targets for %s", id) } target, ok := v.(*dockerfile2llb.SBOMTargets) if !ok { return errors.Errorf("invalid scan targets for %T", v) } var opts []llb.ConstraintsOpt if target.IgnoreCache { opts = append(opts, llb.IgnoreCache) } att, err := scanner(ctx, id, target.Core, target.Extras, opts...) if err != nil { return err } attSolve, err := result.ConvertAttestation(&att, func(st *llb.State) (client.Reference, error) { def, err := st.Marshal(ctx) if err != nil { return nil, err } r, err := c.Solve(ctx, frontend.SolveRequest{ Definition: def.ToPB(), }) if err != nil { return nil, err } return r.Ref, nil }) if err != nil { return err } rb.AddAttestation(id, *attSolve) return nil }); err != nil { return nil, err } } return rb.Finalize() } func forwardGateway(ctx context.Context, c client.Client, ref string, cmdline string) (*client.Result, error) { opts := c.BuildOpts().Opts if opts == nil { opts = map[string]string{} } opts["cmdline"] = cmdline opts["source"] = ref gwcaps := c.BuildOpts().Caps var frontendInputs map[string]*pb.Definition if (&gwcaps).Supports(gwpb.CapFrontendInputs) == nil { inputs, err := c.Inputs(ctx) if err != nil { return nil, errors.Wrapf(err, "failed to get frontend inputs") } frontendInputs = make(map[string]*pb.Definition) for name, state := range inputs { def, err := state.Marshal(ctx) if err != nil { return nil, err } frontendInputs[name] = def.ToPB() } } return c.Solve(ctx, client.SolveRequest{ Frontend: "gateway.v0", FrontendOpt: opts, FrontendInputs: frontendInputs, }) } func warnOpts(r []parser.Range, detail [][]byte, url string) client.WarnOpts { opts := client.WarnOpts{Level: 1, Detail: detail, URL: url} if r == nil { return opts } opts.Range = []*pb.Range{} for _, r := range r { opts.Range = append(opts.Range, &pb.Range{ Start: &pb.Position{ Line: int32(r.Start.Line), Character: int32(r.Start.Character), }, End: &pb.Position{ Line: int32(r.End.Line), Character: int32(r.End.Character), }, }) } return opts } func wrapSource(err error, sm *llb.SourceMap, ranges []parser.Range) error { if sm == nil { return err } s := &errdefs.Source{ Info: &pb.SourceInfo{ Data: sm.Data, Filename: sm.Filename, Language: sm.Language, Definition: sm.Definition.ToPB(), }, Ranges: make([]*pb.Range, 0, len(ranges)), } for _, r := range ranges { s.Ranges = append(s.Ranges, &pb.Range{ Start: &pb.Position{ Line: int32(r.Start.Line), Character: int32(r.Start.Character), }, End: &pb.Position{ Line: int32(r.End.Line), Character: int32(r.End.Character), }, }) } return errdefs.WithSource(err, s) }