Update gomod and vendor

This commit is contained in:
Ettore Di Giacinto
2021-01-20 12:36:07 +01:00
parent 163f93067c
commit c24a3a35f1
149 changed files with 6 additions and 16940 deletions

View File

@@ -1,106 +0,0 @@
package registry
import (
"context"
"encoding/json"
"github.com/moby/buildkit/cache/remotecache"
v1 "github.com/moby/buildkit/cache/remotecache/v1"
"github.com/moby/buildkit/solver"
digest "github.com/opencontainers/go-digest"
"github.com/sirupsen/logrus"
)
func ResolveCacheExporterFunc() remotecache.ResolveCacheExporterFunc {
return func(ctx context.Context, _ map[string]string) (remotecache.Exporter, error) {
return NewExporter(), nil
}
}
func NewExporter() remotecache.Exporter {
cc := v1.NewCacheChains()
return &exporter{CacheExporterTarget: cc, chains: cc}
}
type exporter struct {
solver.CacheExporterTarget
chains *v1.CacheChains
}
func (ce *exporter) Finalize(ctx context.Context) (map[string]string, error) {
return nil, nil
}
func (ce *exporter) reset() {
cc := v1.NewCacheChains()
ce.CacheExporterTarget = cc
ce.chains = cc
}
func (ce *exporter) ExportForLayers(layers []digest.Digest) ([]byte, error) {
config, descs, err := ce.chains.Marshal()
if err != nil {
return nil, err
}
descs2 := map[digest.Digest]v1.DescriptorProviderPair{}
for _, k := range layers {
if v, ok := descs[k]; ok {
descs2[k] = v
continue
}
// fallback for uncompressed digests
for _, v := range descs {
if uc := v.Descriptor.Annotations["containerd.io/uncompressed"]; uc == string(k) {
descs2[v.Descriptor.Digest] = v
}
}
}
cc := v1.NewCacheChains()
if err := v1.ParseConfig(*config, descs2, cc); err != nil {
return nil, err
}
cfg, _, err := cc.Marshal()
if err != nil {
return nil, err
}
if len(cfg.Layers) == 0 {
logrus.Warn("failed to match any cache with layers")
return nil, nil
}
cache := map[int]int{}
// reorder layers based on the order in the image
for i, r := range cfg.Records {
for j, rr := range r.Results {
n := getSortedLayerIndex(rr.LayerIndex, cfg.Layers, cache)
rr.LayerIndex = n
r.Results[j] = rr
cfg.Records[i] = r
}
}
dt, err := json.Marshal(cfg.Records)
if err != nil {
return nil, err
}
ce.reset()
return dt, nil
}
func getSortedLayerIndex(idx int, layers []v1.CacheLayer, cache map[int]int) int {
if idx == -1 {
return -1
}
l := layers[idx]
if i, ok := cache[idx]; ok {
return i
}
cache[idx] = getSortedLayerIndex(l.ParentIndex, layers, cache) + 1
return cache[idx]
}

View File

