2017-08-25 20:08:18 +00:00
|
|
|
package dockerfile2llb
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
2017-09-11 01:11:42 +00:00
|
|
|
"encoding/json"
|
2017-09-01 23:57:22 +00:00
|
|
|
"fmt"
|
2017-12-03 05:45:41 +00:00
|
|
|
"net/url"
|
2017-08-25 20:08:18 +00:00
|
|
|
"path"
|
2017-11-07 00:25:46 +00:00
|
|
|
"path/filepath"
|
2018-04-14 01:16:22 +00:00
|
|
|
"sort"
|
2017-08-25 20:08:18 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
|
2018-06-25 02:32:13 +00:00
|
|
|
"github.com/containerd/containerd/platforms"
|
2017-08-25 20:08:18 +00:00
|
|
|
"github.com/docker/distribution/reference"
|
2017-09-11 01:11:42 +00:00
|
|
|
"github.com/docker/docker/pkg/signal"
|
|
|
|
"github.com/docker/go-connections/nat"
|
2017-08-25 20:08:18 +00:00
|
|
|
"github.com/moby/buildkit/client/llb"
|
2017-12-01 01:29:57 +00:00
|
|
|
"github.com/moby/buildkit/client/llb/imagemetaresolver"
|
2018-06-02 00:30:18 +00:00
|
|
|
"github.com/moby/buildkit/frontend/dockerfile/instructions"
|
|
|
|
"github.com/moby/buildkit/frontend/dockerfile/parser"
|
|
|
|
"github.com/moby/buildkit/frontend/dockerfile/shell"
|
2018-07-25 06:18:21 +00:00
|
|
|
gw "github.com/moby/buildkit/frontend/gateway/client"
|
2018-08-07 01:49:30 +00:00
|
|
|
"github.com/moby/buildkit/solver/pb"
|
2018-09-11 19:14:46 +00:00
|
|
|
"github.com/moby/buildkit/util/system"
|
2018-06-25 02:32:13 +00:00
|
|
|
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
2017-08-25 20:08:18 +00:00
|
|
|
"github.com/pkg/errors"
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2017-09-22 17:30:30 +00:00
|
|
|
emptyImageName = "scratch"
|
|
|
|
localNameContext = "context"
|
2017-12-08 01:33:17 +00:00
|
|
|
historyComment = "buildkit.dockerfile.v0"
|
2018-05-17 06:03:52 +00:00
|
|
|
|
2018-08-31 21:36:35 +00:00
|
|
|
DefaultCopyImage = "tonistiigi/copy:v0.1.4@sha256:d9d49bedbbe2b27df88115e6aff7b9cd11ed2fbd8d9013f02d3da735c08c92e5"
|
2017-08-25 20:08:18 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type ConvertOpt struct {
|
|
|
|
Target string
|
|
|
|
MetaResolver llb.ImageMetaResolver
|
2017-09-12 06:29:22 +00:00
|
|
|
BuildArgs map[string]string
|
2018-05-03 23:24:26 +00:00
|
|
|
Labels map[string]string
|
2017-10-01 00:58:07 +00:00
|
|
|
SessionID string
|
2017-11-07 00:25:46 +00:00
|
|
|
BuildContext *llb.State
|
2017-12-14 02:49:14 +00:00
|
|
|
Excludes []string
|
2018-05-09 04:47:41 +00:00
|
|
|
// IgnoreCache contains names of the stages that should not use build cache.
|
|
|
|
// Empty slice means ignore cache for all stages. Nil doesn't disable cache.
|
|
|
|
IgnoreCache []string
|
2018-06-07 20:57:35 +00:00
|
|
|
// CacheIDNamespace scopes the IDs for different cache mounts
|
2018-08-28 20:56:12 +00:00
|
|
|
CacheIDNamespace string
|
|
|
|
ImageResolveMode llb.ResolveMode
|
|
|
|
TargetPlatform *specs.Platform
|
|
|
|
BuildPlatforms []specs.Platform
|
|
|
|
PrefixPlatform bool
|
|
|
|
ExtraHosts []llb.HostIP
|
|
|
|
ForceNetMode pb.NetMode
|
|
|
|
OverrideCopyImage string
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
|
|
|
|
2017-09-11 05:35:07 +00:00
|
|
|
func Dockerfile2LLB(ctx context.Context, dt []byte, opt ConvertOpt) (*llb.State, *Image, error) {
|
2017-08-25 20:08:18 +00:00
|
|
|
if len(dt) == 0 {
|
2017-09-11 05:35:07 +00:00
|
|
|
return nil, nil, errors.Errorf("the Dockerfile cannot be empty")
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
|
|
|
|
2018-07-23 23:07:00 +00:00
|
|
|
platformOpt := buildPlatformOpt(&opt)
|
2018-06-25 02:32:13 +00:00
|
|
|
|
2018-07-10 23:57:57 +00:00
|
|
|
optMetaArgs := getPlatformArgs(platformOpt)
|
|
|
|
for i, arg := range optMetaArgs {
|
|
|
|
optMetaArgs[i] = setKVValue(arg, opt.BuildArgs)
|
|
|
|
}
|
|
|
|
|
2017-08-25 20:08:18 +00:00
|
|
|
dockerfile, err := parser.Parse(bytes.NewReader(dt))
|
|
|
|
if err != nil {
|
2017-09-11 05:35:07 +00:00
|
|
|
return nil, nil, err
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
|
|
|
|
2018-05-21 23:07:51 +00:00
|
|
|
proxyEnv := proxyEnvFromBuildArgs(opt.BuildArgs)
|
|
|
|
|
2017-08-25 20:08:18 +00:00
|
|
|
stages, metaArgs, err := instructions.Parse(dockerfile.AST)
|
|
|
|
if err != nil {
|
2017-09-11 05:35:07 +00:00
|
|
|
return nil, nil, err
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
|
|
|
|
2018-08-24 23:41:51 +00:00
|
|
|
shlex := shell.NewLex(dockerfile.EscapeToken)
|
|
|
|
|
2018-07-04 10:54:40 +00:00
|
|
|
for _, metaArg := range metaArgs {
|
2018-08-24 23:41:51 +00:00
|
|
|
if metaArg.Value != nil {
|
|
|
|
*metaArg.Value, _ = shlex.ProcessWordWithMap(*metaArg.Value, metaArgsToMap(optMetaArgs))
|
|
|
|
}
|
2018-07-04 10:54:40 +00:00
|
|
|
optMetaArgs = append(optMetaArgs, setKVValue(metaArg.KeyValuePairOptional, opt.BuildArgs))
|
2017-09-12 06:29:22 +00:00
|
|
|
}
|
|
|
|
|
2017-09-22 22:49:37 +00:00
|
|
|
metaResolver := opt.MetaResolver
|
|
|
|
if metaResolver == nil {
|
2017-12-01 01:29:57 +00:00
|
|
|
metaResolver = imagemetaresolver.Default()
|
2017-09-22 22:49:37 +00:00
|
|
|
}
|
|
|
|
|
2018-07-01 09:18:51 +00:00
|
|
|
allDispatchStates := newDispatchStates()
|
2017-08-25 20:08:18 +00:00
|
|
|
|
2017-09-22 17:30:30 +00:00
|
|
|
// set base state for every image
|
2018-07-30 20:52:27 +00:00
|
|
|
for i, st := range stages {
|
2018-07-17 23:54:18 +00:00
|
|
|
name, err := shlex.ProcessWordWithMap(st.BaseName, metaArgsToMap(optMetaArgs))
|
2017-09-12 06:29:22 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
2018-06-27 23:46:29 +00:00
|
|
|
if name == "" {
|
|
|
|
return nil, nil, errors.Errorf("base name (%s) should not be blank", st.BaseName)
|
|
|
|
}
|
2017-09-12 06:29:22 +00:00
|
|
|
st.BaseName = name
|
2017-08-25 20:08:18 +00:00
|
|
|
|
|
|
|
ds := &dispatchState{
|
2018-07-30 20:52:41 +00:00
|
|
|
stage: st,
|
|
|
|
deps: make(map[*dispatchState]struct{}),
|
|
|
|
ctxPaths: make(map[string]struct{}),
|
|
|
|
stageName: st.Name,
|
|
|
|
prefixPlatform: opt.PrefixPlatform,
|
2018-07-30 20:52:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if st.Name == "" {
|
|
|
|
ds.stageName = fmt.Sprintf("stage-%d", i)
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
2018-06-25 02:32:13 +00:00
|
|
|
|
|
|
|
if v := st.Platform; v != "" {
|
2018-07-17 23:54:18 +00:00
|
|
|
v, err := shlex.ProcessWordWithMap(v, metaArgsToMap(optMetaArgs))
|
2018-06-25 02:32:13 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, nil, errors.Wrapf(err, "failed to process arguments for platform %s", v)
|
|
|
|
}
|
|
|
|
|
|
|
|
p, err := platforms.Parse(v)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, errors.Wrapf(err, "failed to parse platform %s", v)
|
|
|
|
}
|
|
|
|
ds.platform = &p
|
|
|
|
}
|
2018-09-17 20:41:11 +00:00
|
|
|
allDispatchStates.addState(ds)
|
2018-06-25 02:32:13 +00:00
|
|
|
|
2018-09-17 20:41:11 +00:00
|
|
|
total := 0
|
|
|
|
if ds.stage.BaseName != emptyImageName && ds.base == nil {
|
|
|
|
total = 1
|
|
|
|
}
|
2018-07-30 20:52:27 +00:00
|
|
|
for _, cmd := range ds.stage.Commands {
|
|
|
|
switch cmd.(type) {
|
|
|
|
case *instructions.AddCommand, *instructions.CopyCommand, *instructions.RunCommand:
|
|
|
|
total++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ds.cmdTotal = total
|
|
|
|
|
2018-05-09 04:47:41 +00:00
|
|
|
if opt.IgnoreCache != nil {
|
|
|
|
if len(opt.IgnoreCache) == 0 {
|
|
|
|
ds.ignoreCache = true
|
|
|
|
} else if st.Name != "" {
|
|
|
|
for _, n := range opt.IgnoreCache {
|
|
|
|
if strings.EqualFold(n, st.Name) {
|
|
|
|
ds.ignoreCache = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
|
|
|
|
2018-07-30 20:52:27 +00:00
|
|
|
if len(allDispatchStates.states) == 1 {
|
|
|
|
allDispatchStates.states[0].stageName = ""
|
|
|
|
}
|
|
|
|
|
2017-12-12 22:58:26 +00:00
|
|
|
var target *dispatchState
|
|
|
|
if opt.Target == "" {
|
2018-07-01 09:18:51 +00:00
|
|
|
target = allDispatchStates.lastTarget()
|
2017-12-12 22:58:26 +00:00
|
|
|
} else {
|
|
|
|
var ok bool
|
2018-07-01 09:18:51 +00:00
|
|
|
target, ok = allDispatchStates.findStateByName(opt.Target)
|
2017-12-12 22:58:26 +00:00
|
|
|
if !ok {
|
|
|
|
return nil, nil, errors.Errorf("target stage %s could not be found", opt.Target)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// fill dependencies to stages so unreachable ones can avoid loading image configs
|
2018-07-01 09:18:51 +00:00
|
|
|
for _, d := range allDispatchStates.states {
|
2018-02-22 00:33:02 +00:00
|
|
|
d.commands = make([]command, len(d.stage.Commands))
|
|
|
|
for i, cmd := range d.stage.Commands {
|
2018-07-01 09:23:54 +00:00
|
|
|
newCmd, err := toCommand(cmd, allDispatchStates)
|
2018-02-22 00:33:02 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
d.commands[i] = newCmd
|
2018-06-07 20:57:35 +00:00
|
|
|
for _, src := range newCmd.sources {
|
|
|
|
if src != nil {
|
|
|
|
d.deps[src] = struct{}{}
|
|
|
|
if src.unregistered {
|
2018-07-01 09:18:51 +00:00
|
|
|
allDispatchStates.addState(src)
|
2018-06-07 20:57:35 +00:00
|
|
|
}
|
2017-12-12 22:58:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-25 20:08:18 +00:00
|
|
|
eg, ctx := errgroup.WithContext(ctx)
|
2018-07-01 09:18:51 +00:00
|
|
|
for i, d := range allDispatchStates.states {
|
2018-04-14 01:16:22 +00:00
|
|
|
reachable := isReachable(target, d)
|
2017-09-22 17:30:30 +00:00
|
|
|
// resolve image config for every stage
|
2017-09-22 22:49:37 +00:00
|
|
|
if d.base == nil {
|
|
|
|
if d.stage.BaseName == emptyImageName {
|
|
|
|
d.state = llb.Scratch()
|
2018-07-23 23:07:00 +00:00
|
|
|
d.image = emptyImage(platformOpt.targetPlatform)
|
2017-09-22 22:49:37 +00:00
|
|
|
continue
|
|
|
|
}
|
2017-08-25 20:08:18 +00:00
|
|
|
func(i int, d *dispatchState) {
|
|
|
|
eg.Go(func() error {
|
|
|
|
ref, err := reference.ParseNormalizedNamed(d.stage.BaseName)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-06-25 02:32:13 +00:00
|
|
|
platform := d.platform
|
|
|
|
if platform == nil {
|
2018-07-23 23:07:00 +00:00
|
|
|
platform = &platformOpt.targetPlatform
|
2018-06-25 02:32:13 +00:00
|
|
|
}
|
2017-08-25 20:08:18 +00:00
|
|
|
d.stage.BaseName = reference.TagNameOnly(ref).String()
|
2018-05-22 22:58:22 +00:00
|
|
|
var isScratch bool
|
2018-07-28 00:27:41 +00:00
|
|
|
if metaResolver != nil && reachable && !d.unregistered {
|
2018-09-17 20:08:58 +00:00
|
|
|
prefix := "["
|
|
|
|
if opt.PrefixPlatform && platform != nil {
|
|
|
|
prefix += platforms.Format(*platform) + " "
|
|
|
|
}
|
|
|
|
prefix += "internal]"
|
2018-07-25 06:18:21 +00:00
|
|
|
dgst, dt, err := metaResolver.ResolveImageConfig(ctx, d.stage.BaseName, gw.ResolveImageConfigOpt{
|
2018-07-25 20:53:55 +00:00
|
|
|
Platform: platform,
|
|
|
|
ResolveMode: opt.ImageResolveMode.String(),
|
2018-09-17 20:08:58 +00:00
|
|
|
LogName: fmt.Sprintf("%s load metadata for %s", prefix, d.stage.BaseName),
|
2018-07-25 06:18:21 +00:00
|
|
|
})
|
2018-06-25 02:32:13 +00:00
|
|
|
if err == nil { // handle the error while builder is actually running
|
2017-09-22 22:49:37 +00:00
|
|
|
var img Image
|
|
|
|
if err := json.Unmarshal(dt, &img); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-05-04 20:29:04 +00:00
|
|
|
img.Created = nil
|
2018-06-26 17:24:10 +00:00
|
|
|
// if there is no explicit target platform, try to match based on image config
|
2018-07-23 23:07:00 +00:00
|
|
|
if d.platform == nil && platformOpt.implicitTarget {
|
|
|
|
p := autoDetectPlatform(img, *platform, platformOpt.buildPlatforms)
|
2018-06-26 17:24:10 +00:00
|
|
|
platform = &p
|
|
|
|
}
|
2017-09-22 22:49:37 +00:00
|
|
|
d.image = img
|
2018-04-20 04:39:58 +00:00
|
|
|
if dgst != "" {
|
|
|
|
ref, err = reference.WithDigest(ref, dgst)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-09-22 22:49:37 +00:00
|
|
|
}
|
|
|
|
d.stage.BaseName = ref.String()
|
|
|
|
_ = ref
|
2018-05-22 22:58:22 +00:00
|
|
|
if len(img.RootFS.DiffIDs) == 0 {
|
|
|
|
isScratch = true
|
|
|
|
}
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
|
|
|
}
|
2018-05-22 22:58:22 +00:00
|
|
|
if isScratch {
|
|
|
|
d.state = llb.Scratch()
|
|
|
|
} else {
|
2018-07-30 20:52:41 +00:00
|
|
|
d.state = llb.Image(d.stage.BaseName, dfCmd(d.stage.SourceCode), llb.Platform(*platform), opt.ImageResolveMode, llb.WithCustomName(prefixCommand(d, "FROM "+d.stage.BaseName, opt.PrefixPlatform, platform)))
|
2018-05-22 22:58:22 +00:00
|
|
|
}
|
2018-07-13 18:28:36 +00:00
|
|
|
d.platform = platform
|
2017-08-25 20:08:18 +00:00
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}(i, d)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := eg.Wait(); err != nil {
|
2017-09-11 05:35:07 +00:00
|
|
|
return nil, nil, err
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
2018-04-14 01:16:22 +00:00
|
|
|
|
2018-04-23 18:37:53 +00:00
|
|
|
buildContext := &mutableOutput{}
|
|
|
|
ctxPaths := map[string]struct{}{}
|
2017-11-07 00:25:46 +00:00
|
|
|
|
2018-07-01 09:18:51 +00:00
|
|
|
for _, d := range allDispatchStates.states {
|
2018-04-14 01:16:22 +00:00
|
|
|
if !isReachable(target, d) {
|
|
|
|
continue
|
|
|
|
}
|
2017-08-25 20:08:18 +00:00
|
|
|
if d.base != nil {
|
|
|
|
d.state = d.base.state
|
2018-07-13 18:28:36 +00:00
|
|
|
d.platform = d.base.platform
|
2017-09-11 05:35:07 +00:00
|
|
|
d.image = clone(d.base.image)
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
|
|
|
|
2018-09-11 19:14:46 +00:00
|
|
|
// make sure that PATH is always set
|
|
|
|
if _, ok := shell.BuildEnvs(d.image.Config.Env)["PATH"]; !ok {
|
|
|
|
d.image.Config.Env = append(d.image.Config.Env, "PATH="+system.DefaultPathEnv)
|
|
|
|
}
|
|
|
|
|
2017-09-22 17:30:30 +00:00
|
|
|
// initialize base metadata from image conf
|
2017-08-25 20:08:18 +00:00
|
|
|
for _, env := range d.image.Config.Env {
|
2018-07-19 12:36:07 +00:00
|
|
|
k, v := parseKeyValue(env)
|
|
|
|
d.state = d.state.AddEnv(k, v)
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
|
|
|
if d.image.Config.WorkingDir != "" {
|
2017-12-08 01:33:17 +00:00
|
|
|
if err = dispatchWorkdir(d, &instructions.WorkdirCommand{Path: d.image.Config.WorkingDir}, false); err != nil {
|
2017-09-11 05:35:07 +00:00
|
|
|
return nil, nil, err
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
|
|
|
}
|
2017-09-22 17:30:30 +00:00
|
|
|
if d.image.Config.User != "" {
|
2017-12-08 01:33:17 +00:00
|
|
|
if err = dispatchUser(d, &instructions.UserCommand{User: d.image.Config.User}, false); err != nil {
|
2017-09-22 17:30:30 +00:00
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
}
|
2018-08-07 01:49:30 +00:00
|
|
|
d.state = d.state.Network(opt.ForceNetMode)
|
2017-08-25 20:08:18 +00:00
|
|
|
|
2017-09-22 20:12:57 +00:00
|
|
|
opt := dispatchOpt{
|
2018-07-01 09:18:51 +00:00
|
|
|
allDispatchStates: allDispatchStates,
|
2018-07-04 10:54:40 +00:00
|
|
|
metaArgs: optMetaArgs,
|
2018-07-01 09:18:51 +00:00
|
|
|
buildArgValues: opt.BuildArgs,
|
|
|
|
shlex: shlex,
|
|
|
|
sessionID: opt.SessionID,
|
|
|
|
buildContext: llb.NewState(buildContext),
|
|
|
|
proxyEnv: proxyEnv,
|
|
|
|
cacheIDNamespace: opt.CacheIDNamespace,
|
2018-07-23 23:07:00 +00:00
|
|
|
buildPlatforms: platformOpt.buildPlatforms,
|
|
|
|
targetPlatform: platformOpt.targetPlatform,
|
2018-08-02 19:00:27 +00:00
|
|
|
extraHosts: opt.ExtraHosts,
|
2018-08-28 20:56:12 +00:00
|
|
|
copyImage: opt.OverrideCopyImage,
|
|
|
|
}
|
|
|
|
if opt.copyImage == "" {
|
|
|
|
opt.copyImage = DefaultCopyImage
|
2017-09-22 20:12:57 +00:00
|
|
|
}
|
2017-09-12 06:29:22 +00:00
|
|
|
|
2017-09-22 20:12:57 +00:00
|
|
|
if err = dispatchOnBuild(d, d.image.Config.OnBuild, opt); err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
2018-02-22 00:33:02 +00:00
|
|
|
for _, cmd := range d.commands {
|
2017-09-22 20:12:57 +00:00
|
|
|
if err := dispatch(d, cmd, opt); err != nil {
|
2017-09-11 05:35:07 +00:00
|
|
|
return nil, nil, err
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
|
|
|
}
|
2018-04-23 18:37:53 +00:00
|
|
|
|
|
|
|
for p := range d.ctxPaths {
|
|
|
|
ctxPaths[p] = struct{}{}
|
|
|
|
}
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
|
|
|
|
2018-05-17 01:02:52 +00:00
|
|
|
if len(opt.Labels) != 0 && target.image.Config.Labels == nil {
|
|
|
|
target.image.Config.Labels = make(map[string]string, len(opt.Labels))
|
|
|
|
}
|
2018-05-03 23:24:26 +00:00
|
|
|
for k, v := range opt.Labels {
|
|
|
|
target.image.Config.Labels[k] = v
|
|
|
|
}
|
|
|
|
|
2018-04-23 18:37:53 +00:00
|
|
|
opts := []llb.LocalOption{
|
|
|
|
llb.SessionID(opt.SessionID),
|
|
|
|
llb.ExcludePatterns(opt.Excludes),
|
|
|
|
llb.SharedKeyHint(localNameContext),
|
2018-07-28 00:27:41 +00:00
|
|
|
WithInternalName("load build context"),
|
2018-04-23 18:37:53 +00:00
|
|
|
}
|
|
|
|
if includePatterns := normalizeContextPaths(ctxPaths); includePatterns != nil {
|
2018-06-01 22:30:54 +00:00
|
|
|
opts = append(opts, llb.FollowPaths(includePatterns))
|
2018-04-23 18:37:53 +00:00
|
|
|
}
|
2018-07-28 00:27:41 +00:00
|
|
|
|
2018-04-23 18:37:53 +00:00
|
|
|
bc := llb.Local(localNameContext, opts...)
|
|
|
|
if opt.BuildContext != nil {
|
|
|
|
bc = *opt.BuildContext
|
|
|
|
}
|
|
|
|
buildContext.Output = bc.Output()
|
|
|
|
|
2018-09-06 04:28:39 +00:00
|
|
|
st := target.state.SetMarshalDefaults(llb.Platform(platformOpt.targetPlatform))
|
2018-06-25 02:32:13 +00:00
|
|
|
|
2018-07-23 23:07:00 +00:00
|
|
|
if !platformOpt.implicitTarget {
|
|
|
|
target.image.OS = platformOpt.targetPlatform.OS
|
|
|
|
target.image.Architecture = platformOpt.targetPlatform.Architecture
|
2018-06-26 17:24:10 +00:00
|
|
|
}
|
2018-06-25 02:32:13 +00:00
|
|
|
|
|
|
|
return &st, &target.image, nil
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
|
|
|
|
2018-07-17 23:54:18 +00:00
|
|
|
func metaArgsToMap(metaArgs []instructions.KeyValuePairOptional) map[string]string {
|
|
|
|
m := map[string]string{}
|
|
|
|
|
|
|
|
for _, arg := range metaArgs {
|
|
|
|
m[arg.Key] = arg.ValueString()
|
|
|
|
}
|
|
|
|
|
|
|
|
return m
|
|
|
|
}
|
|
|
|
|
2018-07-01 09:23:54 +00:00
|
|
|
func toCommand(ic instructions.Command, allDispatchStates *dispatchStates) (command, error) {
|
2018-02-22 00:33:02 +00:00
|
|
|
cmd := command{Command: ic}
|
|
|
|
if c, ok := ic.(*instructions.CopyCommand); ok {
|
|
|
|
if c.From != "" {
|
|
|
|
var stn *dispatchState
|
|
|
|
index, err := strconv.Atoi(c.From)
|
|
|
|
if err != nil {
|
2018-07-01 09:23:54 +00:00
|
|
|
stn, ok = allDispatchStates.findStateByName(c.From)
|
2018-02-22 00:33:02 +00:00
|
|
|
if !ok {
|
|
|
|
stn = &dispatchState{
|
2018-06-07 20:57:35 +00:00
|
|
|
stage: instructions.Stage{BaseName: c.From},
|
|
|
|
deps: make(map[*dispatchState]struct{}),
|
|
|
|
unregistered: true,
|
2018-02-22 00:33:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2018-07-01 09:23:54 +00:00
|
|
|
stn, err = allDispatchStates.findStateByIndex(index)
|
|
|
|
if err != nil {
|
|
|
|
return command{}, err
|
2018-02-22 00:33:02 +00:00
|
|
|
}
|
|
|
|
}
|
2018-06-07 20:57:35 +00:00
|
|
|
cmd.sources = []*dispatchState{stn}
|
2018-02-22 00:33:02 +00:00
|
|
|
}
|
|
|
|
}
|
2018-06-07 20:57:35 +00:00
|
|
|
|
2018-07-01 09:23:54 +00:00
|
|
|
if ok := detectRunMount(&cmd, allDispatchStates); ok {
|
2018-06-07 20:57:35 +00:00
|
|
|
return cmd, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return cmd, nil
|
2018-02-22 00:33:02 +00:00
|
|
|
}
|
|
|
|
|
2017-09-22 20:12:57 +00:00
|
|
|
type dispatchOpt struct {
|
2018-07-01 09:18:51 +00:00
|
|
|
allDispatchStates *dispatchStates
|
2018-07-04 10:54:40 +00:00
|
|
|
metaArgs []instructions.KeyValuePairOptional
|
2018-07-01 09:18:51 +00:00
|
|
|
buildArgValues map[string]string
|
|
|
|
shlex *shell.Lex
|
|
|
|
sessionID string
|
|
|
|
buildContext llb.State
|
|
|
|
proxyEnv *llb.ProxyEnv
|
|
|
|
cacheIDNamespace string
|
|
|
|
targetPlatform specs.Platform
|
|
|
|
buildPlatforms []specs.Platform
|
2018-08-02 19:00:27 +00:00
|
|
|
extraHosts []llb.HostIP
|
2018-08-28 20:56:12 +00:00
|
|
|
copyImage string
|
2017-09-22 20:12:57 +00:00
|
|
|
}
|
|
|
|
|
2018-02-22 00:33:02 +00:00
|
|
|
func dispatch(d *dispatchState, cmd command, opt dispatchOpt) error {
|
|
|
|
if ex, ok := cmd.Command.(instructions.SupportsSingleWordExpansion); ok {
|
2017-09-22 20:12:57 +00:00
|
|
|
err := ex.Expand(func(word string) (string, error) {
|
2018-07-20 23:30:47 +00:00
|
|
|
return opt.shlex.ProcessWordWithMap(word, toEnvMap(d.buildArgs, d.image.Config.Env))
|
2017-09-22 20:12:57 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var err error
|
2018-02-22 00:33:02 +00:00
|
|
|
switch c := cmd.Command.(type) {
|
2018-02-02 01:59:04 +00:00
|
|
|
case *instructions.MaintainerCommand:
|
|
|
|
err = dispatchMaintainer(d, c)
|
2017-09-22 20:12:57 +00:00
|
|
|
case *instructions.EnvCommand:
|
2018-07-18 12:38:37 +00:00
|
|
|
err = dispatchEnv(d, c)
|
2017-09-22 20:12:57 +00:00
|
|
|
case *instructions.RunCommand:
|
2018-06-07 20:57:35 +00:00
|
|
|
err = dispatchRun(d, c, opt.proxyEnv, cmd.sources, opt)
|
2017-09-22 20:12:57 +00:00
|
|
|
case *instructions.WorkdirCommand:
|
2017-12-08 01:33:17 +00:00
|
|
|
err = dispatchWorkdir(d, c, true)
|
2017-09-22 20:12:57 +00:00
|
|
|
case *instructions.AddCommand:
|
2018-06-25 02:32:13 +00:00
|
|
|
err = dispatchCopy(d, c.SourcesAndDest, opt.buildContext, true, c, "", opt)
|
2018-04-23 18:37:53 +00:00
|
|
|
if err == nil {
|
|
|
|
for _, src := range c.Sources() {
|
|
|
|
d.ctxPaths[path.Join("/", filepath.ToSlash(src))] = struct{}{}
|
|
|
|
}
|
|
|
|
}
|
2017-09-22 20:12:57 +00:00
|
|
|
case *instructions.LabelCommand:
|
|
|
|
err = dispatchLabel(d, c)
|
|
|
|
case *instructions.OnbuildCommand:
|
|
|
|
err = dispatchOnbuild(d, c)
|
|
|
|
case *instructions.CmdCommand:
|
|
|
|
err = dispatchCmd(d, c)
|
|
|
|
case *instructions.EntrypointCommand:
|
|
|
|
err = dispatchEntrypoint(d, c)
|
|
|
|
case *instructions.HealthCheckCommand:
|
|
|
|
err = dispatchHealthcheck(d, c)
|
|
|
|
case *instructions.ExposeCommand:
|
2017-12-12 00:35:31 +00:00
|
|
|
err = dispatchExpose(d, c, opt.shlex)
|
2017-09-22 20:12:57 +00:00
|
|
|
case *instructions.UserCommand:
|
2017-12-08 01:33:17 +00:00
|
|
|
err = dispatchUser(d, c, true)
|
2017-09-22 20:12:57 +00:00
|
|
|
case *instructions.VolumeCommand:
|
|
|
|
err = dispatchVolume(d, c)
|
|
|
|
case *instructions.StopSignalCommand:
|
|
|
|
err = dispatchStopSignal(d, c)
|
|
|
|
case *instructions.ShellCommand:
|
|
|
|
err = dispatchShell(d, c)
|
|
|
|
case *instructions.ArgCommand:
|
|
|
|
err = dispatchArg(d, c, opt.metaArgs, opt.buildArgValues)
|
|
|
|
case *instructions.CopyCommand:
|
2017-11-07 00:25:46 +00:00
|
|
|
l := opt.buildContext
|
2018-06-07 20:57:35 +00:00
|
|
|
if len(cmd.sources) != 0 {
|
|
|
|
l = cmd.sources[0].state
|
2017-09-22 20:12:57 +00:00
|
|
|
}
|
2018-06-25 02:32:13 +00:00
|
|
|
err = dispatchCopy(d, c.SourcesAndDest, l, false, c, c.Chown, opt)
|
2018-06-07 20:57:35 +00:00
|
|
|
if err == nil && len(cmd.sources) == 0 {
|
2018-04-23 18:37:53 +00:00
|
|
|
for _, src := range c.Sources() {
|
|
|
|
d.ctxPaths[path.Join("/", filepath.ToSlash(src))] = struct{}{}
|
|
|
|
}
|
|
|
|
}
|
2017-09-22 20:12:57 +00:00
|
|
|
default:
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-08-25 20:08:18 +00:00
|
|
|
type dispatchState struct {
|
2018-07-30 20:52:41 +00:00
|
|
|
state llb.State
|
|
|
|
image Image
|
|
|
|
platform *specs.Platform
|
|
|
|
stage instructions.Stage
|
|
|
|
base *dispatchState
|
|
|
|
deps map[*dispatchState]struct{}
|
|
|
|
buildArgs []instructions.KeyValuePairOptional
|
|
|
|
commands []command
|
|
|
|
ctxPaths map[string]struct{}
|
|
|
|
ignoreCache bool
|
|
|
|
cmdSet bool
|
|
|
|
unregistered bool
|
|
|
|
stageName string
|
|
|
|
cmdIndex int
|
|
|
|
cmdTotal int
|
|
|
|
prefixPlatform bool
|
2018-02-22 00:33:02 +00:00
|
|
|
}
|
|
|
|
|
2018-07-01 09:18:51 +00:00
|
|
|
type dispatchStates struct {
|
|
|
|
states []*dispatchState
|
|
|
|
statesByName map[string]*dispatchState
|
|
|
|
}
|
|
|
|
|
|
|
|
func newDispatchStates() *dispatchStates {
|
|
|
|
return &dispatchStates{statesByName: map[string]*dispatchState{}}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (dss *dispatchStates) addState(ds *dispatchState) {
|
|
|
|
dss.states = append(dss.states, ds)
|
|
|
|
|
|
|
|
if d, ok := dss.statesByName[ds.stage.BaseName]; ok {
|
|
|
|
ds.base = d
|
|
|
|
}
|
|
|
|
if ds.stage.Name != "" {
|
|
|
|
dss.statesByName[strings.ToLower(ds.stage.Name)] = ds
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (dss *dispatchStates) findStateByName(name string) (*dispatchState, bool) {
|
|
|
|
ds, ok := dss.statesByName[strings.ToLower(name)]
|
|
|
|
return ds, ok
|
|
|
|
}
|
|
|
|
|
|
|
|
func (dss *dispatchStates) findStateByIndex(index int) (*dispatchState, error) {
|
|
|
|
if index < 0 || index >= len(dss.states) {
|
|
|
|
return nil, errors.Errorf("invalid stage index %d", index)
|
|
|
|
}
|
|
|
|
|
|
|
|
return dss.states[index], nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (dss *dispatchStates) lastTarget() *dispatchState {
|
|
|
|
return dss.states[len(dss.states)-1]
|
|
|
|
}
|
|
|
|
|
2018-02-22 00:33:02 +00:00
|
|
|
type command struct {
|
|
|
|
instructions.Command
|
2018-06-07 20:57:35 +00:00
|
|
|
sources []*dispatchState
|
2017-09-22 20:12:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func dispatchOnBuild(d *dispatchState, triggers []string, opt dispatchOpt) error {
|
|
|
|
for _, trigger := range triggers {
|
|
|
|
ast, err := parser.Parse(strings.NewReader(trigger))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if len(ast.AST.Children) != 1 {
|
|
|
|
return errors.New("onbuild trigger should be a single expression")
|
|
|
|
}
|
2018-02-22 00:33:02 +00:00
|
|
|
ic, err := instructions.ParseCommand(ast.AST.Children[0])
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-07-01 09:23:54 +00:00
|
|
|
cmd, err := toCommand(ic, opt.allDispatchStates)
|
2017-09-22 20:12:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := dispatch(d, cmd, opt); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
|
|
|
|
2018-07-18 12:38:37 +00:00
|
|
|
func dispatchEnv(d *dispatchState, c *instructions.EnvCommand) error {
|
2017-12-08 01:33:17 +00:00
|
|
|
commitMessage := bytes.NewBufferString("ENV")
|
2017-08-25 20:08:18 +00:00
|
|
|
for _, e := range c.Env {
|
2017-12-08 01:33:17 +00:00
|
|
|
commitMessage.WriteString(" " + e.String())
|
2017-08-25 20:08:18 +00:00
|
|
|
d.state = d.state.AddEnv(e.Key, e.Value)
|
2018-07-20 10:46:50 +00:00
|
|
|
d.image.Config.Env = addEnv(d.image.Config.Env, e.Key, e.Value)
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
2018-07-18 12:38:37 +00:00
|
|
|
return commitToHistory(&d.image, commitMessage.String(), false, nil)
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
|
|
|
|
2018-06-07 20:57:35 +00:00
|
|
|
func dispatchRun(d *dispatchState, c *instructions.RunCommand, proxy *llb.ProxyEnv, sources []*dispatchState, dopt dispatchOpt) error {
|
2017-08-25 20:08:18 +00:00
|
|
|
var args []string = c.CmdLine
|
|
|
|
if c.PrependShell {
|
2018-05-23 19:19:10 +00:00
|
|
|
args = withShell(d.image, args)
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
2017-09-12 06:29:22 +00:00
|
|
|
opt := []llb.RunOption{llb.Args(args)}
|
2017-09-22 20:12:57 +00:00
|
|
|
for _, arg := range d.buildArgs {
|
2018-07-04 10:54:40 +00:00
|
|
|
opt = append(opt, llb.AddEnv(arg.Key, arg.ValueString()))
|
2017-09-12 06:29:22 +00:00
|
|
|
}
|
2017-12-08 00:13:50 +00:00
|
|
|
opt = append(opt, dfCmd(c))
|
2018-05-09 04:47:41 +00:00
|
|
|
if d.ignoreCache {
|
|
|
|
opt = append(opt, llb.IgnoreCache)
|
|
|
|
}
|
2018-05-21 23:07:51 +00:00
|
|
|
if proxy != nil {
|
|
|
|
opt = append(opt, llb.WithProxy(*proxy))
|
|
|
|
}
|
2018-06-07 20:57:35 +00:00
|
|
|
|
2018-06-18 23:51:55 +00:00
|
|
|
runMounts, err := dispatchRunMounts(d, c, sources, dopt)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
opt = append(opt, runMounts...)
|
2018-07-30 20:52:41 +00:00
|
|
|
opt = append(opt, llb.WithCustomName(prefixCommand(d, uppercaseCmd(processCmdEnv(dopt.shlex, c.String(), d.state.Run(opt...).Env())), d.prefixPlatform, d.state.GetPlatform())))
|
2018-08-02 19:00:27 +00:00
|
|
|
for _, h := range dopt.extraHosts {
|
|
|
|
opt = append(opt, llb.AddExtraHost(h.Host, h.IP))
|
|
|
|
}
|
2017-09-12 06:29:22 +00:00
|
|
|
d.state = d.state.Run(opt...).Root()
|
2017-12-08 01:33:17 +00:00
|
|
|
return commitToHistory(&d.image, "RUN "+runCommandString(args, d.buildArgs), true, &d.state)
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
|
|
|
|
2017-12-08 01:33:17 +00:00
|
|
|
func dispatchWorkdir(d *dispatchState, c *instructions.WorkdirCommand, commit bool) error {
|
2017-08-25 20:08:18 +00:00
|
|
|
d.state = d.state.Dir(c.Path)
|
|
|
|
wd := c.Path
|
|
|
|
if !path.IsAbs(c.Path) {
|
|
|
|
wd = path.Join("/", d.image.Config.WorkingDir, wd)
|
|
|
|
}
|
|
|
|
d.image.Config.WorkingDir = wd
|
2017-12-08 01:33:17 +00:00
|
|
|
if commit {
|
|
|
|
return commitToHistory(&d.image, "WORKDIR "+wd, false, nil)
|
|
|
|
}
|
2017-08-25 20:08:18 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-07-28 00:27:41 +00:00
|
|
|
func dispatchCopy(d *dispatchState, c instructions.SourcesAndDest, sourceState llb.State, isAddCommand bool, cmdToPrint fmt.Stringer, chown string, opt dispatchOpt) error {
|
2017-12-05 00:13:52 +00:00
|
|
|
// TODO: this should use CopyOp instead. Current implementation is inefficient
|
2018-08-28 20:56:12 +00:00
|
|
|
img := llb.Image(opt.copyImage, llb.MarkImageInternal, llb.Platform(opt.buildPlatforms[0]), WithInternalName("helper image for file operations"))
|
2017-09-01 23:57:22 +00:00
|
|
|
|
2018-05-22 22:10:21 +00:00
|
|
|
dest := path.Join(".", pathRelativeToWorkingDir(d.state, c.Dest()))
|
2017-11-07 00:28:30 +00:00
|
|
|
if c.Dest() == "." || c.Dest()[len(c.Dest())-1] == filepath.Separator {
|
|
|
|
dest += string(filepath.Separator)
|
|
|
|
}
|
2017-09-01 23:57:22 +00:00
|
|
|
args := []string{"copy"}
|
2018-05-21 05:32:52 +00:00
|
|
|
unpack := isAddCommand
|
2017-12-08 01:33:17 +00:00
|
|
|
|
2017-12-20 04:54:26 +00:00
|
|
|
mounts := make([]llb.RunOption, 0, len(c.Sources()))
|
|
|
|
if chown != "" {
|
|
|
|
args = append(args, fmt.Sprintf("--chown=%s", chown))
|
|
|
|
_, _, err := parseUser(chown)
|
|
|
|
if err != nil {
|
|
|
|
mounts = append(mounts, llb.AddMount("/etc/passwd", d.state, llb.SourcePath("/etc/passwd"), llb.Readonly))
|
|
|
|
mounts = append(mounts, llb.AddMount("/etc/group", d.state, llb.SourcePath("/etc/group"), llb.Readonly))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-08 01:33:17 +00:00
|
|
|
commitMessage := bytes.NewBufferString("")
|
|
|
|
if isAddCommand {
|
|
|
|
commitMessage.WriteString("ADD")
|
|
|
|
} else {
|
|
|
|
commitMessage.WriteString("COPY")
|
|
|
|
}
|
|
|
|
|
2017-09-01 23:57:22 +00:00
|
|
|
for i, src := range c.Sources() {
|
2017-12-08 01:33:17 +00:00
|
|
|
commitMessage.WriteString(" " + src)
|
2018-07-03 11:28:08 +00:00
|
|
|
if strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") {
|
|
|
|
if !isAddCommand {
|
|
|
|
return errors.New("source can't be a URL for COPY")
|
|
|
|
}
|
|
|
|
|
2018-05-21 05:32:52 +00:00
|
|
|
// Resources from remote URLs are not decompressed.
|
|
|
|
// https://docs.docker.com/engine/reference/builder/#add
|
|
|
|
//
|
|
|
|
// Note: mixing up remote archives and local archives in a single ADD instruction
|
|
|
|
// would result in undefined behavior: https://github.com/moby/buildkit/pull/387#discussion_r189494717
|
|
|
|
unpack = false
|
2017-12-03 05:45:41 +00:00
|
|
|
u, err := url.Parse(src)
|
2017-12-04 03:38:37 +00:00
|
|
|
f := "__unnamed__"
|
2017-12-03 05:45:41 +00:00
|
|
|
if err == nil {
|
2017-12-04 03:38:37 +00:00
|
|
|
if base := path.Base(u.Path); base != "." && base != "/" {
|
2017-12-03 05:45:41 +00:00
|
|
|
f = base
|
|
|
|
}
|
|
|
|
}
|
|
|
|
target := path.Join(fmt.Sprintf("/src-%d", i), f)
|
|
|
|
args = append(args, target)
|
2018-05-21 05:32:52 +00:00
|
|
|
mounts = append(mounts, llb.AddMount(path.Dir(target), llb.HTTP(src, llb.Filename(f), dfCmd(c)), llb.Readonly))
|
2017-12-03 05:45:41 +00:00
|
|
|
} else {
|
|
|
|
d, f := splitWildcards(src)
|
2017-12-26 23:41:09 +00:00
|
|
|
targetCmd := fmt.Sprintf("/src-%d", i)
|
|
|
|
targetMount := targetCmd
|
2017-12-03 05:45:41 +00:00
|
|
|
if f == "" {
|
|
|
|
f = path.Base(src)
|
2017-12-26 23:41:09 +00:00
|
|
|
targetMount = path.Join(targetMount, f)
|
2017-12-03 05:45:41 +00:00
|
|
|
}
|
2017-12-26 23:41:09 +00:00
|
|
|
targetCmd = path.Join(targetCmd, f)
|
|
|
|
args = append(args, targetCmd)
|
|
|
|
mounts = append(mounts, llb.AddMount(targetMount, sourceState, llb.SourcePath(d), llb.Readonly))
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-08 01:33:17 +00:00
|
|
|
commitMessage.WriteString(" " + c.Dest())
|
|
|
|
|
2017-09-01 23:57:22 +00:00
|
|
|
args = append(args, dest)
|
2018-05-21 05:32:52 +00:00
|
|
|
if unpack {
|
|
|
|
args = append(args[:1], append([]string{"--unpack"}, args[1:]...)...)
|
|
|
|
}
|
2018-05-22 17:50:36 +00:00
|
|
|
|
2018-09-17 22:19:04 +00:00
|
|
|
platform := opt.targetPlatform
|
|
|
|
if d.platform != nil {
|
|
|
|
platform = *d.platform
|
|
|
|
}
|
|
|
|
|
|
|
|
runOpt := []llb.RunOption{llb.Args(args), llb.Dir("/dest"), llb.ReadonlyRootFS(), dfCmd(cmdToPrint), llb.WithCustomName(prefixCommand(d, uppercaseCmd(processCmdEnv(opt.shlex, cmdToPrint.String(), d.state.Env())), d.prefixPlatform, &platform))}
|
2018-05-22 17:50:36 +00:00
|
|
|
if d.ignoreCache {
|
2018-06-25 02:32:13 +00:00
|
|
|
runOpt = append(runOpt, llb.IgnoreCache)
|
2018-05-22 17:50:36 +00:00
|
|
|
}
|
2018-06-25 02:32:13 +00:00
|
|
|
run := img.Run(append(runOpt, mounts...)...)
|
2018-09-17 22:19:04 +00:00
|
|
|
d.state = run.AddMount("/dest", d.state).Platform(platform)
|
2018-07-13 18:28:36 +00:00
|
|
|
|
2017-12-08 01:33:17 +00:00
|
|
|
return commitToHistory(&d.image, commitMessage.String(), true, &d.state)
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
|
|
|
|
2018-02-02 01:59:04 +00:00
|
|
|
func dispatchMaintainer(d *dispatchState, c *instructions.MaintainerCommand) error {
|
2017-08-25 20:08:18 +00:00
|
|
|
d.image.Author = c.Maintainer
|
2017-12-08 01:33:17 +00:00
|
|
|
return commitToHistory(&d.image, fmt.Sprintf("MAINTAINER %v", c.Maintainer), false, nil)
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
|
|
|
|
2017-09-11 01:11:42 +00:00
|
|
|
func dispatchLabel(d *dispatchState, c *instructions.LabelCommand) error {
|
2017-12-08 01:33:17 +00:00
|
|
|
commitMessage := bytes.NewBufferString("LABEL")
|
2017-08-25 20:08:18 +00:00
|
|
|
if d.image.Config.Labels == nil {
|
2018-05-17 01:02:52 +00:00
|
|
|
d.image.Config.Labels = make(map[string]string, len(c.Labels))
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
|
|
|
for _, v := range c.Labels {
|
|
|
|
d.image.Config.Labels[v.Key] = v.Value
|
2017-12-08 01:33:17 +00:00
|
|
|
commitMessage.WriteString(" " + v.String())
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
2017-12-08 01:33:17 +00:00
|
|
|
return commitToHistory(&d.image, commitMessage.String(), false, nil)
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
|
|
|
|
2017-09-11 01:11:42 +00:00
|
|
|
func dispatchOnbuild(d *dispatchState, c *instructions.OnbuildCommand) error {
|
|
|
|
d.image.Config.OnBuild = append(d.image.Config.OnBuild, c.Expression)
|
|
|
|
return nil
|
|
|
|
}
|
2017-08-25 20:08:18 +00:00
|
|
|
|
|
|
|
func dispatchCmd(d *dispatchState, c *instructions.CmdCommand) error {
|
|
|
|
var args []string = c.CmdLine
|
|
|
|
if c.PrependShell {
|
2018-05-23 19:19:10 +00:00
|
|
|
args = withShell(d.image, args)
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
|
|
|
d.image.Config.Cmd = args
|
2017-09-11 01:11:42 +00:00
|
|
|
d.image.Config.ArgsEscaped = true
|
2018-05-23 19:18:14 +00:00
|
|
|
d.cmdSet = true
|
2017-12-08 01:33:17 +00:00
|
|
|
return commitToHistory(&d.image, fmt.Sprintf("CMD %q", args), false, nil)
|
2017-09-11 01:11:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func dispatchEntrypoint(d *dispatchState, c *instructions.EntrypointCommand) error {
|
|
|
|
var args []string = c.CmdLine
|
|
|
|
if c.PrependShell {
|
2018-05-23 19:19:10 +00:00
|
|
|
args = withShell(d.image, args)
|
2017-09-11 01:11:42 +00:00
|
|
|
}
|
|
|
|
d.image.Config.Entrypoint = args
|
2018-05-23 19:18:14 +00:00
|
|
|
if !d.cmdSet {
|
|
|
|
d.image.Config.Cmd = nil
|
|
|
|
}
|
2017-12-08 01:33:17 +00:00
|
|
|
return commitToHistory(&d.image, fmt.Sprintf("ENTRYPOINT %q", args), false, nil)
|
2017-09-11 01:11:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func dispatchHealthcheck(d *dispatchState, c *instructions.HealthCheckCommand) error {
|
|
|
|
d.image.Config.Healthcheck = &HealthConfig{
|
|
|
|
Test: c.Health.Test,
|
|
|
|
Interval: c.Health.Interval,
|
|
|
|
Timeout: c.Health.Timeout,
|
|
|
|
StartPeriod: c.Health.StartPeriod,
|
|
|
|
Retries: c.Health.Retries,
|
|
|
|
}
|
2017-12-08 01:33:17 +00:00
|
|
|
return commitToHistory(&d.image, fmt.Sprintf("HEALTHCHECK %q", d.image.Config.Healthcheck), false, nil)
|
2017-09-11 01:11:42 +00:00
|
|
|
}
|
|
|
|
|
2018-02-02 01:20:44 +00:00
|
|
|
func dispatchExpose(d *dispatchState, c *instructions.ExposeCommand, shlex *shell.Lex) error {
|
2017-12-12 00:35:31 +00:00
|
|
|
ports := []string{}
|
|
|
|
for _, p := range c.Ports {
|
2018-07-20 23:30:47 +00:00
|
|
|
ps, err := shlex.ProcessWordsWithMap(p, toEnvMap(d.buildArgs, d.image.Config.Env))
|
2017-12-12 00:35:31 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
ports = append(ports, ps...)
|
|
|
|
}
|
|
|
|
c.Ports = ports
|
|
|
|
|
2017-09-11 01:11:42 +00:00
|
|
|
ps, _, err := nat.ParsePortSpecs(c.Ports)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if d.image.Config.ExposedPorts == nil {
|
|
|
|
d.image.Config.ExposedPorts = make(map[string]struct{})
|
|
|
|
}
|
|
|
|
for p := range ps {
|
|
|
|
d.image.Config.ExposedPorts[string(p)] = struct{}{}
|
|
|
|
}
|
|
|
|
|
2017-12-08 01:33:17 +00:00
|
|
|
return commitToHistory(&d.image, fmt.Sprintf("EXPOSE %v", ps), false, nil)
|
2017-09-11 01:11:42 +00:00
|
|
|
}
|
2018-02-02 01:20:44 +00:00
|
|
|
|
2017-12-08 01:33:17 +00:00
|
|
|
func dispatchUser(d *dispatchState, c *instructions.UserCommand, commit bool) error {
|
2017-12-11 23:08:35 +00:00
|
|
|
d.state = d.state.User(c.User)
|
2017-09-11 01:11:42 +00:00
|
|
|
d.image.Config.User = c.User
|
2017-12-08 01:33:17 +00:00
|
|
|
if commit {
|
|
|
|
return commitToHistory(&d.image, fmt.Sprintf("USER %v", c.User), false, nil)
|
|
|
|
}
|
2017-09-11 01:11:42 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func dispatchVolume(d *dispatchState, c *instructions.VolumeCommand) error {
|
|
|
|
if d.image.Config.Volumes == nil {
|
|
|
|
d.image.Config.Volumes = map[string]struct{}{}
|
|
|
|
}
|
|
|
|
for _, v := range c.Volumes {
|
|
|
|
if v == "" {
|
|
|
|
return errors.New("VOLUME specified can not be an empty string")
|
|
|
|
}
|
|
|
|
d.image.Config.Volumes[v] = struct{}{}
|
|
|
|
}
|
2017-12-08 01:33:17 +00:00
|
|
|
return commitToHistory(&d.image, fmt.Sprintf("VOLUME %v", c.Volumes), false, nil)
|
2017-09-11 01:11:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func dispatchStopSignal(d *dispatchState, c *instructions.StopSignalCommand) error {
|
|
|
|
if _, err := signal.ParseSignal(c.Signal); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
d.image.Config.StopSignal = c.Signal
|
2017-12-08 01:33:17 +00:00
|
|
|
return commitToHistory(&d.image, fmt.Sprintf("STOPSIGNAL %v", c.Signal), false, nil)
|
2017-09-11 01:11:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func dispatchShell(d *dispatchState, c *instructions.ShellCommand) error {
|
|
|
|
d.image.Config.Shell = c.Shell
|
2017-12-08 01:33:17 +00:00
|
|
|
return commitToHistory(&d.image, fmt.Sprintf("SHELL %v", c.Shell), false, nil)
|
2017-08-25 20:08:18 +00:00
|
|
|
}
|
2017-09-01 23:57:22 +00:00
|
|
|
|
2018-07-04 10:54:40 +00:00
|
|
|
func dispatchArg(d *dispatchState, c *instructions.ArgCommand, metaArgs []instructions.KeyValuePairOptional, buildArgValues map[string]string) error {
|
2017-12-08 01:33:17 +00:00
|
|
|
commitStr := "ARG " + c.Key
|
2018-07-04 10:54:40 +00:00
|
|
|
buildArg := setKVValue(c.KeyValuePairOptional, buildArgValues)
|
|
|
|
|
2017-12-08 01:33:17 +00:00
|
|
|
if c.Value != nil {
|
|
|
|
commitStr += "=" + *c.Value
|
|
|
|
}
|
2018-07-04 10:54:40 +00:00
|
|
|
if buildArg.Value == nil {
|
2017-09-12 06:29:22 +00:00
|
|
|
for _, ma := range metaArgs {
|
2018-07-04 10:54:40 +00:00
|
|
|
if ma.Key == buildArg.Key {
|
|
|
|
buildArg.Value = ma.Value
|
2017-09-12 06:29:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-04 10:54:40 +00:00
|
|
|
d.buildArgs = append(d.buildArgs, buildArg)
|
2017-12-08 01:33:17 +00:00
|
|
|
return commitToHistory(&d.image, commitStr, false, nil)
|
2017-09-12 06:29:22 +00:00
|
|
|
}
|
|
|
|
|
2017-09-22 17:30:30 +00:00
|
|
|
func pathRelativeToWorkingDir(s llb.State, p string) string {
|
|
|
|
if path.IsAbs(p) {
|
|
|
|
return p
|
|
|
|
}
|
|
|
|
return path.Join(s.GetDir(), p)
|
|
|
|
}
|
|
|
|
|
2017-09-01 23:57:22 +00:00
|
|
|
func splitWildcards(name string) (string, string) {
|
|
|
|
i := 0
|
|
|
|
for ; i < len(name); i++ {
|
|
|
|
ch := name[i]
|
|
|
|
if ch == '\\' {
|
|
|
|
i++
|
|
|
|
} else if ch == '*' || ch == '?' || ch == '[' {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if i == len(name) {
|
|
|
|
return name, ""
|
|
|
|
}
|
2017-12-26 23:41:09 +00:00
|
|
|
|
|
|
|
base := path.Base(name[:i])
|
|
|
|
if name[:i] == "" || strings.HasSuffix(name[:i], string(filepath.Separator)) {
|
|
|
|
base = ""
|
|
|
|
}
|
|
|
|
return path.Dir(name[:i]), base + name[i:]
|
2017-09-01 23:57:22 +00:00
|
|
|
}
|
2017-09-11 01:11:42 +00:00
|
|
|
|
2018-07-20 10:46:50 +00:00
|
|
|
func addEnv(env []string, k, v string) []string {
|
2017-09-11 01:11:42 +00:00
|
|
|
gotOne := false
|
2017-09-12 06:29:22 +00:00
|
|
|
for i, envVar := range env {
|
2018-07-19 12:36:07 +00:00
|
|
|
key, _ := parseKeyValue(envVar)
|
|
|
|
if shell.EqualEnvKeys(key, k) {
|
2018-07-20 10:46:50 +00:00
|
|
|
env[i] = k + "=" + v
|
2017-09-11 01:11:42 +00:00
|
|
|
gotOne = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !gotOne {
|
2017-09-12 06:29:22 +00:00
|
|
|
env = append(env, k+"="+v)
|
2017-09-11 01:11:42 +00:00
|
|
|
}
|
2017-09-12 06:29:22 +00:00
|
|
|
return env
|
2017-09-11 01:11:42 +00:00
|
|
|
}
|
|
|
|
|
2018-07-19 12:36:07 +00:00
|
|
|
func parseKeyValue(env string) (string, string) {
|
|
|
|
parts := strings.SplitN(env, "=", 2)
|
|
|
|
v := ""
|
|
|
|
if len(parts) > 1 {
|
|
|
|
v = parts[1]
|
|
|
|
}
|
|
|
|
|
|
|
|
return parts[0], v
|
|
|
|
}
|
|
|
|
|
2018-07-04 10:54:40 +00:00
|
|
|
func setKVValue(kvpo instructions.KeyValuePairOptional, values map[string]string) instructions.KeyValuePairOptional {
|
|
|
|
if v, ok := values[kvpo.Key]; ok {
|
|
|
|
kvpo.Value = &v
|
2017-09-12 06:29:22 +00:00
|
|
|
}
|
2018-07-04 10:54:40 +00:00
|
|
|
return kvpo
|
2017-09-12 06:29:22 +00:00
|
|
|
}
|
|
|
|
|
2018-07-20 23:30:47 +00:00
|
|
|
func toEnvMap(args []instructions.KeyValuePairOptional, env []string) map[string]string {
|
2018-07-20 10:41:15 +00:00
|
|
|
m := shell.BuildEnvs(env)
|
|
|
|
|
2017-09-12 06:29:22 +00:00
|
|
|
for _, arg := range args {
|
2018-07-20 10:41:15 +00:00
|
|
|
// If key already exists, keep previous value.
|
|
|
|
if _, ok := m[arg.Key]; ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
m[arg.Key] = arg.ValueString()
|
2017-09-12 06:29:22 +00:00
|
|
|
}
|
2018-07-20 10:41:15 +00:00
|
|
|
return m
|
2017-09-12 06:29:22 +00:00
|
|
|
}
|
|
|
|
|
2018-06-21 21:09:38 +00:00
|
|
|
func dfCmd(cmd interface{}) llb.ConstraintsOpt {
|
2017-12-08 01:33:17 +00:00
|
|
|
// TODO: add fmt.Stringer to instructions.Command to remove interface{}
|
2017-12-08 00:13:50 +00:00
|
|
|
var cmdStr string
|
|
|
|
if cmd, ok := cmd.(fmt.Stringer); ok {
|
|
|
|
cmdStr = cmd.String()
|
|
|
|
}
|
|
|
|
if cmd, ok := cmd.(string); ok {
|
|
|
|
cmdStr = cmd
|
|
|
|
}
|
|
|
|
return llb.WithDescription(map[string]string{
|
|
|
|
"com.docker.dockerfile.v1.command": cmdStr,
|
|
|
|
})
|
|
|
|
}
|
2017-12-08 01:33:17 +00:00
|
|
|
|
2018-07-04 10:54:40 +00:00
|
|
|
func runCommandString(args []string, buildArgs []instructions.KeyValuePairOptional) string {
|
2017-12-08 01:33:17 +00:00
|
|
|
var tmpBuildEnv []string
|
|
|
|
for _, arg := range buildArgs {
|
2018-07-04 10:54:40 +00:00
|
|
|
tmpBuildEnv = append(tmpBuildEnv, arg.Key+"="+arg.ValueString())
|
2017-12-08 01:33:17 +00:00
|
|
|
}
|
|
|
|
if len(tmpBuildEnv) > 0 {
|
|
|
|
tmpBuildEnv = append([]string{fmt.Sprintf("|%d", len(tmpBuildEnv))}, tmpBuildEnv...)
|
|
|
|
}
|
|
|
|
|
|
|
|
return strings.Join(append(tmpBuildEnv, args...), " ")
|
|
|
|
}
|
|
|
|
|
|
|
|
func commitToHistory(img *Image, msg string, withLayer bool, st *llb.State) error {
|
|
|
|
if st != nil {
|
2018-04-23 18:37:53 +00:00
|
|
|
msg += " # buildkit"
|
2017-12-08 01:33:17 +00:00
|
|
|
}
|
|
|
|
|
2018-06-25 02:32:13 +00:00
|
|
|
img.History = append(img.History, specs.History{
|
2017-12-08 01:33:17 +00:00
|
|
|
CreatedBy: msg,
|
|
|
|
Comment: historyComment,
|
|
|
|
EmptyLayer: !withLayer,
|
|
|
|
})
|
|
|
|
return nil
|
|
|
|
}
|
2017-12-12 22:58:26 +00:00
|
|
|
|
|
|
|
func isReachable(from, to *dispatchState) (ret bool) {
|
|
|
|
if from == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if from == to || isReachable(from.base, to) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
for d := range from.deps {
|
|
|
|
if isReachable(d, to) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
2017-12-20 04:54:26 +00:00
|
|
|
|
|
|
|
func parseUser(str string) (uid uint32, gid uint32, err error) {
|
|
|
|
if str == "" {
|
|
|
|
return 0, 0, nil
|
|
|
|
}
|
|
|
|
parts := strings.SplitN(str, ":", 2)
|
|
|
|
for i, v := range parts {
|
|
|
|
switch i {
|
|
|
|
case 0:
|
|
|
|
uid, err = parseUID(v)
|
|
|
|
if err != nil {
|
|
|
|
return 0, 0, err
|
|
|
|
}
|
|
|
|
if len(parts) == 1 {
|
|
|
|
gid = uid
|
|
|
|
}
|
|
|
|
case 1:
|
|
|
|
gid, err = parseUID(v)
|
|
|
|
if err != nil {
|
|
|
|
return 0, 0, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseUID(str string) (uint32, error) {
|
|
|
|
if str == "root" {
|
|
|
|
return 0, nil
|
|
|
|
}
|
|
|
|
uid, err := strconv.ParseUint(str, 10, 32)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
return uint32(uid), nil
|
|
|
|
}
|
2018-04-14 01:16:22 +00:00
|
|
|
|
|
|
|
func normalizeContextPaths(paths map[string]struct{}) []string {
|
|
|
|
pathSlice := make([]string, 0, len(paths))
|
|
|
|
for p := range paths {
|
|
|
|
if p == "/" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
pathSlice = append(pathSlice, p)
|
|
|
|
}
|
|
|
|
|
|
|
|
toDelete := map[string]struct{}{}
|
|
|
|
for i := range pathSlice {
|
|
|
|
for j := range pathSlice {
|
|
|
|
if i == j {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if strings.HasPrefix(pathSlice[j], pathSlice[i]+"/") {
|
|
|
|
delete(paths, pathSlice[j])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
toSort := make([]string, 0, len(paths))
|
|
|
|
for p := range paths {
|
|
|
|
if _, ok := toDelete[p]; !ok {
|
|
|
|
toSort = append(toSort, path.Join(".", p))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
sort.Slice(toSort, func(i, j int) bool {
|
|
|
|
return toSort[i] < toSort[j]
|
|
|
|
})
|
|
|
|
return toSort
|
|
|
|
}
|
2018-04-23 18:37:53 +00:00
|
|
|
|
2018-05-21 23:07:51 +00:00
|
|
|
func proxyEnvFromBuildArgs(args map[string]string) *llb.ProxyEnv {
|
|
|
|
pe := &llb.ProxyEnv{}
|
|
|
|
isNil := true
|
|
|
|
for k, v := range args {
|
|
|
|
if strings.EqualFold(k, "http_proxy") {
|
|
|
|
pe.HttpProxy = v
|
|
|
|
isNil = false
|
|
|
|
}
|
|
|
|
if strings.EqualFold(k, "https_proxy") {
|
|
|
|
pe.HttpsProxy = v
|
|
|
|
isNil = false
|
|
|
|
}
|
|
|
|
if strings.EqualFold(k, "ftp_proxy") {
|
|
|
|
pe.FtpProxy = v
|
|
|
|
isNil = false
|
|
|
|
}
|
|
|
|
if strings.EqualFold(k, "no_proxy") {
|
|
|
|
pe.NoProxy = v
|
|
|
|
isNil = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if isNil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return pe
|
|
|
|
}
|
|
|
|
|
2018-04-23 18:37:53 +00:00
|
|
|
type mutableOutput struct {
|
|
|
|
llb.Output
|
|
|
|
}
|
2018-05-23 19:19:10 +00:00
|
|
|
|
|
|
|
func withShell(img Image, args []string) []string {
|
|
|
|
var shell []string
|
|
|
|
if len(img.Config.Shell) > 0 {
|
|
|
|
shell = append([]string{}, img.Config.Shell...)
|
|
|
|
} else {
|
|
|
|
shell = defaultShell()
|
|
|
|
}
|
|
|
|
return append(shell, strings.Join(args, " "))
|
|
|
|
}
|
2018-06-26 17:24:10 +00:00
|
|
|
|
|
|
|
func autoDetectPlatform(img Image, target specs.Platform, supported []specs.Platform) specs.Platform {
|
|
|
|
os := img.OS
|
|
|
|
arch := img.Architecture
|
|
|
|
if target.OS == os && target.Architecture == arch {
|
|
|
|
return target
|
|
|
|
}
|
|
|
|
for _, p := range supported {
|
|
|
|
if p.OS == os && p.Architecture == arch {
|
|
|
|
return p
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return target
|
|
|
|
}
|
2018-07-28 00:27:41 +00:00
|
|
|
|
|
|
|
func WithInternalName(name string, a ...interface{}) llb.ConstraintsOpt {
|
|
|
|
return llb.WithCustomName("[internal] "+name, a...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func uppercaseCmd(str string) string {
|
|
|
|
p := strings.SplitN(str, " ", 2)
|
|
|
|
p[0] = strings.ToUpper(p[0])
|
|
|
|
return strings.Join(p, " ")
|
|
|
|
}
|
|
|
|
|
|
|
|
func processCmdEnv(shlex *shell.Lex, cmd string, env []string) string {
|
|
|
|
w, err := shlex.ProcessWord(cmd, env)
|
|
|
|
if err != nil {
|
|
|
|
return cmd
|
|
|
|
}
|
|
|
|
return w
|
|
|
|
}
|
2018-07-30 20:52:27 +00:00
|
|
|
|
2018-07-30 20:52:41 +00:00
|
|
|
func prefixCommand(ds *dispatchState, str string, prefixPlatform bool, platform *specs.Platform) string {
|
|
|
|
if ds.cmdTotal == 0 {
|
|
|
|
return str
|
|
|
|
}
|
|
|
|
out := "["
|
|
|
|
if prefixPlatform && platform != nil {
|
|
|
|
out += platforms.Format(*platform) + " "
|
|
|
|
}
|
|
|
|
if ds.stageName != "" {
|
|
|
|
out += ds.stageName + " "
|
2018-07-30 20:52:27 +00:00
|
|
|
}
|
2018-07-30 20:52:41 +00:00
|
|
|
ds.cmdIndex++
|
|
|
|
out += fmt.Sprintf("%d/%d] ", ds.cmdIndex, ds.cmdTotal)
|
2018-07-30 20:52:27 +00:00
|
|
|
return out + str
|
|
|
|
}
|