frontend/request.go (250 lines of code) (raw):

package frontend import ( "context" "strconv" "github.com/Azure/dalec" "github.com/goccy/go-yaml" "github.com/moby/buildkit/client/llb" "github.com/moby/buildkit/frontend/dockerfile/parser" "github.com/moby/buildkit/frontend/dockerui" gwclient "github.com/moby/buildkit/frontend/gateway/client" "github.com/moby/buildkit/solver/pb" "github.com/pkg/errors" ) const ( keySkipSigningArg = "DALEC_SKIP_SIGNING" buildArgDalecSigningConfigPath = "DALEC_SIGNING_CONFIG_PATH" buildArgDalecSigningConfigContextName = "DALEC_SIGNING_CONFIG_CONTEXT_NAME" ) type solveRequestOpt func(*gwclient.SolveRequest) error func newSolveRequest(opts ...solveRequestOpt) (gwclient.SolveRequest, error) { var sr gwclient.SolveRequest for _, o := range opts { if err := o(&sr); err != nil { return sr, err } } return sr, nil } func toFrontend(f *dalec.Frontend) solveRequestOpt { return func(req *gwclient.SolveRequest) error { req.Frontend = gatewayFrontend if req.FrontendOpt == nil { req.FrontendOpt = make(map[string]string) } req.FrontendOpt["source"] = f.Image req.FrontendOpt["cmdline"] = f.CmdLine return nil } } func withSpec(ctx context.Context, spec *dalec.Spec, opts ...llb.ConstraintsOpt) solveRequestOpt { return func(req *gwclient.SolveRequest) error { if req.FrontendInputs == nil { req.FrontendInputs = make(map[string]*pb.Definition) } dt, err := yaml.Marshal(spec) if err != nil { return errors.Wrap(err, "error marshalling spec to yaml") } def, err := llb.Scratch().File(llb.Mkfile(dockerui.DefaultDockerfileName, 0600, dt), opts...).Marshal(ctx) if err != nil { return errors.Wrap(err, "error marshaling spec to LLB") } req.FrontendInputs[dockerui.DefaultLocalNameDockerfile] = def.ToPB() return nil } } func withTarget(t string) solveRequestOpt { return func(req *gwclient.SolveRequest) error { if req.FrontendOpt == nil { req.FrontendOpt = make(map[string]string) } req.FrontendOpt["target"] = t return nil } } func withBuildArgs(args map[string]string) solveRequestOpt { return func(req *gwclient.SolveRequest) error { if len(args) == 0 { return nil } if req.FrontendOpt == nil { req.FrontendOpt = make(map[string]string) } for k, v := range args { req.FrontendOpt["build-arg:"+k] = v } return nil } } func toDockerfile(ctx context.Context, bctx llb.State, dt []byte, spec *dalec.SourceBuild, opts ...llb.ConstraintsOpt) solveRequestOpt { return func(req *gwclient.SolveRequest) error { req.Frontend = "dockerfile.v0" bctxDef, err := bctx.Marshal(ctx) if err != nil { return errors.Wrap(err, "error marshaling dockerfile to LLB") } if req.FrontendInputs == nil { req.FrontendInputs = make(map[string]*pb.Definition) } dfDef, err := marshalDockerfile(ctx, dt, opts...) if err != nil { return errors.Wrap(err, "error marshaling dockerfile to LLB") } req.FrontendInputs[dockerui.DefaultLocalNameContext] = bctxDef.ToPB() req.FrontendInputs[dockerui.DefaultLocalNameDockerfile] = dfDef.ToPB() if ref, cmdline, _, ok := parser.DetectSyntax(dt); ok { req.Frontend = gatewayFrontend if req.FrontendOpt == nil { req.FrontendOpt = make(map[string]string) } req.FrontendOpt["source"] = ref req.FrontendOpt["cmdline"] = cmdline } if spec != nil { if req.FrontendOpt == nil { req.FrontendOpt = make(map[string]string) } if spec.Target != "" { req.FrontendOpt["target"] = spec.Target } for k, v := range spec.Args { req.FrontendOpt["build-arg:"+k] = v } } return nil } } func marshalDockerfile(ctx context.Context, dt []byte, opts ...llb.ConstraintsOpt) (*llb.Definition, error) { st := llb.Scratch().File(llb.Mkfile(dockerui.DefaultDockerfileName, 0600, dt), opts...) return st.Marshal(ctx) } func getSigningConfigFromContext(ctx context.Context, client gwclient.Client, cfgPath string, configCtxName string, sOpt dalec.SourceOpts, opts ...llb.ConstraintsOpt) (*dalec.PackageSigner, error) { sc := dalec.SourceContext{Name: configCtxName} signConfigState, err := sc.AsState(cfgPath, []string{cfgPath}, nil, sOpt, opts...) if err != nil { return nil, err } scDef, err := signConfigState.Marshal(ctx) if err != nil { return nil, err } res, err := client.Solve(ctx, gwclient.SolveRequest{ Definition: scDef.ToPB(), }) if err != nil { return nil, err } ref, err := res.SingleRef() if err != nil { return nil, err } dt, err := ref.ReadFile(ctx, gwclient.ReadRequest{ Filename: cfgPath, }) if err != nil { return nil, err } var pc dalec.PackageConfig if err := yaml.Unmarshal(dt, &pc); err != nil { return nil, err } return pc.Signer, nil } func MaybeSign(ctx context.Context, client gwclient.Client, st llb.State, spec *dalec.Spec, targetKey string, sOpt dalec.SourceOpts, opts ...llb.ConstraintsOpt) (llb.State, error) { if signingDisabled(client) { Warnf(ctx, client, st, "Signing disabled by build-arg %q", keySkipSigningArg) return st, nil } cfg, rootSigningSpecOverriddenByTarget := spec.GetSigner(targetKey) cfgPath := getUserSignConfigPath(client) if cfgPath == "" { if cfg == nil { // i.e. there's no signing config. not in the build context, not in the spec. return st, nil } if rootSigningSpecOverriddenByTarget { Warnf(ctx, client, st, "Root signing spec overridden by target signing spec: target %q", targetKey) } return forwardToSigner(ctx, client, cfg, st, opts...) } configCtxName := getSignContextNameWithDefault(client) if specCfg := cfg; specCfg != nil { Warnf(ctx, client, st, "Spec signing config overwritten by config at path %q in build-context %q", cfgPath, configCtxName) } cfg, err := getSigningConfigFromContext(ctx, client, cfgPath, configCtxName, sOpt) if err != nil { return llb.Scratch(), err } return forwardToSigner(ctx, client, cfg, st, opts...) } func getSignContextNameWithDefault(client gwclient.Client) string { configCtxName := dockerui.DefaultLocalNameContext if cn := getSignConfigCtxName(client); cn != "" { configCtxName = cn } return configCtxName } func signingDisabled(client gwclient.Client) bool { bopts := client.BuildOpts().Opts v, ok := bopts["build-arg:"+keySkipSigningArg] if !ok { return false } isDisabled, err := strconv.ParseBool(v) if err != nil { return false } return isDisabled } func getUserSignConfigPath(client gwclient.Client) string { return client.BuildOpts().Opts["build-arg:"+buildArgDalecSigningConfigPath] } func getSignConfigCtxName(client gwclient.Client) string { return client.BuildOpts().Opts["build-arg:"+buildArgDalecSigningConfigContextName] } func forwardToSigner(ctx context.Context, client gwclient.Client, cfg *dalec.PackageSigner, s llb.State, opts ...llb.ConstraintsOpt) (llb.State, error) { const ( // See https://github.com/moby/buildkit/blob/d8d946b85c52095d34a52ce210960832f4e06775/frontend/dockerui/context.go#L29 contextKey = "contextkey" ) bopts := client.BuildOpts().Opts req, err := newSolveRequest(toFrontend(cfg.Frontend), withBuildArgs(cfg.Args)) if err != nil { return llb.Scratch(), err } for k, v := range bopts { if k == "source" || k == "cmdline" { continue } req.FrontendOpt[k] = v } inputs, err := client.Inputs(ctx) if err != nil { return llb.Scratch(), err } m := make(map[string]*pb.Definition) for k, st := range inputs { def, err := st.Marshal(ctx) if err != nil { return llb.Scratch(), err } m[k] = def.ToPB() } req.FrontendInputs = m opts = append(opts, dalec.ProgressGroup("Sign package")) stateDef, err := s.Marshal(ctx, opts...) if err != nil { return llb.Scratch(), err } req.FrontendOpt[contextKey] = dockerui.DefaultLocalNameContext req.FrontendInputs[dockerui.DefaultLocalNameContext] = stateDef.ToPB() req.FrontendOpt["dalec.target"] = bopts["dalec.target"] res, err := client.Solve(ctx, req) if err != nil { return llb.Scratch(), errors.Wrap(err, "error signing packages") } ref, err := res.SingleRef() if err != nil { return llb.Scratch(), err } return ref.ToState() }