浏览代码

Implement simple ACI run

Djordje Lukic 5 年之前
父节点
当前提交
3d363643ad
共有 11 个文件被更改,包括 816 次插入42 次删除
  1. 458 0
      azure/aci.go
  2. 20 1
      azure/backend.go
  3. 67 0
      cli/cmd/run.go
  4. 38 0
      cli/cmd/run_opts.go
  5. 8 7
      cli/main.go
  6. 155 0
      compose/project.go
  7. 12 0
      containers/api.go
  8. 6 0
      example/backend.go
  9. 8 3
      go.mod
  10. 34 29
      go.sum
  11. 10 2
      server/proxy/proxy.go

+ 458 - 0
azure/aci.go

@@ -0,0 +1,458 @@
+package azure
+
+import (
+	"bufio"
+	"context"
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"os/signal"
+	"runtime"
+	"strings"
+
+	"github.com/compose-spec/compose-go/types"
+	"github.com/docker/api/compose"
+	"github.com/docker/api/context/store"
+	"github.com/sirupsen/logrus"
+
+	"github.com/gobwas/ws"
+	"github.com/gobwas/ws/wsutil"
+
+	"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance"
+	"github.com/Azure/azure-sdk-for-go/services/keyvault/auth"
+	"github.com/Azure/go-autorest/autorest"
+	"github.com/Azure/go-autorest/autorest/to"
+
+	tm "github.com/buger/goterm"
+)
+
+const (
+	AzureFileDriverName            = "azure_file"
+	VolumeDriveroptsShareNameKey   = "share_name"
+	VolumeDriveroptsAccountNameKey = "storage_account_name"
+	VolumeDriveroptsAccountKeyKey  = "storage_account_key"
+)
+const singleContainerName = "single--container--aci"
+
+func CreateACIContainers(ctx context.Context, project compose.Project, aciContext store.AciContext) (c containerinstance.ContainerGroup, err error) {
+	containerGroupsClient, err := getContainerGroupsClient(aciContext.SubscriptionID)
+	if err != nil {
+		return c, fmt.Errorf("cannot get container group client: %v", err)
+	}
+
+	groupDefinition, err := convert(project, aciContext)
+	if err != nil {
+		return c, err
+	}
+
+	// Check if the container group already exists
+	_, err = containerGroupsClient.Get(ctx, aciContext.ResourceGroup, *groupDefinition.Name)
+	if err != nil {
+		if err, ok := err.(autorest.DetailedError); ok {
+			if err.StatusCode != http.StatusNotFound {
+				return c, err
+			}
+		} else {
+			return c, err
+		}
+	} else {
+		return c, fmt.Errorf("Container group %q already exists", *groupDefinition.Name)
+	}
+
+	future, err := containerGroupsClient.CreateOrUpdate(
+		ctx,
+		aciContext.ResourceGroup,
+		*groupDefinition.Name,
+		groupDefinition,
+	)
+
+	if err != nil {
+		return c, err
+	}
+
+	err = future.WaitForCompletionRef(ctx, containerGroupsClient.Client)
+	if err != nil {
+		return c, err
+	}
+	containerGroup, err := future.Result(containerGroupsClient)
+	if err != nil {
+		return c, err
+	}
+
+	if len(project.Services) > 1 {
+		var commands []string
+		for _, service := range project.Services {
+			commands = append(commands, fmt.Sprintf("echo 127.0.0.1 %s >> /etc/hosts", service.Name))
+		}
+		commands = append(commands, "exit")
+
+		response, err := ExecACIContainer(ctx, "/bin/sh", project.Name, project.Services[0].Name, aciContext)
+		if err != nil {
+			return c, err
+		}
+
+		err = ExecWebSocketLoopWithCmd(
+			ctx,
+			*response.WebSocketURI,
+			*response.Password,
+			commands,
+			false)
+		if err != nil {
+			return containerinstance.ContainerGroup{}, err
+		}
+	}
+
+	return containerGroup, err
+}
+
+type ProjectAciHelper compose.Project
+
+func (p ProjectAciHelper) getAciSecretVolumes() ([]containerinstance.Volume, error) {
+	var secretVolumes []containerinstance.Volume
+	for secretName, filepathToRead := range p.Secrets {
+		var data []byte
+		if strings.HasPrefix(filepathToRead.File, compose.SecretInlineMark) {
+			data = []byte(filepathToRead.File[len(compose.SecretInlineMark):])
+		} else {
+			var err error
+			data, err = ioutil.ReadFile(filepathToRead.File)
+			if err != nil {
+				return secretVolumes, err
+			}
+		}
+		if len(data) == 0 {
+			continue
+		}
+		dataStr := base64.StdEncoding.EncodeToString(data)
+		secretVolumes = append(secretVolumes, containerinstance.Volume{
+			Name: to.StringPtr(secretName),
+			Secret: map[string]*string{
+				secretName: &dataStr,
+			},
+		})
+	}
+	return secretVolumes, nil
+}
+
+func (p ProjectAciHelper) getAciFileVolumes() (map[string]bool, []containerinstance.Volume, error) {
+	azureFileVolumesMap := make(map[string]bool, len(p.Volumes))
+	var azureFileVolumesSlice []containerinstance.Volume
+	for name, v := range p.Volumes {
+		if v.Driver == AzureFileDriverName {
+			shareName, ok := v.DriverOpts[VolumeDriveroptsShareNameKey]
+			if !ok {
+				return nil, nil, fmt.Errorf("cannot retrieve share name for Azurefile")
+			}
+			accountName, ok := v.DriverOpts[VolumeDriveroptsAccountNameKey]
+			if !ok {
+				return nil, nil, fmt.Errorf("cannot retrieve account name for Azurefile")
+			}
+			accountKey, ok := v.DriverOpts[VolumeDriveroptsAccountKeyKey]
+			if !ok {
+				return nil, nil, fmt.Errorf("cannot retrieve account key for Azurefile")
+			}
+			aciVolume := containerinstance.Volume{
+				Name: to.StringPtr(name),
+				AzureFile: &containerinstance.AzureFileVolume{
+					ShareName:          to.StringPtr(shareName),
+					StorageAccountName: to.StringPtr(accountName),
+					StorageAccountKey:  to.StringPtr(accountKey),
+				},
+			}
+			azureFileVolumesMap[name] = true
+			azureFileVolumesSlice = append(azureFileVolumesSlice, aciVolume)
+		}
+	}
+	return azureFileVolumesMap, azureFileVolumesSlice, nil
+}
+
+type ServiceConfigAciHelper types.ServiceConfig
+
+func (s ServiceConfigAciHelper) getAciFileVolumeMounts(volumesCache map[string]bool) ([]containerinstance.VolumeMount, error) {
+	var aciServiceVolumes []containerinstance.VolumeMount
+	for _, sv := range s.Volumes {
+		if !volumesCache[sv.Source] {
+			return []containerinstance.VolumeMount{}, fmt.Errorf("could not find volume source %q", sv.Source)
+		}
+		aciServiceVolumes = append(aciServiceVolumes, containerinstance.VolumeMount{
+			Name:      to.StringPtr(sv.Source),
+			MountPath: to.StringPtr(sv.Target),
+		})
+	}
+	return aciServiceVolumes, nil
+}
+
+func (s ServiceConfigAciHelper) getAciSecretsVolumeMounts() []containerinstance.VolumeMount {
+	var secretVolumeMounts []containerinstance.VolumeMount
+	for _, secret := range s.Secrets {
+		secretsMountPath := "/run/secrets"
+		if secret.Target == "" {
+			secret.Target = secret.Source
+		}
+		// Specifically use "/" here and not filepath.Join() to avoid windows path being sent and used inside containers
+		secretsMountPath = secretsMountPath + "/" + secret.Target
+		vmName := strings.Split(secret.Source, "=")[0]
+		vm := containerinstance.VolumeMount{
+			Name:      to.StringPtr(vmName),
+			MountPath: to.StringPtr(secretsMountPath),
+			ReadOnly:  to.BoolPtr(true), // TODO Confirm if the secrets are read only
+		}
+		secretVolumeMounts = append(secretVolumeMounts, vm)
+	}
+	return secretVolumeMounts
+}
+
+func (s ServiceConfigAciHelper) getAciContainer(volumesCache map[string]bool) (containerinstance.Container, error) {
+	secretVolumeMounts := s.getAciSecretsVolumeMounts()
+	aciServiceVolumes, err := s.getAciFileVolumeMounts(volumesCache)
+	if err != nil {
+		return containerinstance.Container{}, err
+	}
+	allVolumes := append(aciServiceVolumes, secretVolumeMounts...)
+	var volumes *[]containerinstance.VolumeMount
+	if len(allVolumes) == 0 {
+		volumes = nil
+	} else {
+		volumes = &allVolumes
+	}
+	return containerinstance.Container{
+		Name: to.StringPtr(s.Name),
+		ContainerProperties: &containerinstance.ContainerProperties{
+			Image: to.StringPtr(s.Image),
+			Resources: &containerinstance.ResourceRequirements{
+				Limits: &containerinstance.ResourceLimits{
+					MemoryInGB: to.Float64Ptr(1),
+					CPU:        to.Float64Ptr(1),
+				},
+				Requests: &containerinstance.ResourceRequests{
+					MemoryInGB: to.Float64Ptr(1),
+					CPU:        to.Float64Ptr(1),
+				},
+			},
+			VolumeMounts: volumes,
+		},
+	}, nil
+}
+
+// ListACIContainers List available containers
+func ListACIContainers(aciContext store.AciContext) (c []containerinstance.ContainerGroup, err error) {
+	ctx := context.TODO()
+	containerGroupsClient, err := getContainerGroupsClient(aciContext.SubscriptionID)
+	if err != nil {
+		return c, fmt.Errorf("cannot get container group client: %v", err)
+	}
+
+	var containers []containerinstance.ContainerGroup
+	result, err := containerGroupsClient.ListByResourceGroup(ctx, aciContext.ResourceGroup)
+	if err != nil {
+		return []containerinstance.ContainerGroup{}, err
+	}
+	for result.NotDone() {
+		containers = append(containers, result.Values()...)
+		if err := result.NextWithContext(ctx); err != nil {
+			return []containerinstance.ContainerGroup{}, err
+		}
+	}
+
+	return containers, err
+}
+
+func ExecACIContainer(ctx context.Context, command, containerGroup string, containerName string, aciContext store.AciContext) (c containerinstance.ContainerExecResponse, err error) {
+	containerClient := getContainerClient(aciContext.SubscriptionID)
+	rows, cols := getTermSize()
+	containerExecRequest := containerinstance.ContainerExecRequest{
+		Command: to.StringPtr(command),
+		TerminalSize: &containerinstance.ContainerExecRequestTerminalSize{
+			Rows: rows,
+			Cols: cols,
+		},
+	}
+	return containerClient.ExecuteCommand(
+		ctx,
+		aciContext.ResourceGroup,
+		containerGroup,
+		containerName,
+		containerExecRequest)
+}
+
+func getTermSize() (*int32, *int32) {
+	rows := tm.Height()
+	cols := tm.Width()
+	return to.Int32Ptr(int32(rows)), to.Int32Ptr(int32(cols))
+}
+
+func ExecWebSocketLoop(ctx context.Context, wsURL, passwd string) error {
+	return ExecWebSocketLoopWithCmd(ctx, wsURL, passwd, []string{}, true)
+}
+
+func ExecWebSocketLoopWithCmd(ctx context.Context, wsURL, passwd string, commands []string, outputEnabled bool) error {
+	ctx, cancel := context.WithCancel(ctx)
+	conn, _, _, err := ws.DefaultDialer.Dial(ctx, wsURL)
+	if err != nil {
+		cancel()
+		return err
+	}
+	err = wsutil.WriteClientMessage(conn, ws.OpText, []byte(passwd))
+	if err != nil {
+		cancel()
+		return err
+	}
+	lastCommandLen := 0
+	done := make(chan struct{})
+	go func() {
+		defer close(done)
+		for {
+			msg, _, err := wsutil.ReadServerData(conn)
+			if err != nil {
+				if err != io.EOF {
+					fmt.Printf("read error: %s\n", err)
+				}
+				return
+			}
+			lines := strings.Split(string(msg), "\n")
+			lastCommandLen = len(lines[len(lines)-1])
+			if outputEnabled {
+				fmt.Printf("%s", msg)
+			}
+		}
+	}()
+	interrupt := make(chan os.Signal, 1)
+	signal.Notify(interrupt, os.Interrupt)
+	scanner := bufio.NewScanner(os.Stdin)
+	rc := make(chan string, 10)
+	if len(commands) > 0 {
+		for _, command := range commands {
+			rc <- command
+		}
+	}
+	go func() {
+		for {
+			if !scanner.Scan() {
+				close(done)
+				cancel()
+				fmt.Println("exiting...")
+				break
+			}
+			t := scanner.Text()
+			rc <- t
+			cleanLastCommand(lastCommandLen)
+		}
+	}()
+	for {
+		select {
+		case <-done:
+			return nil
+		case line := <-rc:
+			err = wsutil.WriteClientMessage(conn, ws.OpText, []byte(line+"\n"))
+			if err != nil {
+				fmt.Println("write: ", err)
+				return nil
+			}
+		case <-interrupt:
+			fmt.Println("interrupted...")
+			close(done)
+			cancel()
+			return nil
+		}
+	}
+}
+
+func convert(p compose.Project, aciContext store.AciContext) (containerinstance.ContainerGroup, error) {
+	project := ProjectAciHelper(p)
+	containerGroupName := strings.ToLower(project.Name)
+	volumesCache, volumesSlice, err := project.getAciFileVolumes()
+	if err != nil {
+		return containerinstance.ContainerGroup{}, err
+	}
+	secretVolumes, err := project.getAciSecretVolumes()
+	if err != nil {
+		return containerinstance.ContainerGroup{}, err
+	}
+	allVolumes := append(volumesSlice, secretVolumes...)
+	var volumes *[]containerinstance.Volume
+	if len(allVolumes) == 0 {
+		volumes = nil
+	} else {
+		volumes = &allVolumes
+	}
+	var containers []containerinstance.Container
+	groupDefinition := containerinstance.ContainerGroup{
+		Name:     &containerGroupName,
+		Location: &aciContext.Location,
+		ContainerGroupProperties: &containerinstance.ContainerGroupProperties{
+			OsType:     containerinstance.Linux,
+			Containers: &containers,
+			Volumes:    volumes,
+		},
+	}
+
+	for _, s := range project.Services {
+		service := ServiceConfigAciHelper(s)
+		if s.Name != singleContainerName {
+			logrus.Debugf("Adding %q\n", service.Name)
+		}
+		containerDefinition, err := service.getAciContainer(volumesCache)
+		if err != nil {
+			return containerinstance.ContainerGroup{}, err
+		}
+		if service.Ports != nil {
+			var containerPorts []containerinstance.ContainerPort
+			var groupPorts []containerinstance.Port
+			for _, portConfig := range service.Ports {
+				if portConfig.Published != 0 && portConfig.Published != portConfig.Target {
+					msg := fmt.Sprintf("Port mapping is not supported with ACI, cannot map port %d to %d for container %s",
+						portConfig.Published, portConfig.Target, service.Name)
+					return groupDefinition, errors.New(msg)
+				}
+				portNumber := int32(portConfig.Target)
+				containerPorts = append(containerPorts, containerinstance.ContainerPort{
+					Port: to.Int32Ptr(portNumber),
+				})
+				groupPorts = append(groupPorts, containerinstance.Port{
+					Port:     to.Int32Ptr(portNumber),
+					Protocol: containerinstance.TCP,
+				})
+			}
+			containerDefinition.ContainerProperties.Ports = &containerPorts
+			groupDefinition.ContainerGroupProperties.IPAddress = &containerinstance.IPAddress{
+				Type:  containerinstance.Public,
+				Ports: &groupPorts,
+			}
+		}
+
+		containers = append(containers, containerDefinition)
+	}
+	groupDefinition.ContainerGroupProperties.Containers = &containers
+	return groupDefinition, nil
+}
+
+func cleanLastCommand(lastCommandLen int) {
+	tm.MoveCursorUp(1)
+	tm.MoveCursorForward(lastCommandLen)
+	if runtime.GOOS != "windows" {
+		for i := 0; i < tm.Width(); i++ {
+			_, _ = tm.Print(" ")
+		}
+		tm.MoveCursorUp(1)
+	}
+
+	tm.Flush()
+}
+
+func getContainerGroupsClient(subscriptionID string) (containerinstance.ContainerGroupsClient, error) {
+	auth, _ := auth.NewAuthorizerFromCLI()
+	containerGroupsClient := containerinstance.NewContainerGroupsClient(subscriptionID)
+	containerGroupsClient.Authorizer = auth
+	return containerGroupsClient, nil
+}
+
+func getContainerClient(subscriptionID string) containerinstance.ContainerClient {
+	auth, _ := auth.NewAuthorizerFromCLI()
+	containerClient := containerinstance.NewContainerClient(subscriptionID)
+	containerClient.Authorizer = auth
+	return containerClient
+}