@@ -1,83 +0,0 @@
package local
import (
"context"
"time"
"github.com/containerd/containerd/content"
"github.com/moby/buildkit/cache/remotecache"
"github.com/moby/buildkit/session"
sessioncontent "github.com/moby/buildkit/session/content"
digest "github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
const (
attrDigest = "digest"
attrSrc = "src"
attrDest = "dest"
contentStoreIDPrefix = "local:"
)
// ResolveCacheExporterFunc for "local" cache exporter.
func ResolveCacheExporterFunc(sm *session.Manager) remotecache.ResolveCacheExporterFunc {
return func(ctx context.Context, attrs map[string]string) (remotecache.Exporter, error) {
store := attrs[attrDest]
if store == "" {
return nil, errors.New("local cache exporter requires dest")
}
csID := contentStoreIDPrefix + store
cs, err := getContentStore(ctx, sm, csID)
if err != nil {
return nil, err
}
return remotecache.NewExporter(cs), nil
}
}
// ResolveCacheImporterFunc for "local" cache importer.
func ResolveCacheImporterFunc(sm *session.Manager) remotecache.ResolveCacheImporterFunc {
return func(ctx context.Context, attrs map[string]string) (remotecache.Importer, specs.Descriptor, error) {
dgstStr := attrs[attrDigest]
if dgstStr == "" {
return nil, specs.Descriptor{}, errors.New("local cache importer requires explicit digest")
}
dgst := digest.Digest(dgstStr)
store := attrs[attrSrc]
if store == "" {
return nil, specs.Descriptor{}, errors.New("local cache importer requires src")
}
csID := contentStoreIDPrefix + store
cs, err := getContentStore(ctx, sm, csID)
if err != nil {
return nil, specs.Descriptor{}, err
}
info, err := cs.Info(ctx, dgst)
if err != nil {
return nil, specs.Descriptor{}, err
}
desc := specs.Descriptor{
// MediaType is typically MediaTypeDockerSchema2ManifestList,
// but we leave it empty until we get correct support for local index.json
Digest: dgst,
Size: info.Size,
}
return remotecache.NewImporter(cs), desc, nil
}
}
func getContentStore(ctx context.Context, sm *session.Manager, storeID string) (content.Store, error) {
sessionID := session.FromContext(ctx)
if sessionID == "" {
return nil, errors.New("local cache exporter/importer requires session")
}
timeoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
caller, err := sm.Get(timeoutCtx, sessionID)
if err != nil {
return nil, err
}
return sessioncontent.NewCallerStore(caller, storeID), nil
}

View File

@@ -1,96 +0,0 @@
package registry
import (
"context"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/remotes/docker"
"github.com/docker/distribution/reference"
"github.com/moby/buildkit/cache/remotecache"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/util/contentutil"
"github.com/moby/buildkit/util/resolver"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
func canonicalizeRef(rawRef string) (string, error) {
if rawRef == "" {
return "", errors.New("missing ref")
}
parsed, err := reference.ParseNormalizedNamed(rawRef)
if err != nil {
return "", err
}
return reference.TagNameOnly(parsed).String(), nil
}
const (
attrRef = "ref"
)
func ResolveCacheExporterFunc(sm *session.Manager, hosts docker.RegistryHosts) remotecache.ResolveCacheExporterFunc {
return func(ctx context.Context, attrs map[string]string) (remotecache.Exporter, error) {
ref, err := canonicalizeRef(attrs[attrRef])
if err != nil {
return nil, err
}
remote := resolver.New(ctx, hosts, sm)
pusher, err := remote.Pusher(ctx, ref)
if err != nil {
return nil, err
}
return remotecache.NewExporter(contentutil.FromPusher(pusher)), nil
}
}
func ResolveCacheImporterFunc(sm *session.Manager, cs content.Store, hosts docker.RegistryHosts) remotecache.ResolveCacheImporterFunc {
return func(ctx context.Context, attrs map[string]string) (remotecache.Importer, specs.Descriptor, error) {
ref, err := canonicalizeRef(attrs[attrRef])
if err != nil {
return nil, specs.Descriptor{}, err
}
remote := resolver.New(ctx, hosts, sm)
xref, desc, err := remote.Resolve(ctx, ref)
if err != nil {
return nil, specs.Descriptor{}, err
}
fetcher, err := remote.Fetcher(ctx, xref)
if err != nil {
return nil, specs.Descriptor{}, err
}
src := &withDistributionSourceLabel{
Provider: contentutil.FromFetcher(fetcher),
ref: ref,
source: cs,
}
return remotecache.NewImporter(src), desc, nil
}
}
type withDistributionSourceLabel struct {
content.Provider
ref string
source content.Manager
}
var _ remotecache.DistributionSourceLabelSetter = &withDistributionSourceLabel{}
func (dsl *withDistributionSourceLabel) SetDistributionSourceLabel(ctx context.Context, dgst digest.Digest) error {
hf, err := docker.AppendDistributionSourceLabel(dsl.source, dsl.ref)
if err != nil {
return err
}
_, err = hf(ctx, ocispec.Descriptor{Digest: dgst})
return err
}
func (dsl *withDistributionSourceLabel) SetDistributionSourceAnnotation(desc ocispec.Descriptor) ocispec.Descriptor {
if desc.Annotations == nil {
desc.Annotations = map[string]string{}
}
desc.Annotations["containerd.io/distribution.source.ref"] = dsl.ref
return desc
}

View File

@@ -1,109 +0,0 @@
package imagemetaresolver
import (
"context"
"net/http"
"sync"
"github.com/containerd/containerd/platforms"
"github.com/containerd/containerd/remotes"
"github.com/containerd/containerd/remotes/docker"
"github.com/docker/docker/pkg/locker"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/util/contentutil"
"github.com/moby/buildkit/util/imageutil"
digest "github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go/v1"
)
var defaultImageMetaResolver llb.ImageMetaResolver
var defaultImageMetaResolverOnce sync.Once
var WithDefault = imageOptionFunc(func(ii *llb.ImageInfo) {
llb.WithMetaResolver(Default()).SetImageOption(ii)
})
type imageMetaResolverOpts struct {
platform *specs.Platform
}
type ImageMetaResolverOpt func(o *imageMetaResolverOpts)
func WithDefaultPlatform(p *specs.Platform) ImageMetaResolverOpt {
return func(o *imageMetaResolverOpts) {
o.platform = p
}
}
func New(with ...ImageMetaResolverOpt) llb.ImageMetaResolver {
var opts imageMetaResolverOpts
for _, f := range with {
f(&opts)
}
return &imageMetaResolver{
resolver: docker.NewResolver(docker.ResolverOptions{
Client: http.DefaultClient,
}),
platform: opts.platform,
buffer: contentutil.NewBuffer(),
cache: map[string]resolveResult{},
locker: locker.New(),
}
}
func Default() llb.ImageMetaResolver {
defaultImageMetaResolverOnce.Do(func() {
defaultImageMetaResolver = New()
})
return defaultImageMetaResolver
}
type imageMetaResolver struct {
resolver remotes.Resolver
buffer contentutil.Buffer
platform *specs.Platform
locker *locker.Locker
cache map[string]resolveResult
}
type resolveResult struct {
config []byte
dgst digest.Digest
}
func (imr *imageMetaResolver) ResolveImageConfig(ctx context.Context, ref string, opt llb.ResolveImageConfigOpt) (digest.Digest, []byte, error) {
imr.locker.Lock(ref)
defer imr.locker.Unlock(ref)
platform := opt.Platform
if platform == nil {
platform = imr.platform
}
k := imr.key(ref, platform)
if res, ok := imr.cache[k]; ok {
return res.dgst, res.config, nil
}
dgst, config, err := imageutil.Config(ctx, ref, imr.resolver, imr.buffer, nil, platform)
if err != nil {
return "", nil, err
}
imr.cache[k] = resolveResult{dgst: dgst, config: config}
return dgst, config, nil
}
func (imr *imageMetaResolver) key(ref string, platform *specs.Platform) string {
if platform != nil {
ref += platforms.Format(*platform)
}
return ref
}
type imageOptionFunc func(*llb.ImageInfo)
func (fn imageOptionFunc) SetImageOption(ii *llb.ImageInfo) {
fn(ii)
}

View File

@@ -1,337 +0,0 @@
package runcexecutor
import (
"context"
"encoding/json"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/containerd/containerd/mount"
containerdoci "github.com/containerd/containerd/oci"
"github.com/containerd/continuity/fs"
runc "github.com/containerd/go-runc"
"github.com/docker/docker/pkg/idtools"
"github.com/moby/buildkit/cache"
"github.com/moby/buildkit/executor"
"github.com/moby/buildkit/executor/oci"
"github.com/moby/buildkit/identity"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/network"
rootlessspecconv "github.com/moby/buildkit/util/rootless/specconv"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type Opt struct {
// root directory
Root string
CommandCandidates []string
// without root privileges (has nothing to do with Opt.Root directory)
Rootless bool
// DefaultCgroupParent is the cgroup-parent name for executor
DefaultCgroupParent string
// ProcessMode
ProcessMode oci.ProcessMode
IdentityMapping *idtools.IdentityMapping
// runc run --no-pivot (unrecommended)
NoPivot bool
DNS *oci.DNSConfig
OOMScoreAdj *int
}
var defaultCommandCandidates = []string{"buildkit-runc", "runc"}
type runcExecutor struct {
runc *runc.Runc
root string
cmd string
cgroupParent string
rootless bool
networkProviders map[pb.NetMode]network.Provider
processMode oci.ProcessMode
idmap *idtools.IdentityMapping
noPivot bool
dns *oci.DNSConfig
oomScoreAdj *int
}
func New(opt Opt, networkProviders map[pb.NetMode]network.Provider) (executor.Executor, error) {
cmds := opt.CommandCandidates
if cmds == nil {
cmds = defaultCommandCandidates
}
var cmd string
var found bool
for _, cmd = range cmds {
if _, err := exec.LookPath(cmd); err == nil {
found = true
break
}
}
if !found {
return nil, errors.Errorf("failed to find %s binary", cmd)
}
root := opt.Root
if err := os.MkdirAll(root, 0711); err != nil {
return nil, errors.Wrapf(err, "failed to create %s", root)
}
root, err := filepath.Abs(root)
if err != nil {
return nil, err
}
root, err = filepath.EvalSymlinks(root)
if err != nil {
return nil, err
}
// clean up old hosts/resolv.conf file. ignore errors
os.RemoveAll(filepath.Join(root, "hosts"))
os.RemoveAll(filepath.Join(root, "resolv.conf"))
runtime := &runc.Runc{
Command: cmd,
Log: filepath.Join(root, "runc-log.json"),
LogFormat: runc.JSON,
PdeathSignal: syscall.SIGKILL, // this can still leak the process
Setpgid: true,
// we don't execute runc with --rootless=(true|false) explicitly,
// so as to support non-runc runtimes
}
w := &runcExecutor{
runc: runtime,
root: root,
cgroupParent: opt.DefaultCgroupParent,
rootless: opt.Rootless,
networkProviders: networkProviders,
processMode: opt.ProcessMode,
idmap: opt.IdentityMapping,
noPivot: opt.NoPivot,
dns: opt.DNS,
oomScoreAdj: opt.OOMScoreAdj,
}
return w, nil
}
func (w *runcExecutor) Exec(ctx context.Context, meta executor.Meta, root cache.Mountable, mounts []executor.Mount, stdin io.ReadCloser, stdout, stderr io.WriteCloser) error {
provider, ok := w.networkProviders[meta.NetMode]
if !ok {
return errors.Errorf("unknown network mode %s", meta.NetMode)
}
namespace, err := provider.New()
if err != nil {
return err
}
defer namespace.Close()
if meta.NetMode == pb.NetMode_HOST {
logrus.Info("enabling HostNetworking")
}
resolvConf, err := oci.GetResolvConf(ctx, w.root, w.idmap, w.dns)
if err != nil {
return err
}
hostsFile, clean, err := oci.GetHostsFile(ctx, w.root, meta.ExtraHosts, w.idmap)
if err != nil {
return err
}
if clean != nil {
defer clean()
}
mountable, err := root.Mount(ctx, false)
if err != nil {
return err
}
rootMount, release, err := mountable.Mount()
if err != nil {
return err
}
if release != nil {
defer release()
}
id := identity.NewID()
bundle := filepath.Join(w.root, id)
if err := os.Mkdir(bundle, 0711); err != nil {
return err
}
defer os.RemoveAll(bundle)
identity := idtools.Identity{}
if w.idmap != nil {
identity = w.idmap.RootPair()
}
rootFSPath := filepath.Join(bundle, "rootfs")
if err := idtools.MkdirAllAndChown(rootFSPath, 0700, identity); err != nil {
return err
}
if err := mount.All(rootMount, rootFSPath); err != nil {
return err
}
defer mount.Unmount(rootFSPath, 0)
uid, gid, sgids, err := oci.GetUser(ctx, rootFSPath, meta.User)
if err != nil {
return err
}
f, err := os.Create(filepath.Join(bundle, "config.json"))
if err != nil {
return err
}
defer f.Close()
opts := []containerdoci.SpecOpts{oci.WithUIDGID(uid, gid, sgids)}
if meta.ReadonlyRootFS {
opts = append(opts, containerdoci.WithRootFSReadonly())
}
identity = idtools.Identity{
UID: int(uid),
GID: int(gid),
}
if w.idmap != nil {
identity, err = w.idmap.ToHost(identity)
if err != nil {
return err
}
}
if w.cgroupParent != "" {
var cgroupsPath string
lastSeparator := w.cgroupParent[len(w.cgroupParent)-1:]
if strings.Contains(w.cgroupParent, ".slice") && lastSeparator == ":" {
cgroupsPath = w.cgroupParent + id
} else {
cgroupsPath = filepath.Join("/", w.cgroupParent, "buildkit", id)
}
opts = append(opts, containerdoci.WithCgroup(cgroupsPath))
}
spec, cleanup, err := oci.GenerateSpec(ctx, meta, mounts, id, resolvConf, hostsFile, namespace, w.processMode, w.idmap, opts...)
if err != nil {
return err
}
defer cleanup()
spec.Root.Path = rootFSPath
if _, ok := root.(cache.ImmutableRef); ok { // TODO: pass in with mount, not ref type
spec.Root.Readonly = true
}
newp, err := fs.RootPath(rootFSPath, meta.Cwd)
if err != nil {
return errors.Wrapf(err, "working dir %s points to invalid target", newp)
}
if _, err := os.Stat(newp); err != nil {
if err := idtools.MkdirAllAndChown(newp, 0755, identity); err != nil {
return errors.Wrapf(err, "failed to create working directory %s", newp)
}
}
spec.Process.OOMScoreAdj = w.oomScoreAdj
if w.rootless {
if err := rootlessspecconv.ToRootless(spec); err != nil {
return err
}
}
if err := json.NewEncoder(f).Encode(spec); err != nil {
return err
}
// runCtx/killCtx is used for extra check in case the kill command blocks
runCtx, cancelRun := context.WithCancel(context.Background())
defer cancelRun()
done := make(chan struct{})
go func() {
for {
select {
case <-ctx.Done():
killCtx, timeout := context.WithTimeout(context.Background(), 7*time.Second)
if err := w.runc.Kill(killCtx, id, int(syscall.SIGKILL), nil); err != nil {
logrus.Errorf("failed to kill runc %s: %+v", id, err)
select {
case <-killCtx.Done():
timeout()
cancelRun()
return
default:
}
}
timeout()
select {
case <-time.After(50 * time.Millisecond):
case <-done:
return
}
case <-done:
return
}
}
}()
logrus.Debugf("> creating %s %v", id, meta.Args)
status, err := w.runc.Run(runCtx, id, bundle, &runc.CreateOpts{
IO: &forwardIO{stdin: stdin, stdout: stdout, stderr: stderr},
NoPivot: w.noPivot,
})
close(done)
if status != 0 || err != nil {
if err == nil {
err = errors.Errorf("exit code: %d", status)
}
select {
case <-ctx.Done():
return errors.Wrapf(ctx.Err(), err.Error())
default:
return err
}
}
return nil
}
type forwardIO struct {
stdin io.ReadCloser
stdout, stderr io.WriteCloser
}
func (s *forwardIO) Close() error {
return nil
}
func (s *forwardIO) Set(cmd *exec.Cmd) {
cmd.Stdin = s.stdin
cmd.Stdout = s.stdout
cmd.Stderr = s.stderr
}
func (s *forwardIO) Stdin() io.WriteCloser {
return nil
}
func (s *forwardIO) Stdout() io.ReadCloser {
return nil
}
func (s *forwardIO) Stderr() io.ReadCloser {
return nil
}

View File

@@ -1,641 +0,0 @@
package builder
import (
"archive/tar"
"bytes"
"context"
"encoding/csv"
"encoding/json"
"fmt"
"net"
"path"
"regexp"
"strconv"
"strings"
"github.com/containerd/containerd/platforms"
"github.com/docker/docker/builder/dockerignore"
controlapi "github.com/moby/buildkit/api/services/control"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
"github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb"
"github.com/moby/buildkit/frontend/gateway/client"
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/apicaps"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
)
const (
DefaultLocalNameContext = "context"
DefaultLocalNameDockerfile = "dockerfile"
keyTarget = "target"
keyFilename = "filename"
keyCacheFrom = "cache-from" // for registry only. deprecated in favor of keyCacheImports
keyCacheImports = "cache-imports" // JSON representation of []CacheOptionsEntry
keyCacheNS = "build-arg:BUILDKIT_CACHE_MOUNT_NS"
defaultDockerfileName = "Dockerfile"
dockerignoreFilename = ".dockerignore"
buildArgPrefix = "build-arg:"
labelPrefix = "label:"
keyNoCache = "no-cache"
keyTargetPlatform = "platform"
keyMultiPlatform = "multi-platform"
keyImageResolveMode = "image-resolve-mode"
keyGlobalAddHosts = "add-hosts"
keyForceNetwork = "force-network-mode"
keyOverrideCopyImage = "override-copy-image" // remove after CopyOp implemented
keyNameContext = "contextkey"
keyNameDockerfile = "dockerfilekey"
keyContextSubDir = "contextsubdir"
keyContextKeepGitDir = "build-arg:BUILDKIT_CONTEXT_KEEP_GIT_DIR"
)
var httpPrefix = regexp.MustCompile(`^https?://`)
var gitUrlPathWithFragmentSuffix = regexp.MustCompile(`\.git(?:#.+)?$`)
func Build(ctx context.Context, c client.Client) (*client.Result, error) {
opts := c.BuildOpts().Opts
caps := c.BuildOpts().LLBCaps
gwcaps := c.BuildOpts().Caps
marshalOpts := []llb.ConstraintsOpt{llb.WithCaps(caps)}
localNameContext := DefaultLocalNameContext
if v, ok := opts[keyNameContext]; ok {
localNameContext = v
}
forceLocalDockerfile := false
localNameDockerfile := DefaultLocalNameDockerfile
if v, ok := opts[keyNameDockerfile]; ok {
forceLocalDockerfile = true
localNameDockerfile = v
}
defaultBuildPlatform := platforms.DefaultSpec()
if workers := c.BuildOpts().Workers; len(workers) > 0 && len(workers[0].Platforms) > 0 {
defaultBuildPlatform = workers[0].Platforms[0]
}
buildPlatforms := []specs.Platform{defaultBuildPlatform}
targetPlatforms := []*specs.Platform{nil}
if v := opts[keyTargetPlatform]; v != "" {
var err error
targetPlatforms, err = parsePlatforms(v)
if err != nil {
return nil, err
}
}
resolveMode, err := parseResolveMode(opts[keyImageResolveMode])
if err != nil {
return nil, err
}
extraHosts, err := parseExtraHosts(opts[keyGlobalAddHosts])
if err != nil {
return nil, errors.Wrap(err, "failed to parse additional hosts")
}
defaultNetMode, err := parseNetMode(opts[keyForceNetwork])
if err != nil {
return nil, err
}
filename := opts[keyFilename]
if filename == "" {
filename = defaultDockerfileName
}
var ignoreCache []string
if v, ok := opts[keyNoCache]; ok {
if v == "" {
ignoreCache = []string{} // means all stages
} else {
ignoreCache = strings.Split(v, ",")
}
}
name := "load build definition from " + filename
src := llb.Local(localNameDockerfile,
llb.FollowPaths([]string{filename, filename + ".dockerignore"}),
llb.SessionID(c.BuildOpts().SessionID),
llb.SharedKeyHint(localNameDockerfile),
dockerfile2llb.WithInternalName(name),
)
fileop := useFileOp(opts, &caps)
var buildContext *llb.State
isNotLocalContext := false
if st, ok := detectGitContext(opts[localNameContext], opts[keyContextKeepGitDir]); ok {
if !forceLocalDockerfile {
src = *st
}
buildContext = st
} else if httpPrefix.MatchString(opts[localNameContext]) {
httpContext := llb.HTTP(opts[localNameContext], llb.Filename("context"), dockerfile2llb.WithInternalName("load remote build context"))
def, err := httpContext.Marshal(marshalOpts...)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal httpcontext")
}
res, err := c.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: "context",
Range: &client.FileRange{
Length: 1024,
},
})
if err != nil {
return nil, errors.Errorf("failed to read downloaded context")
}
if isArchive(dt) {
if fileop {
bc := llb.Scratch().File(llb.Copy(httpContext, "/context", "/", &llb.CopyInfo{
AttemptUnpack: true,
}))
if !forceLocalDockerfile {
src = bc
}
buildContext = &bc
} else {
copyImage := opts[keyOverrideCopyImage]
if copyImage == "" {
copyImage = dockerfile2llb.DefaultCopyImage
}
unpack := llb.Image(copyImage, dockerfile2llb.WithInternalName("helper image for file operations")).
Run(llb.Shlex("copy --unpack /src/context /out/"), llb.ReadonlyRootFS(), dockerfile2llb.WithInternalName("extracting build context"))
unpack.AddMount("/src", httpContext, llb.Readonly)
bc := unpack.AddMount("/out", llb.Scratch())
if !forceLocalDockerfile {
src = bc
}
buildContext = &bc
}
} else {
filename = "context"
if !forceLocalDockerfile {
src = httpContext
}
buildContext = &httpContext
isNotLocalContext = true
}
} else if (&gwcaps).Supports(gwpb.CapFrontendInputs) == nil {
inputs, err := c.Inputs(ctx)
if err != nil {
return nil, errors.Wrapf(err, "failed to get frontend inputs")
}
if !forceLocalDockerfile {
inputDockerfile, ok := inputs[DefaultLocalNameDockerfile]
if ok {
src = inputDockerfile
}
}
inputCtx, ok := inputs[DefaultLocalNameContext]
if ok {
buildContext = &inputCtx
isNotLocalContext = true
}
}
if buildContext != nil {
if sub, ok := opts[keyContextSubDir]; ok {
buildContext = scopeToSubDir(buildContext, fileop, sub)
}
}
def, err := src.Marshal(marshalOpts...)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal local source")
}
eg, ctx2 := errgroup.WithContext(ctx)
var dtDockerfile []byte
var dtDockerignore []byte
var dtDockerignoreDefault []byte
eg.Go(func() error {
res, err := c.Solve(ctx2, client.SolveRequest{
Definition: def.ToPB(),
})
if err != nil {
return errors.Wrapf(err, "failed to resolve dockerfile")
}
ref, err := res.SingleRef()
if err != nil {
return err
}
dtDockerfile, err = ref.ReadFile(ctx2, client.ReadRequest{
Filename: filename,
})
if err != nil {
return errors.Wrapf(err, "failed to read dockerfile")
}
dt, err := ref.ReadFile(ctx2, client.ReadRequest{
Filename: filename + ".dockerignore",
})
if err == nil {
dtDockerignore = dt
}
return nil
})
var excludes []string
if !isNotLocalContext {
eg.Go(func() error {
dockerignoreState := buildContext
if dockerignoreState == nil {
st := llb.Local(localNameContext,
llb.SessionID(c.BuildOpts().SessionID),
llb.FollowPaths([]string{dockerignoreFilename}),
llb.SharedKeyHint(localNameContext+"-"+dockerignoreFilename),
dockerfile2llb.WithInternalName("load "+dockerignoreFilename),
)
dockerignoreState = &st
}
def, err := dockerignoreState.Marshal(marshalOpts...)
if err != nil {
return err
}
res, err := c.Solve(ctx2, client.SolveRequest{
Definition: def.ToPB(),
})
if err != nil {
return err
}
ref, err := res.SingleRef()
if err != nil {
return err
}
dtDockerignoreDefault, err = ref.ReadFile(ctx2, client.ReadRequest{
Filename: dockerignoreFilename,
})
if err != nil {
return nil
}
return nil
})
}
if err := eg.Wait(); err != nil {
return nil, err
}
if dtDockerignore == nil {
dtDockerignore = dtDockerignoreDefault
}
if dtDockerignore != nil {
excludes, err = dockerignore.ReadAll(bytes.NewBuffer(dtDockerignore))
if err != nil {
return nil, errors.Wrap(err, "failed to parse dockerignore")
}
}
if _, ok := opts["cmdline"]; !ok {
ref, cmdline, ok := dockerfile2llb.DetectSyntax(bytes.NewBuffer(dtDockerfile))
if ok {
return forwardGateway(ctx, c, ref, cmdline)
}
}
exportMap := len(targetPlatforms) > 1
if v := opts[keyMultiPlatform]; v != "" {
b, err := strconv.ParseBool(v)
if err != nil {
return nil, errors.Errorf("invalid boolean value %s", v)
}
if !b && exportMap {
return nil, errors.Errorf("returning multiple target plaforms is not allowed")
}
exportMap = b
}
expPlatforms := &exptypes.Platforms{
Platforms: make([]exptypes.Platform, len(targetPlatforms)),
}
res := client.NewResult()
eg, ctx = errgroup.WithContext(ctx)
for i, tp := range targetPlatforms {
func(i int, tp *specs.Platform) {
eg.Go(func() error {
st, img, err := dockerfile2llb.Dockerfile2LLB(ctx, dtDockerfile, dockerfile2llb.ConvertOpt{
Target: opts[keyTarget],
MetaResolver: c,
BuildArgs: filter(opts, buildArgPrefix),
Labels: filter(opts, labelPrefix),
CacheIDNamespace: opts[keyCacheNS],
SessionID: c.BuildOpts().SessionID,
BuildContext: buildContext,
Excludes: excludes,
IgnoreCache: ignoreCache,
TargetPlatform: tp,
BuildPlatforms: buildPlatforms,
ImageResolveMode: resolveMode,
PrefixPlatform: exportMap,
ExtraHosts: extraHosts,
ForceNetMode: defaultNetMode,
OverrideCopyImage: opts[keyOverrideCopyImage],
LLBCaps: &caps,
})
if err != nil {
return errors.Wrapf(err, "failed to create LLB definition")
}
def, err := st.Marshal()
if err != nil {
return errors.Wrapf(err, "failed to marshal LLB definition")
}
config, err := json.Marshal(img)
if err != nil {
return errors.Wrapf(err, "failed to marshal image config")
}
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)
}
}
r, err := c.Solve(ctx, client.SolveRequest{
Definition: def.ToPB(),
CacheImports: cacheImports,
})
if err != nil {
return err
}
ref, err := r.SingleRef()
if err != nil {
return err
}
if !exportMap {
res.AddMeta(exptypes.ExporterImageConfigKey, config)
res.SetRef(ref)
} else {
p := platforms.DefaultSpec()
if tp != nil {
p = *tp
}
k := platforms.Format(p)
res.AddMeta(fmt.Sprintf("%s/%s", exptypes.ExporterImageConfigKey, k), config)
res.AddRef(k, ref)
expPlatforms.Platforms[i] = exptypes.Platform{
ID: k,
Platform: p,
}
}
return nil
})
}(i, tp)
}
if err := eg.Wait(); err != nil {
return nil, err
}
if exportMap {
dt, err := json.Marshal(expPlatforms)
if err != nil {
return nil, err
}
res.AddMeta(exptypes.ExporterPlatformsKey, dt)
}
return res, nil
}
func forwardGateway(ctx context.Context, c client.Client, ref string, cmdline string) (*client.Result, error) {
opts := c.BuildOpts().Opts
if opts == nil {
opts = map[string]string{}
}
opts["cmdline"] = cmdline
opts["source"] = ref
gwcaps := c.BuildOpts().Caps
var frontendInputs map[string]*pb.Definition
if (&gwcaps).Supports(gwpb.CapFrontendInputs) == nil {
inputs, err := c.Inputs(ctx)
if err != nil {
return nil, errors.Wrapf(err, "failed to get frontend inputs")
}
frontendInputs = make(map[string]*pb.Definition)
for name, state := range inputs {
def, err := state.Marshal()
if err != nil {
return nil, err
}
frontendInputs[name] = def.ToPB()
}
}
return c.Solve(ctx, client.SolveRequest{
Frontend: "gateway.v0",
FrontendOpt: opts,
FrontendInputs: frontendInputs,
})
}
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
}
func detectGitContext(ref, gitContext string) (*llb.State, bool) {
found := false
if httpPrefix.MatchString(ref) && gitUrlPathWithFragmentSuffix.MatchString(ref) {
found = true
}
keepGit := false
if gitContext != "" {
if v, err := strconv.ParseBool(gitContext); err == nil {
keepGit = v
}
}
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{dockerfile2llb.WithInternalName("load git source " + ref)}
if keepGit {
gitOpts = append(gitOpts, llb.KeepGitDir())
}
st := llb.Git(parts[0], branch, gitOpts...)
return &st, true
}
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 parsePlatforms(v string) ([]*specs.Platform, error) {
var pp []*specs.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)
}
p = platforms.Normalize(p)
pp = append(pp, &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 {
parts := strings.SplitN(field, "=", 2)
if len(parts) != 2 {
return nil, errors.Errorf("invalid key-value pair %s", field)
}
key := strings.ToLower(parts[0])
val := strings.ToLower(parts[1])
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 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 useFileOp(args map[string]string, caps *apicaps.CapSet) bool {
enabled := true
if v, ok := args["build-arg:BUILDKIT_DISABLE_FILEOP"]; ok {
if b, err := strconv.ParseBool(v); err == nil {
enabled = !b
}
}
return enabled && caps != nil && caps.Supports(pb.CapFileBase) == nil
}
func scopeToSubDir(c *llb.State, fileop bool, dir string) *llb.State {
if fileop {
bc := llb.Scratch().File(llb.Copy(*c, dir, "/", &llb.CopyInfo{
CopyDirContentsOnly: true,
}))
return &bc
}
unpack := llb.Image(dockerfile2llb.DefaultCopyImage, dockerfile2llb.WithInternalName("helper image for file operations")).
Run(llb.Shlexf("copy %s/. /out/", path.Join("/src", dir)), llb.ReadonlyRootFS(), dockerfile2llb.WithInternalName("filtering build context"))
unpack.AddMount("/src", *c, llb.Readonly)
bc := unpack.AddMount("/out", llb.Scratch())
return &bc
}

View File

@@ -1,46 +0,0 @@
// Package command contains the set of Dockerfile commands.
package command
// Define constants for the command strings
const (
Add = "add"
Arg = "arg"
Cmd = "cmd"
Copy = "copy"
Entrypoint = "entrypoint"
Env = "env"
Expose = "expose"
From = "from"
Healthcheck = "healthcheck"
Label = "label"
Maintainer = "maintainer"
Onbuild = "onbuild"
Run = "run"
Shell = "shell"
StopSignal = "stopsignal"
User = "user"
Volume = "volume"
Workdir = "workdir"
)
// Commands is list of all Dockerfile commands
var Commands = map[string]struct{}{
Add: {},
Arg: {},
Cmd: {},
Copy: {},
Entrypoint: {},
Env: {},
Expose: {},
From: {},
Healthcheck: {},
Label: {},
Maintainer: {},
Onbuild: {},
Run: {},
Shell: {},
StopSignal: {},
User: {},
Volume: {},
Workdir: {},
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +0,0 @@
// +build !dfrunmount
package dockerfile2llb
import (
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
)
func detectRunMount(cmd *command, allDispatchStates *dispatchStates) bool {
return false
}
func dispatchRunMounts(d *dispatchState, c *instructions.RunCommand, sources []*dispatchState, opt dispatchOpt) ([]llb.RunOption, error) {
return nil, nil
}

View File

@@ -1,12 +0,0 @@
// +build !dfrunnetwork
package dockerfile2llb
import (
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
)
func dispatchRunNetwork(c *instructions.RunCommand) (llb.RunOption, error) {
return nil, nil
}

View File

@@ -1,12 +0,0 @@
// +build !dfrunsecurity
package dockerfile2llb
import (
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
)
func dispatchRunSecurity(c *instructions.RunCommand) (llb.RunOption, error) {
return nil, nil
}

View File

@@ -1,13 +0,0 @@
// +build dfrunmount,!dfsecrets
package dockerfile2llb
import (
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/pkg/errors"
)
func dispatchSecret(m *instructions.Mount) (llb.RunOption, error) {
return nil, errors.Errorf("secret mounts not allowed")
}

View File

@@ -1,13 +0,0 @@
// +build dfrunmount,!dfssh
package dockerfile2llb
import (
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/pkg/errors"
)
func dispatchSSH(m *instructions.Mount) (llb.RunOption, error) {
return nil, errors.Errorf("ssh mounts not allowed")
}

View File

@@ -1,156 +0,0 @@
// +build dfrunmount
package dockerfile2llb
import (
"fmt"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/moby/buildkit/solver/pb"
"github.com/pkg/errors"
)
func detectRunMount(cmd *command, allDispatchStates *dispatchStates) bool {
if c, ok := cmd.Command.(*instructions.RunCommand); ok {
mounts := instructions.GetMounts(c)
sources := make([]*dispatchState, len(mounts))
for i, mount := range mounts {
if mount.From == "" && mount.Type == instructions.MountTypeCache {
mount.From = emptyImageName
}
from := mount.From
if from == "" || mount.Type == instructions.MountTypeTmpfs {
continue
}
stn, ok := allDispatchStates.findStateByName(from)
if !ok {
stn = &dispatchState{
stage: instructions.Stage{BaseName: from},
deps: make(map[*dispatchState]struct{}),
unregistered: true,
}
}
sources[i] = stn
}
cmd.sources = sources
return true
}
return false
}
func setCacheUIDGIDFileOp(m *instructions.Mount, st llb.State) llb.State {
uid := 0
gid := 0
mode := os.FileMode(0755)
if m.UID != nil {
uid = int(*m.UID)
}
if m.GID != nil {
gid = int(*m.GID)
}
if m.Mode != nil {
mode = os.FileMode(*m.Mode)
}
return st.File(llb.Mkdir("/cache", mode, llb.WithUIDGID(uid, gid)), llb.WithCustomName("[internal] settings cache mount permissions"))
}
func setCacheUIDGID(m *instructions.Mount, st llb.State, fileop bool) llb.State {
if fileop {
return setCacheUIDGIDFileOp(m, st)
}
var b strings.Builder
if m.UID != nil {
b.WriteString(fmt.Sprintf("chown %d /mnt/cache;", *m.UID))
}
if m.GID != nil {
b.WriteString(fmt.Sprintf("chown :%d /mnt/cache;", *m.GID))
}
if m.Mode != nil {
b.WriteString(fmt.Sprintf("chmod %s /mnt/cache;", strconv.FormatUint(*m.Mode, 8)))
}
return llb.Image("busybox").Run(llb.Shlex(fmt.Sprintf("sh -c 'mkdir -p /mnt/cache;%s'", b.String())), llb.WithCustomName("[internal] settings cache mount permissions")).AddMount("/mnt", st)
}
func dispatchRunMounts(d *dispatchState, c *instructions.RunCommand, sources []*dispatchState, opt dispatchOpt) ([]llb.RunOption, error) {
var out []llb.RunOption
mounts := instructions.GetMounts(c)
for i, mount := range mounts {
if mount.From == "" && mount.Type == instructions.MountTypeCache {
mount.From = emptyImageName
}
st := opt.buildContext
if mount.From != "" {
st = sources[i].state
}
var mountOpts []llb.MountOption
if mount.Type == instructions.MountTypeTmpfs {
st = llb.Scratch()
mountOpts = append(mountOpts, llb.Tmpfs())
}
if mount.Type == instructions.MountTypeSecret {
secret, err := dispatchSecret(mount)
if err != nil {
return nil, err
}
out = append(out, secret)
continue
}
if mount.Type == instructions.MountTypeSSH {
ssh, err := dispatchSSH(mount)
if err != nil {
return nil, err
}
out = append(out, ssh)
continue
}
if mount.ReadOnly {
mountOpts = append(mountOpts, llb.Readonly)
} else if mount.Type == instructions.MountTypeBind && opt.llbCaps.Supports(pb.CapExecMountBindReadWriteNoOuput) == nil {
mountOpts = append(mountOpts, llb.ForceNoOutput)
}
if mount.Type == instructions.MountTypeCache {
sharing := llb.CacheMountShared
if mount.CacheSharing == instructions.MountSharingPrivate {
sharing = llb.CacheMountPrivate
}
if mount.CacheSharing == instructions.MountSharingLocked {
sharing = llb.CacheMountLocked
}
if mount.CacheID == "" {
mount.CacheID = path.Clean(mount.Target)
}
mountOpts = append(mountOpts, llb.AsPersistentCacheDir(opt.cacheIDNamespace+"/"+mount.CacheID, sharing))
}
target := mount.Target
if !filepath.IsAbs(filepath.Clean(mount.Target)) {
target = filepath.Join("/", d.state.GetDir(), mount.Target)
}
if target == "/" {
return nil, errors.Errorf("invalid mount target %q", target)
}
if src := path.Join("/", mount.Source); src != "/" {
mountOpts = append(mountOpts, llb.SourcePath(src))
} else {
if mount.UID != nil || mount.GID != nil || mount.Mode != nil {
st = setCacheUIDGID(mount, st, useFileOp(opt.buildArgValues, opt.llbCaps))
mountOpts = append(mountOpts, llb.SourcePath("/cache"))
}
}
out = append(out, llb.AddMount(target, st, mountOpts...))
if mount.From == "" {
d.ctxPaths[path.Join("/", filepath.ToSlash(mount.Source))] = struct{}{}
}
}
return out, nil
}

View File

@@ -1,26 +0,0 @@
// +build dfrunnetwork
package dockerfile2llb
import (
"github.com/pkg/errors"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/moby/buildkit/solver/pb"
)
func dispatchRunNetwork(c *instructions.RunCommand) (llb.RunOption, error) {
network := instructions.GetNetwork(c)
switch network {
case instructions.NetworkDefault:
return nil, nil
case instructions.NetworkNone:
return llb.Network(pb.NetMode_NONE), nil
case instructions.NetworkHost:
return llb.Network(pb.NetMode_HOST), nil
default:
return nil, errors.Errorf("unsupported network mode %q", network)
}
}

View File

@@ -1,24 +0,0 @@
// +build dfrunsecurity
package dockerfile2llb
import (
"github.com/pkg/errors"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/moby/buildkit/solver/pb"
)
func dispatchRunSecurity(c *instructions.RunCommand) (llb.RunOption, error) {
security := instructions.GetSecurity(c)
switch security {
case instructions.SecurityInsecure:
return llb.Security(pb.SecurityMode_INSECURE), nil
case instructions.SecuritySandbox:
return llb.Security(pb.SecurityMode_SANDBOX), nil
default:
return nil, errors.Errorf("unsupported security mode %q", security)
}
}

View File

@@ -1,54 +0,0 @@
// +build dfsecrets
package dockerfile2llb
import (
"path"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/pkg/errors"
)
func dispatchSecret(m *instructions.Mount) (llb.RunOption, error) {
id := m.CacheID
if m.Source != "" {
id = m.Source
}
if id == "" {
if m.Target == "" {
return nil, errors.Errorf("one of source, target required")
}
id = path.Base(m.Target)
}
target := m.Target
if target == "" {
target = "/run/secrets/" + path.Base(id)
}
opts := []llb.SecretOption{llb.SecretID(id)}
if !m.Required {
opts = append(opts, llb.SecretOptional)
}
if m.UID != nil || m.GID != nil || m.Mode != nil {
var uid, gid, mode int
if m.UID != nil {
uid = int(*m.UID)
}
if m.GID != nil {
gid = int(*m.GID)
}
if m.Mode != nil {
mode = int(*m.Mode)
} else {
mode = 0400
}
opts = append(opts, llb.SecretFileOpt(uid, gid, mode))
}
return llb.AddSecret(target, opts...), nil
}

View File

@@ -1,42 +0,0 @@
// +build dfssh
package dockerfile2llb
import (
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/pkg/errors"
)
func dispatchSSH(m *instructions.Mount) (llb.RunOption, error) {
if m.Source != "" {
return nil, errors.Errorf("ssh does not support source")
}
opts := []llb.SSHOption{llb.SSHID(m.CacheID)}
if m.Target != "" {
opts = append(opts, llb.SSHSocketTarget(m.Target))
}
if !m.Required {
opts = append(opts, llb.SSHOptional)
}
if m.UID != nil || m.GID != nil || m.Mode != nil {
var uid, gid, mode int
if m.UID != nil {
uid = int(*m.UID)
}
if m.GID != nil {
gid = int(*m.GID)
}
if m.Mode != nil {
mode = int(*m.Mode)
} else {
mode = 0600
}
opts = append(opts, llb.SSHSocketOpt(m.Target, uid, gid, mode))
}
return llb.AddSSHSocket(opts...), nil
}

View File

@@ -1,7 +0,0 @@
// +build !windows
package dockerfile2llb
func defaultShell() []string {
return []string{"/bin/sh", "-c"}
}

View File

@@ -1,7 +0,0 @@
// +build windows
package dockerfile2llb
func defaultShell() []string {
return []string{"cmd", "/S", "/C"}
}

View File

@@ -1,38 +0,0 @@
package dockerfile2llb
import (
"bufio"
"io"
"regexp"
"strings"
)
const keySyntax = "syntax"
var reDirective = regexp.MustCompile(`^#\s*([a-zA-Z][a-zA-Z0-9]*)\s*=\s*(.+?)\s*$`)
func DetectSyntax(r io.Reader) (string, string, bool) {
directives := ParseDirectives(r)
if len(directives) == 0 {
return "", "", false
}
v, ok := directives[keySyntax]
if !ok {
return "", "", false
}
p := strings.SplitN(v, " ", 2)
return p[0], v, true
}
func ParseDirectives(r io.Reader) map[string]string {
m := map[string]string{}
s := bufio.NewScanner(r)
for s.Scan() {
match := reDirective.FindStringSubmatch(s.Text())
if len(match) == 0 {
return m
}
m[strings.ToLower(match[1])] = match[2]
}
return m
}

View File

@@ -1,79 +0,0 @@
package dockerfile2llb
import (
"time"
"github.com/docker/docker/api/types/strslice"
"github.com/moby/buildkit/util/system"
specs "github.com/opencontainers/image-spec/specs-go/v1"
)
// HealthConfig holds configuration settings for the HEALTHCHECK feature.
type HealthConfig struct {
// Test is the test to perform to check that the container is healthy.
// An empty slice means to inherit the default.
// The options are:
// {} : inherit healthcheck
// {"NONE"} : disable healthcheck
// {"CMD", args...} : exec arguments directly
// {"CMD-SHELL", command} : run command with system's default shell
Test []string `json:",omitempty"`
// Zero means to inherit. Durations are expressed as integer nanoseconds.
Interval time.Duration `json:",omitempty"` // Interval is the time to wait between checks.
Timeout time.Duration `json:",omitempty"` // Timeout is the time to wait before considering the check to have hung.
StartPeriod time.Duration `json:",omitempty"` // The start period for the container to initialize before the retries starts to count down.
// Retries is the number of consecutive failures needed to consider a container as unhealthy.
// Zero means inherit.
Retries int `json:",omitempty"`
}
// ImageConfig is a docker compatible config for an image
type ImageConfig struct {
specs.ImageConfig
Healthcheck *HealthConfig `json:",omitempty"` // Healthcheck describes how to check the container is healthy
ArgsEscaped bool `json:",omitempty"` // True if command is already escaped (Windows specific)
// NetworkDisabled bool `json:",omitempty"` // Is network disabled
// MacAddress string `json:",omitempty"` // Mac Address of the container
OnBuild []string // ONBUILD metadata that were defined on the image Dockerfile
StopTimeout *int `json:",omitempty"` // Timeout (in seconds) to stop a container
Shell strslice.StrSlice `json:",omitempty"` // Shell for shell-form of RUN, CMD, ENTRYPOINT
}
// Image is the JSON structure which describes some basic information about the image.
// This provides the `application/vnd.oci.image.config.v1+json` mediatype when marshalled to JSON.
type Image struct {
specs.Image
// Config defines the execution parameters which should be used as a base when running a container using the image.
Config ImageConfig `json:"config,omitempty"`
// Variant defines platform variant. To be added to OCI.
Variant string `json:"variant,omitempty"`
}
func clone(src Image) Image {
img := src
img.Config = src.Config
img.Config.Env = append([]string{}, src.Config.Env...)
img.Config.Cmd = append([]string{}, src.Config.Cmd...)
img.Config.Entrypoint = append([]string{}, src.Config.Entrypoint...)
return img
}
func emptyImage(platform specs.Platform) Image {
img := Image{
Image: specs.Image{
Architecture: platform.Architecture,
OS: platform.OS,
},
Variant: platform.Variant,
}
img.RootFS.Type = "layers"
img.Config.WorkingDir = "/"
img.Config.Env = []string{"PATH=" + system.DefaultPathEnv}
return img
}

View File

@@ -1,58 +0,0 @@
package dockerfile2llb
import (
"github.com/containerd/containerd/platforms"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
specs "github.com/opencontainers/image-spec/specs-go/v1"
)
type platformOpt struct {
targetPlatform specs.Platform
buildPlatforms []specs.Platform
implicitTarget bool
}
func buildPlatformOpt(opt *ConvertOpt) *platformOpt {
buildPlatforms := opt.BuildPlatforms
targetPlatform := opt.TargetPlatform
implicitTargetPlatform := false
if opt.TargetPlatform != nil && opt.BuildPlatforms == nil {
buildPlatforms = []specs.Platform{*opt.TargetPlatform}
}
if len(buildPlatforms) == 0 {
buildPlatforms = []specs.Platform{platforms.DefaultSpec()}
}
if opt.TargetPlatform == nil {
implicitTargetPlatform = true
targetPlatform = &buildPlatforms[0]
}
return &platformOpt{
targetPlatform: *targetPlatform,
buildPlatforms: buildPlatforms,
implicitTarget: implicitTargetPlatform,
}
}
func getPlatformArgs(po *platformOpt) []instructions.KeyValuePairOptional {
bp := po.buildPlatforms[0]
tp := po.targetPlatform
m := map[string]string{
"BUILDPLATFORM": platforms.Format(bp),
"BUILDOS": bp.OS,
"BUILDARCH": bp.Architecture,
"BUILDVARIANT": bp.Variant,
"TARGETPLATFORM": platforms.Format(tp),
"TARGETOS": tp.OS,
"TARGETARCH": tp.Architecture,
"TARGETVARIANT": tp.Variant,
}
opts := make([]instructions.KeyValuePairOptional, 0, len(m))
for k, v := range m {
s := v
opts = append(opts, instructions.KeyValuePairOptional{Key: k, Value: &s})
}
return opts
}

View File

@@ -1,200 +0,0 @@
package instructions
import (
"fmt"
"strings"
)
// FlagType is the type of the build flag
type FlagType int
const (
boolType FlagType = iota
stringType
stringsType
)
// BFlags contains all flags information for the builder
type BFlags struct {
Args []string // actual flags/args from cmd line
flags map[string]*Flag
used map[string]*Flag
Err error
}
// Flag contains all information for a flag
type Flag struct {
bf *BFlags
name string
flagType FlagType
Value string
StringValues []string
}
// NewBFlags returns the new BFlags struct
func NewBFlags() *BFlags {
return &BFlags{
flags: make(map[string]*Flag),
used: make(map[string]*Flag),
}
}
// NewBFlagsWithArgs returns the new BFlags struct with Args set to args
func NewBFlagsWithArgs(args []string) *BFlags {
flags := NewBFlags()
flags.Args = args
return flags
}
// AddBool adds a bool flag to BFlags
// Note, any error will be generated when Parse() is called (see Parse).
func (bf *BFlags) AddBool(name string, def bool) *Flag {
flag := bf.addFlag(name, boolType)
if flag == nil {
return nil
}
if def {
flag.Value = "true"
} else {
flag.Value = "false"
}
return flag
}
// AddString adds a string flag to BFlags
// Note, any error will be generated when Parse() is called (see Parse).
func (bf *BFlags) AddString(name string, def string) *Flag {
flag := bf.addFlag(name, stringType)
if flag == nil {
return nil
}
flag.Value = def
return flag
}
// AddStrings adds a string flag to BFlags that can match multiple values
func (bf *BFlags) AddStrings(name string) *Flag {
flag := bf.addFlag(name, stringsType)
if flag == nil {
return nil
}
return flag
}
// addFlag is a generic func used by the other AddXXX() func
// to add a new flag to the BFlags struct.
// Note, any error will be generated when Parse() is called (see Parse).
func (bf *BFlags) addFlag(name string, flagType FlagType) *Flag {
if _, ok := bf.flags[name]; ok {
bf.Err = fmt.Errorf("Duplicate flag defined: %s", name)
return nil
}
newFlag := &Flag{
bf: bf,
name: name,
flagType: flagType,
}
bf.flags[name] = newFlag
return newFlag
}
// IsUsed checks if the flag is used
func (fl *Flag) IsUsed() bool {
if _, ok := fl.bf.used[fl.name]; ok {
return true
}
return false
}
// IsTrue checks if a bool flag is true
func (fl *Flag) IsTrue() bool {
if fl.flagType != boolType {
// Should never get here
panic(fmt.Errorf("Trying to use IsTrue on a non-boolean: %s", fl.name))
}
return fl.Value == "true"
}
// Parse parses and checks if the BFlags is valid.
// Any error noticed during the AddXXX() funcs will be generated/returned
// here. We do this because an error during AddXXX() is more like a
// compile time error so it doesn't matter too much when we stop our
// processing as long as we do stop it, so this allows the code
// around AddXXX() to be just:
// defFlag := AddString("description", "")
// w/o needing to add an if-statement around each one.
func (bf *BFlags) Parse() error {
// If there was an error while defining the possible flags
// go ahead and bubble it back up here since we didn't do it
// earlier in the processing
if bf.Err != nil {
return fmt.Errorf("Error setting up flags: %s", bf.Err)
}
for _, arg := range bf.Args {
if !strings.HasPrefix(arg, "--") {
return fmt.Errorf("Arg should start with -- : %s", arg)
}
if arg == "--" {
return nil
}
arg = arg[2:]
value := ""
index := strings.Index(arg, "=")
if index >= 0 {
value = arg[index+1:]
arg = arg[:index]
}
flag, ok := bf.flags[arg]
if !ok {
return fmt.Errorf("Unknown flag: %s", arg)
}
if _, ok = bf.used[arg]; ok && flag.flagType != stringsType {
return fmt.Errorf("Duplicate flag specified: %s", arg)
}
bf.used[arg] = flag
switch flag.flagType {
case boolType:
// value == "" is only ok if no "=" was specified
if index >= 0 && value == "" {
return fmt.Errorf("Missing a value on flag: %s", arg)
}
lower := strings.ToLower(value)
if lower == "" {
flag.Value = "true"
} else if lower == "true" || lower == "false" {
flag.Value = lower
} else {
return fmt.Errorf("Expecting boolean value for flag %s, not: %s", arg, value)
}
case stringType:
if index < 0 {
return fmt.Errorf("Missing a value on flag: %s", arg)
}
flag.Value = value
case stringsType:
if index < 0 {
return fmt.Errorf("Missing a value on flag: %s", arg)
}
flag.StringValues = append(flag.StringValues, value)
default:
panic("No idea what kind of flag we have! Should never get here!")
}
}
return nil
}

View File

@@ -1,451 +0,0 @@
package instructions
import (
"errors"
"strings"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/strslice"
)
// KeyValuePair represent an arbitrary named value (useful in slice instead of map[string] string to preserve ordering)
type KeyValuePair struct {
Key string
Value string
}
func (kvp *KeyValuePair) String() string {
return kvp.Key + "=" + kvp.Value
}
// KeyValuePairOptional is the same as KeyValuePair but Value is optional
type KeyValuePairOptional struct {
Key string
Value *string
}
func (kvpo *KeyValuePairOptional) ValueString() string {
v := ""
if kvpo.Value != nil {
v = *kvpo.Value
}
return v
}
// Command is implemented by every command present in a dockerfile
type Command interface {
Name() string
}
// KeyValuePairs is a slice of KeyValuePair
type KeyValuePairs []KeyValuePair
// withNameAndCode is the base of every command in a Dockerfile (String() returns its source code)
type withNameAndCode struct {
code string
name string
}
func (c *withNameAndCode) String() string {
return c.code
}
// Name of the command
func (c *withNameAndCode) Name() string {
return c.name
}
func newWithNameAndCode(req parseRequest) withNameAndCode {
return withNameAndCode{code: strings.TrimSpace(req.original), name: req.command}
}
// SingleWordExpander is a provider for variable expansion where 1 word => 1 output
type SingleWordExpander func(word string) (string, error)
// SupportsSingleWordExpansion interface marks a command as supporting variable expansion
type SupportsSingleWordExpansion interface {
Expand(expander SingleWordExpander) error
}
// PlatformSpecific adds platform checks to a command
type PlatformSpecific interface {
CheckPlatform(platform string) error
}
func expandKvp(kvp KeyValuePair, expander SingleWordExpander) (KeyValuePair, error) {
key, err := expander(kvp.Key)
if err != nil {
return KeyValuePair{}, err
}
value, err := expander(kvp.Value)
if err != nil {
return KeyValuePair{}, err
}
return KeyValuePair{Key: key, Value: value}, nil
}
func expandKvpsInPlace(kvps KeyValuePairs, expander SingleWordExpander) error {
for i, kvp := range kvps {
newKvp, err := expandKvp(kvp, expander)
if err != nil {
return err
}
kvps[i] = newKvp
}
return nil
}
func expandSliceInPlace(values []string, expander SingleWordExpander) error {
for i, v := range values {
newValue, err := expander(v)
if err != nil {
return err
}
values[i] = newValue
}
return nil
}
// EnvCommand : ENV key1 value1 [keyN valueN...]
type EnvCommand struct {
withNameAndCode
Env KeyValuePairs // kvp slice instead of map to preserve ordering
}
// Expand variables
func (c *EnvCommand) Expand(expander SingleWordExpander) error {
return expandKvpsInPlace(c.Env, expander)
}
// MaintainerCommand : MAINTAINER maintainer_name
type MaintainerCommand struct {
withNameAndCode
Maintainer string
}
// NewLabelCommand creates a new 'LABEL' command
func NewLabelCommand(k string, v string, NoExp bool) *LabelCommand {
kvp := KeyValuePair{Key: k, Value: v}
c := "LABEL "
c += kvp.String()
nc := withNameAndCode{code: c, name: "label"}
cmd := &LabelCommand{
withNameAndCode: nc,
Labels: KeyValuePairs{
kvp,
},
noExpand: NoExp,
}
return cmd
}
// LabelCommand : LABEL some json data describing the image
//
// Sets the Label variable foo to bar,
//
type LabelCommand struct {
withNameAndCode
Labels KeyValuePairs // kvp slice instead of map to preserve ordering
noExpand bool
}
// Expand variables
func (c *LabelCommand) Expand(expander SingleWordExpander) error {
if c.noExpand {
return nil
}
return expandKvpsInPlace(c.Labels, expander)
}
// SourcesAndDest represent a list of source files and a destination
type SourcesAndDest []string
// Sources list the source paths
func (s SourcesAndDest) Sources() []string {
res := make([]string, len(s)-1)
copy(res, s[:len(s)-1])
return res
}
// Dest path of the operation
func (s SourcesAndDest) Dest() string {
return s[len(s)-1]
}
// AddCommand : ADD foo /path
//
// Add the file 'foo' to '/path'. Tarball and Remote URL (http, https) handling
// exist here. If you do not wish to have this automatic handling, use COPY.
//
type AddCommand struct {
withNameAndCode
SourcesAndDest
Chown string
}
// Expand variables
func (c *AddCommand) Expand(expander SingleWordExpander) error {
return expandSliceInPlace(c.SourcesAndDest, expander)
}
// CopyCommand : COPY foo /path
//
// Same as 'ADD' but without the tar and remote url handling.
//
type CopyCommand struct {
withNameAndCode
SourcesAndDest
From string
Chown string
}
// Expand variables
func (c *CopyCommand) Expand(expander SingleWordExpander) error {
expandedChown, err := expander(c.Chown)
if err != nil {
return err
}
c.Chown = expandedChown
return expandSliceInPlace(c.SourcesAndDest, expander)
}
// OnbuildCommand : ONBUILD <some other command>
type OnbuildCommand struct {
withNameAndCode
Expression string
}
// WorkdirCommand : WORKDIR /tmp
//
// Set the working directory for future RUN/CMD/etc statements.
//
type WorkdirCommand struct {
withNameAndCode
Path string
}
// Expand variables
func (c *WorkdirCommand) Expand(expander SingleWordExpander) error {
p, err := expander(c.Path)
if err != nil {
return err
}
c.Path = p
return nil
}
// ShellDependantCmdLine represents a cmdline optionally prepended with the shell
type ShellDependantCmdLine struct {
CmdLine strslice.StrSlice
PrependShell bool
}
// RunCommand : RUN some command yo
//
// run a command and commit the image. Args are automatically prepended with
// the current SHELL which defaults to 'sh -c' under linux or 'cmd /S /C' under
// Windows, in the event there is only one argument The difference in processing:
//
// RUN echo hi # sh -c echo hi (Linux)
// RUN echo hi # cmd /S /C echo hi (Windows)
// RUN [ "echo", "hi" ] # echo hi
//
type RunCommand struct {
withNameAndCode
withExternalData
ShellDependantCmdLine
}
// CmdCommand : CMD foo
//
// Set the default command to run in the container (which may be empty).
// Argument handling is the same as RUN.
//
type CmdCommand struct {
withNameAndCode
ShellDependantCmdLine
}
// HealthCheckCommand : HEALTHCHECK foo
//
// Set the default healthcheck command to run in the container (which may be empty).
// Argument handling is the same as RUN.
//
type HealthCheckCommand struct {
withNameAndCode
Health *container.HealthConfig
}
// EntrypointCommand : ENTRYPOINT /usr/sbin/nginx
//
// Set the entrypoint to /usr/sbin/nginx. Will accept the CMD as the arguments
// to /usr/sbin/nginx. Uses the default shell if not in JSON format.
//
// Handles command processing similar to CMD and RUN, only req.runConfig.Entrypoint
// is initialized at newBuilder time instead of through argument parsing.
//
type EntrypointCommand struct {
withNameAndCode
ShellDependantCmdLine
}
// ExposeCommand : EXPOSE 6667/tcp 7000/tcp
//
// Expose ports for links and port mappings. This all ends up in
// req.runConfig.ExposedPorts for runconfig.
//
type ExposeCommand struct {
withNameAndCode
Ports []string
}
// UserCommand : USER foo
//
// Set the user to 'foo' for future commands and when running the
// ENTRYPOINT/CMD at container run time.
//
type UserCommand struct {
withNameAndCode
User string
}
// Expand variables
func (c *UserCommand) Expand(expander SingleWordExpander) error {
p, err := expander(c.User)
if err != nil {
return err
}
c.User = p
return nil
}
// VolumeCommand : VOLUME /foo
//
// Expose the volume /foo for use. Will also accept the JSON array form.
//
type VolumeCommand struct {
withNameAndCode
Volumes []string
}
// Expand variables
func (c *VolumeCommand) Expand(expander SingleWordExpander) error {
return expandSliceInPlace(c.Volumes, expander)
}
// StopSignalCommand : STOPSIGNAL signal
//
// Set the signal that will be used to kill the container.
type StopSignalCommand struct {
withNameAndCode
Signal string
}
// Expand variables
func (c *StopSignalCommand) Expand(expander SingleWordExpander) error {
p, err := expander(c.Signal)
if err != nil {
return err
}
c.Signal = p
return nil
}
// CheckPlatform checks that the command is supported in the target platform
func (c *StopSignalCommand) CheckPlatform(platform string) error {
if platform == "windows" {
return errors.New("The daemon on this platform does not support the command stopsignal")
}
return nil
}
// ArgCommand : ARG name[=value]
//
// Adds the variable foo to the trusted list of variables that can be passed
// to builder using the --build-arg flag for expansion/substitution or passing to 'run'.
// Dockerfile author may optionally set a default value of this variable.
type ArgCommand struct {
withNameAndCode
KeyValuePairOptional
}
// Expand variables
func (c *ArgCommand) Expand(expander SingleWordExpander) error {
p, err := expander(c.Key)
if err != nil {
return err
}
c.Key = p
if c.Value != nil {
p, err = expander(*c.Value)
if err != nil {
return err
}
c.Value = &p
}
return nil
}
// ShellCommand : SHELL powershell -command
//
// Set the non-default shell to use.
type ShellCommand struct {
withNameAndCode
Shell strslice.StrSlice
}
// Stage represents a single stage in a multi-stage build
type Stage struct {
Name string
Commands []Command
BaseName string
SourceCode string
Platform string
}
// AddCommand to the stage
func (s *Stage) AddCommand(cmd Command) {
// todo: validate cmd type
s.Commands = append(s.Commands, cmd)
}
// IsCurrentStage check if the stage name is the current stage
func IsCurrentStage(s []Stage, name string) bool {
if len(s) == 0 {
return false
}
return s[len(s)-1].Name == name
}
// CurrentStage return the last stage in a slice
func CurrentStage(s []Stage) (*Stage, error) {
if len(s) == 0 {
return nil, errors.New("No build stage in current context")
}
return &s[len(s)-1], nil
}
// HasStage looks for the presence of a given stage name
func HasStage(s []Stage, name string) (int, bool) {
for i, stage := range s {
// Stage name is case-insensitive by design
if strings.EqualFold(stage.Name, name) {
return i, true
}
}
return -1, false
}
type withExternalData struct {
m map[interface{}]interface{}
}
func (c *withExternalData) getExternalValue(k interface{}) interface{} {
return c.m[k]
}
func (c *withExternalData) setExternalValue(k, v interface{}) {
if c.m == nil {
c.m = map[interface{}]interface{}{}
}
c.m[k] = v
}

View File

@@ -1,7 +0,0 @@
// +build !dfsecrets
package instructions
func isSecretMountsSupported() bool {
return false
}

View File

@@ -1,7 +0,0 @@
// +build !dfssh
package instructions
func isSSHMountsSupported() bool {
return false
}

View File

@@ -1,263 +0,0 @@
// +build dfrunmount
package instructions
import (
"encoding/csv"
"strconv"
"strings"
"github.com/pkg/errors"
)
const MountTypeBind = "bind"
const MountTypeCache = "cache"
const MountTypeTmpfs = "tmpfs"
const MountTypeSecret = "secret"
const MountTypeSSH = "ssh"
var allowedMountTypes = map[string]struct{}{
MountTypeBind: {},
MountTypeCache: {},
MountTypeTmpfs: {},
MountTypeSecret: {},
MountTypeSSH: {},
}
const MountSharingShared = "shared"
const MountSharingPrivate = "private"
const MountSharingLocked = "locked"
var allowedSharingTypes = map[string]struct{}{
MountSharingShared: {},
MountSharingPrivate: {},
MountSharingLocked: {},
}
type mountsKeyT string
var mountsKey = mountsKeyT("dockerfile/run/mounts")
func init() {
parseRunPreHooks = append(parseRunPreHooks, runMountPreHook)
parseRunPostHooks = append(parseRunPostHooks, runMountPostHook)
}
func isValidMountType(s string) bool {
if s == "secret" {
if !isSecretMountsSupported() {
return false
}
}
if s == "ssh" {
if !isSSHMountsSupported() {
return false
}
}
_, ok := allowedMountTypes[s]
return ok
}
func runMountPreHook(cmd *RunCommand, req parseRequest) error {
st := &mountState{}
st.flag = req.flags.AddStrings("mount")
cmd.setExternalValue(mountsKey, st)
return nil
}
func runMountPostHook(cmd *RunCommand, req parseRequest) error {
st := getMountState(cmd)
if st == nil {
return errors.Errorf("no mount state")
}
var mounts []*Mount
for _, str := range st.flag.StringValues {
m, err := parseMount(str)
if err != nil {
return err
}
mounts = append(mounts, m)
}
st.mounts = mounts
return nil
}
func getMountState(cmd *RunCommand) *mountState {
v := cmd.getExternalValue(mountsKey)
if v == nil {
return nil
}
return v.(*mountState)
}
func GetMounts(cmd *RunCommand) []*Mount {
return getMountState(cmd).mounts
}
type mountState struct {
flag *Flag
mounts []*Mount
}
type Mount struct {
Type string
From string
Source string
Target string
ReadOnly bool
CacheID string
CacheSharing string
Required bool
Mode *uint64
UID *uint64
GID *uint64
}
func parseMount(value string) (*Mount, error) {
csvReader := csv.NewReader(strings.NewReader(value))
fields, err := csvReader.Read()
if err != nil {
return nil, errors.Wrap(err, "failed to parse csv mounts")
}
m := &Mount{Type: MountTypeBind}
roAuto := true
for _, field := range fields {
parts := strings.SplitN(field, "=", 2)
key := strings.ToLower(parts[0])
if len(parts) == 1 {
switch key {
case "readonly", "ro":
m.ReadOnly = true
roAuto = false
continue
case "readwrite", "rw":
m.ReadOnly = false
roAuto = false
continue
case "required":
if m.Type == "secret" || m.Type == "ssh" {
m.Required = true
continue
} else {
return nil, errors.Errorf("unexpected key '%s' for mount type '%s'", key, m.Type)
}
}
}
if len(parts) != 2 {
return nil, errors.Errorf("invalid field '%s' must be a key=value pair", field)
}
value := parts[1]
switch key {
case "type":
if !isValidMountType(strings.ToLower(value)) {
return nil, errors.Errorf("unsupported mount type %q", value)
}
m.Type = strings.ToLower(value)
case "from":
m.From = value
case "source", "src":
m.Source = value
case "target", "dst", "destination":
m.Target = value
case "readonly", "ro":
m.ReadOnly, err = strconv.ParseBool(value)
if err != nil {
return nil, errors.Errorf("invalid value for %s: %s", key, value)
}
roAuto = false
case "readwrite", "rw":
rw, err := strconv.ParseBool(value)
if err != nil {
return nil, errors.Errorf("invalid value for %s: %s", key, value)
}
m.ReadOnly = !rw
roAuto = false
case "required":
if m.Type == "secret" || m.Type == "ssh" {
v, err := strconv.ParseBool(value)
if err != nil {
return nil, errors.Errorf("invalid value for %s: %s", key, value)
}
m.Required = v
} else {
return nil, errors.Errorf("unexpected key '%s' for mount type '%s'", key, m.Type)
}
case "id":
m.CacheID = value
case "sharing":
if _, ok := allowedSharingTypes[strings.ToLower(value)]; !ok {
return nil, errors.Errorf("unsupported sharing value %q", value)
}
m.CacheSharing = strings.ToLower(value)
case "mode":
mode, err := strconv.ParseUint(value, 8, 32)
if err != nil {
return nil, errors.Errorf("invalid value %s for mode", value)
}
m.Mode = &mode
case "uid":
uid, err := strconv.ParseUint(value, 10, 32)
if err != nil {
return nil, errors.Errorf("invalid value %s for uid", value)
}
m.UID = &uid
case "gid":
gid, err := strconv.ParseUint(value, 10, 32)
if err != nil {
return nil, errors.Errorf("invalid value %s for gid", value)
}
m.GID = &gid
default:
return nil, errors.Errorf("unexpected key '%s' in '%s'", key, field)
}
}
fileInfoAllowed := m.Type == MountTypeSecret || m.Type == MountTypeSSH || m.Type == MountTypeCache
if m.Mode != nil && !fileInfoAllowed {
return nil, errors.Errorf("mode not allowed for %q type mounts", m.Type)
}
if m.UID != nil && !fileInfoAllowed {
return nil, errors.Errorf("uid not allowed for %q type mounts", m.Type)
}
if m.GID != nil && !fileInfoAllowed {
return nil, errors.Errorf("gid not allowed for %q type mounts", m.Type)
}
if roAuto {
if m.Type == MountTypeCache || m.Type == MountTypeTmpfs {
m.ReadOnly = false
} else {
m.ReadOnly = true
}
}
if m.CacheSharing != "" && m.Type != MountTypeCache {
return nil, errors.Errorf("invalid cache sharing set for %v mount", m.Type)
}
if m.Type == MountTypeSecret {
if m.From != "" {
return nil, errors.Errorf("secret mount should not have a from")
}
if m.CacheSharing != "" {
return nil, errors.Errorf("secret mount should not define sharing")
}
if m.Source == "" && m.Target == "" && m.CacheID == "" {
return nil, errors.Errorf("invalid secret mount. one of source, target required")
}
if m.Source != "" && m.CacheID != "" {
return nil, errors.Errorf("both source and id can't be set")
}
}
return m, nil
}

View File

@@ -1,63 +0,0 @@
// +build dfrunnetwork
package instructions
import (
"github.com/pkg/errors"
)
const (
NetworkDefault = "default"
NetworkNone = "none"
NetworkHost = "host"
)
var allowedNetwork = map[string]struct{}{
NetworkDefault: {},
NetworkNone: {},
NetworkHost: {},
}
func isValidNetwork(value string) bool {
_, ok := allowedNetwork[value]
return ok
}
var networkKey = "dockerfile/run/network"
func init() {
parseRunPreHooks = append(parseRunPreHooks, runNetworkPreHook)
parseRunPostHooks = append(parseRunPostHooks, runNetworkPostHook)
}
func runNetworkPreHook(cmd *RunCommand, req parseRequest) error {
st := &networkState{}
st.flag = req.flags.AddString("network", NetworkDefault)
cmd.setExternalValue(networkKey, st)
return nil
}
func runNetworkPostHook(cmd *RunCommand, req parseRequest) error {
st := cmd.getExternalValue(networkKey).(*networkState)
if st == nil {
return errors.Errorf("no network state")
}
value := st.flag.Value
if !isValidNetwork(value) {
return errors.Errorf("invalid network mode %q", value)
}
st.networkMode = value
return nil
}
func GetNetwork(cmd *RunCommand) string {
return cmd.getExternalValue(networkKey).(*networkState).networkMode
}
type networkState struct {
flag *Flag
networkMode string
}

View File

@@ -1,61 +0,0 @@
// +build dfrunsecurity
package instructions
import (
"github.com/pkg/errors"
)
const (
SecurityInsecure = "insecure"
SecuritySandbox = "sandbox"
)
var allowedSecurity = map[string]struct{}{
SecurityInsecure: {},
SecuritySandbox: {},
}
func isValidSecurity(value string) bool {
_, ok := allowedSecurity[value]
return ok
}
var securityKey = "dockerfile/run/security"
func init() {
parseRunPreHooks = append(parseRunPreHooks, runSecurityPreHook)
parseRunPostHooks = append(parseRunPostHooks, runSecurityPostHook)
}
func runSecurityPreHook(cmd *RunCommand, req parseRequest) error {
st := &securityState{}
st.flag = req.flags.AddString("security", SecuritySandbox)
cmd.setExternalValue(securityKey, st)
return nil
}
func runSecurityPostHook(cmd *RunCommand, req parseRequest) error {
st := cmd.getExternalValue(securityKey).(*securityState)
if st == nil {
return errors.Errorf("no security state")
}
value := st.flag.Value
if !isValidSecurity(value) {
return errors.Errorf("security %q is not valid", value)
}
st.security = value
return nil
}
func GetSecurity(cmd *RunCommand) string {
return cmd.getExternalValue(securityKey).(*securityState).security
}
type securityState struct {
flag *Flag
security string
}

View File

@@ -1,7 +0,0 @@
// +build dfsecrets
package instructions
func isSecretMountsSupported() bool {
return true
}

View File

@@ -1,7 +0,0 @@
// +build dfssh
package instructions
func isSSHMountsSupported() bool {
return true
}

View File

@@ -1,9 +0,0 @@
// +build !windows
package instructions
import "fmt"
func errNotJSON(command, _ string) error {
return fmt.Errorf("%s requires the arguments to be in JSON form", command)
}

View File

@@ -1,27 +0,0 @@
package instructions
import (
"fmt"
"path/filepath"
"regexp"
"strings"
)
func errNotJSON(command, original string) error {
// For Windows users, give a hint if it looks like it might contain
// a path which hasn't been escaped such as ["c:\windows\system32\prog.exe", "-param"],
// as JSON must be escaped. Unfortunate...
//
// Specifically looking for quote-driveletter-colon-backslash, there's no
// double backslash and a [] pair. No, this is not perfect, but it doesn't
// have to be. It's simply a hint to make life a little easier.
extra := ""
original = filepath.FromSlash(strings.ToLower(strings.Replace(strings.ToLower(original), strings.ToLower(command)+" ", "", -1)))
if len(regexp.MustCompile(`"[a-z]:\\.*`).FindStringSubmatch(original)) > 0 &&
!strings.Contains(original, `\\`) &&
strings.Contains(original, "[") &&
strings.Contains(original, "]") {
extra = fmt.Sprintf(`. It looks like '%s' includes a file path without an escaped back-slash. JSON requires back-slashes to be escaped such as ["c:\\path\\to\\file.exe", "/parameter"]`, original)
}
return fmt.Errorf("%s requires the arguments to be in JSON form%s", command, extra)
}

View File

@@ -1,650 +0,0 @@
package instructions
import (
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/strslice"
"github.com/moby/buildkit/frontend/dockerfile/command"
"github.com/moby/buildkit/frontend/dockerfile/parser"
"github.com/pkg/errors"
)
type parseRequest struct {
command string
args []string
attributes map[string]bool
flags *BFlags
original string
}
var parseRunPreHooks []func(*RunCommand, parseRequest) error
var parseRunPostHooks []func(*RunCommand, parseRequest) error
func nodeArgs(node *parser.Node) []string {
result := []string{}
for ; node.Next != nil; node = node.Next {
arg := node.Next
if len(arg.Children) == 0 {
result = append(result, arg.Value)
} else if len(arg.Children) == 1 {
//sub command
result = append(result, arg.Children[0].Value)
result = append(result, nodeArgs(arg.Children[0])...)
}
}
return result
}
func newParseRequestFromNode(node *parser.Node) parseRequest {
return parseRequest{
command: node.Value,
args: nodeArgs(node),
attributes: node.Attributes,
original: node.Original,
flags: NewBFlagsWithArgs(node.Flags),
}
}
// ParseInstruction converts an AST to a typed instruction (either a command or a build stage beginning when encountering a `FROM` statement)
func ParseInstruction(node *parser.Node) (interface{}, error) {
req := newParseRequestFromNode(node)
switch node.Value {
case command.Env:
return parseEnv(req)
case command.Maintainer:
return parseMaintainer(req)
case command.Label:
return parseLabel(req)
case command.Add:
return parseAdd(req)
case command.Copy:
return parseCopy(req)
case command.From:
return parseFrom(req)
case command.Onbuild:
return parseOnBuild(req)
case command.Workdir:
return parseWorkdir(req)
case command.Run:
return parseRun(req)
case command.Cmd:
return parseCmd(req)
case command.Healthcheck:
return parseHealthcheck(req)
case command.Entrypoint:
return parseEntrypoint(req)
case command.Expose:
return parseExpose(req)
case command.User:
return parseUser(req)
case command.Volume:
return parseVolume(req)
case command.StopSignal:
return parseStopSignal(req)
case command.Arg:
return parseArg(req)
case command.Shell:
return parseShell(req)
}
return nil, &UnknownInstruction{Instruction: node.Value, Line: node.StartLine}
}
// ParseCommand converts an AST to a typed Command
func ParseCommand(node *parser.Node) (Command, error) {
s, err := ParseInstruction(node)
if err != nil {
return nil, err
}
if c, ok := s.(Command); ok {
return c, nil
}
return nil, errors.Errorf("%T is not a command type", s)
}
// UnknownInstruction represents an error occurring when a command is unresolvable
type UnknownInstruction struct {
Line int
Instruction string
}
func (e *UnknownInstruction) Error() string {
return fmt.Sprintf("unknown instruction: %s", strings.ToUpper(e.Instruction))
}
// IsUnknownInstruction checks if the error is an UnknownInstruction or a parseError containing an UnknownInstruction
func IsUnknownInstruction(err error) bool {
_, ok := err.(*UnknownInstruction)
if !ok {
var pe *parseError
if pe, ok = err.(*parseError); ok {
_, ok = pe.inner.(*UnknownInstruction)
}
}
return ok
}
type parseError struct {
inner error
node *parser.Node
}
func (e *parseError) Error() string {
return fmt.Sprintf("Dockerfile parse error line %d: %v", e.node.StartLine, e.inner.Error())
}
// Parse a Dockerfile into a collection of buildable stages.
// metaArgs is a collection of ARG instructions that occur before the first FROM.
func Parse(ast *parser.Node) (stages []Stage, metaArgs []ArgCommand, err error) {
for _, n := range ast.Children {
cmd, err := ParseInstruction(n)
if err != nil {
return nil, nil, &parseError{inner: err, node: n}
}
if len(stages) == 0 {
// meta arg case
if a, isArg := cmd.(*ArgCommand); isArg {
metaArgs = append(metaArgs, *a)
continue
}
}
switch c := cmd.(type) {
case *Stage:
stages = append(stages, *c)
case Command:
stage, err := CurrentStage(stages)
if err != nil {
return nil, nil, err
}
stage.AddCommand(c)
default:
return nil, nil, errors.Errorf("%T is not a command type", cmd)
}
}
return stages, metaArgs, nil
}
func parseKvps(args []string, cmdName string) (KeyValuePairs, error) {
if len(args) == 0 {
return nil, errAtLeastOneArgument(cmdName)
}
if len(args)%2 != 0 {
// should never get here, but just in case
return nil, errTooManyArguments(cmdName)
}
var res KeyValuePairs
for j := 0; j < len(args); j += 2 {
if len(args[j]) == 0 {
return nil, errBlankCommandNames(cmdName)
}
name := args[j]
value := args[j+1]
res = append(res, KeyValuePair{Key: name, Value: value})
}
return res, nil
}
func parseEnv(req parseRequest) (*EnvCommand, error) {
if err := req.flags.Parse(); err != nil {
return nil, err
}
envs, err := parseKvps(req.args, "ENV")
if err != nil {
return nil, err
}
return &EnvCommand{
Env: envs,
withNameAndCode: newWithNameAndCode(req),
}, nil
}
func parseMaintainer(req parseRequest) (*MaintainerCommand, error) {
if len(req.args) != 1 {
return nil, errExactlyOneArgument("MAINTAINER")
}
if err := req.flags.Parse(); err != nil {
return nil, err
}
return &MaintainerCommand{
Maintainer: req.args[0],
withNameAndCode: newWithNameAndCode(req),
}, nil
}
func parseLabel(req parseRequest) (*LabelCommand, error) {
if err := req.flags.Parse(); err != nil {
return nil, err
}
labels, err := parseKvps(req.args, "LABEL")
if err != nil {
return nil, err
}
return &LabelCommand{
Labels: labels,
withNameAndCode: newWithNameAndCode(req),
}, nil
}
func parseAdd(req parseRequest) (*AddCommand, error) {
if len(req.args) < 2 {
return nil, errNoDestinationArgument("ADD")
}
flChown := req.flags.AddString("chown", "")
if err := req.flags.Parse(); err != nil {
return nil, err
}
return &AddCommand{
SourcesAndDest: SourcesAndDest(req.args),
withNameAndCode: newWithNameAndCode(req),
Chown: flChown.Value,
}, nil
}
func parseCopy(req parseRequest) (*CopyCommand, error) {
if len(req.args) < 2 {
return nil, errNoDestinationArgument("COPY")
}
flChown := req.flags.AddString("chown", "")
flFrom := req.flags.AddString("from", "")
if err := req.flags.Parse(); err != nil {
return nil, err
}
return &CopyCommand{
SourcesAndDest: SourcesAndDest(req.args),
From: flFrom.Value,
withNameAndCode: newWithNameAndCode(req),
Chown: flChown.Value,
}, nil
}
func parseFrom(req parseRequest) (*Stage, error) {
stageName, err := parseBuildStageName(req.args)
if err != nil {
return nil, err
}
flPlatform := req.flags.AddString("platform", "")
if err := req.flags.Parse(); err != nil {
return nil, err
}
code := strings.TrimSpace(req.original)
return &Stage{
BaseName: req.args[0],
Name: stageName,
SourceCode: code,
Commands: []Command{},
Platform: flPlatform.Value,
}, nil
}
func parseBuildStageName(args []string) (string, error) {
stageName := ""
switch {
case len(args) == 3 && strings.EqualFold(args[1], "as"):
stageName = strings.ToLower(args[2])
if ok, _ := regexp.MatchString("^[a-z][a-z0-9-_\\.]*$", stageName); !ok {
return "", errors.Errorf("invalid name for build stage: %q, name can't start with a number or contain symbols", args[2])
}
case len(args) != 1:
return "", errors.New("FROM requires either one or three arguments")
}
return stageName, nil
}
func parseOnBuild(req parseRequest) (*OnbuildCommand, error) {
if len(req.args) == 0 {
return nil, errAtLeastOneArgument("ONBUILD")
}
if err := req.flags.Parse(); err != nil {
return nil, err
}
triggerInstruction := strings.ToUpper(strings.TrimSpace(req.args[0]))
switch strings.ToUpper(triggerInstruction) {
case "ONBUILD":
return nil, errors.New("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed")
case "MAINTAINER", "FROM":
return nil, fmt.Errorf("%s isn't allowed as an ONBUILD trigger", triggerInstruction)
}
original := regexp.MustCompile(`(?i)^\s*ONBUILD\s*`).ReplaceAllString(req.original, "")
return &OnbuildCommand{
Expression: original,
withNameAndCode: newWithNameAndCode(req),
}, nil
}
func parseWorkdir(req parseRequest) (*WorkdirCommand, error) {
if len(req.args) != 1 {
return nil, errExactlyOneArgument("WORKDIR")
}
err := req.flags.Parse()
if err != nil {
return nil, err
}
return &WorkdirCommand{
Path: req.args[0],
withNameAndCode: newWithNameAndCode(req),
}, nil
}
func parseShellDependentCommand(req parseRequest, emptyAsNil bool) ShellDependantCmdLine {
args := handleJSONArgs(req.args, req.attributes)
cmd := strslice.StrSlice(args)
if emptyAsNil && len(cmd) == 0 {
cmd = nil
}
return ShellDependantCmdLine{
CmdLine: cmd,
PrependShell: !req.attributes["json"],
}
}
func parseRun(req parseRequest) (*RunCommand, error) {
cmd := &RunCommand{}
for _, fn := range parseRunPreHooks {
if err := fn(cmd, req); err != nil {
return nil, err
}
}
if err := req.flags.Parse(); err != nil {
return nil, err
}
cmd.ShellDependantCmdLine = parseShellDependentCommand(req, false)
cmd.withNameAndCode = newWithNameAndCode(req)
for _, fn := range parseRunPostHooks {
if err := fn(cmd, req); err != nil {
return nil, err
}
}
return cmd, nil
}
func parseCmd(req parseRequest) (*CmdCommand, error) {
if err := req.flags.Parse(); err != nil {
return nil, err
}
return &CmdCommand{
ShellDependantCmdLine: parseShellDependentCommand(req, false),
withNameAndCode: newWithNameAndCode(req),
}, nil
}
func parseEntrypoint(req parseRequest) (*EntrypointCommand, error) {
if err := req.flags.Parse(); err != nil {
return nil, err
}
cmd := &EntrypointCommand{
ShellDependantCmdLine: parseShellDependentCommand(req, true),
withNameAndCode: newWithNameAndCode(req),
}
return cmd, nil
}
// parseOptInterval(flag) is the duration of flag.Value, or 0 if
// empty. An error is reported if the value is given and less than minimum duration.
func parseOptInterval(f *Flag) (time.Duration, error) {
s := f.Value
if s == "" {
return 0, nil
}
d, err := time.ParseDuration(s)
if err != nil {
return 0, err
}
if d < container.MinimumDuration {
return 0, fmt.Errorf("Interval %#v cannot be less than %s", f.name, container.MinimumDuration)
}
return d, nil
}
func parseHealthcheck(req parseRequest) (*HealthCheckCommand, error) {
if len(req.args) == 0 {
return nil, errAtLeastOneArgument("HEALTHCHECK")
}
cmd := &HealthCheckCommand{
withNameAndCode: newWithNameAndCode(req),
}
typ := strings.ToUpper(req.args[0])
args := req.args[1:]
if typ == "NONE" {
if len(args) != 0 {
return nil, errors.New("HEALTHCHECK NONE takes no arguments")
}
test := strslice.StrSlice{typ}
cmd.Health = &container.HealthConfig{
Test: test,
}
} else {
healthcheck := container.HealthConfig{}
flInterval := req.flags.AddString("interval", "")
flTimeout := req.flags.AddString("timeout", "")
flStartPeriod := req.flags.AddString("start-period", "")
flRetries := req.flags.AddString("retries", "")
if err := req.flags.Parse(); err != nil {
return nil, err
}
switch typ {
case "CMD":
cmdSlice := handleJSONArgs(args, req.attributes)
if len(cmdSlice) == 0 {
return nil, errors.New("Missing command after HEALTHCHECK CMD")
}
if !req.attributes["json"] {
typ = "CMD-SHELL"
}
healthcheck.Test = strslice.StrSlice(append([]string{typ}, cmdSlice...))
default:
return nil, fmt.Errorf("Unknown type %#v in HEALTHCHECK (try CMD)", typ)
}
interval, err := parseOptInterval(flInterval)
if err != nil {
return nil, err
}
healthcheck.Interval = interval
timeout, err := parseOptInterval(flTimeout)
if err != nil {
return nil, err
}
healthcheck.Timeout = timeout
startPeriod, err := parseOptInterval(flStartPeriod)
if err != nil {
return nil, err
}
healthcheck.StartPeriod = startPeriod
if flRetries.Value != "" {
retries, err := strconv.ParseInt(flRetries.Value, 10, 32)
if err != nil {
return nil, err
}
if retries < 1 {
return nil, fmt.Errorf("--retries must be at least 1 (not %d)", retries)
}
healthcheck.Retries = int(retries)
} else {
healthcheck.Retries = 0
}
cmd.Health = &healthcheck
}
return cmd, nil
}
func parseExpose(req parseRequest) (*ExposeCommand, error) {
portsTab := req.args
if len(req.args) == 0 {
return nil, errAtLeastOneArgument("EXPOSE")
}
if err := req.flags.Parse(); err != nil {
return nil, err
}
sort.Strings(portsTab)
return &ExposeCommand{
Ports: portsTab,
withNameAndCode: newWithNameAndCode(req),
}, nil
}
func parseUser(req parseRequest) (*UserCommand, error) {
if len(req.args) != 1 {
return nil, errExactlyOneArgument("USER")
}
if err := req.flags.Parse(); err != nil {
return nil, err
}
return &UserCommand{
User: req.args[0],
withNameAndCode: newWithNameAndCode(req),
}, nil
}
func parseVolume(req parseRequest) (*VolumeCommand, error) {
if len(req.args) == 0 {
return nil, errAtLeastOneArgument("VOLUME")
}
if err := req.flags.Parse(); err != nil {
return nil, err
}
cmd := &VolumeCommand{
withNameAndCode: newWithNameAndCode(req),
}
for _, v := range req.args {
v = strings.TrimSpace(v)
if v == "" {
return nil, errors.New("VOLUME specified can not be an empty string")
}
cmd.Volumes = append(cmd.Volumes, v)
}
return cmd, nil
}
func parseStopSignal(req parseRequest) (*StopSignalCommand, error) {
if len(req.args) != 1 {
return nil, errExactlyOneArgument("STOPSIGNAL")
}
sig := req.args[0]
cmd := &StopSignalCommand{
Signal: sig,
withNameAndCode: newWithNameAndCode(req),
}
return cmd, nil
}
func parseArg(req parseRequest) (*ArgCommand, error) {
if len(req.args) != 1 {
return nil, errExactlyOneArgument("ARG")
}
kvpo := KeyValuePairOptional{}
arg := req.args[0]
// 'arg' can just be a name or name-value pair. Note that this is different
// from 'env' that handles the split of name and value at the parser level.
// The reason for doing it differently for 'arg' is that we support just
// defining an arg and not assign it a value (while 'env' always expects a
// name-value pair). If possible, it will be good to harmonize the two.
if strings.Contains(arg, "=") {
parts := strings.SplitN(arg, "=", 2)
if len(parts[0]) == 0 {
return nil, errBlankCommandNames("ARG")
}
kvpo.Key = parts[0]
kvpo.Value = &parts[1]
} else {
kvpo.Key = arg
}
return &ArgCommand{
KeyValuePairOptional: kvpo,
withNameAndCode: newWithNameAndCode(req),
}, nil
}
func parseShell(req parseRequest) (*ShellCommand, error) {
if err := req.flags.Parse(); err != nil {
return nil, err
}
shellSlice := handleJSONArgs(req.args, req.attributes)
switch {
case len(shellSlice) == 0:
// SHELL []
return nil, errAtLeastOneArgument("SHELL")
case req.attributes["json"]:
// SHELL ["powershell", "-command"]
return &ShellCommand{
Shell: strslice.StrSlice(shellSlice),
withNameAndCode: newWithNameAndCode(req),
}, nil
default:
// SHELL powershell -command - not JSON
return nil, errNotJSON("SHELL", req.original)
}
}
func errAtLeastOneArgument(command string) error {
return errors.Errorf("%s requires at least one argument", command)
}
func errExactlyOneArgument(command string) error {
return errors.Errorf("%s requires exactly one argument", command)
}
func errNoDestinationArgument(command string) error {
return errors.Errorf("%s requires at least two arguments, but only one was provided. Destination could not be determined.", command)
}
func errBlankCommandNames(command string) error {
return errors.Errorf("%s names can not be blank", command)
}
func errTooManyArguments(command string) error {
return errors.Errorf("Bad input to %s, too many arguments", command)
}

View File

@@ -1,19 +0,0 @@
package instructions
import "strings"
// handleJSONArgs parses command passed to CMD, ENTRYPOINT, RUN and SHELL instruction in Dockerfile
// for exec form it returns untouched args slice
// for shell form it returns concatenated args as the first element of a slice
func handleJSONArgs(args []string, attributes map[string]bool) []string {
if len(args) == 0 {
return []string{}
}
if attributes != nil && attributes["json"] {
return args
}
// literal string command, not an exec array
return []string{strings.Join(args, " ")}
}

View File

@@ -1,368 +0,0 @@
package parser
// line parsers are dispatch calls that parse a single unit of text into a
// Node object which contains the whole statement. Dockerfiles have varied
// (but not usually unique, see ONBUILD for a unique example) parsing rules
// per-command, and these unify the processing in a way that makes it
// manageable.
import (
"encoding/json"
"errors"
"fmt"
"strings"
"unicode"
"unicode/utf8"
)
var (
errDockerfileNotStringArray = errors.New("when using JSON array syntax, arrays must be comprised of strings only")
)
const (
commandLabel = "LABEL"
)
// ignore the current argument. This will still leave a command parsed, but
// will not incorporate the arguments into the ast.
func parseIgnore(rest string, d *Directive) (*Node, map[string]bool, error) {
return &Node{}, nil, nil
}
// used for onbuild. Could potentially be used for anything that represents a
// statement with sub-statements.
//
// ONBUILD RUN foo bar -> (onbuild (run foo bar))
//
func parseSubCommand(rest string, d *Directive) (*Node, map[string]bool, error) {
if rest == "" {
return nil, nil, nil
}
child, err := newNodeFromLine(rest, d)
if err != nil {
return nil, nil, err
}
return &Node{Children: []*Node{child}}, nil, nil
}
// helper to parse words (i.e space delimited or quoted strings) in a statement.
// The quotes are preserved as part of this function and they are stripped later
// as part of processWords().
func parseWords(rest string, d *Directive) []string {
const (
inSpaces = iota // looking for start of a word
inWord
inQuote
)
words := []string{}
phase := inSpaces
word := ""
quote := '\000'
blankOK := false
var ch rune
var chWidth int
for pos := 0; pos <= len(rest); pos += chWidth {
if pos != len(rest) {
ch, chWidth = utf8.DecodeRuneInString(rest[pos:])
}
if phase == inSpaces { // Looking for start of word
if pos == len(rest) { // end of input
break
}
if unicode.IsSpace(ch) { // skip spaces
continue
}
phase = inWord // found it, fall through
}
if (phase == inWord || phase == inQuote) && (pos == len(rest)) {
if blankOK || len(word) > 0 {
words = append(words, word)
}
break
}
if phase == inWord {
if unicode.IsSpace(ch) {
phase = inSpaces
if blankOK || len(word) > 0 {
words = append(words, word)
}
word = ""
blankOK = false
continue
}
if ch == '\'' || ch == '"' {
quote = ch
blankOK = true
phase = inQuote
}
if ch == d.escapeToken {
if pos+chWidth == len(rest) {
continue // just skip an escape token at end of line
}
// If we're not quoted and we see an escape token, then always just
// add the escape token plus the char to the word, even if the char
// is a quote.
word += string(ch)
pos += chWidth
ch, chWidth = utf8.DecodeRuneInString(rest[pos:])
}
word += string(ch)
continue
}
if phase == inQuote {
if ch == quote {
phase = inWord
}
// The escape token is special except for ' quotes - can't escape anything for '
if ch == d.escapeToken && quote != '\'' {
if pos+chWidth == len(rest) {
phase = inWord
continue // just skip the escape token at end
}
pos += chWidth
word += string(ch)
ch, chWidth = utf8.DecodeRuneInString(rest[pos:])
}
word += string(ch)
}
}
return words
}
// parse environment like statements. Note that this does *not* handle
// variable interpolation, which will be handled in the evaluator.
func parseNameVal(rest string, key string, d *Directive) (*Node, error) {
// This is kind of tricky because we need to support the old
// variant: KEY name value
// as well as the new one: KEY name=value ...
// The trigger to know which one is being used will be whether we hit
// a space or = first. space ==> old, "=" ==> new
words := parseWords(rest, d)
if len(words) == 0 {
return nil, nil
}
// Old format (KEY name value)
if !strings.Contains(words[0], "=") {
parts := tokenWhitespace.Split(rest, 2)
if len(parts) < 2 {
return nil, fmt.Errorf(key + " must have two arguments")
}
return newKeyValueNode(parts[0], parts[1]), nil
}
var rootNode *Node
var prevNode *Node
for _, word := range words {
if !strings.Contains(word, "=") {
return nil, fmt.Errorf("Syntax error - can't find = in %q. Must be of the form: name=value", word)
}
parts := strings.SplitN(word, "=", 2)
node := newKeyValueNode(parts[0], parts[1])
rootNode, prevNode = appendKeyValueNode(node, rootNode, prevNode)
}
return rootNode, nil
}
func newKeyValueNode(key, value string) *Node {
return &Node{
Value: key,
Next: &Node{Value: value},
}
}
func appendKeyValueNode(node, rootNode, prevNode *Node) (*Node, *Node) {
if rootNode == nil {
rootNode = node
}
if prevNode != nil {
prevNode.Next = node
}
prevNode = node.Next
return rootNode, prevNode
}
func parseEnv(rest string, d *Directive) (*Node, map[string]bool, error) {
node, err := parseNameVal(rest, "ENV", d)
return node, nil, err
}
func parseLabel(rest string, d *Directive) (*Node, map[string]bool, error) {
node, err := parseNameVal(rest, commandLabel, d)
return node, nil, err
}
// parses a statement containing one or more keyword definition(s) and/or
// value assignments, like `name1 name2= name3="" name4=value`.
// Note that this is a stricter format than the old format of assignment,
// allowed by parseNameVal(), in a way that this only allows assignment of the
// form `keyword=[<value>]` like `name2=`, `name3=""`, and `name4=value` above.
// In addition, a keyword definition alone is of the form `keyword` like `name1`
// above. And the assignments `name2=` and `name3=""` are equivalent and
// assign an empty value to the respective keywords.
func parseNameOrNameVal(rest string, d *Directive) (*Node, map[string]bool, error) {
words := parseWords(rest, d)
if len(words) == 0 {
return nil, nil, nil
}
var (
rootnode *Node
prevNode *Node
)
for i, word := range words {
node := &Node{}
node.Value = word
if i == 0 {
rootnode = node
} else {
prevNode.Next = node
}
prevNode = node
}
return rootnode, nil, nil
}
// parses a whitespace-delimited set of arguments. The result is effectively a
// linked list of string arguments.
func parseStringsWhitespaceDelimited(rest string, d *Directive) (*Node, map[string]bool, error) {
if rest == "" {
return nil, nil, nil
}
node := &Node{}
rootnode := node
prevnode := node
for _, str := range tokenWhitespace.Split(rest, -1) { // use regexp
prevnode = node
node.Value = str
node.Next = &Node{}
node = node.Next
}
// XXX to get around regexp.Split *always* providing an empty string at the
// end due to how our loop is constructed, nil out the last node in the
// chain.
prevnode.Next = nil
return rootnode, nil, nil
}
// parseString just wraps the string in quotes and returns a working node.
func parseString(rest string, d *Directive) (*Node, map[string]bool, error) {
if rest == "" {
return nil, nil, nil
}
n := &Node{}
n.Value = rest
return n, nil, nil
}
// parseJSON converts JSON arrays to an AST.
func parseJSON(rest string, d *Directive) (*Node, map[string]bool, error) {
rest = strings.TrimLeftFunc(rest, unicode.IsSpace)
if !strings.HasPrefix(rest, "[") {
return nil, nil, fmt.Errorf(`Error parsing "%s" as a JSON array`, rest)
}
var myJSON []interface{}
if err := json.NewDecoder(strings.NewReader(rest)).Decode(&myJSON); err != nil {
return nil, nil, err
}
var top, prev *Node
for _, str := range myJSON {
s, ok := str.(string)
if !ok {
return nil, nil, errDockerfileNotStringArray
}
node := &Node{Value: s}
if prev == nil {
top = node
} else {
prev.Next = node
}
prev = node
}
return top, map[string]bool{"json": true}, nil
}
// parseMaybeJSON determines if the argument appears to be a JSON array. If
// so, passes to parseJSON; if not, quotes the result and returns a single
// node.
func parseMaybeJSON(rest string, d *Directive) (*Node, map[string]bool, error) {
if rest == "" {
return nil, nil, nil
}
node, attrs, err := parseJSON(rest, d)
if err == nil {
return node, attrs, nil
}
if err == errDockerfileNotStringArray {
return nil, nil, err
}
node = &Node{}
node.Value = rest
return node, nil, nil
}
// parseMaybeJSONToList determines if the argument appears to be a JSON array. If
// so, passes to parseJSON; if not, attempts to parse it as a whitespace
// delimited string.
func parseMaybeJSONToList(rest string, d *Directive) (*Node, map[string]bool, error) {
node, attrs, err := parseJSON(rest, d)
if err == nil {
return node, attrs, nil
}
if err == errDockerfileNotStringArray {
return nil, nil, err
}
return parseStringsWhitespaceDelimited(rest, d)
}
// The HEALTHCHECK command is like parseMaybeJSON, but has an extra type argument.
func parseHealthConfig(rest string, d *Directive) (*Node, map[string]bool, error) {
// Find end of first argument
var sep int
for ; sep < len(rest); sep++ {
if unicode.IsSpace(rune(rest[sep])) {
break
}
}
next := sep
for ; next < len(rest); next++ {
if !unicode.IsSpace(rune(rest[next])) {
break
}
}
if sep == 0 {
return nil, nil, nil
}
typ := rest[:sep]
cmd, attrs, err := parseMaybeJSON(rest[next:], d)
if err != nil {
return nil, nil, err
}
return &Node{Value: typ, Next: cmd}, attrs, err
}

View File

@@ -1,332 +0,0 @@
// Package parser implements a parser and parse tree dumper for Dockerfiles.
package parser
import (
"bufio"
"bytes"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"unicode"
"github.com/moby/buildkit/frontend/dockerfile/command"
"github.com/pkg/errors"
)
// Node is a structure used to represent a parse tree.
//
// In the node there are three fields, Value, Next, and Children. Value is the
// current token's string value. Next is always the next non-child token, and
// children contains all the children. Here's an example:
//
// (value next (child child-next child-next-next) next-next)
//
// This data structure is frankly pretty lousy for handling complex languages,
// but lucky for us the Dockerfile isn't very complicated. This structure
// works a little more effectively than a "proper" parse tree for our needs.
//
type Node struct {
Value string // actual content
Next *Node // the next item in the current sexp
Children []*Node // the children of this sexp
Attributes map[string]bool // special attributes for this node
Original string // original line used before parsing
Flags []string // only top Node should have this set
StartLine int // the line in the original dockerfile where the node begins
EndLine int // the line in the original dockerfile where the node ends
}
// Dump dumps the AST defined by `node` as a list of sexps.
// Returns a string suitable for printing.
func (node *Node) Dump() string {
str := ""
str += node.Value
if len(node.Flags) > 0 {
str += fmt.Sprintf(" %q", node.Flags)
}
for _, n := range node.Children {
str += "(" + n.Dump() + ")\n"
}
for n := node.Next; n != nil; n = n.Next {
if len(n.Children) > 0 {
str += " " + n.Dump()
} else {
str += " " + strconv.Quote(n.Value)
}
}
return strings.TrimSpace(str)
}
func (node *Node) lines(start, end int) {
node.StartLine = start
node.EndLine = end
}
// AddChild adds a new child node, and updates line information
func (node *Node) AddChild(child *Node, startLine, endLine int) {
child.lines(startLine, endLine)
if node.StartLine < 0 {
node.StartLine = startLine
}
node.EndLine = endLine
node.Children = append(node.Children, child)
}
var (
dispatch map[string]func(string, *Directive) (*Node, map[string]bool, error)
tokenWhitespace = regexp.MustCompile(`[\t\v\f\r ]+`)
tokenEscapeCommand = regexp.MustCompile(`^#[ \t]*escape[ \t]*=[ \t]*(?P<escapechar>.).*$`)
tokenComment = regexp.MustCompile(`^#.*$`)
)
// DefaultEscapeToken is the default escape token
const DefaultEscapeToken = '\\'
// Directive is the structure used during a build run to hold the state of
// parsing directives.
type Directive struct {
escapeToken rune // Current escape token
lineContinuationRegex *regexp.Regexp // Current line continuation regex
processingComplete bool // Whether we are done looking for directives
escapeSeen bool // Whether the escape directive has been seen
}
// setEscapeToken sets the default token for escaping characters in a Dockerfile.
func (d *Directive) setEscapeToken(s string) error {
if s != "`" && s != "\\" {
return fmt.Errorf("invalid ESCAPE '%s'. Must be ` or \\", s)
}
d.escapeToken = rune(s[0])
d.lineContinuationRegex = regexp.MustCompile(`\` + s + `[ \t]*$`)
return nil
}
// possibleParserDirective looks for parser directives, eg '# escapeToken=<char>'.
// Parser directives must precede any builder instruction or other comments,
// and cannot be repeated.
func (d *Directive) possibleParserDirective(line string) error {
if d.processingComplete {
return nil
}
tecMatch := tokenEscapeCommand.FindStringSubmatch(strings.ToLower(line))
if len(tecMatch) != 0 {
for i, n := range tokenEscapeCommand.SubexpNames() {
if n == "escapechar" {
if d.escapeSeen {
return errors.New("only one escape parser directive can be used")
}
d.escapeSeen = true
return d.setEscapeToken(tecMatch[i])
}
}
}
d.processingComplete = true
return nil
}
// NewDefaultDirective returns a new Directive with the default escapeToken token
func NewDefaultDirective() *Directive {
directive := Directive{}
directive.setEscapeToken(string(DefaultEscapeToken))
return &directive
}
func init() {
// Dispatch Table. see line_parsers.go for the parse functions.
// The command is parsed and mapped to the line parser. The line parser
// receives the arguments but not the command, and returns an AST after
// reformulating the arguments according to the rules in the parser
// functions. Errors are propagated up by Parse() and the resulting AST can
// be incorporated directly into the existing AST as a next.
dispatch = map[string]func(string, *Directive) (*Node, map[string]bool, error){
command.Add: parseMaybeJSONToList,
command.Arg: parseNameOrNameVal,
command.Cmd: parseMaybeJSON,
command.Copy: parseMaybeJSONToList,
command.Entrypoint: parseMaybeJSON,
command.Env: parseEnv,
command.Expose: parseStringsWhitespaceDelimited,
command.From: parseStringsWhitespaceDelimited,
command.Healthcheck: parseHealthConfig,
command.Label: parseLabel,
command.Maintainer: parseString,
command.Onbuild: parseSubCommand,
command.Run: parseMaybeJSON,
command.Shell: parseMaybeJSON,
command.StopSignal: parseString,
command.User: parseString,
command.Volume: parseMaybeJSONToList,
command.Workdir: parseString,
}
}
// newNodeFromLine splits the line into parts, and dispatches to a function
// based on the command and command arguments. A Node is created from the
// result of the dispatch.
func newNodeFromLine(line string, directive *Directive) (*Node, error) {
cmd, flags, args, err := splitCommand(line)
if err != nil {
return nil, err
}
fn := dispatch[cmd]
// Ignore invalid Dockerfile instructions
if fn == nil {
fn = parseIgnore
}
next, attrs, err := fn(args, directive)
if err != nil {
return nil, err
}
return &Node{
Value: cmd,
Original: line,
Flags: flags,
Next: next,
Attributes: attrs,
}, nil
}
// Result is the result of parsing a Dockerfile
type Result struct {
AST *Node
EscapeToken rune
Warnings []string
}
// PrintWarnings to the writer
func (r *Result) PrintWarnings(out io.Writer) {
if len(r.Warnings) == 0 {
return
}
fmt.Fprintf(out, strings.Join(r.Warnings, "\n")+"\n")
}
// Parse reads lines from a Reader, parses the lines into an AST and returns
// the AST and escape token
func Parse(rwc io.Reader) (*Result, error) {
d := NewDefaultDirective()
currentLine := 0
root := &Node{StartLine: -1}
scanner := bufio.NewScanner(rwc)
warnings := []string{}
var err error
for scanner.Scan() {
bytesRead := scanner.Bytes()
if currentLine == 0 {
// First line, strip the byte-order-marker if present
bytesRead = bytes.TrimPrefix(bytesRead, utf8bom)
}
bytesRead, err = processLine(d, bytesRead, true)
if err != nil {
return nil, err
}
currentLine++
startLine := currentLine
line, isEndOfLine := trimContinuationCharacter(string(bytesRead), d)
if isEndOfLine && line == "" {
continue
}
var hasEmptyContinuationLine bool
for !isEndOfLine && scanner.Scan() {
bytesRead, err := processLine(d, scanner.Bytes(), false)
if err != nil {
return nil, err
}
currentLine++
if isComment(scanner.Bytes()) {
// original line was a comment (processLine strips comments)
continue
}
if isEmptyContinuationLine(bytesRead) {
hasEmptyContinuationLine = true
continue
}
continuationLine := string(bytesRead)
continuationLine, isEndOfLine = trimContinuationCharacter(continuationLine, d)
line += continuationLine
}
if hasEmptyContinuationLine {
warnings = append(warnings, "[WARNING]: Empty continuation line found in:\n "+line)
}
child, err := newNodeFromLine(line, d)
if err != nil {
return nil, err
}
root.AddChild(child, startLine, currentLine)
}
if len(warnings) > 0 {
warnings = append(warnings, "[WARNING]: Empty continuation lines will become errors in a future release.")
}
if root.StartLine < 0 {
return nil, errors.New("file with no instructions.")
}
return &Result{
AST: root,
Warnings: warnings,
EscapeToken: d.escapeToken,
}, handleScannerError(scanner.Err())
}
func trimComments(src []byte) []byte {
return tokenComment.ReplaceAll(src, []byte{})
}
func trimWhitespace(src []byte) []byte {
return bytes.TrimLeftFunc(src, unicode.IsSpace)
}
func isComment(line []byte) bool {
return tokenComment.Match(trimWhitespace(line))
}
func isEmptyContinuationLine(line []byte) bool {
return len(trimWhitespace(line)) == 0
}
var utf8bom = []byte{0xEF, 0xBB, 0xBF}
func trimContinuationCharacter(line string, d *Directive) (string, bool) {
if d.lineContinuationRegex.MatchString(line) {
line = d.lineContinuationRegex.ReplaceAllString(line, "")
return line, false
}
return line, true
}
// TODO: remove stripLeftWhitespace after deprecation period. It seems silly
// to preserve whitespace on continuation lines. Why is that done?
func processLine(d *Directive, token []byte, stripLeftWhitespace bool) ([]byte, error) {
if stripLeftWhitespace {
token = trimWhitespace(token)
}
return trimComments(token), d.possibleParserDirective(string(token))
}
func handleScannerError(err error) error {
switch err {
case bufio.ErrTooLong:
return errors.Errorf("dockerfile line greater than max allowed size of %d", bufio.MaxScanTokenSize-1)
default:
return err
}
}

View File

@@ -1,118 +0,0 @@
package parser
import (
"strings"
"unicode"
)
// splitCommand takes a single line of text and parses out the cmd and args,
// which are used for dispatching to more exact parsing functions.
func splitCommand(line string) (string, []string, string, error) {
var args string
var flags []string
// Make sure we get the same results irrespective of leading/trailing spaces
cmdline := tokenWhitespace.Split(strings.TrimSpace(line), 2)
cmd := strings.ToLower(cmdline[0])
if len(cmdline) == 2 {
var err error
args, flags, err = extractBuilderFlags(cmdline[1])
if err != nil {
return "", nil, "", err
}
}
return cmd, flags, strings.TrimSpace(args), nil
}
func extractBuilderFlags(line string) (string, []string, error) {
// Parses the BuilderFlags and returns the remaining part of the line
const (
inSpaces = iota // looking for start of a word
inWord
inQuote
)
words := []string{}
phase := inSpaces
word := ""
quote := '\000'
blankOK := false
var ch rune
for pos := 0; pos <= len(line); pos++ {
if pos != len(line) {
ch = rune(line[pos])
}
if phase == inSpaces { // Looking for start of word
if pos == len(line) { // end of input
break
}
if unicode.IsSpace(ch) { // skip spaces
continue
}
// Only keep going if the next word starts with --
if ch != '-' || pos+1 == len(line) || rune(line[pos+1]) != '-' {
return line[pos:], words, nil
}
phase = inWord // found something with "--", fall through
}
if (phase == inWord || phase == inQuote) && (pos == len(line)) {
if word != "--" && (blankOK || len(word) > 0) {
words = append(words, word)
}
break
}
if phase == inWord {
if unicode.IsSpace(ch) {
phase = inSpaces
if word == "--" {
return line[pos:], words, nil
}
if blankOK || len(word) > 0 {
words = append(words, word)
}
word = ""
blankOK = false
continue
}
if ch == '\'' || ch == '"' {
quote = ch
blankOK = true
phase = inQuote
continue
}
if ch == '\\' {
if pos+1 == len(line) {
continue // just skip \ at end
}
pos++
ch = rune(line[pos])
}
word += string(ch)
continue
}
if phase == inQuote {
if ch == quote {
phase = inWord
continue
}
if ch == '\\' {
if pos+1 == len(line) {
phase = inWord
continue // just skip \ at end
}
pos++
ch = rune(line[pos])
}
word += string(ch)
}
}
return "", words, nil
}

View File

@@ -1,238 +0,0 @@
A|hello | hello
A|he'll'o | hello
A|he'llo | error
A|he\'llo | he'llo
A|he\\'llo | error
A|abc\tdef | abctdef
A|"abc\tdef" | abc\tdef
A|"abc\\tdef" | abc\tdef
A|'abc\tdef' | abc\tdef
A|hello\ | hello
A|hello\\ | hello\
A|"hello | error
A|"hello\" | error
A|"hel'lo" | hel'lo
A|'hello | error
A|'hello\' | hello\
A|'hello\there' | hello\there
A|'hello\\there' | hello\\there
A|"''" | ''
A|$. | $.
A|he$1x | hex
A|he$.x | he$.x
# Next one is different on Windows as $pwd==$PWD
U|he$pwd. | he.
W|he$pwd. | he/home.
A|he$PWD | he/home
A|he\$PWD | he$PWD
A|he\\$PWD | he\/home
A|"he\$PWD" | he$PWD
A|"he\\$PWD" | he\/home
A|\${} | ${}
A|\${}aaa | ${}aaa
A|he\${} | he${}
A|he\${}xx | he${}xx
A|${} | error
A|${}aaa | error
A|he${} | error
A|he${}xx | error
A|he${hi} | he
A|he${hi}xx | hexx
A|he${PWD} | he/home
A|he${.} | error
A|he${XXX:-000}xx | he000xx
A|he${PWD:-000}xx | he/homexx
A|he${XXX:-$PWD}xx | he/homexx
A|he${XXX:-${PWD:-yyy}}xx | he/homexx
A|he${XXX:-${YYY:-yyy}}xx | heyyyxx
A|he${XXX:YYY} | error
A|he${XXX?} | error
A|he${XXX:?} | error
A|he${PWD?} | he/home
A|he${PWD:?} | he/home
A|he${NULL?} | he
A|he${NULL:?} | error
A|he${XXX:+${PWD}}xx | hexx
A|he${PWD:+${XXX}}xx | hexx
A|he${PWD:+${SHELL}}xx | hebashxx
A|he${XXX:+000}xx | hexx
A|he${PWD:+000}xx | he000xx
A|'he${XX}' | he${XX}
A|"he${PWD}" | he/home
A|"he'$PWD'" | he'/home'
A|"$PWD" | /home
A|'$PWD' | $PWD
A|'\$PWD' | \$PWD
A|'"hello"' | "hello"
A|he\$PWD | he$PWD
A|"he\$PWD" | he$PWD
A|'he\$PWD' | he\$PWD
A|he${PWD | error
A|he${PWD:=000}xx | error
A|he${PWD:+${PWD}:}xx | he/home:xx
A|he${XXX:-\$PWD:}xx | he$PWD:xx
A|he${XXX:-\${PWD}z}xx | he${PWDz}xx
A|안녕하세요 | 안녕하세요
A|안'녕'하세요 | 안녕하세요
A|안'녕하세요 | error
A|안녕\'하세요 | 안녕'하세요
A|안\\'녕하세요 | error
A|안녕\t하세요 | 안녕t하세요
A|"안녕\t하세요" | 안녕\t하세요
A|'안녕\t하세요 | error
A|안녕하세요\ | 안녕하세요
A|안녕하세요\\ | 안녕하세요\
A|"안녕하세요 | error
A|"안녕하세요\" | error
A|"안녕'하세요" | 안녕'하세요
A|'안녕하세요 | error
A|'안녕하세요\' | 안녕하세요\
A|안녕$1x | 안녕x
A|안녕$.x | 안녕$.x
# Next one is different on Windows as $pwd==$PWD
U|안녕$pwd. | 안녕.
W|안녕$pwd. | 안녕/home.
A|안녕$PWD | 안녕/home
A|안녕\$PWD | 안녕$PWD
A|안녕\\$PWD | 안녕\/home
A|안녕\${} | 안녕${}
A|안녕\${}xx | 안녕${}xx
A|안녕${} | error
A|안녕${}xx | error
A|안녕${hi} | 안녕
A|안녕${hi}xx | 안녕xx
A|안녕${PWD} | 안녕/home
A|안녕${.} | error
A|안녕${XXX:-000}xx | 안녕000xx
A|안녕${PWD:-000}xx | 안녕/homexx
A|안녕${XXX:-$PWD}xx | 안녕/homexx
A|안녕${XXX:-${PWD:-yyy}}xx | 안녕/homexx
A|안녕${XXX:-${YYY:-yyy}}xx | 안녕yyyxx
A|안녕${XXX:YYY} | error
A|안녕${XXX:+${PWD}}xx | 안녕xx
A|안녕${PWD:+${XXX}}xx | 안녕xx
A|안녕${PWD:+${SHELL}}xx | 안녕bashxx
A|안녕${XXX:+000}xx | 안녕xx
A|안녕${PWD:+000}xx | 안녕000xx
A|'안녕${XX}' | 안녕${XX}
A|"안녕${PWD}" | 안녕/home
A|"안녕'$PWD'" | 안녕'/home'
A|'"안녕"' | "안녕"
A|안녕\$PWD | 안녕$PWD
A|"안녕\$PWD" | 안녕$PWD
A|'안녕\$PWD' | 안녕\$PWD
A|안녕${PWD | error
A|안녕${PWD:=000}xx | error
A|안녕${PWD:+${PWD}:}xx | 안녕/home:xx
A|안녕${XXX:-\$PWD:}xx | 안녕$PWD:xx
A|안녕${XXX:-\${PWD}z}xx | 안녕${PWDz}xx
A|$KOREAN | 한국어
A|안녕$KOREAN | 안녕한국어
A|${{aaa} | error
A|${aaa}} | }
A|${aaa | error
A|${{aaa:-bbb} | error
A|${aaa:-bbb}} | bbb}
A|${aaa:-bbb | error
A|${aaa:-bbb} | bbb
A|${aaa:-${bbb:-ccc}} | ccc
A|${aaa:-bbb ${foo} | error
A|${aaa:-bbb {foo} | bbb {foo
A|${:} | error
A|${:-bbb} | error
A|${:+bbb} | error
# Positional parameters won't be set:
# http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_05_01
A|$1 |
A|${1} |
A|${1:+bbb} |
A|${1:-bbb} | bbb
A|$2 |
A|${2} |
A|${2:+bbb} |
A|${2:-bbb} | bbb
A|$3 |
A|${3} |
A|${3:+bbb} |
A|${3:-bbb} | bbb
A|$4 |
A|${4} |
A|${4:+bbb} |
A|${4:-bbb} | bbb
A|$5 |
A|${5} |
A|${5:+bbb} |
A|${5:-bbb} | bbb
A|$6 |
A|${6} |
A|${6:+bbb} |
A|${6:-bbb} | bbb
A|$7 |
A|${7} |
A|${7:+bbb} |
A|${7:-bbb} | bbb
A|$8 |
A|${8} |
A|${8:+bbb} |
A|${8:-bbb} | bbb
A|$9 |
A|${9} |
A|${9:+bbb} |
A|${9:-bbb} | bbb
A|$999 |
A|${999} |
A|${999:+bbb} |
A|${999:-bbb} | bbb
A|$999aaa | aaa
A|${999}aaa | aaa
A|${999:+bbb}aaa | aaa
A|${999:-bbb}aaa | bbbaaa
A|$001 |
A|${001} |
A|${001:+bbb} |
A|${001:-bbb} | bbb
A|$001aaa | aaa
A|${001}aaa | aaa
A|${001:+bbb}aaa | aaa
A|${001:-bbb}aaa | bbbaaa
# Special parameters won't be set in the Dockerfile:
# http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_05_02
A|$@ |
A|${@} |
A|${@:+bbb} |
A|${@:-bbb} | bbb
A|$@@@ | @@
A|$@aaa | aaa
A|${@}aaa | aaa
A|${@:+bbb}aaa | aaa
A|${@:-bbb}aaa | bbbaaa
A|$* |
A|${*} |
A|${*:+bbb} |
A|${*:-bbb} | bbb
A|$# |
A|${#} |
A|${#:+bbb} |
A|${#:-bbb} | bbb
A|$? |
A|${?} |
A|${?:+bbb} |
A|${?:-bbb} | bbb
A|$- |
A|${-} |
A|${-:+bbb} |
A|${-:-bbb} | bbb
A|$$ |
A|${$} |
A|${$:+bbb} |
A|${$:-bbb} | bbb
A|$! |
A|${!} |
A|${!:+bbb} |
A|${!:-bbb} | bbb
A|$0 |
A|${0} |
A|${0:+bbb} |
A|${0:-bbb} | bbb

View File

@@ -1,10 +0,0 @@
// +build !windows
package shell
// EqualEnvKeys compare two strings and returns true if they are equal.
// On Unix this comparison is case sensitive.
// On Windows this comparison is case insensitive.
func EqualEnvKeys(from, to string) bool {
return from == to
}

View File

@@ -1,10 +0,0 @@
package shell
import "strings"
// EqualEnvKeys compare two strings and returns true if they are equal.
// On Unix this comparison is case sensitive.
// On Windows this comparison is case insensitive.
func EqualEnvKeys(from, to string) bool {
return strings.ToUpper(from) == strings.ToUpper(to)
}

View File

@@ -1,466 +0,0 @@
package shell
import (
"bytes"
"fmt"
"strings"
"text/scanner"
"unicode"
"github.com/pkg/errors"
)
// Lex performs shell word splitting and variable expansion.
//
// Lex takes a string and an array of env variables and
// process all quotes (" and ') as well as $xxx and ${xxx} env variable
// tokens. Tries to mimic bash shell process.
// It doesn't support all flavors of ${xx:...} formats but new ones can
// be added by adding code to the "special ${} format processing" section
type Lex struct {
escapeToken rune
RawQuotes bool
SkipUnsetEnv bool
}
// NewLex creates a new Lex which uses escapeToken to escape quotes.
func NewLex(escapeToken rune) *Lex {
return &Lex{escapeToken: escapeToken}
}
// ProcessWord will use the 'env' list of environment variables,
// and replace any env var references in 'word'.
func (s *Lex) ProcessWord(word string, env []string) (string, error) {
word, _, err := s.process(word, BuildEnvs(env))
return word, err
}
// ProcessWords will use the 'env' list of environment variables,
// and replace any env var references in 'word' then it will also
// return a slice of strings which represents the 'word'
// split up based on spaces - taking into account quotes. Note that
// this splitting is done **after** the env var substitutions are done.
// Note, each one is trimmed to remove leading and trailing spaces (unless
// they are quoted", but ProcessWord retains spaces between words.
func (s *Lex) ProcessWords(word string, env []string) ([]string, error) {
_, words, err := s.process(word, BuildEnvs(env))
return words, err
}
// ProcessWordWithMap will use the 'env' list of environment variables,
// and replace any env var references in 'word'.
func (s *Lex) ProcessWordWithMap(word string, env map[string]string) (string, error) {
word, _, err := s.process(word, env)
return word, err
}
func (s *Lex) ProcessWordsWithMap(word string, env map[string]string) ([]string, error) {
_, words, err := s.process(word, env)
return words, err
}
func (s *Lex) process(word string, env map[string]string) (string, []string, error) {
sw := &shellWord{
envs: env,
escapeToken: s.escapeToken,
skipUnsetEnv: s.SkipUnsetEnv,
rawQuotes: s.RawQuotes,
}
sw.scanner.Init(strings.NewReader(word))
return sw.process(word)
}
type shellWord struct {
scanner scanner.Scanner
envs map[string]string
escapeToken rune
rawQuotes bool
skipUnsetEnv bool
}
func (sw *shellWord) process(source string) (string, []string, error) {
word, words, err := sw.processStopOn(scanner.EOF)
if err != nil {
err = errors.Wrapf(err, "failed to process %q", source)
}
return word, words, err
}
type wordsStruct struct {
word string
words []string
inWord bool
}
func (w *wordsStruct) addChar(ch rune) {
if unicode.IsSpace(ch) && w.inWord {
if len(w.word) != 0 {
w.words = append(w.words, w.word)
w.word = ""
w.inWord = false
}
} else if !unicode.IsSpace(ch) {
w.addRawChar(ch)
}
}
func (w *wordsStruct) addRawChar(ch rune) {
w.word += string(ch)
w.inWord = true
}
func (w *wordsStruct) addString(str string) {
for _, ch := range str {
w.addChar(ch)
}
}
func (w *wordsStruct) addRawString(str string) {
w.word += str
w.inWord = true
}
func (w *wordsStruct) getWords() []string {
if len(w.word) > 0 {
w.words = append(w.words, w.word)
// Just in case we're called again by mistake
w.word = ""
w.inWord = false
}
return w.words
}
// Process the word, starting at 'pos', and stop when we get to the
// end of the word or the 'stopChar' character
func (sw *shellWord) processStopOn(stopChar rune) (string, []string, error) {
var result bytes.Buffer
var words wordsStruct
var charFuncMapping = map[rune]func() (string, error){
'\'': sw.processSingleQuote,
'"': sw.processDoubleQuote,
'$': sw.processDollar,
}
for sw.scanner.Peek() != scanner.EOF {
ch := sw.scanner.Peek()
if stopChar != scanner.EOF && ch == stopChar {
sw.scanner.Next()
return result.String(), words.getWords(), nil
}
if fn, ok := charFuncMapping[ch]; ok {
// Call special processing func for certain chars
tmp, err := fn()
if err != nil {
return "", []string{}, err
}
result.WriteString(tmp)
if ch == rune('$') {
words.addString(tmp)
} else {
words.addRawString(tmp)
}
} else {
// Not special, just add it to the result
ch = sw.scanner.Next()
if ch == sw.escapeToken {
// '\' (default escape token, but ` allowed) escapes, except end of line
ch = sw.scanner.Next()
if ch == scanner.EOF {
break
}
words.addRawChar(ch)
} else {
words.addChar(ch)
}
result.WriteRune(ch)
}
}
if stopChar != scanner.EOF {
return "", []string{}, errors.Errorf("unexpected end of statement while looking for matching %s", string(stopChar))
}
return result.String(), words.getWords(), nil
}
func (sw *shellWord) processSingleQuote() (string, error) {
// All chars between single quotes are taken as-is
// Note, you can't escape '
//
// From the "sh" man page:
// Single Quotes
// Enclosing characters in single quotes preserves the literal meaning of
// all the characters (except single quotes, making it impossible to put
// single-quotes in a single-quoted string).
var result bytes.Buffer
ch := sw.scanner.Next()
if sw.rawQuotes {
result.WriteRune(ch)
}
for {
ch = sw.scanner.Next()
switch ch {
case scanner.EOF:
return "", errors.New("unexpected end of statement while looking for matching single-quote")
case '\'':
if sw.rawQuotes {
result.WriteRune(ch)
}
return result.String(), nil
}
result.WriteRune(ch)
}
}
func (sw *shellWord) processDoubleQuote() (string, error) {
// All chars up to the next " are taken as-is, even ', except any $ chars
// But you can escape " with a \ (or ` if escape token set accordingly)
//
// From the "sh" man page:
// Double Quotes
// Enclosing characters within double quotes preserves the literal meaning
// of all characters except dollarsign ($), backquote (`), and backslash
// (\). The backslash inside double quotes is historically weird, and
// serves to quote only the following characters:
// $ ` " \ <newline>.
// Otherwise it remains literal.
var result bytes.Buffer
ch := sw.scanner.Next()
if sw.rawQuotes {
result.WriteRune(ch)
}
for {
switch sw.scanner.Peek() {
case scanner.EOF:
return "", errors.New("unexpected end of statement while looking for matching double-quote")
case '"':
ch := sw.scanner.Next()
if sw.rawQuotes {
result.WriteRune(ch)
}
return result.String(), nil
case '$':
value, err := sw.processDollar()
if err != nil {
return "", err
}
result.WriteString(value)
default:
ch := sw.scanner.Next()
if ch == sw.escapeToken {
switch sw.scanner.Peek() {
case scanner.EOF:
// Ignore \ at end of word
continue
case '"', '$', sw.escapeToken:
// These chars can be escaped, all other \'s are left as-is
// Note: for now don't do anything special with ` chars.
// Not sure what to do with them anyway since we're not going
// to execute the text in there (not now anyway).
ch = sw.scanner.Next()
}
}
result.WriteRune(ch)
}
}
}
func (sw *shellWord) processDollar() (string, error) {
sw.scanner.Next()
// $xxx case
if sw.scanner.Peek() != '{' {
name := sw.processName()
if name == "" {
return "$", nil
}
value, found := sw.getEnv(name)
if !found && sw.skipUnsetEnv {
return "$" + name, nil
}
return value, nil
}
sw.scanner.Next()
switch sw.scanner.Peek() {
case scanner.EOF:
return "", errors.New("syntax error: missing '}'")
case '{', '}', ':':
// Invalid ${{xx}, ${:xx}, ${:}. ${} case
return "", errors.New("syntax error: bad substitution")
}
name := sw.processName()
ch := sw.scanner.Next()
switch ch {
case '}':
// Normal ${xx} case
value, found := sw.getEnv(name)
if !found && sw.skipUnsetEnv {
return fmt.Sprintf("${%s}", name), nil
}
return value, nil
case '?':
word, _, err := sw.processStopOn('}')
if err != nil {
if sw.scanner.Peek() == scanner.EOF {
return "", errors.New("syntax error: missing '}'")
}
return "", err
}
newValue, found := sw.getEnv(name)
if !found {
if sw.skipUnsetEnv {
return fmt.Sprintf("${%s?%s}", name, word), nil
}
message := "is not allowed to be unset"
if word != "" {
message = word
}
return "", errors.Errorf("%s: %s", name, message)
}
return newValue, nil
case ':':
// Special ${xx:...} format processing
// Yes it allows for recursive $'s in the ... spot
modifier := sw.scanner.Next()
word, _, err := sw.processStopOn('}')
if err != nil {
if sw.scanner.Peek() == scanner.EOF {
return "", errors.New("syntax error: missing '}'")
}
return "", err
}
// Grab the current value of the variable in question so we
// can use to to determine what to do based on the modifier
newValue, found := sw.getEnv(name)
switch modifier {
case '+':
if newValue != "" {
newValue = word
}
if !found && sw.skipUnsetEnv {
return fmt.Sprintf("${%s:%s%s}", name, string(modifier), word), nil
}
return newValue, nil
case '-':
if newValue == "" {
newValue = word
}
if !found && sw.skipUnsetEnv {
return fmt.Sprintf("${%s:%s%s}", name, string(modifier), word), nil
}
return newValue, nil
case '?':
if !found {
if sw.skipUnsetEnv {
return fmt.Sprintf("${%s:%s%s}", name, string(modifier), word), nil
}
message := "is not allowed to be unset"
if word != "" {
message = word
}
return "", errors.Errorf("%s: %s", name, message)
}
if newValue == "" {
message := "is not allowed to be empty"
if word != "" {
message = word
}
return "", errors.Errorf("%s: %s", name, message)
}
return newValue, nil
default:
return "", errors.Errorf("unsupported modifier (%c) in substitution", modifier)
}
}
return "", errors.Errorf("missing ':' in substitution")
}
func (sw *shellWord) processName() string {
// Read in a name (alphanumeric or _)
// If it starts with a numeric then just return $#
var name bytes.Buffer
for sw.scanner.Peek() != scanner.EOF {
ch := sw.scanner.Peek()
if name.Len() == 0 && unicode.IsDigit(ch) {
for sw.scanner.Peek() != scanner.EOF && unicode.IsDigit(sw.scanner.Peek()) {
// Keep reading until the first non-digit character, or EOF
ch = sw.scanner.Next()
name.WriteRune(ch)
}
return name.String()
}
if name.Len() == 0 && isSpecialParam(ch) {
ch = sw.scanner.Next()
return string(ch)
}
if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '_' {
break
}
ch = sw.scanner.Next()
name.WriteRune(ch)
}
return name.String()
}
// isSpecialParam checks if the provided character is a special parameters,
// as defined in http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_05_02
func isSpecialParam(char rune) bool {
switch char {
case '@', '*', '#', '?', '-', '$', '!', '0':
// Special parameters
// http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_05_02
return true
}
return false
}
func (sw *shellWord) getEnv(name string) (string, bool) {
for key, value := range sw.envs {
if EqualEnvKeys(name, key) {
return value, true
}
}
return "", false
}
func BuildEnvs(env []string) map[string]string {
envs := map[string]string{}
for _, e := range env {
i := strings.Index(e, "=")
if i < 0 {
envs[e] = ""
} else {
k := e[:i]
v := e[i+1:]
// overwrite value if key already exists
envs[k] = v
}
}
return envs
}

