mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-05-29 08:57:44 +08:00
Merge pull request #37 from tonistiigi/multi-driver
build: multi-node build support
This commit is contained in:
commit
e7e57b626d
770
build/build.go
770
build/build.go
@ -3,6 +3,7 @@ package build
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
@ -11,17 +12,21 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/images"
|
||||||
"github.com/containerd/containerd/platforms"
|
"github.com/containerd/containerd/platforms"
|
||||||
|
clitypes "github.com/docker/cli/cli/config/types"
|
||||||
"github.com/docker/distribution/reference"
|
"github.com/docker/distribution/reference"
|
||||||
dockerclient "github.com/docker/docker/client"
|
dockerclient "github.com/docker/docker/client"
|
||||||
"github.com/docker/docker/pkg/urlutil"
|
"github.com/docker/docker/pkg/urlutil"
|
||||||
"github.com/moby/buildkit/client"
|
"github.com/moby/buildkit/client"
|
||||||
"github.com/moby/buildkit/session"
|
"github.com/moby/buildkit/session"
|
||||||
"github.com/moby/buildkit/session/upload/uploadprovider"
|
"github.com/moby/buildkit/session/upload/uploadprovider"
|
||||||
|
"github.com/opencontainers/go-digest"
|
||||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/tonistiigi/buildx/driver"
|
"github.com/tonistiigi/buildx/driver"
|
||||||
|
"github.com/tonistiigi/buildx/util/imagetools"
|
||||||
"github.com/tonistiigi/buildx/util/progress"
|
"github.com/tonistiigi/buildx/util/progress"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
@ -66,245 +71,620 @@ type DriverInfo struct {
|
|||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Auth interface {
|
||||||
|
GetAuthConfig(registryHostname string) (clitypes.AuthConfig, error)
|
||||||
|
}
|
||||||
|
|
||||||
type DockerAPI interface {
|
type DockerAPI interface {
|
||||||
DockerAPI(name string) (dockerclient.APIClient, error)
|
DockerAPI(name string) (dockerclient.APIClient, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFirstDriver(drivers []DriverInfo) (driver.Driver, error) {
|
func filterAvailableDrivers(drivers []DriverInfo) ([]DriverInfo, error) {
|
||||||
|
out := make([]DriverInfo, 0, len(drivers))
|
||||||
err := errors.Errorf("no drivers found")
|
err := errors.Errorf("no drivers found")
|
||||||
for _, di := range drivers {
|
for _, di := range drivers {
|
||||||
if di.Driver != nil {
|
if di.Err == nil && di.Driver != nil {
|
||||||
return di.Driver, nil
|
out = append(out, di)
|
||||||
}
|
}
|
||||||
if di.Err != nil {
|
if di.Err != nil {
|
||||||
err = di.Err
|
err = di.Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(out) > 0 {
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func Build(ctx context.Context, drivers []DriverInfo, opt map[string]Options, docker DockerAPI, pw progress.Writer) (map[string]*client.SolveResponse, error) {
|
type driverPair struct {
|
||||||
if len(drivers) == 0 {
|
driverIndex int
|
||||||
return nil, errors.Errorf("driver required for build")
|
platforms []specs.Platform
|
||||||
|
so *client.SolveOpt
|
||||||
|
}
|
||||||
|
|
||||||
|
func driverIndexes(m map[string][]driverPair) []int {
|
||||||
|
out := make([]int, 0, len(m))
|
||||||
|
visited := map[int]struct{}{}
|
||||||
|
for _, dp := range m {
|
||||||
|
for _, d := range dp {
|
||||||
|
if _, ok := visited[d.driverIndex]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
visited[d.driverIndex] = struct{}{}
|
||||||
|
out = append(out, d.driverIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func allIndexes(l int) []int {
|
||||||
|
out := make([]int, 0, l)
|
||||||
|
for i := 0; i < l; i++ {
|
||||||
|
out = append(out, i)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureBooted(ctx context.Context, drivers []DriverInfo, idxs []int, pw progress.Writer) ([]*client.Client, error) {
|
||||||
|
clients := make([]*client.Client, len(drivers))
|
||||||
|
|
||||||
|
eg, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
|
for _, i := range idxs {
|
||||||
|
func(i int) {
|
||||||
|
eg.Go(func() error {
|
||||||
|
c, err := driver.Boot(ctx, drivers[i].Driver, pw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
clients[i] = c
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(drivers) > 1 {
|
if err := eg.Wait(); err != nil {
|
||||||
return nil, errors.Errorf("multiple drivers currently not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
d, err := getFirstDriver(drivers)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return clients, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitToDriverPairs(availablePlatforms map[string]int, opt map[string]Options) map[string][]driverPair {
|
||||||
|
m := map[string][]driverPair{}
|
||||||
|
for k, opt := range opt {
|
||||||
|
mm := map[int][]specs.Platform{}
|
||||||
|
for _, p := range opt.Platforms {
|
||||||
|
k := platforms.Format(p)
|
||||||
|
idx := availablePlatforms[k] // default 0
|
||||||
|
pp := mm[idx]
|
||||||
|
pp = append(pp, p)
|
||||||
|
mm[idx] = pp
|
||||||
|
}
|
||||||
|
dps := make([]driverPair, 0, 2)
|
||||||
|
for idx, pp := range mm {
|
||||||
|
dps = append(dps, driverPair{driverIndex: idx, platforms: pp})
|
||||||
|
}
|
||||||
|
m[k] = dps
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveDrivers(ctx context.Context, drivers []DriverInfo, opt map[string]Options, pw progress.Writer) (map[string][]driverPair, []*client.Client, error) {
|
||||||
|
|
||||||
|
availablePlatforms := map[string]int{}
|
||||||
|
for i, d := range drivers {
|
||||||
|
for _, p := range d.Platform {
|
||||||
|
availablePlatforms[platforms.Format(p)] = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
undetectedPlatform := false
|
||||||
|
allPlatforms := map[string]int{}
|
||||||
|
for _, opt := range opt {
|
||||||
|
for _, p := range opt.Platforms {
|
||||||
|
k := platforms.Format(p)
|
||||||
|
allPlatforms[k] = -1
|
||||||
|
if _, ok := availablePlatforms[k]; !ok {
|
||||||
|
undetectedPlatform = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fast path
|
||||||
|
if len(drivers) == 1 || len(allPlatforms) == 0 {
|
||||||
|
m := map[string][]driverPair{}
|
||||||
|
for k, opt := range opt {
|
||||||
|
m[k] = []driverPair{{driverIndex: 0, platforms: opt.Platforms}}
|
||||||
|
}
|
||||||
|
clients, err := ensureBooted(ctx, drivers, driverIndexes(m), pw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return m, clients, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// map based on existing platforms
|
||||||
|
if !undetectedPlatform {
|
||||||
|
m := splitToDriverPairs(availablePlatforms, opt)
|
||||||
|
clients, err := ensureBooted(ctx, drivers, driverIndexes(m), pw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return m, clients, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// boot all drivers in k
|
||||||
|
clients, err := ensureBooted(ctx, drivers, allIndexes(len(drivers)), pw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
eg, ctx := errgroup.WithContext(ctx)
|
||||||
|
workers := make([][]*client.WorkerInfo, len(clients))
|
||||||
|
|
||||||
|
for i, c := range clients {
|
||||||
|
if c == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
func(i int) {
|
||||||
|
eg.Go(func() error {
|
||||||
|
ww, err := clients[i].ListWorkers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "listing workers")
|
||||||
|
}
|
||||||
|
workers[i] = ww
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := eg.Wait(); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, ww := range workers {
|
||||||
|
for _, w := range ww {
|
||||||
|
for _, p := range w.Platforms {
|
||||||
|
p = platforms.Normalize(p)
|
||||||
|
ps := platforms.Format(p)
|
||||||
|
|
||||||
|
if _, ok := availablePlatforms[ps]; !ok {
|
||||||
|
availablePlatforms[ps] = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return splitToDriverPairs(availablePlatforms, opt), clients, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toRepoOnly(in string) (string, error) {
|
||||||
|
m := map[string]struct{}{}
|
||||||
|
p := strings.Split(in, ",")
|
||||||
|
for _, pp := range p {
|
||||||
|
n, err := reference.ParseNormalizedNamed(pp)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
m[n.Name()] = struct{}{}
|
||||||
|
}
|
||||||
|
out := make([]string, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
out = append(out, k)
|
||||||
|
}
|
||||||
|
return strings.Join(out, ","), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDefaultMobyDriver(d driver.Driver) bool {
|
||||||
|
_, ok := d.(interface {
|
||||||
|
IsDefaultMobyDriver()
|
||||||
|
})
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSolveOpt(d driver.Driver, multiDriver bool, opt Options, dl dockerLoadCallback) (solveOpt *client.SolveOpt, release func(), err error) {
|
||||||
|
defers := make([]func(), 0, 2)
|
||||||
|
releaseF := func() {
|
||||||
|
for _, f := range defers {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
releaseF()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if opt.ImageIDFile != "" {
|
||||||
|
if multiDriver || len(opt.Platforms) != 0 {
|
||||||
|
return nil, nil, errors.Errorf("image ID file cannot be specified when building for multiple platforms")
|
||||||
|
}
|
||||||
|
// Avoid leaving a stale file if we eventually fail
|
||||||
|
if err := os.Remove(opt.ImageIDFile); err != nil && !os.IsNotExist(err) {
|
||||||
|
return nil, nil, errors.Wrap(err, "removing image ID file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// inline cache from build arg
|
||||||
|
if v, ok := opt.BuildArgs["BUILDKIT_INLINE_CACHE"]; ok {
|
||||||
|
if v, _ := strconv.ParseBool(v); v {
|
||||||
|
opt.CacheTo = append(opt.CacheTo, client.CacheOptionsEntry{
|
||||||
|
Type: "inline",
|
||||||
|
Attrs: map[string]string{},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range opt.CacheTo {
|
||||||
|
if e.Type != "inline" && !d.Features()[driver.CacheExport] {
|
||||||
|
return nil, nil, notSupported(d, driver.CacheExport)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
so := client.SolveOpt{
|
||||||
|
Frontend: "dockerfile.v0",
|
||||||
|
FrontendAttrs: map[string]string{},
|
||||||
|
LocalDirs: map[string]string{},
|
||||||
|
CacheExports: opt.CacheTo,
|
||||||
|
CacheImports: opt.CacheFrom,
|
||||||
|
}
|
||||||
|
|
||||||
|
if multiDriver {
|
||||||
|
// force creation of manifest list
|
||||||
|
so.FrontendAttrs["multi-platform"] = "true"
|
||||||
|
}
|
||||||
|
|
||||||
_, isDefaultMobyDriver := d.(interface {
|
_, isDefaultMobyDriver := d.(interface {
|
||||||
IsDefaultMobyDriver()
|
IsDefaultMobyDriver()
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, opt := range opt {
|
switch len(opt.Exports) {
|
||||||
if !isDefaultMobyDriver && len(opt.Exports) == 0 {
|
case 1:
|
||||||
logrus.Warnf("No output specified for %s driver. Build result will only remain in the build cache. To push result image into registry use --push or to load image into docker use --load", d.Factory().Name())
|
// valid
|
||||||
|
case 0:
|
||||||
|
if isDefaultMobyDriver {
|
||||||
|
// backwards compat for docker driver only:
|
||||||
|
// this ensures the build results in a docker image.
|
||||||
|
opt.Exports = []client.ExportEntry{{Type: "image", Attrs: map[string]string{}}}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, nil, errors.Errorf("multiple outputs currently unsupported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill in image exporter names from tags
|
||||||
|
if len(opt.Tags) > 0 {
|
||||||
|
tags := make([]string, len(opt.Tags))
|
||||||
|
for i, tag := range opt.Tags {
|
||||||
|
ref, err := reference.Parse(tag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrapf(err, "invalid tag %q", tag)
|
||||||
|
}
|
||||||
|
tags[i] = ref.String()
|
||||||
|
}
|
||||||
|
for i, e := range opt.Exports {
|
||||||
|
switch e.Type {
|
||||||
|
case "image", "oci", "docker":
|
||||||
|
opt.Exports[i].Attrs["name"] = strings.Join(tags, ",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, e := range opt.Exports {
|
||||||
|
if e.Type == "image" && e.Attrs["name"] == "" && e.Attrs["push"] != "" {
|
||||||
|
if ok, _ := strconv.ParseBool(e.Attrs["push"]); ok {
|
||||||
|
return nil, nil, errors.Errorf("tag is needed when pushing to registry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set up exporters
|
||||||
|
for i, e := range opt.Exports {
|
||||||
|
if (e.Type == "local" || e.Type == "tar") && opt.ImageIDFile != "" {
|
||||||
|
return nil, nil, errors.Errorf("local and tar exporters are incompatible with image ID file")
|
||||||
|
}
|
||||||
|
if e.Type == "oci" && !d.Features()[driver.OCIExporter] {
|
||||||
|
return nil, nil, notSupported(d, driver.OCIExporter)
|
||||||
|
}
|
||||||
|
if e.Type == "docker" {
|
||||||
|
if e.Output == nil {
|
||||||
|
if isDefaultMobyDriver {
|
||||||
|
e.Type = "image"
|
||||||
|
} else {
|
||||||
|
w, cancel, err := dl(e.Attrs["context"])
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
defers = append(defers, cancel)
|
||||||
|
opt.Exports[i].Output = w
|
||||||
|
}
|
||||||
|
} else if !d.Features()[driver.DockerExporter] {
|
||||||
|
return nil, nil, notSupported(d, driver.DockerExporter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e.Type == "image" && isDefaultMobyDriver {
|
||||||
|
opt.Exports[i].Type = "moby"
|
||||||
|
if e.Attrs["push"] != "" {
|
||||||
|
if ok, _ := strconv.ParseBool(e.Attrs["push"]); ok {
|
||||||
|
return nil, nil, errors.Errorf("auto-push is currently not implemented for docker driver")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
so.Exports = opt.Exports
|
||||||
|
so.Session = opt.Session
|
||||||
|
|
||||||
|
releaseLoad, err := LoadInputs(opt.Inputs, &so)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
defers = append(defers, releaseLoad)
|
||||||
|
|
||||||
|
if opt.Pull {
|
||||||
|
so.FrontendAttrs["image-resolve-mode"] = "pull"
|
||||||
|
}
|
||||||
|
if opt.Target != "" {
|
||||||
|
so.FrontendAttrs["target"] = opt.Target
|
||||||
|
}
|
||||||
|
if opt.NoCache {
|
||||||
|
so.FrontendAttrs["no-cache"] = ""
|
||||||
|
}
|
||||||
|
for k, v := range opt.BuildArgs {
|
||||||
|
so.FrontendAttrs["build-arg:"+k] = v
|
||||||
|
}
|
||||||
|
for k, v := range opt.Labels {
|
||||||
|
so.FrontendAttrs["label:"+k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// set platforms
|
||||||
|
if len(opt.Platforms) != 0 {
|
||||||
|
pp := make([]string, len(opt.Platforms))
|
||||||
|
for i, p := range opt.Platforms {
|
||||||
|
pp[i] = platforms.Format(p)
|
||||||
|
}
|
||||||
|
if len(pp) > 1 && !d.Features()[driver.MultiPlatform] {
|
||||||
|
return nil, nil, notSupported(d, driver.MultiPlatform)
|
||||||
|
}
|
||||||
|
so.FrontendAttrs["platform"] = strings.Join(pp, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup networkmode
|
||||||
|
switch opt.NetworkMode {
|
||||||
|
case "host", "none":
|
||||||
|
so.FrontendAttrs["force-network-mode"] = opt.NetworkMode
|
||||||
|
case "", "default":
|
||||||
|
default:
|
||||||
|
return nil, nil, errors.Errorf("network mode %q not supported by buildkit", opt.NetworkMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup extrahosts
|
||||||
|
extraHosts, err := toBuildkitExtraHosts(opt.ExtraHosts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
so.FrontendAttrs["add-hosts"] = extraHosts
|
||||||
|
|
||||||
|
return &so, releaseF, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Build(ctx context.Context, drivers []DriverInfo, opt map[string]Options, docker DockerAPI, auth Auth, pw progress.Writer) (resp map[string]*client.SolveResponse, err error) {
|
||||||
|
if len(drivers) == 0 {
|
||||||
|
return nil, errors.Errorf("driver required for build")
|
||||||
|
}
|
||||||
|
|
||||||
|
drivers, err = filterAvailableDrivers(drivers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "no valid drivers found")
|
||||||
|
}
|
||||||
|
|
||||||
|
var noMobyDriver driver.Driver
|
||||||
|
for _, d := range drivers {
|
||||||
|
if !isDefaultMobyDriver(d.Driver) {
|
||||||
|
noMobyDriver = d.Driver
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := driver.Boot(ctx, d, pw)
|
if noMobyDriver != nil {
|
||||||
|
for _, opt := range opt {
|
||||||
|
if len(opt.Exports) == 0 {
|
||||||
|
logrus.Warnf("No output specified for %s driver. Build result will only remain in the build cache. To push result image into registry use --push or to load image into docker use --load", noMobyDriver.Factory().Name())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m, clients, err := resolveDrivers(ctx, drivers, opt, pw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
close(pw.Status())
|
close(pw.Status())
|
||||||
<-pw.Done()
|
<-pw.Done()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
withPrefix := len(opt) > 1
|
defers := make([]func(), 0, 2)
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
for _, f := range defers {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
mw := progress.NewMultiWriter(pw)
|
mw := progress.NewMultiWriter(pw)
|
||||||
|
|
||||||
eg, ctx := errgroup.WithContext(ctx)
|
eg, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
resp := map[string]*client.SolveResponse{}
|
for k, opt := range opt {
|
||||||
var mu sync.Mutex
|
multiDriver := len(m[k]) > 1
|
||||||
|
for i, dp := range m[k] {
|
||||||
|
d := drivers[dp.driverIndex].Driver
|
||||||
|
opt.Platforms = dp.platforms
|
||||||
|
so, release, err := toSolveOpt(d, multiDriver, opt, func(name string) (io.WriteCloser, func(), error) {
|
||||||
|
return newDockerLoader(ctx, docker, name, mw)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defers = append(defers, release)
|
||||||
|
m[k][i].so = so
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = map[string]*client.SolveResponse{}
|
||||||
|
var respMu sync.Mutex
|
||||||
|
|
||||||
|
multiTarget := len(opt) > 1
|
||||||
|
|
||||||
for k, opt := range opt {
|
for k, opt := range opt {
|
||||||
pw := mw.WithPrefix(k, withPrefix)
|
err := func() error {
|
||||||
|
opt := opt
|
||||||
|
dps := m[k]
|
||||||
|
multiDriver := len(m[k]) > 1
|
||||||
|
|
||||||
if opt.ImageIDFile != "" {
|
res := make([]*client.SolveResponse, len(dps))
|
||||||
if len(opt.Platforms) != 0 {
|
wg := &sync.WaitGroup{}
|
||||||
return nil, errors.Errorf("image ID file cannot be specified when building for multiple platforms")
|
wg.Add(len(dps))
|
||||||
}
|
|
||||||
// Avoid leaving a stale file if we eventually fail
|
|
||||||
if err := os.Remove(opt.ImageIDFile); err != nil && !os.IsNotExist(err) {
|
|
||||||
return nil, errors.Wrap(err, "removing image ID file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := opt.BuildArgs["BUILDKIT_INLINE_CACHE"]; ok {
|
var pushNames string
|
||||||
if v, _ := strconv.ParseBool(v); v {
|
|
||||||
opt.CacheTo = append(opt.CacheTo, client.CacheOptionsEntry{
|
|
||||||
Type: "inline",
|
|
||||||
Attrs: map[string]string{},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, e := range opt.CacheTo {
|
|
||||||
if e.Type != "inline" && !d.Features()[driver.CacheExport] {
|
|
||||||
return nil, notSupported(d, driver.CacheExport)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
so := client.SolveOpt{
|
|
||||||
Frontend: "dockerfile.v0",
|
|
||||||
FrontendAttrs: map[string]string{},
|
|
||||||
LocalDirs: map[string]string{},
|
|
||||||
CacheExports: opt.CacheTo,
|
|
||||||
CacheImports: opt.CacheFrom,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch len(opt.Exports) {
|
|
||||||
case 1:
|
|
||||||
// valid
|
|
||||||
case 0:
|
|
||||||
if isDefaultMobyDriver {
|
|
||||||
// backwards compat for docker driver only:
|
|
||||||
// this ensures the build results in a docker image.
|
|
||||||
opt.Exports = []client.ExportEntry{{Type: "image", Attrs: map[string]string{}}}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, errors.Errorf("multiple outputs currently unsupported")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(opt.Tags) > 0 {
|
|
||||||
tags := make([]string, len(opt.Tags))
|
|
||||||
for i, tag := range opt.Tags {
|
|
||||||
ref, err := reference.Parse(tag)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "invalid tag %q", tag)
|
|
||||||
}
|
|
||||||
tags[i] = ref.String()
|
|
||||||
}
|
|
||||||
for i, e := range opt.Exports {
|
|
||||||
switch e.Type {
|
|
||||||
case "image", "oci", "docker":
|
|
||||||
opt.Exports[i].Attrs["name"] = strings.Join(tags, ",")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for _, e := range opt.Exports {
|
|
||||||
if e.Type == "image" && e.Attrs["name"] == "" && e.Attrs["push"] != "" {
|
|
||||||
if ok, _ := strconv.ParseBool(e.Attrs["push"]); ok {
|
|
||||||
return nil, errors.Errorf("tag is needed when pushing to registry")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, e := range opt.Exports {
|
|
||||||
if (e.Type == "local" || e.Type == "tar") && opt.ImageIDFile != "" {
|
|
||||||
return nil, errors.Errorf("local and tar exporters are incompatible with image ID file")
|
|
||||||
}
|
|
||||||
if e.Type == "oci" && !d.Features()[driver.OCIExporter] {
|
|
||||||
return nil, notSupported(d, driver.OCIExporter)
|
|
||||||
}
|
|
||||||
if e.Type == "docker" {
|
|
||||||
if e.Output == nil {
|
|
||||||
if isDefaultMobyDriver {
|
|
||||||
e.Type = "image"
|
|
||||||
} else {
|
|
||||||
w, cancel, err := newDockerLoader(ctx, docker, e.Attrs["context"], mw)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer cancel()
|
|
||||||
opt.Exports[i].Output = w
|
|
||||||
}
|
|
||||||
} else if !d.Features()[driver.DockerExporter] {
|
|
||||||
return nil, notSupported(d, driver.DockerExporter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if e.Type == "image" && isDefaultMobyDriver {
|
|
||||||
opt.Exports[i].Type = "moby"
|
|
||||||
if e.Attrs["push"] != "" {
|
|
||||||
if ok, _ := strconv.ParseBool(e.Attrs["push"]); ok {
|
|
||||||
return nil, errors.Errorf("auto-push is currently not implemented for docker driver")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: handle loading to docker daemon
|
|
||||||
|
|
||||||
so.Exports = opt.Exports
|
|
||||||
so.Session = opt.Session
|
|
||||||
|
|
||||||
release, err := LoadInputs(opt.Inputs, &so)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer release()
|
|
||||||
|
|
||||||
if opt.Pull {
|
|
||||||
so.FrontendAttrs["image-resolve-mode"] = "pull"
|
|
||||||
}
|
|
||||||
if opt.Target != "" {
|
|
||||||
so.FrontendAttrs["target"] = opt.Target
|
|
||||||
}
|
|
||||||
if opt.NoCache {
|
|
||||||
so.FrontendAttrs["no-cache"] = ""
|
|
||||||
}
|
|
||||||
for k, v := range opt.BuildArgs {
|
|
||||||
so.FrontendAttrs["build-arg:"+k] = v
|
|
||||||
}
|
|
||||||
for k, v := range opt.Labels {
|
|
||||||
so.FrontendAttrs["label:"+k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(opt.Platforms) != 0 {
|
|
||||||
pp := make([]string, len(opt.Platforms))
|
|
||||||
for i, p := range opt.Platforms {
|
|
||||||
pp[i] = platforms.Format(p)
|
|
||||||
}
|
|
||||||
if len(pp) > 1 && !d.Features()[driver.MultiPlatform] {
|
|
||||||
return nil, notSupported(d, driver.MultiPlatform)
|
|
||||||
}
|
|
||||||
so.FrontendAttrs["platform"] = strings.Join(pp, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch opt.NetworkMode {
|
|
||||||
case "host", "none":
|
|
||||||
so.FrontendAttrs["force-network-mode"] = opt.NetworkMode
|
|
||||||
case "", "default":
|
|
||||||
default:
|
|
||||||
return nil, errors.Errorf("network mode %q not supported by buildkit", opt.NetworkMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
extraHosts, err := toBuildkitExtraHosts(opt.ExtraHosts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
so.FrontendAttrs["add-hosts"] = extraHosts
|
|
||||||
|
|
||||||
var statusCh chan *client.SolveStatus
|
|
||||||
if pw != nil {
|
|
||||||
pw = progress.ResetTime(pw)
|
|
||||||
statusCh = pw.Status()
|
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
<-pw.Done()
|
pw := mw.WithPrefix("default", false)
|
||||||
return pw.Err()
|
defer close(pw.Status())
|
||||||
})
|
wg.Wait()
|
||||||
}
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
eg.Go(func() error {
|
respMu.Lock()
|
||||||
rr, err := c.Solve(ctx, nil, so, statusCh)
|
resp[k] = res[0]
|
||||||
if err != nil {
|
respMu.Unlock()
|
||||||
return err
|
if len(res) == 1 {
|
||||||
}
|
if opt.ImageIDFile != "" {
|
||||||
mu.Lock()
|
return ioutil.WriteFile(opt.ImageIDFile, []byte(res[0].ExporterResponse["containerimage.digest"]), 0644)
|
||||||
resp[k] = rr
|
}
|
||||||
mu.Unlock()
|
return nil
|
||||||
if opt.ImageIDFile != "" {
|
}
|
||||||
return ioutil.WriteFile(opt.ImageIDFile, []byte(rr.ExporterResponse["containerimage.digest"]), 0644)
|
|
||||||
|
if pushNames != "" {
|
||||||
|
progress.Write(pw, fmt.Sprintf("merging manifest list %s", pushNames), func() error {
|
||||||
|
descs := make([]specs.Descriptor, 0, len(res))
|
||||||
|
|
||||||
|
for _, r := range res {
|
||||||
|
s, ok := r.ExporterResponse["containerimage.digest"]
|
||||||
|
if ok {
|
||||||
|
descs = append(descs, specs.Descriptor{
|
||||||
|
Digest: digest.Digest(s),
|
||||||
|
MediaType: images.MediaTypeDockerSchema2ManifestList,
|
||||||
|
Size: -1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(descs) > 0 {
|
||||||
|
itpull := imagetools.New(imagetools.Opt{
|
||||||
|
Auth: auth,
|
||||||
|
})
|
||||||
|
|
||||||
|
names := strings.Split(pushNames, ",")
|
||||||
|
dt, desc, err := itpull.Combine(ctx, names[0], descs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if opt.ImageIDFile != "" {
|
||||||
|
return ioutil.WriteFile(opt.ImageIDFile, []byte(desc.Digest), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
itpush := imagetools.New(imagetools.Opt{
|
||||||
|
Auth: auth,
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, n := range names {
|
||||||
|
nn, err := reference.ParseNormalizedNamed(n)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := itpush.Push(ctx, nn, desc, dt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
respMu.Lock()
|
||||||
|
resp[k] = &client.SolveResponse{
|
||||||
|
ExporterResponse: map[string]string{
|
||||||
|
"containerimage.digest": desc.Digest.String(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
respMu.Unlock()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
for i, dp := range dps {
|
||||||
|
so := *dp.so
|
||||||
|
|
||||||
|
if multiDriver {
|
||||||
|
for i, e := range so.Exports {
|
||||||
|
switch e.Type {
|
||||||
|
case "oci", "tar":
|
||||||
|
return errors.Errorf("%s for multi-node builds currently not supported", e.Type)
|
||||||
|
case "image":
|
||||||
|
if pushNames == "" && e.Attrs["push"] != "" {
|
||||||
|
if ok, _ := strconv.ParseBool(e.Attrs["push"]); ok {
|
||||||
|
pushNames = e.Attrs["name"]
|
||||||
|
if pushNames == "" {
|
||||||
|
return errors.Errorf("tag is needed when pushing to registry")
|
||||||
|
}
|
||||||
|
names, err := toRepoOnly(e.Attrs["name"])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.Attrs["name"] = names
|
||||||
|
e.Attrs["push-by-digest"] = "true"
|
||||||
|
so.Exports[i].Attrs = e.Attrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func(i int, dp driverPair, so client.SolveOpt) {
|
||||||
|
pw := mw.WithPrefix(k, multiTarget)
|
||||||
|
|
||||||
|
c := clients[dp.driverIndex]
|
||||||
|
|
||||||
|
var statusCh chan *client.SolveStatus
|
||||||
|
if pw != nil {
|
||||||
|
pw = progress.ResetTime(pw)
|
||||||
|
statusCh = pw.Status()
|
||||||
|
eg.Go(func() error {
|
||||||
|
<-pw.Done()
|
||||||
|
return pw.Err()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
eg.Go(func() error {
|
||||||
|
defer wg.Done()
|
||||||
|
rr, err := c.Solve(ctx, nil, so, statusCh)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
res[i] = rr
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
}(i, dp, so)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := eg.Wait(); err != nil {
|
if err := eg.Wait(); err != nil {
|
||||||
@ -423,6 +803,8 @@ func notSupported(d driver.Driver, f driver.Feature) error {
|
|||||||
return errors.Errorf("%s feature is currently not supported for %s driver. Please switch to a different driver (eg. \"docker buildx create --use\")", f, d.Factory().Name())
|
return errors.Errorf("%s feature is currently not supported for %s driver. Please switch to a different driver (eg. \"docker buildx create --use\")", f, d.Factory().Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type dockerLoadCallback func(name string) (io.WriteCloser, func(), error)
|
||||||
|
|
||||||
func newDockerLoader(ctx context.Context, d DockerAPI, name string, mw *progress.MultiWriter) (io.WriteCloser, func(), error) {
|
func newDockerLoader(ctx context.Context, d DockerAPI, name string, mw *progress.MultiWriter) (io.WriteCloser, func(), error) {
|
||||||
c, err := d.DockerAPI(name)
|
c, err := d.DockerAPI(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -180,7 +180,7 @@ func buildTargets(ctx context.Context, dockerCli command.Cli, opts map[string]bu
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
pw := progress.NewPrinter(ctx2, os.Stderr, progressMode)
|
pw := progress.NewPrinter(ctx2, os.Stderr, progressMode)
|
||||||
|
|
||||||
_, err = build.Build(ctx, dis, opts, dockerAPI(dockerCli), pw)
|
_, err = build.Build(ctx, dis, opts, dockerAPI(dockerCli), dockerCli.ConfigFile(), pw)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,27 +1,226 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/distribution/reference"
|
||||||
|
"github.com/moby/buildkit/util/appcontext"
|
||||||
|
"github.com/opencontainers/go-digest"
|
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/tonistiigi/buildx/util/imagetools"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
type createOptions struct {
|
type createOptions struct {
|
||||||
files []string
|
files []string
|
||||||
tags []string
|
tags []string
|
||||||
dryrun bool
|
dryrun bool
|
||||||
append bool
|
actionAppend bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCreate(dockerCli command.Cli, in createOptions, args []string) error {
|
func runCreate(dockerCli command.Cli, in createOptions, args []string) error {
|
||||||
return errors.Errorf("not-implemented")
|
if len(args) == 0 && len(in.files) == 0 {
|
||||||
|
return errors.Errorf("no sources specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !in.dryrun && len(in.tags) == 0 {
|
||||||
|
return errors.Errorf("can't push with no tags specified, please set --tag or --dry-run")
|
||||||
|
}
|
||||||
|
|
||||||
|
fileArgs := make([]string, len(in.files))
|
||||||
|
for i, f := range in.files {
|
||||||
|
dt, err := ioutil.ReadFile(f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fileArgs[i] = string(dt)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(fileArgs, args...)
|
||||||
|
|
||||||
|
tags, err := parseRefs(in.tags)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if in.actionAppend && len(in.tags) > 0 {
|
||||||
|
args = append([]string{in.tags[0]}, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
srcs, err := parseSources(args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
repos := map[string]struct{}{}
|
||||||
|
|
||||||
|
for _, t := range tags {
|
||||||
|
repos[t.Name()] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceRefs := false
|
||||||
|
for _, s := range srcs {
|
||||||
|
if s.Ref != nil {
|
||||||
|
repos[s.Ref.Name()] = struct{}{}
|
||||||
|
sourceRefs = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(repos) == 0 {
|
||||||
|
return errors.Errorf("no repositories specified, please set a reference in tag or source")
|
||||||
|
}
|
||||||
|
if len(repos) > 1 {
|
||||||
|
return errors.Errorf("multiple repositories currently not supported, found %v", repos)
|
||||||
|
}
|
||||||
|
|
||||||
|
var repo string
|
||||||
|
for r := range repos {
|
||||||
|
repo = r
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, s := range srcs {
|
||||||
|
if s.Ref == nil && s.Desc.MediaType == "" && s.Desc.Digest != "" {
|
||||||
|
n, err := reference.ParseNormalizedNamed(repo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r, err := reference.WithDigest(n, s.Desc.Digest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
srcs[i].Ref = r
|
||||||
|
sourceRefs = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := appcontext.Context()
|
||||||
|
|
||||||
|
r := imagetools.New(imagetools.Opt{
|
||||||
|
Auth: dockerCli.ConfigFile(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if sourceRefs {
|
||||||
|
eg, ctx2 := errgroup.WithContext(ctx)
|
||||||
|
for i, s := range srcs {
|
||||||
|
if s.Ref == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
func(i int) {
|
||||||
|
eg.Go(func() error {
|
||||||
|
_, desc, err := r.Resolve(ctx2, srcs[i].Ref.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
srcs[i].Ref = nil
|
||||||
|
srcs[i].Desc = desc
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
if err := eg.Wait(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
descs := make([]ocispec.Descriptor, len(srcs))
|
||||||
|
for i := range descs {
|
||||||
|
descs[i] = srcs[i].Desc
|
||||||
|
}
|
||||||
|
|
||||||
|
dt, desc, err := r.Combine(ctx, repo, descs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if in.dryrun {
|
||||||
|
fmt.Printf("%s\n", dt)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// new resolver cause need new auth
|
||||||
|
r = imagetools.New(imagetools.Opt{
|
||||||
|
Auth: dockerCli.ConfigFile(),
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, t := range tags {
|
||||||
|
if err := r.Push(ctx, t, desc, dt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(t.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type src struct {
|
||||||
|
Desc ocispec.Descriptor
|
||||||
|
Ref reference.Named
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSources(in []string) ([]*src, error) {
|
||||||
|
out := make([]*src, len(in))
|
||||||
|
for i, in := range in {
|
||||||
|
s, err := parseSource(in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to parse source %q, valid sources are digests, refereces and descriptors", in)
|
||||||
|
}
|
||||||
|
out[i] = s
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRefs(in []string) ([]reference.Named, error) {
|
||||||
|
refs := make([]reference.Named, len(in))
|
||||||
|
for i, in := range in {
|
||||||
|
n, err := reference.ParseNormalizedNamed(in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
refs[i] = n
|
||||||
|
}
|
||||||
|
return refs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSource(in string) (*src, error) {
|
||||||
|
// source can be a digest, reference or a descriptor JSON
|
||||||
|
dgst, err := digest.Parse(in)
|
||||||
|
if err == nil {
|
||||||
|
return &src{
|
||||||
|
Desc: ocispec.Descriptor{
|
||||||
|
Digest: dgst,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
} else if strings.HasPrefix(in, "sha256") {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ref, err := reference.ParseNormalizedNamed(in)
|
||||||
|
if err == nil {
|
||||||
|
return &src{
|
||||||
|
Ref: ref,
|
||||||
|
}, nil
|
||||||
|
} else if !strings.HasPrefix(in, "{") {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var s src
|
||||||
|
if err := json.Unmarshal([]byte(in), &s.Desc); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
return &s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createCmd(dockerCli command.Cli) *cobra.Command {
|
func createCmd(dockerCli command.Cli) *cobra.Command {
|
||||||
var options createOptions
|
var options createOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "create [OPTIONS] [SOURCE...]",
|
Use: "create [OPTIONS] [SOURCE] [SOURCE...]",
|
||||||
Short: "Create a new image based on source images",
|
Short: "Create a new image based on source images",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runCreate(dockerCli, options, args)
|
return runCreate(dockerCli, options, args)
|
||||||
@ -33,7 +232,7 @@ func createCmd(dockerCli command.Cli) *cobra.Command {
|
|||||||
flags.StringArrayVarP(&options.files, "file", "f", []string{}, "Read source descriptor from file")
|
flags.StringArrayVarP(&options.files, "file", "f", []string{}, "Read source descriptor from file")
|
||||||
flags.StringArrayVarP(&options.tags, "tag", "t", []string{}, "Set reference for new image")
|
flags.StringArrayVarP(&options.tags, "tag", "t", []string{}, "Set reference for new image")
|
||||||
flags.BoolVar(&options.dryrun, "dry-run", false, "Show final image instead of pushing")
|
flags.BoolVar(&options.dryrun, "dry-run", false, "Show final image instead of pushing")
|
||||||
flags.BoolVar(&options.append, "append", false, "Append to existing manifest")
|
flags.BoolVar(&options.actionAppend, "append", false, "Append to existing manifest")
|
||||||
|
|
||||||
_ = flags
|
_ = flags
|
||||||
|
|
||||||
|
@ -25,7 +25,6 @@ var buildkitImage = "moby/buildkit:master" // TODO: make this verified and confi
|
|||||||
type Driver struct {
|
type Driver struct {
|
||||||
driver.InitConfig
|
driver.InitConfig
|
||||||
factory driver.Factory
|
factory driver.Factory
|
||||||
version dockertypes.Version
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) Bootstrap(ctx context.Context, l progress.Logger) error {
|
func (d *Driver) Bootstrap(ctx context.Context, l progress.Logger) error {
|
||||||
|
@ -38,12 +38,7 @@ func (f *factory) New(ctx context.Context, cfg driver.InitConfig) (driver.Driver
|
|||||||
return nil, errors.Errorf("%s driver requires docker API access", f.Name())
|
return nil, errors.Errorf("%s driver requires docker API access", f.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
v, err := cfg.DockerAPI.ServerVersion(ctx)
|
return &Driver{factory: f, InitConfig: cfg}, nil
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(driver.ErrNotConnecting, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Driver{factory: f, InitConfig: cfg, version: v}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *factory) AllowsInstances() bool {
|
func (f *factory) AllowsInstances() bool {
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
dockertypes "github.com/docker/docker/api/types"
|
|
||||||
"github.com/moby/buildkit/client"
|
"github.com/moby/buildkit/client"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/tonistiigi/buildx/driver"
|
"github.com/tonistiigi/buildx/driver"
|
||||||
@ -15,7 +14,6 @@ import (
|
|||||||
type Driver struct {
|
type Driver struct {
|
||||||
factory driver.Factory
|
factory driver.Factory
|
||||||
driver.InitConfig
|
driver.InitConfig
|
||||||
version dockertypes.Version
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) Bootstrap(ctx context.Context, l progress.Logger) error {
|
func (d *Driver) Bootstrap(ctx context.Context, l progress.Logger) error {
|
||||||
|
@ -45,12 +45,7 @@ func (f *factory) New(ctx context.Context, cfg driver.InitConfig) (driver.Driver
|
|||||||
return nil, errors.Errorf("docker driver requires docker API access")
|
return nil, errors.Errorf("docker driver requires docker API access")
|
||||||
}
|
}
|
||||||
|
|
||||||
v, err := cfg.DockerAPI.ServerVersion(ctx)
|
return &Driver{factory: f, InitConfig: cfg}, nil
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(driver.ErrNotConnecting, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Driver{factory: f, InitConfig: cfg, version: v}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *factory) AllowsInstances() bool {
|
func (f *factory) AllowsInstances() bool {
|
||||||
|
@ -72,7 +72,7 @@ func Boot(ctx context.Context, d Driver, pw progress.Writer) (*client.Client, er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := d.Client(ctx)
|
c, err := d.Client(context.TODO())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Cause(err) == ErrNotRunning && try <= 2 {
|
if errors.Cause(err) == ErrNotRunning && try <= 2 {
|
||||||
continue
|
continue
|
||||||
|
212
util/imagetools/create.go
Normal file
212
util/imagetools/create.go
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
package imagetools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/content"
|
||||||
|
"github.com/containerd/containerd/errdefs"
|
||||||
|
"github.com/containerd/containerd/images"
|
||||||
|
"github.com/docker/distribution/reference"
|
||||||
|
"github.com/opencontainers/go-digest"
|
||||||
|
"github.com/opencontainers/image-spec/specs-go"
|
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *Resolver) Combine(ctx context.Context, in string, descs []ocispec.Descriptor) ([]byte, ocispec.Descriptor, error) {
|
||||||
|
ref, err := parseRef(in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ocispec.Descriptor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
eg, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
|
dts := make([][]byte, len(descs))
|
||||||
|
for i := range dts {
|
||||||
|
func(i int) {
|
||||||
|
eg.Go(func() error {
|
||||||
|
dt, err := r.GetDescriptor(ctx, ref.String(), descs[i])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dts[i] = dt
|
||||||
|
|
||||||
|
if descs[i].MediaType == "" {
|
||||||
|
mt, err := detectMediaType(dt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
descs[i].MediaType = mt
|
||||||
|
}
|
||||||
|
|
||||||
|
mt := descs[i].MediaType
|
||||||
|
|
||||||
|
switch mt {
|
||||||
|
case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
|
||||||
|
if descs[i].Platform == nil {
|
||||||
|
cfg, err := r.loadConfig(ctx, in, dt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
descs[i].Platform = &ocispec.Platform{
|
||||||
|
OS: cfg.OS,
|
||||||
|
Architecture: cfg.Architecture,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case images.MediaTypeDockerSchema1Manifest:
|
||||||
|
return errors.Errorf("schema1 manifests are not allowed in manifest lists")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := eg.Wait(); err != nil {
|
||||||
|
return nil, ocispec.Descriptor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// on single source, return original bytes
|
||||||
|
if len(descs) == 1 {
|
||||||
|
if mt := descs[0].MediaType; mt == images.MediaTypeDockerSchema2ManifestList || mt == ocispec.MediaTypeImageIndex {
|
||||||
|
return dts[0], descs[0], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m := map[digest.Digest]int{}
|
||||||
|
newDescs := make([]ocispec.Descriptor, 0, len(descs))
|
||||||
|
|
||||||
|
addDesc := func(d ocispec.Descriptor) {
|
||||||
|
idx, ok := m[d.Digest]
|
||||||
|
if ok {
|
||||||
|
old := newDescs[idx]
|
||||||
|
if old.MediaType == "" {
|
||||||
|
old.MediaType = d.MediaType
|
||||||
|
}
|
||||||
|
if d.Platform != nil {
|
||||||
|
old.Platform = d.Platform
|
||||||
|
}
|
||||||
|
if old.Annotations == nil {
|
||||||
|
old.Annotations = map[string]string{}
|
||||||
|
}
|
||||||
|
for k, v := range d.Annotations {
|
||||||
|
old.Annotations[k] = v
|
||||||
|
}
|
||||||
|
newDescs[idx] = old
|
||||||
|
} else {
|
||||||
|
m[d.Digest] = len(newDescs)
|
||||||
|
newDescs = append(newDescs, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, desc := range descs {
|
||||||
|
switch desc.MediaType {
|
||||||
|
case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
|
||||||
|
var mfst ocispec.Index
|
||||||
|
if err := json.Unmarshal(dts[i], &mfst); err != nil {
|
||||||
|
return nil, ocispec.Descriptor{}, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
for _, d := range mfst.Manifests {
|
||||||
|
addDesc(d)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
addDesc(desc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mt := images.MediaTypeDockerSchema2ManifestList //ocispec.MediaTypeImageIndex
|
||||||
|
idx := struct {
|
||||||
|
// MediaType is reserved in the OCI spec but
|
||||||
|
// excluded from go types.
|
||||||
|
MediaType string `json:"mediaType,omitempty"`
|
||||||
|
|
||||||
|
ocispec.Index
|
||||||
|
}{
|
||||||
|
MediaType: mt,
|
||||||
|
Index: ocispec.Index{
|
||||||
|
Versioned: specs.Versioned{
|
||||||
|
SchemaVersion: 2,
|
||||||
|
},
|
||||||
|
Manifests: newDescs,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
idxBytes, err := json.MarshalIndent(idx, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, ocispec.Descriptor{}, errors.Wrap(err, "failed to marshal index")
|
||||||
|
}
|
||||||
|
|
||||||
|
return idxBytes, ocispec.Descriptor{
|
||||||
|
MediaType: mt,
|
||||||
|
Size: int64(len(idxBytes)),
|
||||||
|
Digest: digest.FromBytes(idxBytes),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resolver) Push(ctx context.Context, ref reference.Named, desc ocispec.Descriptor, dt []byte) error {
|
||||||
|
ref = reference.TagNameOnly(ref)
|
||||||
|
|
||||||
|
p, err := r.r.Pusher(ctx, ref.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cw, err := p.Push(ctx, desc)
|
||||||
|
if err != nil {
|
||||||
|
if errdefs.IsAlreadyExists(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = content.Copy(ctx, cw, bytes.NewReader(dt), desc.Size, desc.Digest)
|
||||||
|
if errdefs.IsAlreadyExists(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resolver) loadConfig(ctx context.Context, in string, dt []byte) (*ocispec.Image, error) {
|
||||||
|
var manifest ocispec.Manifest
|
||||||
|
if err := json.Unmarshal(dt, &manifest); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dt, err := r.GetDescriptor(ctx, in, manifest.Config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var img ocispec.Image
|
||||||
|
if err := json.Unmarshal(dt, &img); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &img, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectMediaType(dt []byte) (string, error) {
|
||||||
|
var mfst struct {
|
||||||
|
MediaType string `json:"mediaType"`
|
||||||
|
Config json.RawMessage `json:"config"`
|
||||||
|
FSLayers []string `json:"fsLayers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(dt, &mfst); err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mfst.MediaType != "" {
|
||||||
|
return mfst.MediaType, nil
|
||||||
|
}
|
||||||
|
if mfst.Config != nil {
|
||||||
|
return images.MediaTypeDockerSchema2Manifest, nil
|
||||||
|
}
|
||||||
|
if len(mfst.FSLayers) > 0 {
|
||||||
|
return images.MediaTypeDockerSchema1Manifest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return images.MediaTypeDockerSchema2ManifestList, nil
|
||||||
|
}
|
@ -35,35 +35,52 @@ func New(opt Opt) *Resolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resolver) Get(ctx context.Context, in string) ([]byte, ocispec.Descriptor, error) {
|
func (r *Resolver) Resolve(ctx context.Context, in string) (string, ocispec.Descriptor, error) {
|
||||||
ref, err := parseRef(in)
|
ref, err := parseRef(in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ocispec.Descriptor{}, err
|
return "", ocispec.Descriptor{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
in, desc, err := r.r.Resolve(ctx, ref.String())
|
in, desc, err := r.r.Resolve(ctx, ref.String())
|
||||||
|
if err != nil {
|
||||||
|
return "", ocispec.Descriptor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return in, desc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resolver) Get(ctx context.Context, in string) ([]byte, ocispec.Descriptor, error) {
|
||||||
|
in, desc, err := r.Resolve(ctx, in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ocispec.Descriptor{}, err
|
return nil, ocispec.Descriptor{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
fetcher, err := r.r.Fetcher(ctx, in)
|
dt, err := r.GetDescriptor(ctx, in, desc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ocispec.Descriptor{}, err
|
return nil, ocispec.Descriptor{}, err
|
||||||
}
|
}
|
||||||
|
return dt, desc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resolver) GetDescriptor(ctx context.Context, in string, desc ocispec.Descriptor) ([]byte, error) {
|
||||||
|
fetcher, err := r.r.Fetcher(ctx, in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
rc, err := fetcher.Fetch(ctx, desc)
|
rc, err := fetcher.Fetch(ctx, desc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ocispec.Descriptor{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
_, err = io.Copy(buf, rc)
|
_, err = io.Copy(buf, rc)
|
||||||
rc.Close()
|
rc.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ocispec.Descriptor{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return buf.Bytes(), desc, nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseRef(s string) (reference.Named, error) {
|
func parseRef(s string) (reference.Named, error) {
|
||||||
|
@ -3,14 +3,17 @@ package progress
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/moby/buildkit/client"
|
"github.com/moby/buildkit/client"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MultiWriter struct {
|
type MultiWriter struct {
|
||||||
w Writer
|
w Writer
|
||||||
eg *errgroup.Group
|
eg *errgroup.Group
|
||||||
|
once sync.Once
|
||||||
|
ready chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mw *MultiWriter) WithPrefix(pfx string, force bool) Writer {
|
func (mw *MultiWriter) WithPrefix(pfx string, force bool) Writer {
|
||||||
@ -21,6 +24,9 @@ func (mw *MultiWriter) WithPrefix(pfx string, force bool) Writer {
|
|||||||
in: in,
|
in: in,
|
||||||
}
|
}
|
||||||
mw.eg.Go(func() error {
|
mw.eg.Go(func() error {
|
||||||
|
mw.once.Do(func() {
|
||||||
|
close(mw.ready)
|
||||||
|
})
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case v, ok := <-in:
|
case v, ok := <-in:
|
||||||
@ -77,14 +83,18 @@ func NewMultiWriter(pw Writer) *MultiWriter {
|
|||||||
}
|
}
|
||||||
eg, _ := errgroup.WithContext(context.TODO())
|
eg, _ := errgroup.WithContext(context.TODO())
|
||||||
|
|
||||||
|
ready := make(chan struct{})
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
<-ready
|
||||||
eg.Wait()
|
eg.Wait()
|
||||||
close(pw.Status())
|
close(pw.Status())
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return &MultiWriter{
|
return &MultiWriter{
|
||||||
w: pw,
|
w: pw,
|
||||||
eg: eg,
|
eg: eg,
|
||||||
|
ready: ready,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
package progress
|
package progress
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/moby/buildkit/client"
|
"github.com/moby/buildkit/client"
|
||||||
|
"github.com/moby/buildkit/identity"
|
||||||
|
"github.com/opencontainers/go-digest"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Writer interface {
|
type Writer interface {
|
||||||
@ -9,3 +13,31 @@ type Writer interface {
|
|||||||
Err() error
|
Err() error
|
||||||
Status() chan *client.SolveStatus
|
Status() chan *client.SolveStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Write(w Writer, name string, f func() error) {
|
||||||
|
status := w.Status()
|
||||||
|
dgst := digest.FromBytes([]byte(identity.NewID()))
|
||||||
|
tm := time.Now()
|
||||||
|
|
||||||
|
vtx := client.Vertex{
|
||||||
|
Digest: dgst,
|
||||||
|
Name: name,
|
||||||
|
Started: &tm,
|
||||||
|
}
|
||||||
|
|
||||||
|
status <- &client.SolveStatus{
|
||||||
|
Vertexes: []*client.Vertex{&vtx},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := f()
|
||||||
|
|
||||||
|
tm2 := time.Now()
|
||||||
|
vtx2 := vtx
|
||||||
|
vtx2.Completed = &tm2
|
||||||
|
if err != nil {
|
||||||
|
vtx2.Error = err.Error()
|
||||||
|
}
|
||||||
|
status <- &client.SolveStatus{
|
||||||
|
Vertexes: []*client.Vertex{&vtx2},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
8
vendor/modules.txt
vendored
8
vendor/modules.txt
vendored
@ -27,13 +27,13 @@ github.com/beorn7/perks/quantile
|
|||||||
# github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50
|
# github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50
|
||||||
github.com/containerd/console
|
github.com/containerd/console
|
||||||
# github.com/containerd/containerd v1.3.0-0.20190321141026-ceba56893a76
|
# github.com/containerd/containerd v1.3.0-0.20190321141026-ceba56893a76
|
||||||
github.com/containerd/containerd/platforms
|
|
||||||
github.com/containerd/containerd/images
|
github.com/containerd/containerd/images
|
||||||
|
github.com/containerd/containerd/platforms
|
||||||
|
github.com/containerd/containerd/content
|
||||||
|
github.com/containerd/containerd/errdefs
|
||||||
github.com/containerd/containerd/remotes
|
github.com/containerd/containerd/remotes
|
||||||
github.com/containerd/containerd/remotes/docker
|
github.com/containerd/containerd/remotes/docker
|
||||||
github.com/containerd/containerd/errdefs
|
|
||||||
github.com/containerd/containerd/log
|
github.com/containerd/containerd/log
|
||||||
github.com/containerd/containerd/content
|
|
||||||
github.com/containerd/containerd/content/local
|
github.com/containerd/containerd/content/local
|
||||||
github.com/containerd/containerd/labels
|
github.com/containerd/containerd/labels
|
||||||
github.com/containerd/containerd/reference
|
github.com/containerd/containerd/reference
|
||||||
@ -98,6 +98,7 @@ github.com/davecgh/go-spew/spew
|
|||||||
# github.com/docker/cli v0.0.0-20190321234815-f40f9c240ab0 => github.com/tiborvass/cli v0.0.0-20190419012645-1ed02c40fe68
|
# github.com/docker/cli v0.0.0-20190321234815-f40f9c240ab0 => github.com/tiborvass/cli v0.0.0-20190419012645-1ed02c40fe68
|
||||||
github.com/docker/cli/cli/compose/loader
|
github.com/docker/cli/cli/compose/loader
|
||||||
github.com/docker/cli/cli/compose/types
|
github.com/docker/cli/cli/compose/types
|
||||||
|
github.com/docker/cli/cli/config/types
|
||||||
github.com/docker/cli/cli-plugins/manager
|
github.com/docker/cli/cli-plugins/manager
|
||||||
github.com/docker/cli/cli-plugins/plugin
|
github.com/docker/cli/cli-plugins/plugin
|
||||||
github.com/docker/cli/cli/command
|
github.com/docker/cli/cli/command
|
||||||
@ -106,7 +107,6 @@ github.com/docker/cli/cli
|
|||||||
github.com/docker/cli/cli/config
|
github.com/docker/cli/cli/config
|
||||||
github.com/docker/cli/cli/context/docker
|
github.com/docker/cli/cli/context/docker
|
||||||
github.com/docker/cli/opts
|
github.com/docker/cli/opts
|
||||||
github.com/docker/cli/cli/config/types
|
|
||||||
github.com/docker/cli/cli/compose/interpolation
|
github.com/docker/cli/cli/compose/interpolation
|
||||||
github.com/docker/cli/cli/compose/schema
|
github.com/docker/cli/cli/compose/schema
|
||||||
github.com/docker/cli/cli/compose/template
|
github.com/docker/cli/cli/compose/template
|
||||||
|
Loading…
x
Reference in New Issue
Block a user