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

Removes gogo/protobuf from buildx and updates to a version of moby/buildkit where gogo is removed. This also changes how the proto files are generated. This is because newer versions of protobuf are more strict about name conflicts. If two files have the same name (even if they are relative paths) and are used in different protoc commands, they'll conflict in the registry. Since protobuf file generation doesn't work very well with `paths=source_relative`, this removes the `go:generate` expression and just relies on the dockerfile to perform the generation. Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
550 lines
14 KiB
Go
550 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
|
|
}
|
|
|
|
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 {
|
|
src, ok := attrs["generator"]
|
|
if !ok {
|
|
return errors.Errorf("sbom scanner cannot be empty")
|
|
}
|
|
ref, err := reference.ParseNormalizedNamed(src)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to parse sbom scanner %s", src)
|
|
}
|
|
ref = reference.TagNameOnly(ref)
|
|
bc.SBOM = &SBOM{
|
|
Generator: ref.String(),
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|