View File

@@ -1,30 +0,0 @@
hello | hello
hello${hi}bye | hellobye
ENV hi=hi
hello${hi}bye | hellohibye
ENV space=abc def
hello${space}bye | helloabc,defbye
hello"${space}"bye | helloabc defbye
hello "${space}"bye | hello,abc defbye
ENV leading= ab c
hello${leading}def | hello,ab,cdef
hello"${leading}" def | hello ab c,def
hello"${leading}" | hello ab c
hello${leading} | hello,ab,c
# next line MUST have 3 trailing spaces, don't erase them!
ENV trailing=ab c
hello${trailing} | helloab,c
hello${trailing}d | helloab,c,d
hello"${trailing}"d | helloab c d
# next line MUST have 3 trailing spaces, don't erase them!
hel"lo${trailing}" | helloab c
hello" there " | hello there
hello there | hello,there
hello\ there | hello there
hello" there | error
hello\" there | hello",there
hello"\\there" | hello\there
hello"\there" | hello\there
hello'\\there' | hello\\there
hello'\there' | hello\there
hello'$there' | hello$there

View File

@@ -1,217 +0,0 @@
package forwarder
import (
"context"
"sync"
"github.com/moby/buildkit/cache"
cacheutil "github.com/moby/buildkit/cache/util"
clienttypes "github.com/moby/buildkit/client"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/frontend"
"github.com/moby/buildkit/frontend/gateway/client"
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/solver"
opspb "github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/apicaps"
"github.com/moby/buildkit/worker"
"github.com/pkg/errors"
fstypes "github.com/tonistiigi/fsutil/types"
)
func llbBridgeToGatewayClient(ctx context.Context, llbBridge frontend.FrontendLLBBridge, opts map[string]string, inputs map[string]*opspb.Definition, workerInfos []clienttypes.WorkerInfo) (*bridgeClient, error) {
return &bridgeClient{
opts: opts,
inputs: inputs,
FrontendLLBBridge: llbBridge,
sid: session.FromContext(ctx),
workerInfos: workerInfos,
final: map[*ref]struct{}{},
}, nil
}
type bridgeClient struct {
frontend.FrontendLLBBridge
mu sync.Mutex
opts map[string]string
inputs map[string]*opspb.Definition
final map[*ref]struct{}
sid string
exporterAttr map[string][]byte
refs []*ref
workerInfos []clienttypes.WorkerInfo
}
func (c *bridgeClient) Solve(ctx context.Context, req client.SolveRequest) (*client.Result, error) {
res, err := c.FrontendLLBBridge.Solve(ctx, frontend.SolveRequest{
Definition: req.Definition,
Frontend: req.Frontend,
FrontendOpt: req.FrontendOpt,
FrontendInputs: req.FrontendInputs,
CacheImports: req.CacheImports,
})
if err != nil {
return nil, err
}
cRes := &client.Result{}
c.mu.Lock()
for k, r := range res.Refs {
rr, err := newRef(r)
if err != nil {
return nil, err
}
c.refs = append(c.refs, rr)
cRes.AddRef(k, rr)
}
if r := res.Ref; r != nil {
rr, err := newRef(r)
if err != nil {
return nil, err
}
c.refs = append(c.refs, rr)
cRes.SetRef(rr)
}
c.mu.Unlock()
cRes.Metadata = res.Metadata
return cRes, nil
}
func (c *bridgeClient) BuildOpts() client.BuildOpts {
workers := make([]client.WorkerInfo, 0, len(c.workerInfos))
for _, w := range c.workerInfos {
workers = append(workers, client.WorkerInfo{
ID: w.ID,
Labels: w.Labels,
Platforms: w.Platforms,
})
}
return client.BuildOpts{
Opts: c.opts,
SessionID: c.sid,
Workers: workers,
Product: apicaps.ExportedProduct,
Caps: gwpb.Caps.CapSet(gwpb.Caps.All()),
LLBCaps: opspb.Caps.CapSet(opspb.Caps.All()),
}
}
func (c *bridgeClient) Inputs(ctx context.Context) (map[string]llb.State, error) {
inputs := make(map[string]llb.State)
for key, def := range c.inputs {
defop, err := llb.NewDefinitionOp(def)
if err != nil {
return nil, err
}
inputs[key] = llb.NewState(defop)
}
return inputs, nil
}
func (c *bridgeClient) toFrontendResult(r *client.Result) (*frontend.Result, error) {
if r == nil {
return nil, nil
}
res := &frontend.Result{}
if r.Refs != nil {
res.Refs = make(map[string]solver.ResultProxy, len(r.Refs))
for k, r := range r.Refs {
rr, ok := r.(*ref)
if !ok {
return nil, errors.Errorf("invalid reference type for forward %T", r)
}
c.final[rr] = struct{}{}
res.Refs[k] = rr.ResultProxy
}
}
if r := r.Ref; r != nil {
rr, ok := r.(*ref)
if !ok {
return nil, errors.Errorf("invalid reference type for forward %T", r)
}
c.final[rr] = struct{}{}
res.Ref = rr.ResultProxy
}
res.Metadata = r.Metadata
return res, nil
}
func (c *bridgeClient) discard(err error) {
for _, r := range c.refs {
if r != nil {
if _, ok := c.final[r]; !ok || err != nil {
r.Release(context.TODO())
}
}
}
}
type ref struct {
solver.ResultProxy
}
func newRef(r solver.ResultProxy) (*ref, error) {
return &ref{ResultProxy: r}, nil
}
func (r *ref) ToState() (st llb.State, err error) {
defop, err := llb.NewDefinitionOp(r.Definition())
if err != nil {
return st, err
}
return llb.NewState(defop), nil
}
func (r *ref) ReadFile(ctx context.Context, req client.ReadRequest) ([]byte, error) {
ref, err := r.getImmutableRef(ctx)
if err != nil {
return nil, err
}
newReq := cacheutil.ReadRequest{
Filename: req.Filename,
}
if r := req.Range; r != nil {
newReq.Range = &cacheutil.FileRange{
Offset: r.Offset,
Length: r.Length,
}
}
return cacheutil.ReadFile(ctx, ref, newReq)
}
func (r *ref) ReadDir(ctx context.Context, req client.ReadDirRequest) ([]*fstypes.Stat, error) {
ref, err := r.getImmutableRef(ctx)
if err != nil {
return nil, err
}
newReq := cacheutil.ReadDirRequest{
Path: req.Path,
IncludePattern: req.IncludePattern,
}
return cacheutil.ReadDir(ctx, ref, newReq)
}
func (r *ref) StatFile(ctx context.Context, req client.StatRequest) (*fstypes.Stat, error) {
ref, err := r.getImmutableRef(ctx)
if err != nil {
return nil, err
}
return cacheutil.StatFile(ctx, ref, req.Path)
}
func (r *ref) getImmutableRef(ctx context.Context) (cache.ImmutableRef, error) {
rr, err := r.ResultProxy.Result(ctx)
if err != nil {
return nil, err
}
ref, ok := rr.Sys().(*worker.WorkerRef)
if !ok {
return nil, errors.Errorf("invalid ref: %T", rr.Sys())
}
return ref.ImmutableRef, nil
}