+ 20 - 1
azure/backend.go

@@ -5,12 +5,16 @@ import (
 
 	"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance"
 	"github.com/Azure/go-autorest/autorest/azure/auth"
+	"github.com/compose-spec/compose-go/types"
 	"github.com/pkg/errors"
+	"github.com/sirupsen/logrus"
+
+	"github.com/docker/api/context/store"
 
 	"github.com/docker/api/backend"
+	"github.com/docker/api/compose"
 	"github.com/docker/api/containers"
 	apicontext "github.com/docker/api/context"
-	"github.com/docker/api/context/store"
 )
 
 type containerService struct {
@@ -86,3 +90,18 @@ func (cs *containerService) List(ctx context.Context) ([]containers.Container, e
 
 	return res, nil
 }
+
+func (cs *containerService) Run(ctx context.Context, r containers.ContainerConfig) error {
+	var project compose.Project
+	project.Name = r.ID
+	project.Services = []types.ServiceConfig{
+		{
+			Name:  r.ID,
+			Image: r.Image,
+		},
+	}
+
+	logrus.Debugf("Running container %q with name %q\n", r.Image, r.ID)
+	_, err := CreateACIContainers(ctx, project, cs.ctx)
+	return err
+}

+ 67 - 0
cli/cmd/run.go

@@ -0,0 +1,67 @@
+/*
+	Copyright (c) 2020 Docker Inc.
+
+	Permission is hereby granted, free of charge, to any person
+	obtaining a copy of this software and associated documentation
+	files (the "Software"), to deal in the Software without
+	restriction, including without limitation the rights to use, copy,
+	modify, merge, publish, distribute, sublicense, and/or sell copies
+	of the Software, and to permit persons to whom the Software is
+	furnished to do so, subject to the following conditions:
+
+	The above copyright notice and this permission notice shall be
+	included in all copies or substantial portions of the Software.
+
+	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+	EXPRESS OR IMPLIED,
+	INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+	IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+	HOLDERS BE LIABLE FOR ANY CLAIM,
+	DAMAGES OR OTHER LIABILITY,
+	WHETHER IN AN ACTION OF CONTRACT,
+	TORT OR OTHERWISE,
+	ARISING FROM, OUT OF OR IN CONNECTION WITH
+	THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+package cmd
+
+import (
+	"context"
+
+	"github.com/docker/api/client"
+	"github.com/google/uuid"
+	"github.com/spf13/cobra"
+)
+
+func RunCommand() *cobra.Command {
+	var opts runOpts
+	cmd := &cobra.Command{
+		Use:   "run",
+		Short: "Run a container",
+		Args:  cobra.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runRun(cmd.Context(), args[0], opts)
+		},
+	}
+
+	cmd.Flags().StringArrayVarP(&opts.publish, "publish", "p", []string{}, "Publish a container's port(s)")
+	cmd.Flags().StringVar(&opts.name, "name", uuid.New().String(), "Assign a name to the container")
+
+	return cmd
+}
+
+func runRun(ctx context.Context, image string, opts runOpts) error {
+	c, err := client.New(ctx)
+	if err != nil {
+		return err
+	}
+
+	project, err := opts.ToContainerConfig(image)
+	if err != nil {
+		return err
+	}
+
+	return c.ContainerService().Run(ctx, project)
+}

+ 38 - 0
cli/cmd/run_opts.go

@@ -0,0 +1,38 @@
+package cmd
+
+import (
+	"strings"
+
+	"github.com/docker/api/containers"
+)
+
+type runOpts struct {
+	name    string
+	publish []string
+}
+
+func toPorts(ports []string) ([]containers.Port, error) {
+	var result []containers.Port
+
+	for _, port := range ports {
+		parts := strings.Split(port, ":")
+		result = append(result, containers.Port{
+			Source:      parts[0],
+			Destination: parts[1],
+		})
+	}
+	return result, nil
+}
+
+func (r *runOpts) ToContainerConfig(image string) (containers.ContainerConfig, error) {
+	publish, err := toPorts(r.publish)
+	if err != nil {
+		return containers.ContainerConfig{}, err
+	}
+
+	return containers.ContainerConfig{
+		ID:    r.name,
+		Image: image,
+		Ports: publish,
+	}, nil
+}

+ 8 - 7
cli/main.go

@@ -93,6 +93,14 @@ func main() {
 		},
 	}
 
+	root.AddCommand(
+		cmd.ContextCommand(),
+		&cmd.PsCommand,
+		cmd.ServeCommand(),
+		&cmd.ExampleCommand,
+		cmd.RunCommand(),
+	)
+
 	helpFunc := root.HelpFunc()
 	root.SetHelpFunc(func(cmd *cobra.Command, args []string) {
 		if !isOwnCommand(cmd) {
@@ -110,13 +118,6 @@ func main() {
 		logrus.SetLevel(logrus.DebugLevel)
 	}
 
-	root.AddCommand(
-		cmd.ContextCommand(),
-		&cmd.PsCommand,
-		cmd.ServeCommand(),
-		&cmd.ExampleCommand,
-	)
-
 	ctx, cancel := util.NewSigContext()
 	defer cancel()
 

+ 155 - 0
compose/project.go

@@ -0,0 +1,155 @@
+package compose
+
+import (
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"regexp"
+	"strings"
+
+	"github.com/compose-spec/compose-go/loader"
+	"github.com/compose-spec/compose-go/types"
+	"github.com/sirupsen/logrus"
+)
+
+const (
+	SecretInlineMark = "inline:"
+)
+
+var SupportedFilenames = []string{
+	"compose.yml",
+	"compose.yaml",
+	"docker-compose.yml",
+	"docker-compose.yaml",
+}
+
+type ProjectOptions struct {
+	Name        string
+	WorkDir     string
+	ConfigPaths []string
+	Environment []string
+}
+
+type Project struct {
+	types.Config
+	projectDir string
+	Name       string `yaml:"-" json:"-"`
+}
+
+// ProjectFromOptions load a compose project based on given options
+func ProjectFromOptions(options *ProjectOptions) (*Project, error) {
+	configPath, err := getConfigPathFromOptions(options)
+	if err != nil {
+		return nil, err
+	}
+	name := options.Name
+	if name == "" {
+		r := regexp.MustCompile(`[^a-z0-9\\-_]+`)
+		name = r.ReplaceAllString(strings.ToLower(filepath.Base(options.WorkDir)), "")
+	}
+	configs, err := parseConfigs(configPath)
+	if err != nil {
+		return nil, err
+	}
+	return newProject(types.ConfigDetails{
+		WorkingDir:  options.WorkDir,
+		ConfigFiles: configs,
+		Environment: getAsEqualsMap(options.Environment),
+	}, name)
+}
+
+func newProject(config types.ConfigDetails, name string) (*Project, error) {
+	model, err := loader.Load(config)
+	if err != nil {
+		return nil, err
+	}
+
+	p := Project{
+		Config:     *model,
+		projectDir: config.WorkingDir,
+		Name:       name,
+	}
+	return &p, nil
+}
+
+func getConfigPathFromOptions(options *ProjectOptions) ([]string, error) {
+	var paths []string
+	pwd := options.WorkDir
+
+	if len(options.ConfigPaths) != 0 {
+		for _, f := range options.ConfigPaths {
+			if f == "-" {
+				paths = append(paths, f)
+				continue
+			}
+			if !filepath.IsAbs(f) {
+				f = filepath.Join(pwd, f)
+			}
+			if _, err := os.Stat(f); err != nil {
+				return nil, err
+			}
+			paths = append(paths, f)
+		}
+		return paths, nil
+	}
+
+	for {
+		var candidates []string
+		for _, n := range SupportedFilenames {
+			f := filepath.Join(pwd, n)
+			if _, err := os.Stat(f); err == nil {
+				candidates = append(candidates, f)
+			}
+		}
+		if len(candidates) > 0 {
+			winner := candidates[0]
+			if len(candidates) > 1 {
+				logrus.Warnf("Found multiple config files with supported names: %s", strings.Join(candidates, ", "))
+				logrus.Warnf("Using %s\n", winner)
+			}
+			return []string{winner}, nil
+		}
+		parent := filepath.Dir(pwd)
+		if parent == pwd {
+			return nil, fmt.Errorf("Can't find a suitable configuration file in this directory or any parent. Is %q the right directory?", pwd)
+		}
+		pwd = parent
+	}
+}
+
+func parseConfigs(configPaths []string) ([]types.ConfigFile, error) {
+	var files []types.ConfigFile
+	for _, f := range configPaths {
+		var b []byte
+		var err error
+		if f == "-" {
+			return []types.ConfigFile{}, errors.New("reading compose file from stdin is not supported")
+		} else {
+			if _, err := os.Stat(f); err != nil {
+				return nil, err
+			}
+			b, err = ioutil.ReadFile(f)
+		}
+		if err != nil {
+			return nil, err
+		}
+		config, err := loader.ParseYAML(b)
+		if err != nil {
+			return nil, err
+		}
+		files = append(files, types.ConfigFile{Filename: f, Config: config})
+	}
+	return files, nil
+}
+
+// getAsEqualsMap split key=value formatted strings into a key : value map
+func getAsEqualsMap(em []string) map[string]string {
+	m := make(map[string]string)
+	for _, v := range em {
+		kv := strings.SplitN(v, "=", 2)
+		m[kv[0]] = kv[1]
+	}
+	return m
+}

+ 12 - 0
containers/api.go

@@ -17,6 +17,18 @@ type Container struct {
 	Labels      []string
 }
 
+type Port struct {
+	Source      string
+	Destination string
+}
+
+type ContainerConfig struct {
+	ID    string
+	Image string
+	Ports []Port
+}
+
 type ContainerService interface {
 	List(context.Context) ([]Container, error)
+	Run(context.Context, ContainerConfig) error
 }

+ 6 - 0
example/backend.go

@@ -2,6 +2,7 @@ package example
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/docker/api/backend"
 	"github.com/docker/api/containers"
@@ -31,3 +32,8 @@ func (cs *containerService) List(ctx context.Context) ([]containers.Container, e
 		},
 	}, nil
 }
+
+func (cs *containerService) Run(ctx context.Context, r containers.ContainerConfig) error {
+	fmt.Printf("Running container %q with name %q\n", r.Image, r.ID)
+	return nil
+}

+ 8 - 3
go.mod

@@ -4,11 +4,17 @@ go 1.13
 
 require (
 	github.com/Azure/azure-sdk-for-go v42.0.0+incompatible
-	github.com/Azure/go-autorest/autorest v0.10.0 // indirect
+	github.com/Azure/go-autorest/autorest v0.10.0
 	github.com/Azure/go-autorest/autorest/azure/auth v0.4.2
-	github.com/Azure/go-autorest/autorest/to v0.3.0 // indirect
+	github.com/Azure/go-autorest/autorest/to v0.3.0
 	github.com/Azure/go-autorest/autorest/validation v0.2.0 // indirect
+	github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129
+	github.com/compose-spec/compose-go v0.0.0-20200423124427-63dcf8c22cae
+	github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect
+	github.com/gobwas/pool v0.2.0 // indirect
+	github.com/gobwas/ws v1.0.3
 	github.com/golang/protobuf v1.4.0
+	github.com/google/uuid v1.1.1
 	github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/opencontainers/go-digest v1.0.0-rc1
@@ -22,5 +28,4 @@ require (
 	golang.org/x/text v0.3.2 // indirect
 	google.golang.org/grpc v1.29.1
 	google.golang.org/protobuf v1.21.0
-	gopkg.in/yaml.v2 v2.2.8 // indirect
 )

+ 34 - 29
go.sum

@@ -2,13 +2,11 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
 github.com/Azure/azure-sdk-for-go v42.0.0+incompatible h1:yz6sFf5bHZ+gEOQVuK5JhPqTTAmv+OvSLSaqgzqaCwY=
 github.com/Azure/azure-sdk-for-go v42.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
-github.com/Azure/go-autorest/autorest v0.9.3 h1:OZEIaBbMdUE/Js+BQKlpO81XlISgipr6yDJ+PSwsgi4=
 github.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8GNUx2nRB378IPt/1p0=
 github.com/Azure/go-autorest/autorest v0.10.0 h1:mvdtztBqcL8se7MdrUweNieTNi4kfNG6GOJuurQJpuY=
 github.com/Azure/go-autorest/autorest v0.10.0/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630=
 github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
 github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc=
-github.com/Azure/go-autorest/autorest/adal v0.8.1 h1:pZdL8o72rK+avFWl+p9nE8RWi1JInZrWJYlnpfXJwHk=
 github.com/Azure/go-autorest/autorest/adal v0.8.1/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
 github.com/Azure/go-autorest/autorest/adal v0.8.2 h1:O1X4oexUxnZCaEUGsvMnr8ZGj8HI37tNezwY4npRqA0=
 github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
@@ -34,14 +32,18 @@ github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbt
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129 h1:gfAMKE626QEuKG3si0pdTRcr/YEbBoxY+3GOH3gWvl4=
+github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
@@ -49,13 +51,13 @@ github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/compose-spec/compose-go v0.0.0-20200423124427-63dcf8c22cae h1:5zRbbF5Gbkl7ZEJrKwYha2JMWgnfpPjSmv8+jCmkeSA=
+github.com/compose-spec/compose-go v0.0.0-20200423124427-63dcf8c22cae/go.mod h1:1PUpzRF1O/65VOqXZuwpCuYY7pJxbIq1jbAvAf62FGM=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
-github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
-github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -65,10 +67,13 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4=
 github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
+github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
+github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
+github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -76,6 +81,12 @@ github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
+github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
+github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.0.3 h1:ZOigqf7iBxkA4jdQ3am7ATzdlOFp9YzA6NmuvEEZc9g=
+github.com/gobwas/ws v1.0.3/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -98,19 +109,20 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
 github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
 github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
-github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
+github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
 github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
@@ -123,25 +135,24 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw=
+github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4=
+github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
 github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
-github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -171,9 +182,7 @@ github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLk
 github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
-github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
@@ -181,30 +190,30 @@ github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q
 github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
 github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
-github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
 github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
-github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
 github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
+github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
+github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
+github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@@ -212,7 +221,6 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
 golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -225,11 +233,9 @@ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73r
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU=
 golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U=
 golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
@@ -238,7 +244,6 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -246,13 +251,10 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU=
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -263,6 +265,7 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -282,6 +285,7 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
 google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
 google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw=
 google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -292,9 +296,10 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
 gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E=
+gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

+ 10 - 2
server/proxy/proxy.go

@@ -4,6 +4,7 @@ import (
 	"context"
 
 	"github.com/docker/api/client"
+	"github.com/docker/api/containers"
 	v1 "github.com/docker/api/containers/v1"
 )
 
@@ -45,8 +46,15 @@ func (p *proxyContainerApi) List(ctx context.Context, _ *v1.ListRequest) (*v1.Li
 	return response, nil
 }
 
-func (p *proxyContainerApi) Create(_ context.Context, _ *v1.CreateRequest) (*v1.CreateResponse, error) {
-	panic("not implemented") // TODO: Implement
+func (p *proxyContainerApi) Create(ctx context.Context, request *v1.CreateRequest) (*v1.CreateResponse, error) {
+	client := Client(ctx)
+
+	err := client.ContainerService().Run(ctx, containers.ContainerConfig{
+		ID:    request.Id,
+		Image: request.Image,
+	})
+
+	return &v1.CreateResponse{}, err
 }
 
 func (p *proxyContainerApi) Start(_ context.Context, _ *v1.StartRequest) (*v1.StartResponse, error) {