Jonathan A. Sternberg 64c5139ab6
hack: generate vtproto files for buildx
Integrates vtproto into buildx. The generated files dockerfile has been
modified to copy the buildkit equivalent file to ensure files are laid
out in the appropriate way for imports.

An import has also been included to change the grpc codec to the version
in buildkit that supports vtproto. This will allow buildx to utilize the
speed and memory improvements from that.

Also updates the gc control options for prune.

Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
2024-10-08 13:35:06 -05:00

560 lines
14 KiB
Go

package dockerui
import (
"bytes"
"context"
"encoding/json"
"path"
"strconv"
"strings"
"time"
"github.com/containerd/platforms"
"github.com/distribution/reference"
controlapi "github.com/moby/buildkit/api/services/control"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/frontend/attestations"
"github.com/moby/buildkit/frontend/dockerfile/linter"
"github.com/moby/buildkit/frontend/gateway/client"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/flightcontrol"
dockerspec "github.com/moby/docker-image-spec/specs-go/v1"
"github.com/moby/patternmatcher/ignorefile"
digest "github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
const (
buildArgPrefix = "build-arg:"
labelPrefix = "label:"
localSessionIDPrefix = "local-sessionid:"
keyTarget = "target"
keyCgroupParent = "cgroup-parent"
keyForceNetwork = "force-network-mode"
keyGlobalAddHosts = "add-hosts"
keyHostname = "hostname"
keyImageResolveMode = "image-resolve-mode"
keyMultiPlatform = "multi-platform"
keyNoCache = "no-cache"
keyShmSize = "shm-size"
keyTargetPlatform = "platform"
keyUlimit = "ulimit"
keyCacheFrom = "cache-from" // for registry only. deprecated in favor of keyCacheImports
keyCacheImports = "cache-imports" // JSON representation of []CacheOptionsEntry
// Don't forget to update frontend documentation if you add
// a new build-arg: frontend/dockerfile/docs/reference.md
keyCacheNSArg = "build-arg:BUILDKIT_CACHE_MOUNT_NS"
keyMultiPlatformArg = "build-arg:BUILDKIT_MULTI_PLATFORM"
keyHostnameArg = "build-arg:BUILDKIT_SANDBOX_HOSTNAME"
keyDockerfileLintArg = "build-arg:BUILDKIT_DOCKERFILE_CHECK"
keyContextKeepGitDirArg = "build-arg:BUILDKIT_CONTEXT_KEEP_GIT_DIR"
keySourceDateEpoch = "build-arg:SOURCE_DATE_EPOCH"
)
type Config struct {
BuildArgs map[string]string
CacheIDNamespace string
CgroupParent string
Epoch *time.Time
ExtraHosts []llb.HostIP
Hostname string
ImageResolveMode llb.ResolveMode
Labels map[string]string
NetworkMode pb.NetMode
ShmSize int64
Target string
Ulimits []*pb.Ulimit
LinterConfig *linter.Config
CacheImports []client.CacheOptionsEntry
TargetPlatforms []ocispecs.Platform // nil means default
BuildPlatforms []ocispecs.Platform
MultiPlatformRequested bool
SBOM *SBOM
}
type Client struct {
Config
client client.Client
ignoreCache []string
g flightcontrol.CachedGroup[*buildContext]
bopts client.BuildOpts
localsSessionIDs map[string]string
dockerignore []byte
dockerignoreName string
}
type SBOM struct {
Generator string
Parameters map[string]string
}
type Source struct {
*llb.SourceMap
Warn func(context.Context, string, client.WarnOpts)
}
type ContextOpt struct {
NoDockerignore bool
AsyncLocalOpts func() []llb.LocalOption
Platform *ocispecs.Platform
ResolveMode string
CaptureDigest *digest.Digest
}
func validateMinCaps(c client.Client) error {
opts := c.BuildOpts().Opts
caps := c.BuildOpts().LLBCaps
if err := caps.Supports(pb.CapFileBase); err != nil {
return errors.Wrap(err, "needs BuildKit 0.5 or later")
}
if opts["override-copy-image"] != "" {
return errors.New("support for \"override-copy-image\" was removed in BuildKit 0.11")
}
if v, ok := opts["build-arg:BUILDKIT_DISABLE_FILEOP"]; ok {
if b, err := strconv.ParseBool(v); err == nil && b {
return errors.New("support for \"BUILDKIT_DISABLE_FILEOP\" build-arg was removed in BuildKit 0.11")
}
}
return nil
}
func NewClient(c client.Client) (*Client, error) {
if err := validateMinCaps(c); err != nil {
return nil, err
}
bc := &Client{
client: c,
bopts: c.BuildOpts(), // avoid grpc on every call
}
if err := bc.init(); err != nil {
return nil, err
}
return bc, nil
}
func (bc *Client) BuildOpts() client.BuildOpts {
return bc.bopts
}
func (bc *Client) init() error {
opts := bc.bopts.Opts
defaultBuildPlatform := platforms.Normalize(platforms.DefaultSpec())
if workers := bc.bopts.Workers; len(workers) > 0 && len(workers[0].Platforms) > 0 {
defaultBuildPlatform = workers[0].Platforms[0]
}
buildPlatforms := []ocispecs.Platform{defaultBuildPlatform}
targetPlatforms := []ocispecs.Platform{}
if v := opts[keyTargetPlatform]; v != "" {
var err error
targetPlatforms, err = parsePlatforms(v)
if err != nil {
return err
}
}
bc.BuildPlatforms = buildPlatforms
bc.TargetPlatforms = targetPlatforms
resolveMode, err := parseResolveMode(opts[keyImageResolveMode])
if err != nil {
return err
}
bc.ImageResolveMode = resolveMode
extraHosts, err := parseExtraHosts(opts[keyGlobalAddHosts])
if err != nil {
return errors.Wrap(err, "failed to parse additional hosts")
}
bc.ExtraHosts = extraHosts
shmSize, err := parseShmSize(opts[keyShmSize])
if err != nil {
return errors.Wrap(err, "failed to parse shm size")
}
bc.ShmSize = shmSize
ulimits, err := parseUlimits(opts[keyUlimit])
if err != nil {
return errors.Wrap(err, "failed to parse ulimit")
}
bc.Ulimits = ulimits
defaultNetMode, err := parseNetMode(opts[keyForceNetwork])
if err != nil {
return err
}
bc.NetworkMode = defaultNetMode
var ignoreCache []string
if v, ok := opts[keyNoCache]; ok {
if v == "" {
ignoreCache = []string{} // means all stages
} else {
ignoreCache = strings.Split(v, ",")
}
}
bc.ignoreCache = ignoreCache
multiPlatform := len(targetPlatforms) > 1
if v := opts[keyMultiPlatformArg]; v != "" {
opts[keyMultiPlatform] = v
}
if v := opts[keyMultiPlatform]; v != "" {
b, err := strconv.ParseBool(v)
if err != nil {
return errors.Errorf("invalid boolean value for multi-platform: %s", v)
}
if !b && multiPlatform {
return errors.Errorf("conflicting config: returning multiple target platforms is not allowed")
}
multiPlatform = b
}
bc.MultiPlatformRequested = multiPlatform
var cacheImports []client.CacheOptionsEntry
// new API
if cacheImportsStr := opts[keyCacheImports]; cacheImportsStr != "" {
var cacheImportsUM []*controlapi.CacheOptionsEntry
if err := json.Unmarshal([]byte(cacheImportsStr), &cacheImportsUM); err != nil {
return errors.Wrapf(err, "failed to unmarshal %s (%q)", keyCacheImports, cacheImportsStr)
}
for _, um := range cacheImportsUM {
cacheImports = append(cacheImports, client.CacheOptionsEntry{Type: um.Type, Attrs: um.Attrs})
}
}
// old API
if cacheFromStr := opts[keyCacheFrom]; cacheFromStr != "" {
cacheFrom := strings.Split(cacheFromStr, ",")
for _, s := range cacheFrom {
im := client.CacheOptionsEntry{
Type: "registry",
Attrs: map[string]string{
"ref": s,
},
}
// FIXME(AkihiroSuda): skip append if already exists
cacheImports = append(cacheImports, im)
}
}
bc.CacheImports = cacheImports
epoch, err := parseSourceDateEpoch(opts[keySourceDateEpoch])
if err != nil {
return err
}
bc.Epoch = epoch
attests, err := attestations.Parse(opts)
if err != nil {
return err
}
if attrs, ok := attests[attestations.KeyTypeSbom]; ok {
params := make(map[string]string)
var ref reference.Named
for k, v := range attrs {
if k == "generator" {
ref, err = reference.ParseNormalizedNamed(v)
if err != nil {
return errors.Wrapf(err, "failed to parse sbom scanner %s", v)
}
ref = reference.TagNameOnly(ref)
} else {
params[k] = v
}
}
if ref == nil {
return errors.Errorf("sbom scanner cannot be empty")
}
bc.SBOM = &SBOM{
Generator: ref.String(),
Parameters: params,
}
}
bc.BuildArgs = filter(opts, buildArgPrefix)
bc.Labels = filter(opts, labelPrefix)
bc.CacheIDNamespace = opts[keyCacheNSArg]
bc.CgroupParent = opts[keyCgroupParent]
bc.Target = opts[keyTarget]
if v, ok := opts[keyHostnameArg]; ok && len(v) > 0 {
opts[keyHostname] = v
}
bc.Hostname = opts[keyHostname]
if v, ok := opts[keyDockerfileLintArg]; ok {
bc.LinterConfig, err = linter.ParseLintOptions(v)
if err != nil {
return errors.Wrapf(err, "failed to parse %s", keyDockerfileLintArg)
}
}
bc.localsSessionIDs = parseLocalSessionIDs(opts)
return nil
}
func (bc *Client) buildContext(ctx context.Context) (*buildContext, error) {
return bc.g.Do(ctx, "initcontext", func(ctx context.Context) (*buildContext, error) {
return bc.initContext(ctx)
})
}
func (bc *Client) ReadEntrypoint(ctx context.Context, lang string, opts ...llb.LocalOption) (*Source, error) {
bctx, err := bc.buildContext(ctx)
if err != nil {
return nil, err
}
var src *llb.State
if !bctx.forceLocalDockerfile {
if bctx.dockerfile != nil {
src = bctx.dockerfile
}
}
if src == nil {
name := "load build definition from " + bctx.filename
filenames := []string{bctx.filename, bctx.filename + ".dockerignore"}
// dockerfile is also supported casing moby/moby#10858
if path.Base(bctx.filename) == DefaultDockerfileName {
filenames = append(filenames, path.Join(path.Dir(bctx.filename), strings.ToLower(DefaultDockerfileName)))
}
sessionID := bc.bopts.SessionID
if v, ok := bc.localsSessionIDs[bctx.dockerfileLocalName]; ok {
sessionID = v
}
opts = append([]llb.LocalOption{
llb.FollowPaths(filenames),
llb.SessionID(sessionID),
llb.SharedKeyHint(bctx.dockerfileLocalName),
WithInternalName(name),
llb.Differ(llb.DiffNone, false),
}, opts...)
lsrc := llb.Local(bctx.dockerfileLocalName, opts...)
src = &lsrc
}
def, err := src.Marshal(ctx, bc.marshalOpts()...)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal local source")
}
defVtx, err := def.Head()
if err != nil {
return nil, err
}
res, err := bc.client.Solve(ctx, client.SolveRequest{
Definition: def.ToPB(),
})
if err != nil {
return nil, errors.Wrapf(err, "failed to resolve dockerfile")
}
ref, err := res.SingleRef()
if err != nil {
return nil, err
}
dt, err := ref.ReadFile(ctx, client.ReadRequest{
Filename: bctx.filename,
})
if err != nil {
if path.Base(bctx.filename) == DefaultDockerfileName {
var err1 error
dt, err1 = ref.ReadFile(ctx, client.ReadRequest{
Filename: path.Join(path.Dir(bctx.filename), strings.ToLower(DefaultDockerfileName)),
})
if err1 == nil {
err = nil
}
}
if err != nil {
return nil, errors.Wrapf(err, "failed to read dockerfile")
}
}
smap := llb.NewSourceMap(src, bctx.filename, lang, dt)
smap.Definition = def
dt, err = ref.ReadFile(ctx, client.ReadRequest{
Filename: bctx.filename + ".dockerignore",
})
if err == nil {
bc.dockerignore = dt
bc.dockerignoreName = bctx.filename + ".dockerignore"
}
return &Source{
SourceMap: smap,
Warn: func(ctx context.Context, msg string, opts client.WarnOpts) {
if opts.Level == 0 {
opts.Level = 1
}
if opts.SourceInfo == nil {
opts.SourceInfo = &pb.SourceInfo{
Data: smap.Data,
Filename: smap.Filename,
Language: smap.Language,
Definition: smap.Definition.ToPB(),
}
}
bc.client.Warn(ctx, defVtx, msg, opts)
},
}, nil
}
func (bc *Client) MainContext(ctx context.Context, opts ...llb.LocalOption) (*llb.State, error) {
bctx, err := bc.buildContext(ctx)
if err != nil {
return nil, err
}
if bctx.context != nil {
return bctx.context, nil
}
excludes, err := bc.dockerIgnorePatterns(ctx, bctx)
if err != nil {
return nil, errors.Wrapf(err, "failed to read dockerignore patterns")
}
sessionID := bc.bopts.SessionID
if v, ok := bc.localsSessionIDs[bctx.contextLocalName]; ok {
sessionID = v
}
opts = append([]llb.LocalOption{
llb.SessionID(sessionID),
llb.ExcludePatterns(excludes),
llb.SharedKeyHint(bctx.contextLocalName),
WithInternalName("load build context"),
}, opts...)
st := llb.Local(bctx.contextLocalName, opts...)
return &st, nil
}
func (bc *Client) NamedContext(ctx context.Context, name string, opt ContextOpt) (*llb.State, *dockerspec.DockerOCIImage, error) {
named, err := reference.ParseNormalizedNamed(name)
if err != nil {
return nil, nil, errors.Wrapf(err, "invalid context name %s", name)
}
name = strings.TrimSuffix(reference.FamiliarString(named), ":latest")
pp := platforms.DefaultSpec()
if opt.Platform != nil {
pp = *opt.Platform
}
pname := name + "::" + platforms.Format(platforms.Normalize(pp))
st, img, err := bc.namedContext(ctx, name, pname, opt)
if err != nil || st != nil {
return st, img, err
}
return bc.namedContext(ctx, name, name, opt)
}
func (bc *Client) IsNoCache(name string) bool {
if len(bc.ignoreCache) == 0 {
return bc.ignoreCache != nil
}
for _, n := range bc.ignoreCache {
if strings.EqualFold(n, name) {
return true
}
}
return false
}
func (bc *Client) DockerIgnorePatterns(ctx context.Context) ([]string, error) {
if bc == nil {
return nil, nil
}
bctx, err := bc.buildContext(ctx)
if err != nil {
return nil, err
}
if bctx.context != nil {
return nil, nil
}
return bc.dockerIgnorePatterns(ctx, bctx)
}
func DefaultMainContext(opts ...llb.LocalOption) *llb.State {
opts = append([]llb.LocalOption{
llb.SharedKeyHint(DefaultLocalNameContext),
WithInternalName("load build context"),
}, opts...)
st := llb.Local(DefaultLocalNameContext, opts...)
return &st
}
func WithInternalName(name string) llb.ConstraintsOpt {
return llb.WithCustomName("[internal] " + name)
}
func (bc *Client) dockerIgnorePatterns(ctx context.Context, bctx *buildContext) ([]string, error) {
if bc.dockerignore == nil {
sessionID := bc.bopts.SessionID
if v, ok := bc.localsSessionIDs[bctx.contextLocalName]; ok {
sessionID = v
}
st := llb.Local(bctx.contextLocalName,
llb.SessionID(sessionID),
llb.FollowPaths([]string{DefaultDockerignoreName}),
llb.SharedKeyHint(bctx.contextLocalName+"-"+DefaultDockerignoreName),
WithInternalName("load "+DefaultDockerignoreName),
llb.Differ(llb.DiffNone, false),
)
def, err := st.Marshal(ctx, bc.marshalOpts()...)
if err != nil {
return nil, err
}
res, err := bc.client.Solve(ctx, client.SolveRequest{
Definition: def.ToPB(),
})
if err != nil {
return nil, err
}
ref, err := res.SingleRef()
if err != nil {
return nil, err
}
dt, _ := ref.ReadFile(ctx, client.ReadRequest{ // ignore error
Filename: DefaultDockerignoreName,
})
if dt == nil {
dt = []byte{}
}
bc.dockerignore = dt
bc.dockerignoreName = DefaultDockerignoreName
}
var err error
var excludes []string
if len(bc.dockerignore) != 0 {
excludes, err = ignorefile.ReadAll(bytes.NewBuffer(bc.dockerignore))
if err != nil {
return nil, errors.Wrapf(err, "failed parsing %s", bc.dockerignoreName)
}
}
return excludes, nil
}