Browse Source

Reinstate --bundle-file argument to 'docker deploy'

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
Aanand Prasad 8 years ago
parent
commit
aa5e7d038a

+ 69 - 0
cli/command/bundlefile/bundlefile.go

@@ -0,0 +1,69 @@
+package bundlefile
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+)
+
+// Bundlefile stores the contents of a bundlefile
+type Bundlefile struct {
+	Version  string
+	Services map[string]Service
+}
+
+// Service is a service from a bundlefile
+type Service struct {
+	Image      string
+	Command    []string          `json:",omitempty"`
+	Args       []string          `json:",omitempty"`
+	Env        []string          `json:",omitempty"`
+	Labels     map[string]string `json:",omitempty"`
+	Ports      []Port            `json:",omitempty"`
+	WorkingDir *string           `json:",omitempty"`
+	User       *string           `json:",omitempty"`
+	Networks   []string          `json:",omitempty"`
+}
+
+// Port is a port as defined in a bundlefile
+type Port struct {
+	Protocol string
+	Port     uint32
+}
+
+// LoadFile loads a bundlefile from a path to the file
+func LoadFile(reader io.Reader) (*Bundlefile, error) {
+	bundlefile := &Bundlefile{}
+
+	decoder := json.NewDecoder(reader)
+	if err := decoder.Decode(bundlefile); err != nil {
+		switch jsonErr := err.(type) {
+		case *json.SyntaxError:
+			return nil, fmt.Errorf(
+				"JSON syntax error at byte %v: %s",
+				jsonErr.Offset,
+				jsonErr.Error())
+		case *json.UnmarshalTypeError:
+			return nil, fmt.Errorf(
+				"Unexpected type at byte %v. Expected %s but received %s.",
+				jsonErr.Offset,
+				jsonErr.Type,
+				jsonErr.Value)
+		}
+		return nil, err
+	}
+
+	return bundlefile, nil
+}
+
+// Print writes the contents of the bundlefile to the output writer
+// as human readable json
+func Print(out io.Writer, bundle *Bundlefile) error {
+	bytes, err := json.MarshalIndent(*bundle, "", "    ")
+	if err != nil {
+		return err
+	}
+
+	_, err = out.Write(bytes)
+	return err
+}

+ 77 - 0
cli/command/bundlefile/bundlefile_test.go

@@ -0,0 +1,77 @@
+package bundlefile
+
+import (
+	"bytes"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/pkg/testutil/assert"
+)
+
+func TestLoadFileV01Success(t *testing.T) {
+	reader := strings.NewReader(`{
+		"Version": "0.1",
+		"Services": {
+			"redis": {
+				"Image": "redis@sha256:4b24131101fa0117bcaa18ac37055fffd9176aa1a240392bb8ea85e0be50f2ce",
+				"Networks": ["default"]
+			},
+			"web": {
+				"Image": "dockercloud/hello-world@sha256:fe79a2cfbd17eefc344fb8419420808df95a1e22d93b7f621a7399fd1e9dca1d",
+				"Networks": ["default"],
+				"User": "web"
+			}
+		}
+	}`)
+
+	bundle, err := LoadFile(reader)
+	assert.NilError(t, err)
+	assert.Equal(t, bundle.Version, "0.1")
+	assert.Equal(t, len(bundle.Services), 2)
+}
+
+func TestLoadFileSyntaxError(t *testing.T) {
+	reader := strings.NewReader(`{
+		"Version": "0.1",
+		"Services": unquoted string
+	}`)
+
+	_, err := LoadFile(reader)
+	assert.Error(t, err, "syntax error at byte 37: invalid character 'u'")
+}
+
+func TestLoadFileTypeError(t *testing.T) {
+	reader := strings.NewReader(`{
+		"Version": "0.1",
+		"Services": {
+			"web": {
+				"Image": "redis",
+				"Networks": "none"
+			}
+		}
+	}`)
+
+	_, err := LoadFile(reader)
+	assert.Error(t, err, "Unexpected type at byte 94. Expected []string but received string")
+}
+
+func TestPrint(t *testing.T) {
+	var buffer bytes.Buffer
+	bundle := &Bundlefile{
+		Version: "0.1",
+		Services: map[string]Service{
+			"web": {
+				Image:   "image",
+				Command: []string{"echo", "something"},
+			},
+		},
+	}
+	assert.NilError(t, Print(&buffer, bundle))
+	output := buffer.String()
+	assert.Contains(t, output, "\"Image\": \"image\"")
+	assert.Contains(t, output,
+		`"Command": [
+                "echo",
+                "something"
+            ]`)
+}

+ 166 - 37
cli/command/stack/deploy.go