View File

@@ -1,39 +0,0 @@
package forwarder
import (
"context"
"github.com/moby/buildkit/frontend"
"github.com/moby/buildkit/frontend/gateway/client"
"github.com/moby/buildkit/solver/pb"
)
func NewGatewayForwarder(w frontend.WorkerInfos, f client.BuildFunc) frontend.Frontend {
return &GatewayForwarder{
workers: w,
f: f,
}
}
type GatewayForwarder struct {
workers frontend.WorkerInfos
f client.BuildFunc
}
func (gf *GatewayForwarder) Solve(ctx context.Context, llbBridge frontend.FrontendLLBBridge, opts map[string]string, inputs map[string]*pb.Definition) (retRes *frontend.Result, retErr error) {
c, err := llbBridgeToGatewayClient(ctx, llbBridge, opts, inputs, gf.workers.WorkerInfos())
if err != nil {
return nil, err
}
defer func() {
c.discard(retErr)
}()
res, err := gf.f(ctx, c)
if err != nil {
return nil, err
}
return c.toFrontendResult(res)
}

View File

@@ -1,459 +0,0 @@
package bboltcachestorage
import (
"bytes"
"encoding/json"
"fmt"
"github.com/moby/buildkit/solver"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
bolt "go.etcd.io/bbolt"
)
const (
resultBucket = "_result"
linksBucket = "_links"
byResultBucket = "_byresult"
backlinksBucket = "_backlinks"
)
type Store struct {
db *bolt.DB
}
func NewStore(dbPath string) (*Store, error) {
db, err := bolt.Open(dbPath, 0600, nil)
if err != nil {
return nil, errors.Wrapf(err, "failed to open database file %s", dbPath)
}
if err := db.Update(func(tx *bolt.Tx) error {
for _, b := range []string{resultBucket, linksBucket, byResultBucket, backlinksBucket} {
if _, err := tx.CreateBucketIfNotExists([]byte(b)); err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err
}
db.NoSync = true
return &Store{db: db}, nil
}
func (s *Store) Exists(id string) bool {
exists := false
err := s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(linksBucket)).Bucket([]byte(id))
exists = b != nil
return nil
})
if err != nil {
return false
}
return exists
}
func (s *Store) Walk(fn func(id string) error) error {
ids := make([]string, 0)
if err := s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(linksBucket))
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
if v == nil {
ids = append(ids, string(k))
}
}
return nil
}); err != nil {
return err
}
for _, id := range ids {
if err := fn(id); err != nil {
return err
}
}
return nil
}
func (s *Store) WalkResults(id string, fn func(solver.CacheResult) error) error {
var list []solver.CacheResult
if err := s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(resultBucket))
if b == nil {
return nil
}
b = b.Bucket([]byte(id))
if b == nil {
return nil
}
return b.ForEach(func(k, v []byte) error {
var res solver.CacheResult
if err := json.Unmarshal(v, &res); err != nil {
return err
}
list = append(list, res)
return nil
})
}); err != nil {
return err
}
for _, res := range list {
if err := fn(res); err != nil {
return err
}
}
return nil
}
func (s *Store) Load(id string, resultID string) (solver.CacheResult, error) {
var res solver.CacheResult
if err := s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(resultBucket))
if b == nil {
return errors.WithStack(solver.ErrNotFound)
}
b = b.Bucket([]byte(id))
if b == nil {
return errors.WithStack(solver.ErrNotFound)
}
v := b.Get([]byte(resultID))
if v == nil {
return errors.WithStack(solver.ErrNotFound)
}
return json.Unmarshal(v, &res)
}); err != nil {
return solver.CacheResult{}, err
}
return res, nil
}
func (s *Store) AddResult(id string, res solver.CacheResult) error {
return s.db.Update(func(tx *bolt.Tx) error {
_, err := tx.Bucket([]byte(linksBucket)).CreateBucketIfNotExists([]byte(id))
if err != nil {
return err
}
b, err := tx.Bucket([]byte(resultBucket)).CreateBucketIfNotExists([]byte(id))
if err != nil {
return err
}
dt, err := json.Marshal(res)
if err != nil {
return err
}
if err := b.Put([]byte(res.ID), dt); err != nil {
return err
}
b, err = tx.Bucket([]byte(byResultBucket)).CreateBucketIfNotExists([]byte(res.ID))
if err != nil {
return err
}
if err := b.Put([]byte(id), []byte{}); err != nil {
return err
}
return nil
})
}
func (s *Store) WalkIDsByResult(resultID string, fn func(string) error) error {
ids := map[string]struct{}{}
if err := s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(byResultBucket))
if b == nil {
return nil
}
b = b.Bucket([]byte(resultID))
if b == nil {
return nil
}
return b.ForEach(func(k, v []byte) error {
ids[string(k)] = struct{}{}
return nil
})
}); err != nil {
return err
}
for id := range ids {
if err := fn(id); err != nil {
return err
}
}
return nil
}
func (s *Store) Release(resultID string) error {
return s.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(byResultBucket))
if b == nil {
return errors.WithStack(solver.ErrNotFound)
}
b = b.Bucket([]byte(resultID))
if b == nil {
return errors.WithStack(solver.ErrNotFound)
}
if err := b.ForEach(func(k, v []byte) error {
return s.releaseHelper(tx, string(k), resultID)
}); err != nil {
return err
}
return nil
})
}
func (s *Store) releaseHelper(tx *bolt.Tx, id, resultID string) error {
results := tx.Bucket([]byte(resultBucket)).Bucket([]byte(id))
if results == nil {
return nil
}
if err := results.Delete([]byte(resultID)); err != nil {
return err
}
ids := tx.Bucket([]byte(byResultBucket))
ids = ids.Bucket([]byte(resultID))
if ids == nil {
return nil
}
if err := ids.Delete([]byte(id)); err != nil {
return err
}
if isEmptyBucket(ids) {
if err := tx.Bucket([]byte(byResultBucket)).DeleteBucket([]byte(resultID)); err != nil {
return err
}
}
links := tx.Bucket([]byte(resultBucket))
if results == nil {
return nil
}
links = links.Bucket([]byte(id))
return s.emptyBranchWithParents(tx, []byte(id))
}
func (s *Store) emptyBranchWithParents(tx *bolt.Tx, id []byte) error {
results := tx.Bucket([]byte(resultBucket)).Bucket(id)
if results == nil {
return nil
}
isEmptyLinks := true
links := tx.Bucket([]byte(linksBucket)).Bucket(id)
if links != nil {
isEmptyLinks = isEmptyBucket(links)
}
if !isEmptyBucket(results) || !isEmptyLinks {
return nil
}
if backlinks := tx.Bucket([]byte(backlinksBucket)).Bucket(id); backlinks != nil {
if err := backlinks.ForEach(func(k, v []byte) error {
if subLinks := tx.Bucket([]byte(linksBucket)).Bucket(k); subLinks != nil {
if err := subLinks.ForEach(func(k, v []byte) error {
parts := bytes.Split(k, []byte("@"))
if len(parts) != 2 {
return errors.Errorf("invalid key %s", k)
}
if bytes.Equal(id, parts[1]) {
return subLinks.Delete(k)
}
return nil
}); err != nil {
return err
}
if isEmptyBucket(subLinks) {
if err := tx.Bucket([]byte(linksBucket)).DeleteBucket(k); err != nil {
return err
}
}
}
return s.emptyBranchWithParents(tx, k)
}); err != nil {
return err
}
if err := tx.Bucket([]byte(backlinksBucket)).DeleteBucket(id); err != nil {
return err
}
}
// intentionally ignoring errors
tx.Bucket([]byte(linksBucket)).DeleteBucket([]byte(id))
tx.Bucket([]byte(resultBucket)).DeleteBucket([]byte(id))
return nil
}
func (s *Store) AddLink(id string, link solver.CacheInfoLink, target string) error {
return s.db.Update(func(tx *bolt.Tx) error {
b, err := tx.Bucket([]byte(linksBucket)).CreateBucketIfNotExists([]byte(id))
if err != nil {
return err
}
dt, err := json.Marshal(link)
if err != nil {
return err
}
if err := b.Put(bytes.Join([][]byte{dt, []byte(target)}, []byte("@")), []byte{}); err != nil {
return err
}
b, err = tx.Bucket([]byte(backlinksBucket)).CreateBucketIfNotExists([]byte(target))
if err != nil {
return err
}
if err := b.Put([]byte(id), []byte{}); err != nil {
return err
}
return nil
})
}
func (s *Store) WalkLinks(id string, link solver.CacheInfoLink, fn func(id string) error) error {
var links []string
if err := s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(linksBucket))
if b == nil {
return nil
}
b = b.Bucket([]byte(id))
if b == nil {
return nil
}
dt, err := json.Marshal(link)
if err != nil {
return err
}
index := bytes.Join([][]byte{dt, {}}, []byte("@"))
c := b.Cursor()
k, _ := c.Seek([]byte(index))
for {
if k != nil && bytes.HasPrefix(k, index) {
target := bytes.TrimPrefix(k, index)
links = append(links, string(target))
k, _ = c.Next()
} else {
break
}
}
return nil
}); err != nil {
return err
}
for _, l := range links {
if err := fn(l); err != nil {
return err
}
}
return nil
}
func (s *Store) HasLink(id string, link solver.CacheInfoLink, target string) bool {
var v bool
if err := s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(linksBucket))
if b == nil {
return nil
}
b = b.Bucket([]byte(id))
if b == nil {
return nil
}
dt, err := json.Marshal(link)
if err != nil {
return err
}
v = b.Get(bytes.Join([][]byte{dt, []byte(target)}, []byte("@"))) != nil
return nil
}); err != nil {
return false
}
return v
}
func (s *Store) WalkBacklinks(id string, fn func(id string, link solver.CacheInfoLink) error) error {
var outIDs []string
var outLinks []solver.CacheInfoLink
if err := s.db.View(func(tx *bolt.Tx) error {
links := tx.Bucket([]byte(linksBucket))
if links == nil {
return nil
}
backLinks := tx.Bucket([]byte(backlinksBucket))
if backLinks == nil {
return nil
}
b := backLinks.Bucket([]byte(id))
if b == nil {
return nil
}
if err := b.ForEach(func(bid, v []byte) error {
b = links.Bucket(bid)
if b == nil {
return nil
}
if err := b.ForEach(func(k, v []byte) error {
parts := bytes.Split(k, []byte("@"))
if len(parts) == 2 {
if string(parts[1]) != id {
return nil
}
var l solver.CacheInfoLink
if err := json.Unmarshal(parts[0], &l); err != nil {
return err
}
l.Digest = digest.FromBytes([]byte(fmt.Sprintf("%s@%d", l.Digest, l.Output)))
l.Output = 0
outIDs = append(outIDs, string(bid))
outLinks = append(outLinks, l)
}
return nil
}); err != nil {
return err
}
return nil
}); err != nil {
return err
}
return nil
}); err != nil {
return err
}
for i := range outIDs {
if err := fn(outIDs[i], outLinks[i]); err != nil {
return err
}
}
return nil
}
func isEmptyBucket(b *bolt.Bucket) bool {
if b == nil {
return true
}
k, _ := b.Cursor().First()
return k == nil
}

