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

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
This commit is contained in:
Aanand Prasad 2016-11-08 17:05:23 +00:00 committed by Daniel Nephin
parent eefccc25c5
commit aa5e7d038a
4 changed files with 351 additions and 39 deletions

View File

@ -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
}

View File

@ -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"
]`)
}

View File

@ -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)
}
if err := createNetworks(ctx, dockerCli, networks, namespace); err != nil {
networks := convertNetworks(namespace, config.Networks)
if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil {
return err
}
return deployServices(ctx, dockerCli, config, namespace, opts.sendRegistryAuth)
services, err := convertServices(namespace, config)
if err != nil {
return err
}
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),
}

View File

@ -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
}