bake: update ReadRemoteFiles to use buildkit api

Signed-off-by: Justin Chadwell <me@jedevc.com>
This commit is contained in:
Justin Chadwell 2023-04-12 10:57:54 +01:00
parent 62a21520ea
commit 871f865ac8
11 changed files with 1417 additions and 41 deletions

View File

@ -27,9 +27,6 @@ import (
)
var (
httpPrefix = regexp.MustCompile(`^https?://`)
gitURLPathWithFragmentSuffix = regexp.MustCompile(`\.git(?:#.+)?$`)
validTargetNameChars = `[a-zA-Z0-9_-]+`
targetNamePattern = regexp.MustCompile(`^` + validTargetNameChars + `$`)
)

View File

@ -4,7 +4,6 @@ import (
"archive/tar"
"bytes"
"context"
"strings"
"github.com/docker/buildx/builder"
controllerapi "github.com/docker/buildx/controller/pb"
@ -12,6 +11,7 @@ import (
"github.com/docker/buildx/util/progress"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/frontend/dockerui"
gwclient "github.com/moby/buildkit/frontend/gateway/client"
"github.com/moby/buildkit/session"
"github.com/pkg/errors"
@ -25,14 +25,14 @@ type Input struct {
func ReadRemoteFiles(ctx context.Context, nodes []builder.Node, url string, names []string, pw progress.Writer) ([]File, *Input, error) {
var session []session.Attachable
var filename string
st, ok := detectGitContext(url)
st, ok := dockerui.DetectGitContext(url, false)
if ok {
ssh, err := controllerapi.CreateSSH([]*controllerapi.SSH{{ID: "default"}})
if err == nil {
session = append(session, ssh)
}
} else {
st, filename, ok = detectHTTPContext(url)
st, filename, ok = dockerui.DetectHTTPContext(url)
if !ok {
return nil, nil, errors.Errorf("not url context")
}
@ -91,41 +91,6 @@ func ReadRemoteFiles(ctx context.Context, nodes []builder.Node, url string, name
return files, inp, nil
}
func detectHTTPContext(url string) (*llb.State, string, bool) {
if httpPrefix.MatchString(url) {
httpContext := llb.HTTP(url, llb.Filename("context"), llb.WithCustomName("[internal] load remote build context"))
return &httpContext, "context", true
}
return nil, "", false
}
func detectGitContext(ref string) (*llb.State, bool) {
found := false
if httpPrefix.MatchString(ref) && gitURLPathWithFragmentSuffix.MatchString(ref) {
found = true
}
for _, prefix := range []string{"git://", "github.com/", "git@"} {
if strings.HasPrefix(ref, prefix) {
found = true
break
}
}
if !found {
return nil, false
}
parts := strings.SplitN(ref, "#", 2)
branch := ""
if len(parts) > 1 {
branch = parts[1]
}
gitOpts := []llb.GitOption{llb.WithCustomName("[internal] load git source " + ref)}
st := llb.Git(parts[0], branch, gitOpts...)
return &st, true
}
func isArchive(header []byte) bool {
for _, m := range [][]byte{
{0x42, 0x5A, 0x68}, // bzip2

View File

@ -0,0 +1,81 @@
package attestations
import (
"encoding/csv"
"strings"
"github.com/pkg/errors"
)
const (
KeyTypeSbom = "sbom"
KeyTypeProvenance = "provenance"
)
const (
defaultSBOMGenerator = "docker/buildkit-syft-scanner:stable-1"
)
func Filter(v map[string]string) map[string]string {
attests := make(map[string]string)
for k, v := range v {
if strings.HasPrefix(k, "attest:") {
attests[k] = v
continue
}
if strings.HasPrefix(k, "build-arg:BUILDKIT_ATTEST_") {
attests[k] = v
continue
}
}
return attests
}
func Validate(values map[string]map[string]string) (map[string]map[string]string, error) {
for k := range values {
if k != KeyTypeSbom && k != KeyTypeProvenance {
return nil, errors.Errorf("unknown attestation type %q", k)
}
}
return values, nil
}
func Parse(values map[string]string) (map[string]map[string]string, error) {
attests := make(map[string]string)
for k, v := range values {
if strings.HasPrefix(k, "attest:") {
attests[strings.ToLower(strings.TrimPrefix(k, "attest:"))] = v
continue
}
if strings.HasPrefix(k, "build-arg:BUILDKIT_ATTEST_") {
attests[strings.ToLower(strings.TrimPrefix(k, "build-arg:BUILDKIT_ATTEST_"))] = v
continue
}
}
out := make(map[string]map[string]string)
for k, v := range attests {
attrs := make(map[string]string)
out[k] = attrs
if k == KeyTypeSbom {
attrs["generator"] = defaultSBOMGenerator
}
if v == "" {
continue
}
csvReader := csv.NewReader(strings.NewReader(v))
fields, err := csvReader.Read()
if err != nil {
return nil, errors.Wrapf(err, "failed to parse %s", k)
}
for _, field := range fields {
parts := strings.SplitN(field, "=", 2)
if len(parts) != 2 {
parts = append(parts, "")
}
attrs[parts[0]] = parts[1]
}
}
return Validate(out)
}

View File

@ -0,0 +1,65 @@
package dockerignore
import (
"bufio"
"bytes"
"io"
"path/filepath"
"strings"
"github.com/pkg/errors"
)
// ReadAll reads a .dockerignore file and returns the list of file patterns
// to ignore. Note this will trim whitespace from each line as well
// as use GO's "clean" func to get the shortest/cleanest path for each.
func ReadAll(reader io.Reader) ([]string, error) {
if reader == nil {
return nil, nil
}
scanner := bufio.NewScanner(reader)
var excludes []string
currentLine := 0
utf8bom := []byte{0xEF, 0xBB, 0xBF}
for scanner.Scan() {
scannedBytes := scanner.Bytes()
// We trim UTF8 BOM
if currentLine == 0 {
scannedBytes = bytes.TrimPrefix(scannedBytes, utf8bom)
}
pattern := string(scannedBytes)
currentLine++
// Lines starting with # (comments) are ignored before processing
if strings.HasPrefix(pattern, "#") {
continue
}
pattern = strings.TrimSpace(pattern)
if pattern == "" {
continue
}
// normalize absolute paths to paths relative to the context
// (taking care of '!' prefix)
invert := pattern[0] == '!'
if invert {
pattern = strings.TrimSpace(pattern[1:])
}
if len(pattern) > 0 {
pattern = filepath.Clean(pattern)
pattern = filepath.ToSlash(pattern)
if len(pattern) > 1 && pattern[0] == '/' {
pattern = pattern[1:]
}
}
if invert {
pattern = "!" + pattern
}
excludes = append(excludes, pattern)
}
if err := scanner.Err(); err != nil {
return nil, errors.Wrap(err, "error reading .dockerignore")
}
return excludes, nil
}

View File

@ -0,0 +1,138 @@
package dockerui
import (
"encoding/csv"
"net"
"strconv"
"strings"
"time"
"github.com/containerd/containerd/platforms"
"github.com/docker/go-units"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/solver/pb"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
func parsePlatforms(v string) ([]ocispecs.Platform, error) {
var pp []ocispecs.Platform
for _, v := range strings.Split(v, ",") {
p, err := platforms.Parse(v)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse target platform %s", v)
}
pp = append(pp, platforms.Normalize(p))
}
return pp, nil
}
func parseResolveMode(v string) (llb.ResolveMode, error) {
switch v {
case pb.AttrImageResolveModeDefault, "":
return llb.ResolveModeDefault, nil
case pb.AttrImageResolveModeForcePull:
return llb.ResolveModeForcePull, nil
case pb.AttrImageResolveModePreferLocal:
return llb.ResolveModePreferLocal, nil
default:
return 0, errors.Errorf("invalid image-resolve-mode: %s", v)
}
}
func parseExtraHosts(v string) ([]llb.HostIP, error) {
if v == "" {
return nil, nil
}
out := make([]llb.HostIP, 0)
csvReader := csv.NewReader(strings.NewReader(v))
fields, err := csvReader.Read()
if err != nil {
return nil, err
}
for _, field := range fields {
key, val, ok := strings.Cut(strings.ToLower(field), "=")
if !ok {
return nil, errors.Errorf("invalid key-value pair %s", field)
}
ip := net.ParseIP(val)
if ip == nil {
return nil, errors.Errorf("failed to parse IP %s", val)
}
out = append(out, llb.HostIP{Host: key, IP: ip})
}
return out, nil
}
func parseShmSize(v string) (int64, error) {
if len(v) == 0 {
return 0, nil
}
kb, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return 0, err
}
return kb, nil
}
func parseUlimits(v string) ([]pb.Ulimit, error) {
if v == "" {
return nil, nil
}
out := make([]pb.Ulimit, 0)
csvReader := csv.NewReader(strings.NewReader(v))
fields, err := csvReader.Read()
if err != nil {
return nil, err
}
for _, field := range fields {
ulimit, err := units.ParseUlimit(field)
if err != nil {
return nil, err
}
out = append(out, pb.Ulimit{
Name: ulimit.Name,
Soft: ulimit.Soft,
Hard: ulimit.Hard,
})
}
return out, nil
}
func parseNetMode(v string) (pb.NetMode, error) {
if v == "" {
return llb.NetModeSandbox, nil
}
switch v {
case "none":
return llb.NetModeNone, nil
case "host":
return llb.NetModeHost, nil
case "sandbox":
return llb.NetModeSandbox, nil
default:
return 0, errors.Errorf("invalid netmode %s", v)
}
}
func parseSourceDateEpoch(v string) (*time.Time, error) {
if v == "" {
return nil, nil
}
sde, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "invalid SOURCE_DATE_EPOCH: %s", v)
}
tm := time.Unix(sde, 0).UTC()
return &tm, nil
}
func filter(opt map[string]string, key string) map[string]string {
m := map[string]string{}
for k, v := range opt {
if strings.HasPrefix(k, key) {
m[strings.TrimPrefix(k, key)] = v
}
}
return m
}

View File

@ -0,0 +1,114 @@
package dockerui
import (
"context"
"encoding/json"
"fmt"
"github.com/containerd/containerd/platforms"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
"github.com/moby/buildkit/exporter/containerimage/image"
"github.com/moby/buildkit/frontend/gateway/client"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
)
type BuildFunc func(ctx context.Context, platform *ocispecs.Platform, idx int) (client.Reference, *image.Image, error)
func (bc *Client) Build(ctx context.Context, fn BuildFunc) (*ResultBuilder, error) {
res := client.NewResult()
targets := make([]*ocispecs.Platform, 0, len(bc.TargetPlatforms))
for _, p := range bc.TargetPlatforms {
p := p
targets = append(targets, &p)
}
if len(targets) == 0 {
targets = append(targets, nil)
}
expPlatforms := &exptypes.Platforms{
Platforms: make([]exptypes.Platform, len(targets)),
}
eg, ctx := errgroup.WithContext(ctx)
for i, tp := range targets {
i, tp := i, tp
eg.Go(func() error {
ref, img, err := fn(ctx, tp, i)
if err != nil {
return err
}
config, err := json.Marshal(img)
if err != nil {
return errors.Wrapf(err, "failed to marshal image config")
}
p := platforms.DefaultSpec()
if tp != nil {
p = *tp
}
// in certain conditions we allow input platform to be extended from base image
if p.OS == "windows" && img.OS == p.OS {
if p.OSVersion == "" && img.OSVersion != "" {
p.OSVersion = img.OSVersion
}
if p.OSFeatures == nil && len(img.OSFeatures) > 0 {
p.OSFeatures = img.OSFeatures
}
}
p = platforms.Normalize(p)
k := platforms.Format(p)
if bc.MultiPlatformRequested {
res.AddRef(k, ref)
res.AddMeta(fmt.Sprintf("%s/%s", exptypes.ExporterImageConfigKey, k), config)
} else {
res.SetRef(ref)
res.AddMeta(exptypes.ExporterImageConfigKey, config)
}
expPlatforms.Platforms[i] = exptypes.Platform{
ID: k,
Platform: p,
}
return nil
})
}
if err := eg.Wait(); err != nil {
return nil, err
}
return &ResultBuilder{
Result: res,
expPlatforms: expPlatforms,
}, nil
}
type ResultBuilder struct {
*client.Result
expPlatforms *exptypes.Platforms
}
func (rb *ResultBuilder) Finalize() (*client.Result, error) {
dt, err := json.Marshal(rb.expPlatforms)
if err != nil {
return nil, err
}
rb.AddMeta(exptypes.ExporterPlatformsKey, dt)
return rb.Result, nil
}
func (rb *ResultBuilder) EachPlatform(ctx context.Context, fn func(ctx context.Context, id string, p ocispecs.Platform) error) error {
eg, ctx := errgroup.WithContext(ctx)
for _, p := range rb.expPlatforms.Platforms {
p := p
eg.Go(func() error {
return fn(ctx, p.ID, p.Platform)
})
}
return eg.Wait()
}

View File

@ -0,0 +1,497 @@
package dockerui
import (
"bytes"
"context"
"encoding/json"
"path"
"strconv"
"strings"
"time"
"github.com/containerd/containerd/platforms"
"github.com/docker/distribution/reference"
controlapi "github.com/moby/buildkit/api/services/control"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/exporter/containerimage/image"
"github.com/moby/buildkit/frontend/attestations"
"github.com/moby/buildkit/frontend/dockerfile/dockerignore"
"github.com/moby/buildkit/frontend/gateway/client"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/flightcontrol"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
const (
buildArgPrefix = "build-arg:"
labelPrefix = "label:"
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"
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
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
bctx *buildContext
g flightcontrol.Group
bopts client.BuildOpts
dockerignore []byte
}
type SBOM struct {
Generator string
}
type Source struct {
*llb.SourceMap
Warn func(context.Context, string, client.WarnOpts)
}
type ContextOpt struct {
NoDockerignore bool
LocalOpts []llb.LocalOption
Platform *ocispecs.Platform
ResolveMode string
}
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]
return nil
}
func (bc *Client) buildContext(ctx context.Context) (*buildContext, error) {
bctx, err := bc.g.Do(ctx, "initcontext", func(ctx context.Context) (interface{}, error) {
if bc.bctx != nil {
return bc.bctx, nil
}
bctx, err := bc.initContext(ctx)
if err == nil {
bc.bctx = bctx
}
return bctx, err
})
if err != nil {
return nil, err
}
return bctx.(*buildContext), nil
}
func (bc *Client) ReadEntrypoint(ctx context.Context, 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)))
}
opts = append([]llb.LocalOption{
llb.FollowPaths(filenames),
llb.SessionID(bc.bopts.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, dt)
smap.Definition = def
dt, err = ref.ReadFile(ctx, client.ReadRequest{
Filename: bctx.filename + ".dockerignore",
})
if err == nil {
bc.dockerignore = dt
}
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,
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
}
if bc.dockerignore == nil {
st := llb.Local(bctx.contextLocalName,
llb.SessionID(bc.bopts.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
}
var excludes []string
if len(bc.dockerignore) != 0 {
excludes, err = dockerignore.ReadAll(bytes.NewBuffer(bc.dockerignore))
if err != nil {
return nil, errors.Wrap(err, "failed to parse dockerignore")
}
}
opts = append([]llb.LocalOption{
llb.SessionID(bc.bopts.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, *image.Image, 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 {
return nil, nil, err
}
if st != nil {
return st, img, nil
}
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 WithInternalName(name string) llb.ConstraintsOpt {
return llb.WithCustomName("[internal] " + name)
}

View File

@ -0,0 +1,194 @@
package dockerui
import (
"archive/tar"
"bytes"
"context"
"path/filepath"
"regexp"
"strconv"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/frontend/gateway/client"
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
"github.com/moby/buildkit/util/gitutil"
"github.com/pkg/errors"
)
const (
DefaultLocalNameContext = "context"
DefaultLocalNameDockerfile = "dockerfile"
DefaultDockerfileName = "Dockerfile"
DefaultDockerignoreName = ".dockerignore"
EmptyImageName = "scratch"
)
const (
keyFilename = "filename"
keyContextSubDir = "contextsubdir"
keyNameContext = "contextkey"
keyNameDockerfile = "dockerfilekey"
)
var httpPrefix = regexp.MustCompile(`^https?://`)
type buildContext struct {
context *llb.State // set if not local
dockerfile *llb.State // override remoteContext if set
contextLocalName string
dockerfileLocalName string
filename string
forceLocalDockerfile bool
}
func (bc *Client) marshalOpts() []llb.ConstraintsOpt {
return []llb.ConstraintsOpt{llb.WithCaps(bc.bopts.Caps)}
}
func (bc *Client) initContext(ctx context.Context) (*buildContext, error) {
opts := bc.bopts.Opts
gwcaps := bc.bopts.Caps
localNameContext := DefaultLocalNameContext
if v, ok := opts[keyNameContext]; ok {
localNameContext = v
}
bctx := &buildContext{
contextLocalName: DefaultLocalNameContext,
dockerfileLocalName: DefaultLocalNameDockerfile,
filename: DefaultDockerfileName,
}
if v, ok := opts[keyFilename]; ok {
bctx.filename = v
}
if v, ok := opts[keyNameDockerfile]; ok {
bctx.forceLocalDockerfile = true
bctx.dockerfileLocalName = v
}
keepGit := false
if v, err := strconv.ParseBool(opts[keyContextKeepGitDirArg]); err == nil {
keepGit = v
}
if st, ok := DetectGitContext(opts[localNameContext], keepGit); ok {
bctx.context = st
bctx.dockerfile = st
} else if st, filename, ok := DetectHTTPContext(opts[localNameContext]); ok {
def, err := st.Marshal(ctx, bc.marshalOpts()...)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal httpcontext")
}
res, err := bc.client.Solve(ctx, client.SolveRequest{
Definition: def.ToPB(),
})
if err != nil {
return nil, errors.Wrapf(err, "failed to resolve httpcontext")
}
ref, err := res.SingleRef()
if err != nil {
return nil, err
}
dt, err := ref.ReadFile(ctx, client.ReadRequest{
Filename: filename,
Range: &client.FileRange{
Length: 1024,
},
})
if err != nil {
return nil, errors.Wrapf(err, "failed to read downloaded context")
}
if isArchive(dt) {
bc := llb.Scratch().File(llb.Copy(*st, filepath.Join("/", filename), "/", &llb.CopyInfo{
AttemptUnpack: true,
}))
bctx.context = &bc
} else {
bctx.filename = filename
bctx.context = st
}
bctx.dockerfile = bctx.context
} else if (&gwcaps).Supports(gwpb.CapFrontendInputs) == nil {
inputs, err := bc.client.Inputs(ctx)
if err != nil {
return nil, errors.Wrapf(err, "failed to get frontend inputs")
}
if !bctx.forceLocalDockerfile {
inputDockerfile, ok := inputs[bctx.dockerfileLocalName]
if ok {
bctx.dockerfile = &inputDockerfile
}
}
inputCtx, ok := inputs[DefaultLocalNameContext]
if ok {
bctx.context = &inputCtx
}
}
if bctx.context != nil {
if sub, ok := opts[keyContextSubDir]; ok {
bctx.context = scopeToSubDir(bctx.context, sub)
}
}
return bctx, nil
}
func DetectGitContext(ref string, keepGit bool) (*llb.State, bool) {
g, err := gitutil.ParseGitRef(ref)
if err != nil {
return nil, false
}
commit := g.Commit
if g.SubDir != "" {
commit += ":" + g.SubDir
}
gitOpts := []llb.GitOption{WithInternalName("load git source " + ref)}
if keepGit {
gitOpts = append(gitOpts, llb.KeepGitDir())
}
st := llb.Git(g.Remote, commit, gitOpts...)
return &st, true
}
func DetectHTTPContext(ref string) (*llb.State, string, bool) {
filename := "context"
if httpPrefix.MatchString(ref) {
st := llb.HTTP(ref, llb.Filename(filename), WithInternalName("load remote build context"))
return &st, filename, true
}
return nil, "", false
}
func isArchive(header []byte) bool {
for _, m := range [][]byte{
{0x42, 0x5A, 0x68}, // bzip2
{0x1F, 0x8B, 0x08}, // gzip
{0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00}, // xz
} {
if len(header) < len(m) {
continue
}
if bytes.Equal(m, header[:len(m)]) {
return true
}
}
r := tar.NewReader(bytes.NewBuffer(header))
_, err := r.Next()
return err == nil
}
func scopeToSubDir(c *llb.State, dir string) *llb.State {
bc := llb.Scratch().File(llb.Copy(*c, dir, "/", &llb.CopyInfo{
CopyDirContentsOnly: true,
}))
return &bc
}

View File

@ -0,0 +1,231 @@
package dockerui
import (
"bytes"
"context"
"encoding/json"
"fmt"
"strings"
"github.com/docker/distribution/reference"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
"github.com/moby/buildkit/exporter/containerimage/image"
"github.com/moby/buildkit/frontend/dockerfile/dockerignore"
"github.com/moby/buildkit/frontend/gateway/client"
"github.com/pkg/errors"
)
const (
contextPrefix = "context:"
inputMetadataPrefix = "input-metadata:"
)
func (bc *Client) namedContext(ctx context.Context, name string, nameWithPlatform string, opt ContextOpt) (*llb.State, *image.Image, error) {
opts := bc.bopts.Opts
v, ok := opts[contextPrefix+nameWithPlatform]
if !ok {
return nil, nil, nil
}
vv := strings.SplitN(v, ":", 2)
if len(vv) != 2 {
return nil, nil, errors.Errorf("invalid context specifier %s for %s", v, nameWithPlatform)
}
// allow git@ without protocol for SSH URLs for backwards compatibility
if strings.HasPrefix(vv[0], "git@") {
vv[0] = "git"
}
switch vv[0] {
case "docker-image":
ref := strings.TrimPrefix(vv[1], "//")
if ref == EmptyImageName {
st := llb.Scratch()
return &st, nil, nil
}
imgOpt := []llb.ImageOption{
llb.WithCustomName("[context " + nameWithPlatform + "] " + ref),
}
if opt.Platform != nil {
imgOpt = append(imgOpt, llb.Platform(*opt.Platform))
}
named, err := reference.ParseNormalizedNamed(ref)
if err != nil {
return nil, nil, err
}
named = reference.TagNameOnly(named)
_, data, err := bc.client.ResolveImageConfig(ctx, named.String(), llb.ResolveImageConfigOpt{
Platform: opt.Platform,
ResolveMode: opt.ResolveMode,
LogName: fmt.Sprintf("[context %s] load metadata for %s", nameWithPlatform, ref),
ResolverType: llb.ResolverTypeRegistry,
})
if err != nil {
return nil, nil, err
}
var img image.Image
if err := json.Unmarshal(data, &img); err != nil {
return nil, nil, err
}
img.Created = nil
st := llb.Image(ref, imgOpt...)
st, err = st.WithImageConfig(data)
if err != nil {
return nil, nil, err
}
return &st, &img, nil
case "git":
st, ok := DetectGitContext(v, true)
if !ok {
return nil, nil, errors.Errorf("invalid git context %s", v)
}
return st, nil, nil
case "http", "https":
st, ok := DetectGitContext(v, true)
if !ok {
httpst := llb.HTTP(v, llb.WithCustomName("[context "+nameWithPlatform+"] "+v))
st = &httpst
}
return st, nil, nil
case "oci-layout":
refSpec := strings.TrimPrefix(vv[1], "//")
ref, err := reference.Parse(refSpec)
if err != nil {
return nil, nil, errors.Wrapf(err, "could not parse oci-layout reference %q", refSpec)
}
named, ok := ref.(reference.Named)
if !ok {
return nil, nil, errors.Errorf("oci-layout reference %q has no name", ref.String())
}
dgstd, ok := named.(reference.Digested)
if !ok {
return nil, nil, errors.Errorf("oci-layout reference %q has no digest", named.String())
}
// for the dummy ref primarily used in log messages, we can use the
// original name, since the store key may not be significant
dummyRef, err := reference.ParseNormalizedNamed(name)
if err != nil {
return nil, nil, errors.Wrapf(err, "could not parse oci-layout reference %q", name)
}
dummyRef, err = reference.WithDigest(dummyRef, dgstd.Digest())
if err != nil {
return nil, nil, errors.Wrapf(err, "could not wrap %q with digest", name)
}
_, data, err := bc.client.ResolveImageConfig(ctx, dummyRef.String(), llb.ResolveImageConfigOpt{
Platform: opt.Platform,
ResolveMode: opt.ResolveMode,
LogName: fmt.Sprintf("[context %s] load metadata for %s", nameWithPlatform, dummyRef.String()),
ResolverType: llb.ResolverTypeOCILayout,
Store: llb.ResolveImageConfigOptStore{
SessionID: bc.bopts.SessionID,
StoreID: named.Name(),
},
})
if err != nil {
return nil, nil, err
}
var img image.Image
if err := json.Unmarshal(data, &img); err != nil {
return nil, nil, errors.Wrap(err, "could not parse oci-layout image config")
}
ociOpt := []llb.OCILayoutOption{
llb.WithCustomName("[context " + nameWithPlatform + "] OCI load from client"),
llb.OCIStore(bc.bopts.SessionID, named.Name()),
}
if opt.Platform != nil {
ociOpt = append(ociOpt, llb.Platform(*opt.Platform))
}
st := llb.OCILayout(
dummyRef.String(),
ociOpt...,
)
st, err = st.WithImageConfig(data)
if err != nil {
return nil, nil, err
}
return &st, &img, nil
case "local":
st := llb.Local(vv[1],
llb.SessionID(bc.bopts.SessionID),
llb.FollowPaths([]string{DefaultDockerignoreName}),
llb.SharedKeyHint("context:"+nameWithPlatform+"-"+DefaultDockerignoreName),
llb.WithCustomName("[context "+nameWithPlatform+"] load "+DefaultDockerignoreName),
llb.Differ(llb.DiffNone, false),
)
def, err := st.Marshal(ctx)
if err != nil {
return nil, nil, err
}
res, err := bc.client.Solve(ctx, client.SolveRequest{
Evaluate: true,
Definition: def.ToPB(),
})
if err != nil {
return nil, nil, err
}
ref, err := res.SingleRef()
if err != nil {
return nil, nil, err
}
var excludes []string
if !opt.NoDockerignore {
dt, _ := ref.ReadFile(ctx, client.ReadRequest{
Filename: DefaultDockerignoreName,
}) // error ignored
if len(dt) != 0 {
excludes, err = dockerignore.ReadAll(bytes.NewBuffer(dt))
if err != nil {
return nil, nil, err
}
}
}
st = llb.Local(vv[1],
llb.WithCustomName("[context "+nameWithPlatform+"] load from client"),
llb.SessionID(bc.bopts.SessionID),
llb.SharedKeyHint("context:"+nameWithPlatform),
llb.ExcludePatterns(excludes),
)
return &st, nil, nil
case "input":
inputs, err := bc.client.Inputs(ctx)
if err != nil {
return nil, nil, err
}
st, ok := inputs[vv[1]]
if !ok {
return nil, nil, errors.Errorf("invalid input %s for %s", vv[1], nameWithPlatform)
}
md, ok := opts[inputMetadataPrefix+vv[1]]
if ok {
m := make(map[string][]byte)
if err := json.Unmarshal([]byte(md), &m); err != nil {
return nil, nil, errors.Wrapf(err, "failed to parse input metadata %s", md)
}
var img *image.Image
if dtic, ok := m[exptypes.ExporterImageConfigKey]; ok {
st, err = st.WithImageConfig(dtic)
if err != nil {
return nil, nil, err
}
if err := json.Unmarshal(dtic, &img); err != nil {
return nil, nil, errors.Wrapf(err, "failed to parse image config for %s", nameWithPlatform)
}
}
return &st, img, nil
}
return &st, nil, nil
default:
return nil, nil, errors.Errorf("unsupported context source %s for %s", vv[0], nameWithPlatform)
}
}

View File

@ -0,0 +1,91 @@
package dockerui
import (
"bytes"
"context"
"encoding/json"
"github.com/moby/buildkit/frontend/gateway/client"
"github.com/moby/buildkit/frontend/subrequests"
"github.com/moby/buildkit/frontend/subrequests/outline"
"github.com/moby/buildkit/frontend/subrequests/targets"
"github.com/moby/buildkit/solver/errdefs"
)
const (
keyRequestID = "requestid"
)
type RequestHandler struct {
Outline func(context.Context) (*outline.Outline, error)
ListTargets func(context.Context) (*targets.List, error)
AllowOther bool
}
func (bc *Client) HandleSubrequest(ctx context.Context, h RequestHandler) (*client.Result, bool, error) {
req, ok := bc.bopts.Opts[keyRequestID]
if !ok {
return nil, false, nil
}
switch req {
case subrequests.RequestSubrequestsDescribe:
res, err := describe(h)
return res, true, err
case outline.SubrequestsOutlineDefinition.Name:
if f := h.Outline; f != nil {
o, err := f(ctx)
if err != nil {
return nil, false, err
}
if o == nil {
return nil, true, nil
}
res, err := o.ToResult()
return res, true, err
}
case targets.SubrequestsTargetsDefinition.Name:
if f := h.ListTargets; f != nil {
targets, err := f(ctx)
if err != nil {
return nil, false, err
}
if targets == nil {
return nil, true, nil
}
res, err := targets.ToResult()
return res, true, err
}
}
if h.AllowOther {
return nil, false, nil
}
return nil, false, errdefs.NewUnsupportedSubrequestError(req)
}
func describe(h RequestHandler) (*client.Result, error) {
all := []subrequests.Request{}
if h.Outline != nil {
all = append(all, outline.SubrequestsOutlineDefinition)
}
if h.ListTargets != nil {
all = append(all, targets.SubrequestsTargetsDefinition)
}
all = append(all, subrequests.SubrequestsDescribeDefinition)
dt, err := json.MarshalIndent(all, "", " ")
if err != nil {
return nil, err
}
b := bytes.NewBuffer(nil)
if err := subrequests.PrintDescribe(dt, b); err != nil {
return nil, err
}
res := client.NewResult()
res.Metadata = map[string][]byte{
"result.json": dt,
"result.txt": b.Bytes(),
"version": []byte(subrequests.SubrequestsDescribeDefinition.Version),
}
return res, nil
}

3
vendor/modules.txt vendored
View File

@ -510,6 +510,9 @@ github.com/moby/buildkit/client/ociindex
github.com/moby/buildkit/cmd/buildkitd/config
github.com/moby/buildkit/exporter/containerimage/exptypes
github.com/moby/buildkit/exporter/containerimage/image
github.com/moby/buildkit/frontend/attestations
github.com/moby/buildkit/frontend/dockerfile/dockerignore
github.com/moby/buildkit/frontend/dockerui
github.com/moby/buildkit/frontend/gateway/client
github.com/moby/buildkit/frontend/gateway/grpcclient
github.com/moby/buildkit/frontend/gateway/pb