View File

@@ -1,120 +0,0 @@
package cniprovider
import (
"context"
"os"
"path/filepath"
"syscall"
"github.com/containerd/containerd/oci"
"github.com/containerd/go-cni"
"github.com/gofrs/flock"
"github.com/moby/buildkit/identity"
"github.com/moby/buildkit/util/network"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"golang.org/x/sys/unix"
)
type Opt struct {
Root string
ConfigPath string
BinaryDir string
}
func New(opt Opt) (network.Provider, error) {
if _, err := os.Stat(opt.ConfigPath); err != nil {
return nil, errors.Wrapf(err, "failed to read cni config %q", opt.ConfigPath)
}
if _, err := os.Stat(opt.BinaryDir); err != nil {
return nil, errors.Wrapf(err, "failed to read cni binary dir %q", opt.BinaryDir)
}
cniHandle, err := cni.New(
cni.WithMinNetworkCount(2),
cni.WithConfFile(opt.ConfigPath),
cni.WithPluginDir([]string{opt.BinaryDir}),
cni.WithLoNetwork,
cni.WithInterfacePrefix(("eth")))
if err != nil {
return nil, err
}
if err != nil {
return nil, err
}
cp := &cniProvider{CNI: cniHandle, root: opt.Root}
if err := cp.initNetwork(); err != nil {
return nil, err
}
return cp, nil
}
type cniProvider struct {
cni.CNI
root string
}
func (c *cniProvider) initNetwork() error {
if v := os.Getenv("BUILDKIT_CNI_INIT_LOCK_PATH"); v != "" {
l := flock.New(v)
if err := l.Lock(); err != nil {
return err
}
defer l.Unlock()
}
ns, err := c.New()
if err != nil {
return err
}
return ns.Close()
}
func (c *cniProvider) New() (network.Namespace, error) {
id := identity.NewID()
nsPath := filepath.Join(c.root, "net/cni", id)
if err := os.MkdirAll(filepath.Dir(nsPath), 0700); err != nil {
return nil, err
}
if err := createNetNS(nsPath); err != nil {
os.RemoveAll(filepath.Dir(nsPath))
return nil, err
}
if _, err := c.CNI.Setup(context.TODO(), id, nsPath); err != nil {
os.RemoveAll(filepath.Dir(nsPath))
return nil, errors.Wrap(err, "CNI setup error")
}
return &cniNS{path: nsPath, id: id, handle: c.CNI}, nil
}
type cniNS struct {
handle cni.CNI
id string
path string
}
func (ns *cniNS) Set(s *specs.Spec) {
oci.WithLinuxNamespace(specs.LinuxNamespace{
Type: specs.NetworkNamespace,
Path: ns.path,
})(nil, nil, nil, s)
}
func (ns *cniNS) Close() error {
err := ns.handle.Remove(context.TODO(), ns.id, ns.path)
if err1 := unix.Unmount(ns.path, unix.MNT_DETACH); err1 != nil {
if err1 != syscall.EINVAL && err1 != syscall.ENOENT && err == nil {
err = errors.Wrap(err1, "error unmounting network namespace")
}
}
if err1 := os.RemoveAll(filepath.Dir(ns.path)); err1 != nil && !os.IsNotExist(err1) && err == nil {
err = errors.Wrap(err, "error removing network namespace")
}
return err
}

