diff --git a/cli/command/stack/deploy.go b/cli/command/stack/deploy.go index d01add7b98..106dd83e8b 100644 --- a/cli/command/stack/deploy.go +++ b/cli/command/stack/deploy.go @@ -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) diff --git a/cli/command/stack/kubernetes/deploy.go b/cli/command/stack/kubernetes/deploy.go index ff9c55ca2b..f8558e59a1 100644 --- a/cli/command/stack/kubernetes/deploy.go +++ b/cli/command/stack/kubernetes/deploy.go @@ -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() @@ -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 } diff --git a/cli/command/stack/kubernetes/loader.go b/cli/command/stack/kubernetes/loader.go index c32517527a..f24b029c4b 100644 --- a/cli/command/stack/kubernetes/loader.go +++ b/cli/command/stack/kubernetes/loader.go @@ -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 { diff --git a/cli/command/stack/options/opts.go b/cli/command/stack/options/opts.go index cbf5398dbf..8c8a1acf26 100644 --- a/cli/command/stack/options/opts.go +++ b/cli/command/stack/options/opts.go @@ -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 diff --git a/cli/command/stack/swarm/deploy.go b/cli/command/stack/swarm/deploy.go index 2337873719..a308e2ee69 100644 --- a/cli/command/stack/swarm/deploy.go +++ b/cli/command/stack/swarm/deploy.go @@ -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) diff --git a/cli/command/stack/swarm/deploy_composefile.go b/cli/command/stack/swarm/deploy_composefile.go index 95700b8b90..ef3d77a7c6 100644 --- a/cli/command/stack/swarm/deploy_composefile.go +++ b/cli/command/stack/swarm/deploy_composefile.go @@ -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 } @@ -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)) @@ -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 { @@ -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 } @@ -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 diff --git a/cli/command/stack/swarm/deploy_composefile_test.go b/cli/command/stack/swarm/deploy_composefile_test.go index 6c1eb3a748..33c3490b33 100644 --- a/cli/command/stack/swarm/deploy_composefile_test.go +++ b/cli/command/stack/swarm/deploy_composefile_test.go @@ -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) @@ -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) diff --git a/cli/compose/loader/loader.go b/cli/compose/loader/loader.go index 7629477519..12bdf4b128 100644 --- a/cli/compose/loader/loader.go +++ b/cli/compose/loader/loader.go @@ -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") + + configs := []*types.Config{} + + 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) + } + + if err := validateForbidden(configDict); 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) } - configDict := getConfigDict(configDetails) - configDetails.Version = schema.Version(configDict) - - if err := validateForbidden(configDict); 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 - } - return loadSections(configDict, configDetails) + return merge(configs) } func validateForbidden(configDict map[string]interface{}) error { @@ -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 + } } } } @@ -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 { @@ -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 { diff --git a/cli/compose/loader/loader_test.go b/cli/compose/loader/loader_test.go index 75fdce2c2a..f3586cfc36 100644 --- a/cli/compose/loader/loader_test.go +++ b/cli/compose/loader/loader_test.go @@ -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", @@ -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) } @@ -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") diff --git a/cli/compose/loader/merge.go b/cli/compose/loader/merge.go new file mode 100644 index 0000000000..a6b7269b9a --- /dev/null +++ b/cli/compose/loader/merge.go @@ -0,0 +1,233 @@ +package loader + +import ( + "reflect" + "sort" + + "github.com/docker/cli/cli/compose/types" + "github.com/imdario/mergo" + "github.com/pkg/errors" +) + +type specials struct { + m map[reflect.Type]func(dst, src reflect.Value) error +} + +func (s *specials) Transformer(t reflect.Type) func(dst, src reflect.Value) error { + if fn, ok := s.m[t]; ok { + return fn + } + return nil +} + +func merge(configs []*types.Config) (*types.Config, error) { + base := configs[0] + for _, override := range configs[1:] { + var err error + base.Services, err = mergeServices(base.Services, override.Services) + if err != nil { + return base, errors.Wrapf(err, "cannot merge services from %s", override.Filename) + } + base.Volumes, err = mergeVolumes(base.Volumes, override.Volumes) + if err != nil { + return base, errors.Wrapf(err, "cannot merge volumes from %s", override.Filename) + } + base.Networks, err = mergeNetworks(base.Networks, override.Networks) + if err != nil { + return base, errors.Wrapf(err, "cannot merge networks from %s", override.Filename) + } + base.Secrets, err = mergeSecrets(base.Secrets, override.Secrets) + if err != nil { + return base, errors.Wrapf(err, "cannot merge secrets from %s", override.Filename) + } + base.Configs, err = mergeConfigs(base.Configs, override.Configs) + if err != nil { + return base, errors.Wrapf(err, "cannot merge configs from %s", override.Filename) + } + } + return base, nil +} + +func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig, error) { + baseServices := mapByName(base) + overrideServices := mapByName(override) + specials := &specials{ + m: map[reflect.Type]func(dst, src reflect.Value) error{ + reflect.TypeOf(&types.LoggingConfig{}): safelyMerge(mergeLoggingConfig), + reflect.TypeOf([]types.ServicePortConfig{}): mergeSlice(toServicePortConfigsMap, toServicePortConfigsSlice), + reflect.TypeOf([]types.ServiceSecretConfig{}): mergeSlice(toServiceSecretConfigsMap, toServiceSecretConfigsSlice), + reflect.TypeOf([]types.ServiceConfigObjConfig{}): mergeSlice(toServiceConfigObjConfigsMap, toSServiceConfigObjConfigsSlice), + }, + } + for name, overrideService := range overrideServices { + if baseService, ok := baseServices[name]; ok { + if err := mergo.Merge(&baseService, &overrideService, mergo.WithOverride, mergo.WithTransformers(specials)); err != nil { + return base, errors.Wrapf(err, "cannot merge service %s", name) + } + baseServices[name] = baseService + continue + } + baseServices[name] = overrideService + } + services := []types.ServiceConfig{} + for _, baseService := range baseServices { + services = append(services, baseService) + } + sort.Slice(services, func(i, j int) bool { return services[i].Name < services[j].Name }) + return services, nil +} + +func toServiceSecretConfigsMap(s interface{}) (map[interface{}]interface{}, error) { + secrets, ok := s.([]types.ServiceSecretConfig) + if !ok { + return nil, errors.Errorf("not a serviceSecretConfig: %v", s) + } + m := map[interface{}]interface{}{} + for _, secret := range secrets { + m[secret.Source] = secret + } + return m, nil +} + +func toServiceConfigObjConfigsMap(s interface{}) (map[interface{}]interface{}, error) { + secrets, ok := s.([]types.ServiceConfigObjConfig) + if !ok { + return nil, errors.Errorf("not a serviceSecretConfig: %v", s) + } + m := map[interface{}]interface{}{} + for _, secret := range secrets { + m[secret.Source] = secret + } + return m, nil +} + +func toServicePortConfigsMap(s interface{}) (map[interface{}]interface{}, error) { + ports, ok := s.([]types.ServicePortConfig) + if !ok { + return nil, errors.Errorf("not a servicePortConfig slice: %v", s) + } + m := map[interface{}]interface{}{} + for _, p := range ports { + m[p.Published] = p + } + return m, nil +} + +func toServiceSecretConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error { + s := []types.ServiceSecretConfig{} + for _, v := range m { + s = append(s, v.(types.ServiceSecretConfig)) + } + sort.Slice(s, func(i, j int) bool { return s[i].Source < s[j].Source }) + dst.Set(reflect.ValueOf(s)) + return nil +} + +func toSServiceConfigObjConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error { + s := []types.ServiceConfigObjConfig{} + for _, v := range m { + s = append(s, v.(types.ServiceConfigObjConfig)) + } + sort.Slice(s, func(i, j int) bool { return s[i].Source < s[j].Source }) + dst.Set(reflect.ValueOf(s)) + return nil +} + +func toServicePortConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error { + s := []types.ServicePortConfig{} + for _, v := range m { + s = append(s, v.(types.ServicePortConfig)) + } + sort.Slice(s, func(i, j int) bool { return s[i].Published < s[j].Published }) + dst.Set(reflect.ValueOf(s)) + return nil +} + +type tomapFn func(s interface{}) (map[interface{}]interface{}, error) +type writeValueFromMapFn func(reflect.Value, map[interface{}]interface{}) error + +func safelyMerge(mergeFn func(dst, src reflect.Value) error) func(dst, src reflect.Value) error { + return func(dst, src reflect.Value) error { + if src.IsNil() { + return nil + } + if dst.IsNil() { + dst.Set(src) + return nil + } + return mergeFn(dst, src) + } +} + +func mergeSlice(tomap tomapFn, writeValue writeValueFromMapFn) func(dst, src reflect.Value) error { + return func(dst, src reflect.Value) error { + dstMap, err := sliceToMap(tomap, dst) + if err != nil { + return err + } + srcMap, err := sliceToMap(tomap, src) + if err != nil { + return err + } + if err := mergo.Map(&dstMap, srcMap, mergo.WithOverride); err != nil { + return err + } + return writeValue(dst, dstMap) + } +} + +func sliceToMap(tomap tomapFn, v reflect.Value) (map[interface{}]interface{}, error) { + // check if valid + if !v.IsValid() { + return nil, errors.Errorf("invalid value : %+v", v) + } + return tomap(v.Interface()) +} + +func mergeLoggingConfig(dst, src reflect.Value) error { + // Same driver, merging options + if getLoggingDriver(dst.Elem()) == getLoggingDriver(src.Elem()) || + getLoggingDriver(dst.Elem()) == "" || getLoggingDriver(src.Elem()) == "" { + if getLoggingDriver(dst.Elem()) == "" { + dst.Elem().FieldByName("Driver").SetString(getLoggingDriver(src.Elem())) + } + dstOptions := dst.Elem().FieldByName("Options").Interface().(map[string]string) + srcOptions := src.Elem().FieldByName("Options").Interface().(map[string]string) + return mergo.Merge(&dstOptions, srcOptions, mergo.WithOverride) + } + // Different driver, override with src + dst.Set(src) + return nil +} + +func getLoggingDriver(v reflect.Value) string { + return v.FieldByName("Driver").String() +} + +func mapByName(services []types.ServiceConfig) map[string]types.ServiceConfig { + m := map[string]types.ServiceConfig{} + for _, service := range services { + m[service.Name] = service + } + return m +} + +func mergeVolumes(base, override map[string]types.VolumeConfig) (map[string]types.VolumeConfig, error) { + err := mergo.Map(&base, &override) + return base, err +} + +func mergeNetworks(base, override map[string]types.NetworkConfig) (map[string]types.NetworkConfig, error) { + err := mergo.Map(&base, &override) + return base, err +} + +func mergeSecrets(base, override map[string]types.SecretConfig) (map[string]types.SecretConfig, error) { + err := mergo.Map(&base, &override) + return base, err +} + +func mergeConfigs(base, override map[string]types.ConfigObjConfig) (map[string]types.ConfigObjConfig, error) { + err := mergo.Map(&base, &override) + return base, err +} diff --git a/cli/compose/loader/merge_test.go b/cli/compose/loader/merge_test.go new file mode 100644 index 0000000000..97a68043a8 --- /dev/null +++ b/cli/compose/loader/merge_test.go @@ -0,0 +1,938 @@ +package loader + +import ( + "testing" + + "github.com/docker/cli/cli/compose/types" + "github.com/stretchr/testify/require" +) + +func TestLoadTwoDifferentVersion(t *testing.T) { + configDetails := types.ConfigDetails{ + ConfigFiles: []types.ConfigFile{ + {Filename: "base.yml", Config: map[string]interface{}{ + "version": "3.1", + }}, + {Filename: "override.yml", Config: map[string]interface{}{ + "version": "3.4", + }}, + }, + } + _, err := Load(configDetails) + require.EqualError(t, err, "version mismatched between two composefiles : 3.1 and 3.4") +} + +func TestLoadLogging(t *testing.T) { + loggingCases := []struct { + name string + loggingBase map[string]interface{} + loggingOverride map[string]interface{} + expected *types.LoggingConfig + }{ + { + name: "no_override_driver", + loggingBase: map[string]interface{}{ + "logging": map[string]interface{}{ + "driver": "json-file", + "options": map[string]interface{}{ + "frequency": "2000", + "timeout": "23", + }, + }, + }, + loggingOverride: map[string]interface{}{ + "logging": map[string]interface{}{ + "options": map[string]interface{}{ + "timeout": "360", + "pretty-print": "on", + }, + }, + }, + expected: &types.LoggingConfig{ + Driver: "json-file", + Options: map[string]string{ + "frequency": "2000", + "timeout": "360", + "pretty-print": "on", + }, + }, + }, + { + name: "override_driver", + loggingBase: map[string]interface{}{ + "logging": map[string]interface{}{ + "driver": "json-file", + "options": map[string]interface{}{ + "frequency": "2000", + "timeout": "23", + }, + }, + }, + loggingOverride: map[string]interface{}{ + "logging": map[string]interface{}{ + "driver": "syslog", + "options": map[string]interface{}{ + "timeout": "360", + "pretty-print": "on", + }, + }, + }, + expected: &types.LoggingConfig{ + Driver: "syslog", + Options: map[string]string{ + "timeout": "360", + "pretty-print": "on", + }, + }, + }, + { + name: "no_base_driver", + loggingBase: map[string]interface{}{ + "logging": map[string]interface{}{ + "options": map[string]interface{}{ + "frequency": "2000", + "timeout": "23", + }, + }, + }, + loggingOverride: map[string]interface{}{ + "logging": map[string]interface{}{ + "driver": "json-file", + "options": map[string]interface{}{ + "timeout": "360", + "pretty-print": "on", + }, + }, + }, + expected: &types.LoggingConfig{ + Driver: "json-file", + Options: map[string]string{ + "frequency": "2000", + "timeout": "360", + "pretty-print": "on", + }, + }, + }, + { + name: "no_driver", + loggingBase: map[string]interface{}{ + "logging": map[string]interface{}{ + "options": map[string]interface{}{ + "frequency": "2000", + "timeout": "23", + }, + }, + }, + loggingOverride: map[string]interface{}{ + "logging": map[string]interface{}{ + "options": map[string]interface{}{ + "timeout": "360", + "pretty-print": "on", + }, + }, + }, + expected: &types.LoggingConfig{ + Options: map[string]string{ + "frequency": "2000", + "timeout": "360", + "pretty-print": "on", + }, + }, + }, + { + name: "no_override_options", + loggingBase: map[string]interface{}{ + "logging": map[string]interface{}{ + "driver": "json-file", + "options": map[string]interface{}{ + "frequency": "2000", + "timeout": "23", + }, + }, + }, + loggingOverride: map[string]interface{}{ + "logging": map[string]interface{}{ + "driver": "syslog", + }, + }, + expected: &types.LoggingConfig{ + Driver: "syslog", + }, + }, + { + name: "no_base", + loggingBase: map[string]interface{}{}, + loggingOverride: map[string]interface{}{ + "logging": map[string]interface{}{ + "driver": "json-file", + "options": map[string]interface{}{ + "frequency": "2000", + }, + }, + }, + expected: &types.LoggingConfig{ + Driver: "json-file", + Options: map[string]string{ + "frequency": "2000", + }, + }, + }, + } + + for _, tc := range loggingCases { + t.Run(tc.name, func(t *testing.T) { + configDetails := types.ConfigDetails{ + ConfigFiles: []types.ConfigFile{ + { + Filename: "base.yml", + Config: map[string]interface{}{ + "version": "3.4", + "services": map[string]interface{}{ + "foo": tc.loggingBase, + }, + }, + }, + { + Filename: "override.yml", + Config: map[string]interface{}{ + "version": "3.4", + "services": map[string]interface{}{ + "foo": tc.loggingOverride, + }, + }, + }, + }, + } + config, err := Load(configDetails) + require.NoError(t, err) + require.Equal(t, &types.Config{ + Filename: "base.yml", + Services: []types.ServiceConfig{ + { + Name: "foo", + Logging: tc.expected, + Environment: types.MappingWithEquals{}, + }, + }, + Networks: map[string]types.NetworkConfig{}, + Volumes: map[string]types.VolumeConfig{}, + Secrets: map[string]types.SecretConfig{}, + Configs: map[string]types.ConfigObjConfig{}, + }, config) + }) + } +} + +func TestLoadMultipleServicePorts(t *testing.T) { + portsCases := []struct { + name string + portBase map[string]interface{} + portOverride map[string]interface{} + expected []types.ServicePortConfig + }{ + { + name: "no_override", + portBase: map[string]interface{}{ + "ports": []interface{}{ + "8080:80", + }, + }, + portOverride: map[string]interface{}{}, + expected: []types.ServicePortConfig{ + { + Mode: "ingress", + Published: 8080, + Target: 80, + Protocol: "tcp", + }, + }, + }, + { + name: "override_different_published", + portBase: map[string]interface{}{ + "ports": []interface{}{ + "8080:80", + }, + }, + portOverride: map[string]interface{}{ + "ports": []interface{}{ + "8081:80", + }, + }, + expected: []types.ServicePortConfig{ + { + Mode: "ingress", + Published: 8080, + Target: 80, + Protocol: "tcp", + }, + { + Mode: "ingress", + Published: 8081, + Target: 80, + Protocol: "tcp", + }, + }, + }, + { + name: "override_same_published", + portBase: map[string]interface{}{ + "ports": []interface{}{ + "8080:80", + }, + }, + portOverride: map[string]interface{}{ + "ports": []interface{}{ + "8080:81", + }, + }, + expected: []types.ServicePortConfig{ + { + Mode: "ingress", + Published: 8080, + Target: 81, + Protocol: "tcp", + }, + }, + }, + } + + for _, tc := range portsCases { + t.Run(tc.name, func(t *testing.T) { + configDetails := types.ConfigDetails{ + ConfigFiles: []types.ConfigFile{ + { + Filename: "base.yml", + Config: map[string]interface{}{ + "version": "3.4", + "services": map[string]interface{}{ + "foo": tc.portBase, + }, + }, + }, + { + Filename: "override.yml", + Config: map[string]interface{}{ + "version": "3.4", + "services": map[string]interface{}{ + "foo": tc.portOverride, + }, + }, + }, + }, + } + config, err := Load(configDetails) + require.NoError(t, err) + require.Equal(t, &types.Config{ + Filename: "base.yml", + Services: []types.ServiceConfig{ + { + Name: "foo", + Ports: tc.expected, + Environment: types.MappingWithEquals{}, + }, + }, + Networks: map[string]types.NetworkConfig{}, + Volumes: map[string]types.VolumeConfig{}, + Secrets: map[string]types.SecretConfig{}, + Configs: map[string]types.ConfigObjConfig{}, + }, config) + }) + } +} + +func TestLoadMultipleSecretsConfig(t *testing.T) { + portsCases := []struct { + name string + secretBase map[string]interface{} + secretOverride map[string]interface{} + expected []types.ServiceSecretConfig + }{ + { + name: "no_override", + secretBase: map[string]interface{}{ + "secrets": []interface{}{ + "my_secret", + }, + }, + secretOverride: map[string]interface{}{}, + expected: []types.ServiceSecretConfig{ + { + Source: "my_secret", + }, + }, + }, + { + name: "override_simple", + secretBase: map[string]interface{}{ + "secrets": []interface{}{ + "foo_secret", + }, + }, + secretOverride: map[string]interface{}{ + "secrets": []interface{}{ + "bar_secret", + }, + }, + expected: []types.ServiceSecretConfig{ + { + Source: "bar_secret", + }, + { + Source: "foo_secret", + }, + }, + }, + { + name: "override_same_source", + secretBase: map[string]interface{}{ + "secrets": []interface{}{ + "foo_secret", + map[string]interface{}{ + "source": "bar_secret", + "target": "waw_secret", + }, + }, + }, + secretOverride: map[string]interface{}{ + "secrets": []interface{}{ + map[string]interface{}{ + "source": "bar_secret", + "target": "bof_secret", + }, + map[string]interface{}{ + "source": "baz_secret", + "target": "waw_secret", + }, + }, + }, + expected: []types.ServiceSecretConfig{ + { + Source: "bar_secret", + Target: "bof_secret", + }, + { + Source: "baz_secret", + Target: "waw_secret", + }, + { + Source: "foo_secret", + }, + }, + }, + } + + for _, tc := range portsCases { + t.Run(tc.name, func(t *testing.T) { + configDetails := types.ConfigDetails{ + ConfigFiles: []types.ConfigFile{ + { + Filename: "base.yml", + Config: map[string]interface{}{ + "version": "3.4", + "services": map[string]interface{}{ + "foo": tc.secretBase, + }, + }, + }, + { + Filename: "override.yml", + Config: map[string]interface{}{ + "version": "3.4", + "services": map[string]interface{}{ + "foo": tc.secretOverride, + }, + }, + }, + }, + } + config, err := Load(configDetails) + require.NoError(t, err) + require.Equal(t, &types.Config{ + Filename: "base.yml", + Services: []types.ServiceConfig{ + { + Name: "foo", + Secrets: tc.expected, + Environment: types.MappingWithEquals{}, + }, + }, + Networks: map[string]types.NetworkConfig{}, + Volumes: map[string]types.VolumeConfig{}, + Secrets: map[string]types.SecretConfig{}, + Configs: map[string]types.ConfigObjConfig{}, + }, config) + }) + } +} + +func TestLoadMultipleConfigobjsConfig(t *testing.T) { + portsCases := []struct { + name string + configBase map[string]interface{} + configOverride map[string]interface{} + expected []types.ServiceConfigObjConfig + }{ + { + name: "no_override", + configBase: map[string]interface{}{ + "configs": []interface{}{ + "my_config", + }, + }, + configOverride: map[string]interface{}{}, + expected: []types.ServiceConfigObjConfig{ + { + Source: "my_config", + }, + }, + }, + { + name: "override_simple", + configBase: map[string]interface{}{ + "configs": []interface{}{ + "foo_config", + }, + }, + configOverride: map[string]interface{}{ + "configs": []interface{}{ + "bar_config", + }, + }, + expected: []types.ServiceConfigObjConfig{ + { + Source: "bar_config", + }, + { + Source: "foo_config", + }, + }, + }, + { + name: "override_same_source", + configBase: map[string]interface{}{ + "configs": []interface{}{ + "foo_config", + map[string]interface{}{ + "source": "bar_config", + "target": "waw_config", + }, + }, + }, + configOverride: map[string]interface{}{ + "configs": []interface{}{ + map[string]interface{}{ + "source": "bar_config", + "target": "bof_config", + }, + map[string]interface{}{ + "source": "baz_config", + "target": "waw_config", + }, + }, + }, + expected: []types.ServiceConfigObjConfig{ + { + Source: "bar_config", + Target: "bof_config", + }, + { + Source: "baz_config", + Target: "waw_config", + }, + { + Source: "foo_config", + }, + }, + }, + } + + for _, tc := range portsCases { + t.Run(tc.name, func(t *testing.T) { + configDetails := types.ConfigDetails{ + ConfigFiles: []types.ConfigFile{ + { + Filename: "base.yml", + Config: map[string]interface{}{ + "version": "3.4", + "services": map[string]interface{}{ + "foo": tc.configBase, + }, + }, + }, + { + Filename: "override.yml", + Config: map[string]interface{}{ + "version": "3.4", + "services": map[string]interface{}{ + "foo": tc.configOverride, + }, + }, + }, + }, + } + config, err := Load(configDetails) + require.NoError(t, err) + require.Equal(t, &types.Config{ + Filename: "base.yml", + Services: []types.ServiceConfig{ + { + Name: "foo", + Configs: tc.expected, + Environment: types.MappingWithEquals{}, + }, + }, + Networks: map[string]types.NetworkConfig{}, + Volumes: map[string]types.VolumeConfig{}, + Secrets: map[string]types.SecretConfig{}, + Configs: map[string]types.ConfigObjConfig{}, + }, config) + }) + } +} + +func TestLoadMultipleUlimits(t *testing.T) { + ulimitCases := []struct { + name string + ulimitBase map[string]interface{} + ulimitOverride map[string]interface{} + expected map[string]*types.UlimitsConfig + }{ + { + name: "no_override", + ulimitBase: map[string]interface{}{ + "ulimits": map[string]interface{}{ + "noproc": 65535, + }, + }, + ulimitOverride: map[string]interface{}{}, + expected: map[string]*types.UlimitsConfig{ + "noproc": { + Single: 65535, + }, + }, + }, + { + name: "override_simple", + ulimitBase: map[string]interface{}{ + "ulimits": map[string]interface{}{ + "noproc": 65535, + }, + }, + ulimitOverride: map[string]interface{}{ + "ulimits": map[string]interface{}{ + "noproc": 44444, + }, + }, + expected: map[string]*types.UlimitsConfig{ + "noproc": { + Single: 44444, + }, + }, + }, + { + name: "override_different_notation", + ulimitBase: map[string]interface{}{ + "ulimits": map[string]interface{}{ + "nofile": map[string]interface{}{ + "soft": 11111, + "hard": 99999, + }, + "noproc": 44444, + }, + }, + ulimitOverride: map[string]interface{}{ + "ulimits": map[string]interface{}{ + "nofile": 55555, + "noproc": map[string]interface{}{ + "soft": 22222, + "hard": 33333, + }, + }, + }, + expected: map[string]*types.UlimitsConfig{ + "noproc": { + Soft: 22222, + Hard: 33333, + }, + "nofile": { + Single: 55555, + }, + }, + }, + } + + for _, tc := range ulimitCases { + t.Run(tc.name, func(t *testing.T) { + configDetails := types.ConfigDetails{ + ConfigFiles: []types.ConfigFile{ + { + Filename: "base.yml", + Config: map[string]interface{}{ + "version": "3.4", + "services": map[string]interface{}{ + "foo": tc.ulimitBase, + }, + }, + }, + { + Filename: "override.yml", + Config: map[string]interface{}{ + "version": "3.4", + "services": map[string]interface{}{ + "foo": tc.ulimitOverride, + }, + }, + }, + }, + } + config, err := Load(configDetails) + require.NoError(t, err) + require.Equal(t, &types.Config{ + Filename: "base.yml", + Services: []types.ServiceConfig{ + { + Name: "foo", + Ulimits: tc.expected, + Environment: types.MappingWithEquals{}, + }, + }, + Networks: map[string]types.NetworkConfig{}, + Volumes: map[string]types.VolumeConfig{}, + Secrets: map[string]types.SecretConfig{}, + Configs: map[string]types.ConfigObjConfig{}, + }, config) + }) + } +} + +func TestLoadMultipleNetworks(t *testing.T) { + networkCases := []struct { + name string + networkBase map[string]interface{} + networkOverride map[string]interface{} + expected map[string]*types.ServiceNetworkConfig + }{ + { + name: "no_override", + networkBase: map[string]interface{}{ + "networks": []interface{}{ + "net1", + "net2", + }, + }, + networkOverride: map[string]interface{}{}, + expected: map[string]*types.ServiceNetworkConfig{ + "net1": nil, + "net2": nil, + }, + }, + { + name: "override_simple", + networkBase: map[string]interface{}{ + "networks": []interface{}{ + "net1", + "net2", + }, + }, + networkOverride: map[string]interface{}{ + "networks": []interface{}{ + "net1", + "net3", + }, + }, + expected: map[string]*types.ServiceNetworkConfig{ + "net1": nil, + "net2": nil, + "net3": nil, + }, + }, + { + name: "override_with_aliases", + networkBase: map[string]interface{}{ + "networks": map[string]interface{}{ + "net1": map[string]interface{}{ + "aliases": []interface{}{ + "alias1", + }, + }, + "net2": nil, + }, + }, + networkOverride: map[string]interface{}{ + "networks": map[string]interface{}{ + "net1": map[string]interface{}{ + "aliases": []interface{}{ + "alias2", + "alias3", + }, + }, + "net3": map[string]interface{}{}, + }, + }, + expected: map[string]*types.ServiceNetworkConfig{ + "net1": { + Aliases: []string{"alias2", "alias3"}, + }, + "net2": nil, + "net3": {}, + }, + }, + } + + for _, tc := range networkCases { + t.Run(tc.name, func(t *testing.T) { + configDetails := types.ConfigDetails{ + ConfigFiles: []types.ConfigFile{ + { + Filename: "base.yml", + Config: map[string]interface{}{ + "version": "3.4", + "services": map[string]interface{}{ + "foo": tc.networkBase, + }, + }, + }, + { + Filename: "override.yml", + Config: map[string]interface{}{ + "version": "3.4", + "services": map[string]interface{}{ + "foo": tc.networkOverride, + }, + }, + }, + }, + } + config, err := Load(configDetails) + require.NoError(t, err) + require.Equal(t, &types.Config{ + Filename: "base.yml", + Services: []types.ServiceConfig{ + { + Name: "foo", + Networks: tc.expected, + Environment: types.MappingWithEquals{}, + }, + }, + Networks: map[string]types.NetworkConfig{}, + Volumes: map[string]types.VolumeConfig{}, + Secrets: map[string]types.SecretConfig{}, + Configs: map[string]types.ConfigObjConfig{}, + }, config) + }) + } +} + +func TestLoadMultipleConfigs(t *testing.T) { + base := map[string]interface{}{ + "version": "3.4", + "services": map[string]interface{}{ + "foo": map[string]interface{}{ + "image": "foo", + "build": map[string]interface{}{ + "context": ".", + "dockerfile": "bar.Dockerfile", + }, + "ports": []interface{}{ + "8080:80", + "9090:90", + }, + "labels": []interface{}{ + "foo=bar", + }, + "cap_add": []interface{}{ + "NET_ADMIN", + }, + }, + }, + "volumes": map[string]interface{}{}, + "networks": map[string]interface{}{}, + "secrets": map[string]interface{}{}, + "configs": map[string]interface{}{}, + } + override := map[string]interface{}{ + "version": "3.4", + "services": map[string]interface{}{ + "foo": map[string]interface{}{ + "image": "baz", + "build": map[string]interface{}{ + "dockerfile": "foo.Dockerfile", + "args": []interface{}{ + "buildno=1", + "password=secret", + }, + }, + "ports": []interface{}{ + map[string]interface{}{ + "target": 81, + "published": 8080, + }, + }, + "labels": map[string]interface{}{ + "foo": "baz", + }, + "cap_add": []interface{}{ + "SYS_ADMIN", + }, + }, + "bar": map[string]interface{}{ + "image": "bar", + }, + }, + "volumes": map[string]interface{}{}, + "networks": map[string]interface{}{}, + "secrets": map[string]interface{}{}, + "configs": map[string]interface{}{}, + } + configDetails := types.ConfigDetails{ + ConfigFiles: []types.ConfigFile{ + {Filename: "base.yml", Config: base}, + {Filename: "override.yml", Config: override}, + }, + } + config, err := Load(configDetails) + require.NoError(t, err) + require.Equal(t, &types.Config{ + Filename: "base.yml", + Services: []types.ServiceConfig{ + { + Name: "bar", + Image: "bar", + Environment: types.MappingWithEquals{}, + }, + { + Name: "foo", + Image: "baz", + Build: types.BuildConfig{ + Context: ".", + Dockerfile: "foo.Dockerfile", + Args: types.MappingWithEquals{ + "buildno": strPtr("1"), + "password": strPtr("secret"), + }, + }, + Ports: []types.ServicePortConfig{ + { + Target: 81, + Published: 8080, + }, + { + Mode: "ingress", + Target: 90, + Published: 9090, + Protocol: "tcp", + }, + }, + Labels: types.Labels{ + "foo": "baz", + }, + CapAdd: []string{"NET_ADMIN", "SYS_ADMIN"}, + Environment: types.MappingWithEquals{}, + }}, + Networks: map[string]types.NetworkConfig{}, + Volumes: map[string]types.VolumeConfig{}, + Secrets: map[string]types.SecretConfig{}, + Configs: map[string]types.ConfigObjConfig{}, + }, config) +} diff --git a/cli/compose/types/types.go b/cli/compose/types/types.go index 6bfc21b23e..8922a6d413 100644 --- a/cli/compose/types/types.go +++ b/cli/compose/types/types.go @@ -69,6 +69,7 @@ func (cd ConfigDetails) LookupEnv(key string) (string, bool) { // Config is a full compose file configuration type Config struct { + Filename string Services []ServiceConfig Networks map[string]NetworkConfig Volumes map[string]VolumeConfig diff --git a/vendor.conf b/vendor.conf index afb6439205..fb8fddd8f6 100755 --- a/vendor.conf +++ b/vendor.conf @@ -36,7 +36,7 @@ github.com/go-openapi/swag 1d0bd113de87027671077d3c71eb3ac5d7dbba72 github.com/gregjones/httpcache c1f8028e62adb3d518b823a2f8e6a95c38bdd3aa github.com/grpc-ecosystem/grpc-gateway 1a03ca3bad1e1ebadaedd3abb76bc58d4ac8143b github.com/howeyc/gopass 3ca23474a7c7203e0a0a070fd33508f6efdb9b3d -github.com/imdario/mergo 6633656539c1639d9d78127b7d47c622b5d7b6dc +github.com/imdario/mergo ea74e0177b4df59af68c076af5008b427d00d40f github.com/inconshreveable/mousetrap 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 github.com/juju/ratelimit 5b9ff866471762aa2ab2dced63c9fb6f53921342 github.com/json-iterator/go 6240e1e7983a85228f7fd9c3e1b6932d46ec58e2 @@ -82,4 +82,3 @@ k8s.io/client-go kubernetes-1.8.2 k8s.io/kubernetes v1.8.2 k8s.io/kube-openapi 61b46af70dfed79c6d24530cd23b41440a7f22a5 vbom.ml/util 928aaa586d7718c70f4090ddf83f2b34c16fdc8d - diff --git a/vendor/github.com/imdario/mergo/README.md b/vendor/github.com/imdario/mergo/README.md index cdcea0f659..36c96ae8e6 100644 --- a/vendor/github.com/imdario/mergo/README.md +++ b/vendor/github.com/imdario/mergo/README.md @@ -2,14 +2,67 @@ A helper to merge structs and maps in Golang. Useful for configuration default values, avoiding messy if-statements. -Also a lovely [comune](http://en.wikipedia.org/wiki/Mergo) (municipality) in the Province of Ancona in the Italian region Marche. - -![Mergo dall'alto](http://www.comune.mergo.an.it/Siti/Mergo/Immagini/Foto/mergo_dall_alto.jpg) +Also a lovely [comune](http://en.wikipedia.org/wiki/Mergo) (municipality) in the Province of Ancona in the Italian region of Marche. ## Status -It is ready for production use. It works fine although it may use more of testing. Here some projects in the wild using Mergo: +It is ready for production use. [It is used in several projects by Docker, Google, The Linux Foundation, VMWare, Shopify, etc](https://github.com/imdario/mergo#mergo-in-the-wild). +[![Build Status][1]][2] +[![GoDoc][3]][4] +[![GoCard][5]][6] +[![Coverage Status][7]][8] + +[1]: https://travis-ci.org/imdario/mergo.png +[2]: https://travis-ci.org/imdario/mergo +[3]: https://godoc.org/github.com/imdario/mergo?status.svg +[4]: https://godoc.org/github.com/imdario/mergo +[5]: https://goreportcard.com/badge/imdario/mergo +[6]: https://goreportcard.com/report/github.com/imdario/mergo +[7]: https://coveralls.io/repos/github/imdario/mergo/badge.svg?branch=master +[8]: https://coveralls.io/github/imdario/mergo?branch=master + +### Latest release + +[Release 0.3.2](https://github.com/imdario/mergo/releases/tag/0.3.2) is an important release because it changes `Merge()`and `Map()` signatures to support [transformers](#transformers). An optional/variadic argument has been added, so it won't break existing code. + +### Important note + +If you were using Mergo **before** April 6th 2015, please check your project works as intended after updating your local copy with ```go get -u github.com/imdario/mergo```. I apologize for any issue caused by its previous behavior and any future bug that Mergo could cause (I hope it won't!) in existing projects after the change (release 0.2.0). + +### Donations + +If Mergo is useful to you, consider buying me a coffe, a beer or making a monthly donation so I can keep building great free software. :heart_eyes: + +Buy Me a Coffee at ko-fi.com +[![Beerpay](https://beerpay.io/imdario/mergo/badge.svg)](https://beerpay.io/imdario/mergo) +[![Beerpay](https://beerpay.io/imdario/mergo/make-wish.svg)](https://beerpay.io/imdario/mergo) +Donate using Liberapay + +### Mergo in the wild + +- [moby/moby](https://github.com/moby/moby) +- [kubernetes/kubernetes](https://github.com/kubernetes/kubernetes) +- [vmware/dispatch](https://github.com/vmware/dispatch) +- [Shopify/themekit](https://github.com/Shopify/themekit) +- [imdario/zas](https://github.com/imdario/zas) +- [matcornic/hermes](https://github.com/matcornic/hermes) +- [OpenBazaar/openbazaar-go](https://github.com/OpenBazaar/openbazaar-go) +- [kataras/iris](https://github.com/kataras/iris) +- [michaelsauter/crane](https://github.com/michaelsauter/crane) +- [go-task/task](https://github.com/go-task/task) +- [sensu/uchiwa](https://github.com/sensu/uchiwa) +- [ory/hydra](https://github.com/ory/hydra) +- [sisatech/vcli](https://github.com/sisatech/vcli) +- [dairycart/dairycart](https://github.com/dairycart/dairycart) +- [projectcalico/felix](https://github.com/projectcalico/felix) +- [resin-os/balena](https://github.com/resin-os/balena) +- [go-kivik/kivik](https://github.com/go-kivik/kivik) +- [Telefonica/govice](https://github.com/Telefonica/govice) +- [supergiant/supergiant](supergiant/supergiant) +- [SergeyTsalkov/brooce](https://github.com/SergeyTsalkov/brooce) +- [soniah/dnsmadeeasy](https://github.com/soniah/dnsmadeeasy) +- [ohsu-comp-bio/funnel](https://github.com/ohsu-comp-bio/funnel) - [EagerIO/Stout](https://github.com/EagerIO/Stout) - [lynndylanhurley/defsynth-api](https://github.com/lynndylanhurley/defsynth-api) - [russross/canvasassignments](https://github.com/russross/canvasassignments) @@ -17,12 +70,17 @@ It is ready for production use. It works fine although it may use more of testin - [casualjim/exeggutor](https://github.com/casualjim/exeggutor) - [divshot/gitling](https://github.com/divshot/gitling) - [RWJMurphy/gorl](https://github.com/RWJMurphy/gorl) - -[![Build Status][1]][2] -[![GoDoc](https://godoc.org/github.com/imdario/mergo?status.svg)](https://godoc.org/github.com/imdario/mergo) - -[1]: https://travis-ci.org/imdario/mergo.png -[2]: https://travis-ci.org/imdario/mergo +- [andrerocker/deploy42](https://github.com/andrerocker/deploy42) +- [elwinar/rambler](https://github.com/elwinar/rambler) +- [tmaiaroto/gopartman](https://github.com/tmaiaroto/gopartman) +- [jfbus/impressionist](https://github.com/jfbus/impressionist) +- [Jmeyering/zealot](https://github.com/Jmeyering/zealot) +- [godep-migrator/rigger-host](https://github.com/godep-migrator/rigger-host) +- [Dronevery/MultiwaySwitch-Go](https://github.com/Dronevery/MultiwaySwitch-Go) +- [thoas/picfit](https://github.com/thoas/picfit) +- [mantasmatelis/whooplist-server](https://github.com/mantasmatelis/whooplist-server) +- [jnuthong/item_search](https://github.com/jnuthong/item_search) +- [bukalapak/snowboard](https://github.com/bukalapak/snowboard) ## Installation @@ -37,23 +95,113 @@ It is ready for production use. It works fine although it may use more of testin You can only merge same-type structs with exported fields initialized as zero value of their type and same-types maps. Mergo won't merge unexported (private) fields but will do recursively any exported one. Also maps will be merged recursively except for structs inside maps (because they are not addressable using Go reflection). - if err := mergo.Merge(&dst, src); err != nil { - // ... - } +```go +if err := mergo.Merge(&dst, src); err != nil { + // ... +} +``` -Additionally, you can map a map[string]interface{} to a struct (and otherwise, from struct to map), following the same restrictions as in Merge(). Keys are capitalized to find each corresponding exported field. +Also, you can merge overwriting values using the transformer `WithOverride`. - if err := mergo.Map(&dst, srcMap); err != nil { - // ... - } +```go +if err := mergo.Merge(&dst, src, WithOverride); err != nil { + // ... +} +``` -Warning: if you map a struct to map, it won't do it recursively. Don't expect Mergo to map struct members of your struct as map[string]interface{}. They will be just assigned as values. +Additionally, you can map a `map[string]interface{}` to a struct (and otherwise, from struct to map), following the same restrictions as in `Merge()`. Keys are capitalized to find each corresponding exported field. + +```go +if err := mergo.Map(&dst, srcMap); err != nil { + // ... +} +``` + +Warning: if you map a struct to map, it won't do it recursively. Don't expect Mergo to map struct members of your struct as `map[string]interface{}`. They will be just assigned as values. More information and examples in [godoc documentation](http://godoc.org/github.com/imdario/mergo). +### Nice example + +```go +package main + +import ( + "fmt" + "github.com/imdario/mergo" +) + +type Foo struct { + A string + B int64 +} + +func main() { + src := Foo{ + A: "one", + B: 2, + } + dest := Foo{ + A: "two", + } + mergo.Merge(&dest, src) + fmt.Println(dest) + // Will print + // {two 2} +} +``` + Note: if test are failing due missing package, please execute: - go get gopkg.in/yaml.v1 + go get gopkg.in/yaml.v2 + +### Transformers + +Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, `time.Time` is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero `time.Time`? + +```go +package main + +import ( + "fmt" + "reflect" + "time" +) + +type timeTransfomer struct { +} + +func (t timeTransfomer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error { + if typ == reflect.TypeOf(time.Time{}) { + return func(dst, src reflect.Value) error { + if dst.CanSet() { + isZero := dst.MethodByName("IsZero") + result := isZero.Call([]reflect.Value{}) + if result[0].Bool() { + dst.Set(src) + } + } + return nil + } + } + return nil +} + +type Snapshot struct { + Time time.Time + // ... +} + +func main() { + src := Snapshot{time.Now()} + dest := Snapshot{} + mergo.Merge(&dest, src, WithTransformers(timeTransfomer{})) + fmt.Println(dest) + // Will print + // { 2018-01-12 01:15:00 +0000 UTC m=+0.000000001 } +} +``` + ## Contact me diff --git a/vendor/github.com/imdario/mergo/map.go b/vendor/github.com/imdario/mergo/map.go index 44361e88be..2098143292 100644 --- a/vendor/github.com/imdario/mergo/map.go +++ b/vendor/github.com/imdario/mergo/map.go @@ -31,7 +31,8 @@ func isExported(field reflect.StructField) bool { // Traverses recursively both values, assigning src's fields values to dst. // The map argument tracks comparisons that have already been seen, which allows // short circuiting on recursive types. -func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err error) { +func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int, config *config) (err error) { + overwrite := config.overwrite if dst.CanAddr() { addr := dst.UnsafeAddr() h := 17 * addr @@ -57,10 +58,17 @@ func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err } fieldName := field.Name fieldName = changeInitialCase(fieldName, unicode.ToLower) - if v, ok := dstMap[fieldName]; !ok || isEmptyValue(reflect.ValueOf(v)) { + if v, ok := dstMap[fieldName]; !ok || (isEmptyValue(reflect.ValueOf(v)) || overwrite) { dstMap[fieldName] = src.Field(i).Interface() } } + case reflect.Ptr: + if dst.IsNil() { + v := reflect.New(dst.Type().Elem()) + dst.Set(v) + } + dst = dst.Elem() + fallthrough case reflect.Struct: srcMap := src.Interface().(map[string]interface{}) for key := range srcMap { @@ -85,21 +93,24 @@ func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err srcKind = reflect.Ptr } } + if !srcElement.IsValid() { continue } if srcKind == dstKind { - if err = deepMerge(dstElement, srcElement, visited, depth+1); err != nil { + if err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil { + return + } + } else if dstKind == reflect.Interface && dstElement.Kind() == reflect.Interface { + if err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil { + return + } + } else if srcKind == reflect.Map { + if err = deepMap(dstElement, srcElement, visited, depth+1, config); err != nil { return } } else { - if srcKind == reflect.Map { - if err = deepMap(dstElement, srcElement, visited, depth+1); err != nil { - return - } - } else { - return fmt.Errorf("type mismatch on %s field: found %v, expected %v", fieldName, srcKind, dstKind) - } + return fmt.Errorf("type mismatch on %s field: found %v, expected %v", fieldName, srcKind, dstKind) } } } @@ -117,18 +128,35 @@ func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err // doesn't apply if dst is a map. // This is separated method from Merge because it is cleaner and it keeps sane // semantics: merging equal types, mapping different (restricted) types. -func Map(dst, src interface{}) error { +func Map(dst, src interface{}, opts ...func(*config)) error { + return _map(dst, src, opts...) +} + +// MapWithOverwrite will do the same as Map except that non-empty dst attributes will be overriden by +// non-empty src attribute values. +// Deprecated: Use Map(…) with WithOverride +func MapWithOverwrite(dst, src interface{}, opts ...func(*config)) error { + return _map(dst, src, append(opts, WithOverride)...) +} + +func _map(dst, src interface{}, opts ...func(*config)) error { var ( vDst, vSrc reflect.Value err error ) + config := &config{} + + for _, opt := range opts { + opt(config) + } + if vDst, vSrc, err = resolveValues(dst, src); err != nil { return err } // To be friction-less, we redirect equal-type arguments // to deepMerge. Only because arguments can be anything. if vSrc.Kind() == vDst.Kind() { - return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0) + return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0, config) } switch vSrc.Kind() { case reflect.Struct: @@ -142,5 +170,5 @@ func Map(dst, src interface{}) error { default: return ErrNotSupported } - return deepMap(vDst, vSrc, make(map[uintptr]*visit), 0) + return deepMap(vDst, vSrc, make(map[uintptr]*visit), 0, config) } diff --git a/vendor/github.com/imdario/mergo/merge.go b/vendor/github.com/imdario/mergo/merge.go index 5d328b1fe7..520ef40bf1 100644 --- a/vendor/github.com/imdario/mergo/merge.go +++ b/vendor/github.com/imdario/mergo/merge.go @@ -8,14 +8,35 @@ package mergo -import ( - "reflect" -) +import "reflect" + +func hasExportedField(dst reflect.Value) (exported bool) { + for i, n := 0, dst.NumField(); i < n; i++ { + field := dst.Type().Field(i) + if field.Anonymous && dst.Field(i).Kind() == reflect.Struct { + exported = exported || hasExportedField(dst.Field(i)) + } else { + exported = exported || len(field.PkgPath) == 0 + } + } + return +} + +type config struct { + overwrite bool + transformers transformers +} + +type transformers interface { + Transformer(reflect.Type) func(dst, src reflect.Value) error +} // Traverses recursively both values, assigning src's fields values to dst. // The map argument tracks comparisons that have already been seen, which allows // short circuiting on recursive types. -func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err error) { +func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, config *config) (err error) { + overwrite := config.overwrite + if !src.IsValid() { return } @@ -32,68 +53,167 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (e // Remember, remember... visited[h] = &visit{addr, typ, seen} } + + if config.transformers != nil && !isEmptyValue(dst) { + if fn := config.transformers.Transformer(dst.Type()); fn != nil { + err = fn(dst, src) + return + } + } + switch dst.Kind() { case reflect.Struct: - for i, n := 0, dst.NumField(); i < n; i++ { - if err = deepMerge(dst.Field(i), src.Field(i), visited, depth+1); err != nil { - return + if hasExportedField(dst) { + for i, n := 0, dst.NumField(); i < n; i++ { + if err = deepMerge(dst.Field(i), src.Field(i), visited, depth+1, config); err != nil { + return + } + } + } else { + if dst.CanSet() && !isEmptyValue(src) && (overwrite || isEmptyValue(dst)) { + dst.Set(src) } } case reflect.Map: + if len(src.MapKeys()) == 0 && !src.IsNil() && len(dst.MapKeys()) == 0 { + dst.Set(reflect.MakeMap(dst.Type())) + return + } for _, key := range src.MapKeys() { srcElement := src.MapIndex(key) if !srcElement.IsValid() { continue } dstElement := dst.MapIndex(key) - switch reflect.TypeOf(srcElement.Interface()).Kind() { - case reflect.Struct: + switch srcElement.Kind() { + case reflect.Chan, reflect.Func, reflect.Map, reflect.Interface, reflect.Slice: + if srcElement.IsNil() { + continue + } fallthrough - case reflect.Map: - if err = deepMerge(dstElement, srcElement, visited, depth+1); err != nil { - return + default: + if !srcElement.CanInterface() { + continue + } + switch reflect.TypeOf(srcElement.Interface()).Kind() { + case reflect.Struct: + fallthrough + case reflect.Ptr: + fallthrough + case reflect.Map: + if err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil { + return + } + case reflect.Slice: + srcSlice := reflect.ValueOf(srcElement.Interface()) + + var dstSlice reflect.Value + if !dstElement.IsValid() || dstElement.IsNil() { + dstSlice = reflect.MakeSlice(srcSlice.Type(), 0, srcSlice.Len()) + } else { + dstSlice = reflect.ValueOf(dstElement.Interface()) + } + + dstSlice = reflect.AppendSlice(dstSlice, srcSlice) + dst.SetMapIndex(key, dstSlice) } } - if !dstElement.IsValid() { + if dstElement.IsValid() && reflect.TypeOf(srcElement.Interface()).Kind() == reflect.Map { + continue + } + + if srcElement.IsValid() && (overwrite || (!dstElement.IsValid() || isEmptyValue(dst))) { + if dst.IsNil() { + dst.Set(reflect.MakeMap(dst.Type())) + } dst.SetMapIndex(key, srcElement) } } + case reflect.Slice: + dst.Set(reflect.AppendSlice(dst, src)) case reflect.Ptr: fallthrough case reflect.Interface: if src.IsNil() { break - } else if dst.IsNil() { - if dst.CanSet() && isEmptyValue(dst) { + } + if src.Kind() != reflect.Interface { + if dst.IsNil() || overwrite { + if dst.CanSet() && (overwrite || isEmptyValue(dst)) { + dst.Set(src) + } + } else if src.Kind() == reflect.Ptr { + if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1, config); err != nil { + return + } + } else if dst.Elem().Type() == src.Type() { + if err = deepMerge(dst.Elem(), src, visited, depth+1, config); err != nil { + return + } + } else { + return ErrDifferentArgumentsTypes + } + break + } + if dst.IsNil() || overwrite { + if dst.CanSet() && (overwrite || isEmptyValue(dst)) { dst.Set(src) } - } else if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1); err != nil { + } else if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1, config); err != nil { return } default: - if dst.CanSet() && !isEmptyValue(src) { + if dst.CanSet() && !isEmptyValue(src) && (overwrite || isEmptyValue(dst)) { dst.Set(src) } } return } -// Merge sets fields' values in dst from src if they have a zero -// value of their type. -// dst and src must be valid same-type structs and dst must be -// a pointer to struct. -// It won't merge unexported (private) fields and will do recursively -// any exported field. -func Merge(dst, src interface{}) error { +// Merge will fill any empty for value type attributes on the dst struct using corresponding +// src attributes if they themselves are not empty. dst and src must be valid same-type structs +// and dst must be a pointer to struct. +// It won't merge unexported (private) fields and will do recursively any exported field. +func Merge(dst, src interface{}, opts ...func(*config)) error { + return merge(dst, src, opts...) +} + +// MergeWithOverwrite will do the same as Merge except that non-empty dst attributes will be overriden by +// non-empty src attribute values. +// Deprecated: use Merge(…) with WithOverride +func MergeWithOverwrite(dst, src interface{}, opts ...func(*config)) error { + return merge(dst, src, append(opts, WithOverride)...) +} + +// WithTransformers adds transformers to merge, allowing to customize the merging of some types. +func WithTransformers(transformers transformers) func(*config) { + return func(config *config) { + config.transformers = transformers + } +} + +// WithOverride will make merge override non-empty dst attributes with non-empty src attributes values. +func WithOverride(config *config) { + config.overwrite = true +} + +func merge(dst, src interface{}, opts ...func(*config)) error { var ( vDst, vSrc reflect.Value err error ) + + config := &config{} + + for _, opt := range opts { + opt(config) + } + if vDst, vSrc, err = resolveValues(dst, src); err != nil { return err } if vDst.Type() != vSrc.Type() { return ErrDifferentArgumentsTypes } - return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0) + return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0, config) } diff --git a/vendor/github.com/imdario/mergo/mergo.go b/vendor/github.com/imdario/mergo/mergo.go index f8a0991ec6..785618cd07 100644 --- a/vendor/github.com/imdario/mergo/mergo.go +++ b/vendor/github.com/imdario/mergo/mergo.go @@ -32,7 +32,7 @@ type visit struct { next *visit } -// From src/pkg/encoding/json. +// From src/pkg/encoding/json/encode.go. func isEmptyValue(v reflect.Value) bool { switch v.Kind() { case reflect.Array, reflect.Map, reflect.Slice, reflect.String: @@ -45,8 +45,10 @@ func isEmptyValue(v reflect.Value) bool { return v.Uint() == 0 case reflect.Float32, reflect.Float64: return v.Float() == 0 - case reflect.Interface, reflect.Ptr: + case reflect.Interface, reflect.Ptr, reflect.Func: return v.IsNil() + case reflect.Invalid: + return true } return false }