Skip to content

Commit

Permalink
Add support for multiple composefile when deploying
Browse files Browse the repository at this point in the history
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
  • Loading branch information
vdemeester committed Jan 25, 2018
1 parent 11dfa23 commit c76178c
Show file tree
Hide file tree
Showing 17 changed files with 1,636 additions and 114 deletions.
2 changes: 1 addition & 1 deletion cli/command/stack/deploy.go
Expand Up @@ -34,7 +34,7 @@ func newDeployCommand(dockerCli command.Cli) *cobra.Command {
flags.StringVar(&opts.Bundlefile, "bundle-file", "", "Path to a Distributed Application Bundle file")
flags.SetAnnotation("bundle-file", "experimental", nil)
flags.SetAnnotation("bundle-file", "swarm", nil)
flags.StringVarP(&opts.Composefile, "compose-file", "c", "", "Path to a Compose file")
flags.StringSliceVarP(&opts.Composefiles, "compose-file", "c", []string{}, "Path to a Compose file")
flags.SetAnnotation("compose-file", "version", []string{"1.25"})
flags.BoolVar(&opts.SendRegistryAuth, "with-registry-auth", false, "Send registry authentication details to Swarm agents")
flags.SetAnnotation("with-registry-auth", "swarm", nil)
Expand Down
6 changes: 3 additions & 3 deletions cli/command/stack/kubernetes/deploy.go
Expand Up @@ -16,8 +16,8 @@ import (
func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error {
cmdOut := dockerCli.Out()
// Check arguments
if opts.Composefile == "" {
return errors.Errorf("Please specify a Compose file (with --compose-file).")
if len(opts.Composefiles) == 0 {
return errors.Errorf("Please specify only one compose file (with --compose-file).")
}
// Initialize clients
stacks, err := dockerCli.stacks()
Expand All @@ -37,7 +37,7 @@ func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error {
}

// Parse the compose file
stack, cfg, err := LoadStack(opts.Namespace, opts.Composefile)
stack, cfg, err := LoadStack(opts.Namespace, opts.Composefiles)
if err != nil {
return err
}
Expand Down
7 changes: 4 additions & 3 deletions cli/command/stack/kubernetes/loader.go
Expand Up @@ -18,10 +18,11 @@ import (

// LoadStack loads a stack from a Compose file, with a given name.
// FIXME(vdemeester) remove this and use cli/compose/loader for both swarm and kubernetes
func LoadStack(name, composeFile string) (*apiv1beta1.Stack, *composetypes.Config, error) {
if composeFile == "" {
return nil, nil, errors.New("compose-file must be set")
func LoadStack(name string, composeFiles []string) (*apiv1beta1.Stack, *composetypes.Config, error) {
if len(composeFiles) != 1 {
return nil, nil, errors.New("compose-file must be set (and only one)")
}
composeFile := composeFiles[0]

workingDir, err := os.Getwd()
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cli/command/stack/options/opts.go
Expand Up @@ -5,7 +5,7 @@ import "github.com/docker/cli/opts"
// Deploy holds docker stack deploy options
type Deploy struct {
Bundlefile string
Composefile string
Composefiles []string
Namespace string
ResolveImage string
SendRegistryAuth bool
Expand Down
4 changes: 2 additions & 2 deletions cli/command/stack/swarm/deploy.go
Expand Up @@ -29,9 +29,9 @@ func RunDeploy(dockerCli command.Cli, opts options.Deploy) error {
}

switch {
case opts.Bundlefile == "" && opts.Composefile == "":
case opts.Bundlefile == "" && len(opts.Composefiles) == 0:
return errors.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).")
case opts.Bundlefile != "" && opts.Composefile != "":
case opts.Bundlefile != "" && len(opts.Composefiles) != 0:
return errors.Errorf("You cannot specify both a bundle file and a Compose file.")
case opts.Bundlefile != "":
return deployBundle(ctx, dockerCli, opts)
Expand Down
48 changes: 38 additions & 10 deletions cli/command/stack/swarm/deploy_composefile.go
Expand Up @@ -24,7 +24,7 @@ import (
)

func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error {
configDetails, err := getConfigDetails(opts.Composefile, dockerCli.In())
configDetails, err := getConfigDetails(opts.Composefiles, dockerCli.In())
if err != nil {
return err
}
Expand All @@ -39,13 +39,14 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Depl
return err
}

unsupportedProperties := loader.GetUnsupportedProperties(configDetails)
dicts := getDictsFrom(configDetails.ConfigFiles)
unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
if len(unsupportedProperties) > 0 {
fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n",
strings.Join(unsupportedProperties, ", "))
}

deprecatedProperties := loader.GetDeprecatedProperties(configDetails)
deprecatedProperties := loader.GetDeprecatedProperties(dicts...)
if len(deprecatedProperties) > 0 {
fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n",
propertyWarnings(deprecatedProperties))
Expand Down Expand Up @@ -97,6 +98,16 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Depl
return deployServices(ctx, dockerCli, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
}

func getDictsFrom(configFiles []composetypes.ConfigFile) []map[string]interface{} {
dicts := []map[string]interface{}{}

for _, configFile := range configFiles {
dicts = append(dicts, configFile.Config)
}

return dicts
}

func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
serviceNetworks := map[string]struct{}{}
for _, serviceConfig := range serviceConfigs {
Expand All @@ -120,29 +131,32 @@ func propertyWarnings(properties map[string]string) string {
return strings.Join(msgs, "\n\n")
}

func getConfigDetails(composefile string, stdin io.Reader) (composetypes.ConfigDetails, error) {
func getConfigDetails(composefiles []string, stdin io.Reader) (composetypes.ConfigDetails, error) {
var details composetypes.ConfigDetails

if composefile == "-" {
if len(composefiles) == 0 {
return details, errors.New("no composefile(s)")
}

if composefiles[0] == "-" && len(composefiles) == 1 {
workingDir, err := os.Getwd()
if err != nil {
return details, err
}
details.WorkingDir = workingDir
} else {
absPath, err := filepath.Abs(composefile)
absPath, err := filepath.Abs(composefiles[0])
if err != nil {
return details, err
}
details.WorkingDir = filepath.Dir(absPath)
}

configFile, err := getConfigFile(composefile, stdin)
var err error
details.ConfigFiles, err = loadConfigFiles(composefiles, stdin)
if err != nil {
return details, err
}
// TODO: support multiple files
details.ConfigFiles = []composetypes.ConfigFile{*configFile}
details.Environment, err = buildEnvironment(os.Environ())
return details, err
}
Expand All @@ -160,7 +174,21 @@ func buildEnvironment(env []string) (map[string]string, error) {
return result, nil
}

func getConfigFile(filename string, stdin io.Reader) (*composetypes.ConfigFile, error) {
func loadConfigFiles(filenames []string, stdin io.Reader) ([]composetypes.ConfigFile, error) {
var configFiles []composetypes.ConfigFile

for _, filename := range filenames {
configFile, err := loadConfigFile(filename, stdin)
if err != nil {
return configFiles, err
}
configFiles = append(configFiles, *configFile)
}

return configFiles, nil
}

func loadConfigFile(filename string, stdin io.Reader) (*composetypes.ConfigFile, error) {
var bytes []byte
var err error

Expand Down
4 changes: 2 additions & 2 deletions cli/command/stack/swarm/deploy_composefile_test.go
Expand Up @@ -26,7 +26,7 @@ services:
file := fs.NewFile(t, "test-get-config-details", fs.WithContent(content))
defer file.Remove()

details, err := getConfigDetails(file.Path(), nil)
details, err := getConfigDetails([]string{file.Path()}, nil)
require.NoError(t, err)
assert.Equal(t, filepath.Dir(file.Path()), details.WorkingDir)
require.Len(t, details.ConfigFiles, 1)
Expand All @@ -41,7 +41,7 @@ services:
foo:
image: alpine:3.5
`
details, err := getConfigDetails("-", strings.NewReader(content))
details, err := getConfigDetails([]string{"-"}, strings.NewReader(content))
require.NoError(t, err)
cwd, err := os.Getwd()
require.NoError(t, err)
Expand Down
80 changes: 51 additions & 29 deletions cli/compose/loader/loader.go
Expand Up @@ -45,27 +45,43 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) {
if len(configDetails.ConfigFiles) < 1 {
return nil, errors.Errorf("No files specified")
}
if len(configDetails.ConfigFiles) > 1 {
return nil, errors.Errorf("Multiple files are not yet supported")
}

configDict := getConfigDict(configDetails)
configDetails.Version = schema.Version(configDict)
configs := []*types.Config{}

if err := validateForbidden(configDict); err != nil {
return nil, err
}
for _, file := range configDetails.ConfigFiles {
configDict := file.Config
version := schema.Version(configDict)
if configDetails.Version == "" {
configDetails.Version = version
}
if configDetails.Version != version {
return nil, errors.Errorf("version mismatched between two composefiles : %v and %v", configDetails.Version, version)
}

var err error
configDict, err = interpolateConfig(configDict, configDetails.LookupEnv)
if err != nil {
return nil, err
}
if err := validateForbidden(configDict); err != nil {
return nil, err
}

if err := schema.Validate(configDict, configDetails.Version); err != nil {
return nil, err
var err error
configDict, err = interpolateConfig(configDict, configDetails.LookupEnv)
if err != nil {
return nil, err
}

if err := schema.Validate(configDict, configDetails.Version); err != nil {
return nil, err
}

cfg, err := loadSections(configDict, configDetails)
if err != nil {
return nil, err
}
cfg.Filename = file.Filename

configs = append(configs, cfg)
}
return loadSections(configDict, configDetails)

return merge(configs)
}

func validateForbidden(configDict map[string]interface{}) error {
Expand Down Expand Up @@ -142,14 +158,16 @@ func getSection(config map[string]interface{}, key string) map[string]interface{

// GetUnsupportedProperties returns the list of any unsupported properties that are
// used in the Compose files.
func GetUnsupportedProperties(configDetails types.ConfigDetails) []string {
func GetUnsupportedProperties(configDicts ...map[string]interface{}) []string {
unsupported := map[string]bool{}

for _, service := range getServices(getConfigDict(configDetails)) {
serviceDict := service.(map[string]interface{})
for _, property := range types.UnsupportedProperties {
if _, isSet := serviceDict[property]; isSet {
unsupported[property] = true
for _, configDict := range configDicts {
for _, service := range getServices(configDict) {
serviceDict := service.(map[string]interface{})
for _, property := range types.UnsupportedProperties {
if _, isSet := serviceDict[property]; isSet {
unsupported[property] = true
}
}
}
}
Expand All @@ -168,8 +186,17 @@ func sortedKeys(set map[string]bool) []string {

// GetDeprecatedProperties returns the list of any deprecated properties that
// are used in the compose files.
func GetDeprecatedProperties(configDetails types.ConfigDetails) map[string]string {
return getProperties(getServices(getConfigDict(configDetails)), types.DeprecatedProperties)
func GetDeprecatedProperties(configDicts ...map[string]interface{}) map[string]string {
deprecated := map[string]string{}

for _, configDict := range configDicts {
deprecatedProperties := getProperties(getServices(configDict), types.DeprecatedProperties)
for key, value := range deprecatedProperties {
deprecated[key] = value
}
}

return deprecated
}

func getProperties(services map[string]interface{}, propertyMap map[string]string) map[string]string {
Expand Down Expand Up @@ -198,11 +225,6 @@ func (e *ForbiddenPropertiesError) Error() string {
return "Configuration contains forbidden properties"
}

// TODO: resolve multiple files into a single config
func getConfigDict(configDetails types.ConfigDetails) map[string]interface{} {
return configDetails.ConfigFiles[0].Config
}

func getServices(configDict map[string]interface{}) map[string]interface{} {
if services, ok := configDict["services"]; ok {
if servicesDict, ok := services.(map[string]interface{}); ok {
Expand Down
5 changes: 3 additions & 2 deletions cli/compose/loader/loader_test.go
Expand Up @@ -572,6 +572,7 @@ networks:
config, err := Load(buildConfigDetails(dict, env))
require.NoError(t, err)
expected := &types.Config{
Filename: "filename.yml",
Services: []types.ServiceConfig{
{
Name: "web",
Expand Down Expand Up @@ -670,7 +671,7 @@ services:
_, err = Load(configDetails)
require.NoError(t, err)

unsupported := GetUnsupportedProperties(configDetails)
unsupported := GetUnsupportedProperties(dict)
assert.Equal(t, []string{"build", "links", "pid"}, unsupported)
}

Expand Down Expand Up @@ -713,7 +714,7 @@ services:
_, err = Load(configDetails)
require.NoError(t, err)

deprecated := GetDeprecatedProperties(configDetails)
deprecated := GetDeprecatedProperties(dict)
assert.Len(t, deprecated, 2)
assert.Contains(t, deprecated, "container_name")
assert.Contains(t, deprecated, "expose")
Expand Down

0 comments on commit c76178c

Please sign in to comment.