View File

@@ -1,16 +0,0 @@
// +build linux
package cniprovider
import (
_ "unsafe" // required for go:linkname.
)
//go:linkname beforeFork syscall.runtime_BeforeFork
func beforeFork()
//go:linkname afterFork syscall.runtime_AfterFork
func afterFork()
//go:linkname afterForkInChild syscall.runtime_AfterForkInChild
func afterForkInChild()

View File

@@ -1,59 +0,0 @@
// +build linux
package cniprovider
import (
"os"
"syscall"
"unsafe"
"github.com/pkg/errors"
"golang.org/x/sys/unix"
)
func createNetNS(p string) error {
f, err := os.Create(p)
if err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
procNetNSBytes, err := syscall.BytePtrFromString("/proc/self/ns/net")
if err != nil {
return err
}
pBytes, err := syscall.BytePtrFromString(p)
if err != nil {
return err
}
beforeFork()
pid, _, errno := syscall.RawSyscall6(syscall.SYS_CLONE, uintptr(syscall.SIGCHLD)|unix.CLONE_NEWNET, 0, 0, 0, 0, 0)
if errno != 0 {
afterFork()
return errno
}
if pid != 0 {
afterFork()
var ws unix.WaitStatus
_, err = unix.Wait4(int(pid), &ws, 0, nil)
for err == syscall.EINTR {
_, err = unix.Wait4(int(pid), &ws, 0, nil)
}
if err != nil {
return errors.Wrapf(err, "failed to find pid=%d process", pid)
}
errno = syscall.Errno(ws.ExitStatus())
if errno != 0 {
return errors.Wrap(errno, "failed to mount")
}
return nil
}
afterForkInChild()
_, _, errno = syscall.RawSyscall6(syscall.SYS_MOUNT, uintptr(unsafe.Pointer(procNetNSBytes)), uintptr(unsafe.Pointer(pBytes)), 0, uintptr(unix.MS_BIND), 0, 0)
syscall.RawSyscall(syscall.SYS_EXIT, uintptr(errno), 0, 0)
panic("unreachable")
}