@@ -29,6 +29,7 @@ const (
 )
 
 type deployOptions struct {
+	bundlefile       string
 	composefile      string
 	namespace        string
 	sendRegistryAuth bool
@@ -50,12 +51,108 @@ func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command {
 	}
 
 	flags := cmd.Flags()
+	addBundlefileFlag(&opts.bundlefile, flags)
 	addComposefileFlag(&opts.composefile, flags)
 	addRegistryAuthFlag(&opts.sendRegistryAuth, flags)
 	return cmd
 }
 
 func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error {
+	if opts.bundlefile == "" && opts.composefile == "" {
+		return fmt.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).")
+	}
+
+	if opts.bundlefile != "" && opts.composefile != "" {
+		return fmt.Errorf("You cannot specify both a bundle file and a Compose file.")
+	}
+
+	info, err := dockerCli.Client().Info(context.Background())
+	if err != nil {
+		return err
+	}
+	if !info.Swarm.ControlAvailable {
+		return fmt.Errorf("This node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again.")
+	}
+
+	if opts.bundlefile != "" {
+		return deployBundle(dockerCli, opts)
+	} else {
+		return deployCompose(dockerCli, opts)
+	}
+}
+
+func deployBundle(dockerCli *command.DockerCli, opts deployOptions) error {
+	bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile)
+	if err != nil {
+		return err
+	}
+
+	namespace := namespace{name: opts.namespace}
+
+	networks := make(map[string]types.NetworkCreate)
+	for _, service := range bundle.Services {
+		for _, networkName := range service.Networks {
+			networks[networkName] = types.NetworkCreate{
+				Labels: getStackLabels(namespace.name, nil),
+			}
+		}
+	}
+
+	services := make(map[string]swarm.ServiceSpec)
+	for internalName, service := range bundle.Services {
+		name := namespace.scope(internalName)
+
+		var ports []swarm.PortConfig
+		for _, portSpec := range service.Ports {
+			ports = append(ports, swarm.PortConfig{
+				Protocol:   swarm.PortConfigProtocol(portSpec.Protocol),
+				TargetPort: portSpec.Port,
+			})
+		}
+
+		nets := []swarm.NetworkAttachmentConfig{}
+		for _, networkName := range service.Networks {
+			nets = append(nets, swarm.NetworkAttachmentConfig{
+				Target:  namespace.scope(networkName),
+				Aliases: []string{networkName},
+			})
+		}
+
+		serviceSpec := swarm.ServiceSpec{
+			Annotations: swarm.Annotations{
+				Name:   name,
+				Labels: getStackLabels(namespace.name, service.Labels),
+			},
+			TaskTemplate: swarm.TaskSpec{
+				ContainerSpec: swarm.ContainerSpec{
+					Image:   service.Image,
+					Command: service.Command,
+					Args:    service.Args,
+					Env:     service.Env,
+					// Service Labels will not be copied to Containers
+					// automatically during the deployment so we apply
+					// it here.
+					Labels: getStackLabels(namespace.name, nil),
+				},
+			},
+			EndpointSpec: &swarm.EndpointSpec{
+				Ports: ports,
+			},
+			Networks: nets,
+		}
+
+		services[internalName] = serviceSpec
+	}
+
+	ctx := context.Background()
+
+	if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil {
+		return err
+	}
+	return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth)
+}
+
+func deployCompose(dockerCli *command.DockerCli, opts deployOptions) error {
 	configDetails, err := getConfigDetails(opts)
 	if err != nil {
 		return err
@@ -86,14 +183,15 @@ func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error {
 	ctx := context.Background()
 	namespace := namespace{name: opts.namespace}
 
-	networks := config.Networks
-	if networks == nil {
-		networks = make(map[string]composetypes.NetworkConfig)
+	networks := convertNetworks(namespace, config.Networks)
+	if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil {
+		return err
 	}
-	if err := createNetworks(ctx, dockerCli, networks, namespace); err != nil {
+	services, err := convertServices(namespace, config)
+	if err != nil {
 		return err
 	}
-	return deployServices(ctx, dockerCli, config, namespace, opts.sendRegistryAuth)
+	return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth)
 }
 
 func propertyWarnings(properties map[string]string) string {
@@ -138,37 +236,24 @@ func getConfigFile(filename string) (*composetypes.ConfigFile, error) {
 	}, nil
 }
 
-func createNetworks(
-	ctx context.Context,
-	dockerCli *command.DockerCli,
-	networks map[string]composetypes.NetworkConfig,
+func convertNetworks(
 	namespace namespace,
-) error {
-	client := dockerCli.Client()
-
-	existingNetworks, err := getNetworks(ctx, client, namespace.name)
-	if err != nil {
-		return err
-	}
-
-	existingNetworkMap := make(map[string]types.NetworkResource)
-	for _, network := range existingNetworks {
-		existingNetworkMap[network.Name] = network
+	networks map[string]composetypes.NetworkConfig,
+) map[string]types.NetworkCreate {
+	if networks == nil {
+		networks = make(map[string]composetypes.NetworkConfig)
 	}
 
 	// TODO: only add default network if it's used
 	networks["default"] = composetypes.NetworkConfig{}
 
+	result := make(map[string]types.NetworkCreate)
+
 	for internalName, network := range networks {
 		if network.External.Name != "" {
 			continue
 		}
 
-		name := namespace.scope(internalName)
-		if _, exists := existingNetworkMap[name]; exists {
-			continue
-		}
-
 		createOpts := types.NetworkCreate{
 			Labels:  getStackLabels(namespace.name, network.Labels),
 			Driver:  network.Driver,
@@ -182,6 +267,36 @@ func createNetworks(
 		}
 		// TODO: IPAMConfig.Config
 
+		result[internalName] = createOpts
+	}
+
+	return result
+}
+
+func createNetworks(
+	ctx context.Context,
+	dockerCli *command.DockerCli,
+	namespace namespace,
+	networks map[string]types.NetworkCreate,
+) error {
+	client := dockerCli.Client()
+
+	existingNetworks, err := getNetworks(ctx, client, namespace.name)
+	if err != nil {
+		return err
+	}
+
+	existingNetworkMap := make(map[string]types.NetworkResource)
+	for _, network := range existingNetworks {
+		existingNetworkMap[network.Name] = network
+	}
+
+	for internalName, createOpts := range networks {
+		name := namespace.scope(internalName)
+		if _, exists := existingNetworkMap[name]; exists {
+			continue
+		}
+
 		if createOpts.Driver == "" {
 			createOpts.Driver = defaultNetworkDriver
 		}
@@ -191,10 +306,11 @@ func createNetworks(
 			return err
 		}
 	}
+
 	return nil
 }
 
-func convertNetworks(
+func convertServiceNetworks(
 	networks map[string]*composetypes.ServiceNetworkConfig,
 	namespace namespace,
 	name string,
@@ -294,14 +410,12 @@ func convertVolumes(
 func deployServices(
 	ctx context.Context,
 	dockerCli *command.DockerCli,
-	config *composetypes.Config,
+	services map[string]swarm.ServiceSpec,
 	namespace namespace,
 	sendAuth bool,
 ) error {
 	apiClient := dockerCli.Client()
 	out := dockerCli.Out()
-	services := config.Services
-	volumes := config.Volumes
 
 	existingServices, err := getServices(ctx, apiClient, namespace.name)
 	if err != nil {
@@ -313,13 +427,8 @@ func deployServices(
 		existingServiceMap[service.Spec.Name] = service
 	}
 
-	for _, service := range services {
-		name := namespace.scope(service.Name)
-
-		serviceSpec, err := convertService(namespace, service, volumes)
-		if err != nil {
-			return err
-		}
+	for internalName, serviceSpec := range services {
+		name := namespace.scope(internalName)
 
 		encodedAuth := ""
 		if sendAuth {
@@ -363,6 +472,26 @@ func deployServices(
 	return nil
 }
 
+func convertServices(
+	namespace namespace,
+	config *composetypes.Config,
+) (map[string]swarm.ServiceSpec, error) {
+	result := make(map[string]swarm.ServiceSpec)
+
+	services := config.Services
+	volumes := config.Volumes
+
+	for _, service := range services {
+		serviceSpec, err := convertService(namespace, service, volumes)
+		if err != nil {
+			return nil, err
+		}
+		result[service.Name] = serviceSpec
+	}
+
+	return result, nil
+}
+
 func convertService(
 	namespace namespace,
 	service composetypes.ServiceConfig,
@@ -422,7 +551,7 @@ func convertService(
 		},
 		EndpointSpec: endpoint,
 		Mode:         mode,
-		Networks:     convertNetworks(service.Networks, namespace, service.Name),
+		Networks:     convertServiceNetworks(service.Networks, namespace, service.Name),
 		UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig),
 	}
 

+ 38 - 1
cli/command/stack/opts.go

@@ -1,11 +1,48 @@
 package stack
 
-import "github.com/spf13/pflag"
+import (
+	"fmt"
+	"io"
+	"os"
+
+	"github.com/docker/docker/cli/command/bundlefile"
+	"github.com/spf13/pflag"
+)
 
 func addComposefileFlag(opt *string, flags *pflag.FlagSet) {
 	flags.StringVar(opt, "compose-file", "", "Path to a Compose file")
 }
 
+func addBundlefileFlag(opt *string, flags *pflag.FlagSet) {
+	flags.StringVar(opt, "bundle-file", "", "Path to a Distributed Application Bundle file")
+}
+
 func addRegistryAuthFlag(opt *bool, flags *pflag.FlagSet) {
 	flags.BoolVar(opt, "with-registry-auth", false, "Send registry authentication details to Swarm agents")
 }
+
+func loadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefile.Bundlefile, error) {
+	defaultPath := fmt.Sprintf("%s.dab", namespace)
+
+	if path == "" {
+		path = defaultPath
+	}
+	if _, err := os.Stat(path); err != nil {
+		return nil, fmt.Errorf(
+			"Bundle %s not found. Specify the path with --file",
+			path)
+	}
+
+	fmt.Fprintf(stderr, "Loading bundle from %s\n", path)
+	reader, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer reader.Close()
+
+	bundle, err := bundlefile.LoadFile(reader)
+	if err != nil {
+		return nil, fmt.Errorf("Error reading %s: %v\n", path, err)
+	}
+	return bundle, err
+}