mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-05-18 09:17:49 +08:00

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>
560 lines
14 KiB
Go
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
|
|
}
|