View File

@@ -1,9 +0,0 @@
// +build !linux
package cniprovider
import "github.com/pkg/errors"
func createNetNS(p string) error {
return errors.Errorf("creating netns for cni not supported")
}

View File

@@ -1,50 +0,0 @@
package netproviders
import (
"os"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/network"
"github.com/moby/buildkit/util/network/cniprovider"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type Opt struct {
CNI cniprovider.Opt
Mode string
}
// Providers returns the network provider set
func Providers(opt Opt) (map[pb.NetMode]network.Provider, error) {
var defaultProvider network.Provider
switch opt.Mode {
case "cni":
cniProvider, err := cniprovider.New(opt.CNI)
if err != nil {
return nil, err
}
defaultProvider = cniProvider
case "host":
defaultProvider = network.NewHostProvider()
case "auto", "":
if _, err := os.Stat(opt.CNI.ConfigPath); err == nil {
cniProvider, err := cniprovider.New(opt.CNI)
if err != nil {
return nil, err
}
defaultProvider = cniProvider
} else {
logrus.Warnf("using host network as the default")
defaultProvider = network.NewHostProvider()
}
default:
return nil, errors.Errorf("invalid network mode: %q", opt.Mode)
}
return map[pb.NetMode]network.Provider{
pb.NetMode_UNSET: defaultProvider,
pb.NetMode_HOST: network.NewHostProvider(),
pb.NetMode_NONE: network.NewNoneProvider(),
}, nil
}

View File

@@ -1,40 +0,0 @@
package specconv
import (
"strings"
"github.com/opencontainers/runtime-spec/specs-go"
)
// ToRootless converts spec to be compatible with "rootless" runc.
// * Remove /sys mount
// * Remove cgroups
//
// See docs/rootless.md for the supported runc revision.
func ToRootless(spec *specs.Spec) error {
// Remove /sys mount because we can't mount /sys when the daemon netns
// is not unshared from the host.
//
// Instead, we could bind-mount /sys from the host, however, `rbind, ro`
// does not make /sys/fs/cgroup read-only (and we can't bind-mount /sys
// without rbind)
//
// PR for making /sys/fs/cgroup read-only is proposed, but it is very
// complicated: https://github.com/opencontainers/runc/pull/1869
//
// For buildkit usecase, we suppose we don't need to provide /sys to
// containers and remove /sys mount as a workaround.
var mounts []specs.Mount
for _, mount := range spec.Mounts {
if strings.HasPrefix(mount.Destination, "/sys") {
continue
}
mounts = append(mounts, mount)
}
spec.Mounts = mounts
// Remove cgroups so as to avoid `container_linux.go:337: starting container process caused "process_linux.go:280: applying cgroup configuration for process caused \"mkdir /sys/fs/cgroup/cpuset/buildkit: permission denied\""`
spec.Linux.Resources = nil
spec.Linux.CgroupsPath = ""
return nil
}