Merge pull request #569 from vdemeester/compose-multiple-version-mergo

Add support for multiple composefile when deploying
This commit is contained in:
Sebastiaan van Stijn 2018-02-06 00:41:43 -08:00 committed by GitHub
commit 25e969c854
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1638 additions and 117 deletions

View File

@ -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.StringVar(&opts.Bundlefile, "bundle-file", "", "Path to a Distributed Application Bundle file")
flags.SetAnnotation("bundle-file", "experimental", nil) flags.SetAnnotation("bundle-file", "experimental", nil)
flags.SetAnnotation("bundle-file", "swarm", 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.SetAnnotation("compose-file", "version", []string{"1.25"})
flags.BoolVar(&opts.SendRegistryAuth, "with-registry-auth", false, "Send registry authentication details to Swarm agents") flags.BoolVar(&opts.SendRegistryAuth, "with-registry-auth", false, "Send registry authentication details to Swarm agents")
flags.SetAnnotation("with-registry-auth", "swarm", nil) flags.SetAnnotation("with-registry-auth", "swarm", nil)

View File

@ -16,8 +16,8 @@ import (
func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error { func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error {
cmdOut := dockerCli.Out() cmdOut := dockerCli.Out()
// Check arguments // Check arguments
if opts.Composefile == "" { if len(opts.Composefiles) == 0 {
return errors.Errorf("Please specify a Compose file (with --compose-file).") return errors.Errorf("Please specify only one compose file (with --compose-file).")
} }
// Initialize clients // Initialize clients
stacks, err := dockerCli.stacks() stacks, err := dockerCli.stacks()
@ -37,7 +37,7 @@ func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error {
} }
// Parse the compose file // Parse the compose file
stack, cfg, err := LoadStack(opts.Namespace, opts.Composefile) stack, cfg, err := LoadStack(opts.Namespace, opts.Composefiles)
if err != nil { if err != nil {
return err return err
} }

View File

@ -18,10 +18,11 @@ import (
// LoadStack loads a stack from a Compose file, with a given name. // 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 // FIXME(vdemeester) remove this and use cli/compose/loader for both swarm and kubernetes
func LoadStack(name, composeFile string) (*apiv1beta1.Stack, *composetypes.Config, error) { func LoadStack(name string, composeFiles []string) (*apiv1beta1.Stack, *composetypes.Config, error) {
if composeFile == "" { if len(composeFiles) != 1 {
return nil, nil, errors.New("compose-file must be set") return nil, nil, errors.New("compose-file must be set (and only one)")
} }
composeFile := composeFiles[0]
workingDir, err := os.Getwd() workingDir, err := os.Getwd()
if err != nil { if err != nil {

View File

@ -5,7 +5,7 @@ import "github.com/docker/cli/opts"
// Deploy holds docker stack deploy options // Deploy holds docker stack deploy options
type Deploy struct { type Deploy struct {
Bundlefile string Bundlefile string
Composefile string Composefiles []string
Namespace string Namespace string
ResolveImage string ResolveImage string
SendRegistryAuth bool SendRegistryAuth bool

View File

@ -29,9 +29,9 @@ func RunDeploy(dockerCli command.Cli, opts options.Deploy) error {
} }
switch { 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).") 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.") return errors.Errorf("You cannot specify both a bundle file and a Compose file.")
case opts.Bundlefile != "": case opts.Bundlefile != "":
return deployBundle(ctx, dockerCli, opts) return deployBundle(ctx, dockerCli, opts)

View File

@ -24,7 +24,7 @@ import (
) )
func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error { 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 { if err != nil {
return err return err
} }
@ -39,13 +39,14 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Depl
return err return err
} }
unsupportedProperties := loader.GetUnsupportedProperties(configDetails) dicts := getDictsFrom(configDetails.ConfigFiles)
unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
if len(unsupportedProperties) > 0 { if len(unsupportedProperties) > 0 {
fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n", fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n",
strings.Join(unsupportedProperties, ", ")) strings.Join(unsupportedProperties, ", "))
} }
deprecatedProperties := loader.GetDeprecatedProperties(configDetails) deprecatedProperties := loader.GetDeprecatedProperties(dicts...)
if len(deprecatedProperties) > 0 { if len(deprecatedProperties) > 0 {
fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n", fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n",
propertyWarnings(deprecatedProperties)) 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) 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{} { func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
serviceNetworks := map[string]struct{}{} serviceNetworks := map[string]struct{}{}
for _, serviceConfig := range serviceConfigs { for _, serviceConfig := range serviceConfigs {
@ -120,29 +131,32 @@ func propertyWarnings(properties map[string]string) string {
return strings.Join(msgs, "\n\n") 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 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() workingDir, err := os.Getwd()
if err != nil { if err != nil {
return details, err return details, err
} }
details.WorkingDir = workingDir details.WorkingDir = workingDir
} else { } else {
absPath, err := filepath.Abs(composefile) absPath, err := filepath.Abs(composefiles[0])
if err != nil { if err != nil {
return details, err return details, err
} }
details.WorkingDir = filepath.Dir(absPath) details.WorkingDir = filepath.Dir(absPath)
} }
configFile, err := getConfigFile(composefile, stdin) var err error
details.ConfigFiles, err = loadConfigFiles(composefiles, stdin)
if err != nil { if err != nil {
return details, err return details, err
} }
// TODO: support multiple files
details.ConfigFiles = []composetypes.ConfigFile{*configFile}
details.Environment, err = buildEnvironment(os.Environ()) details.Environment, err = buildEnvironment(os.Environ())
return details, err return details, err
} }
@ -160,7 +174,21 @@ func buildEnvironment(env []string) (map[string]string, error) {
return result, nil 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 bytes []byte
var err error var err error

View File

@ -26,7 +26,7 @@ services:
file := fs.NewFile(t, "test-get-config-details", fs.WithContent(content)) file := fs.NewFile(t, "test-get-config-details", fs.WithContent(content))
defer file.Remove() defer file.Remove()
details, err := getConfigDetails(file.Path(), nil) details, err := getConfigDetails([]string{file.Path()}, nil)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, filepath.Dir(file.Path()), details.WorkingDir) assert.Equal(t, filepath.Dir(file.Path()), details.WorkingDir)
require.Len(t, details.ConfigFiles, 1) require.Len(t, details.ConfigFiles, 1)
@ -41,7 +41,7 @@ services:
foo: foo:
image: alpine:3.5 image: alpine:3.5
` `
details, err := getConfigDetails("-", strings.NewReader(content)) details, err := getConfigDetails([]string{"-"}, strings.NewReader(content))
require.NoError(t, err) require.NoError(t, err)
cwd, err := os.Getwd() cwd, err := os.Getwd()
require.NoError(t, err) require.NoError(t, err)

View File

@ -45,27 +45,43 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) {
if len(configDetails.ConfigFiles) < 1 { if len(configDetails.ConfigFiles) < 1 {
return nil, errors.Errorf("No files specified") 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) return merge(configs)
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)
} }
func validateForbidden(configDict map[string]interface{}) error { 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 // GetUnsupportedProperties returns the list of any unsupported properties that are
// used in the Compose files. // used in the Compose files.
func GetUnsupportedProperties(configDetails types.ConfigDetails) []string { func GetUnsupportedProperties(configDicts ...map[string]interface{}) []string {
unsupported := map[string]bool{} unsupported := map[string]bool{}
for _, service := range getServices(getConfigDict(configDetails)) { for _, configDict := range configDicts {
serviceDict := service.(map[string]interface{}) for _, service := range getServices(configDict) {
for _, property := range types.UnsupportedProperties { serviceDict := service.(map[string]interface{})
if _, isSet := serviceDict[property]; isSet { for _, property := range types.UnsupportedProperties {
unsupported[property] = true 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 // GetDeprecatedProperties returns the list of any deprecated properties that
// are used in the compose files. // are used in the compose files.
func GetDeprecatedProperties(configDetails types.ConfigDetails) map[string]string { func GetDeprecatedProperties(configDicts ...map[string]interface{}) map[string]string {
return getProperties(getServices(getConfigDict(configDetails)), types.DeprecatedProperties) 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 { 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" 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{} { func getServices(configDict map[string]interface{}) map[string]interface{} {
if services, ok := configDict["services"]; ok { if services, ok := configDict["services"]; ok {
if servicesDict, ok := services.(map[string]interface{}); ok { if servicesDict, ok := services.(map[string]interface{}); ok {

View File

@ -572,6 +572,7 @@ networks:
config, err := Load(buildConfigDetails(dict, env)) config, err := Load(buildConfigDetails(dict, env))
require.NoError(t, err) require.NoError(t, err)
expected := &types.Config{ expected := &types.Config{
Filename: "filename.yml",
Services: []types.ServiceConfig{ Services: []types.ServiceConfig{
{ {
Name: "web", Name: "web",
@ -670,7 +671,7 @@ services:
_, err = Load(configDetails) _, err = Load(configDetails)
require.NoError(t, err) require.NoError(t, err)
unsupported := GetUnsupportedProperties(configDetails) unsupported := GetUnsupportedProperties(dict)
assert.Equal(t, []string{"build", "links", "pid"}, unsupported) assert.Equal(t, []string{"build", "links", "pid"}, unsupported)
} }
@ -713,7 +714,7 @@ services:
_, err = Load(configDetails) _, err = Load(configDetails)
require.NoError(t, err) require.NoError(t, err)
deprecated := GetDeprecatedProperties(configDetails) deprecated := GetDeprecatedProperties(dict)
assert.Len(t, deprecated, 2) assert.Len(t, deprecated, 2)
assert.Contains(t, deprecated, "container_name") assert.Contains(t, deprecated, "container_name")
assert.Contains(t, deprecated, "expose") assert.Contains(t, deprecated, "expose")

233
cli/compose/loader/merge.go Normal file
View File

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

View File

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

View File

@ -69,6 +69,7 @@ func (cd ConfigDetails) LookupEnv(key string) (string, bool) {
// Config is a full compose file configuration // Config is a full compose file configuration
type Config struct { type Config struct {
Filename string
Services []ServiceConfig Services []ServiceConfig
Networks map[string]NetworkConfig Networks map[string]NetworkConfig
Volumes map[string]VolumeConfig Volumes map[string]VolumeConfig

View File

@ -36,7 +36,7 @@ github.com/go-openapi/swag 1d0bd113de87027671077d3c71eb3ac5d7dbba72
github.com/gregjones/httpcache c1f8028e62adb3d518b823a2f8e6a95c38bdd3aa github.com/gregjones/httpcache c1f8028e62adb3d518b823a2f8e6a95c38bdd3aa
github.com/grpc-ecosystem/grpc-gateway 1a03ca3bad1e1ebadaedd3abb76bc58d4ac8143b github.com/grpc-ecosystem/grpc-gateway 1a03ca3bad1e1ebadaedd3abb76bc58d4ac8143b
github.com/howeyc/gopass 3ca23474a7c7203e0a0a070fd33508f6efdb9b3d github.com/howeyc/gopass 3ca23474a7c7203e0a0a070fd33508f6efdb9b3d
github.com/imdario/mergo 6633656539c1639d9d78127b7d47c622b5d7b6dc github.com/imdario/mergo ea74e0177b4df59af68c076af5008b427d00d40f
github.com/inconshreveable/mousetrap 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 github.com/inconshreveable/mousetrap 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
github.com/juju/ratelimit 5b9ff866471762aa2ab2dced63c9fb6f53921342 github.com/juju/ratelimit 5b9ff866471762aa2ab2dced63c9fb6f53921342
github.com/json-iterator/go 6240e1e7983a85228f7fd9c3e1b6932d46ec58e2 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/kubernetes v1.8.2
k8s.io/kube-openapi 61b46af70dfed79c6d24530cd23b41440a7f22a5 k8s.io/kube-openapi 61b46af70dfed79c6d24530cd23b41440a7f22a5
vbom.ml/util 928aaa586d7718c70f4090ddf83f2b34c16fdc8d vbom.ml/util 928aaa586d7718c70f4090ddf83f2b34c16fdc8d

View File

@ -2,14 +2,67 @@
A helper to merge structs and maps in Golang. Useful for configuration default values, avoiding messy if-statements. 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. Also a lovely [comune](http://en.wikipedia.org/wiki/Mergo) (municipality) in the Province of Ancona in the Italian region of Marche.
![Mergo dall'alto](http://www.comune.mergo.an.it/Siti/Mergo/Immagini/Foto/mergo_dall_alto.jpg)
## Status ## 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:
<a href='https://ko-fi.com/B0B58839' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://az743702.vo.msecnd.net/cdn/kofi1.png?v=0' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
[![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)
<a href="https://liberapay.com/dario/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a>
### 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) - [EagerIO/Stout](https://github.com/EagerIO/Stout)
- [lynndylanhurley/defsynth-api](https://github.com/lynndylanhurley/defsynth-api) - [lynndylanhurley/defsynth-api](https://github.com/lynndylanhurley/defsynth-api)
- [russross/canvasassignments](https://github.com/russross/canvasassignments) - [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) - [casualjim/exeggutor](https://github.com/casualjim/exeggutor)
- [divshot/gitling](https://github.com/divshot/gitling) - [divshot/gitling](https://github.com/divshot/gitling)
- [RWJMurphy/gorl](https://github.com/RWJMurphy/gorl) - [RWJMurphy/gorl](https://github.com/RWJMurphy/gorl)
- [andrerocker/deploy42](https://github.com/andrerocker/deploy42)
[![Build Status][1]][2] - [elwinar/rambler](https://github.com/elwinar/rambler)
[![GoDoc](https://godoc.org/github.com/imdario/mergo?status.svg)](https://godoc.org/github.com/imdario/mergo) - [tmaiaroto/gopartman](https://github.com/tmaiaroto/gopartman)
- [jfbus/impressionist](https://github.com/jfbus/impressionist)
[1]: https://travis-ci.org/imdario/mergo.png - [Jmeyering/zealot](https://github.com/Jmeyering/zealot)
[2]: https://travis-ci.org/imdario/mergo - [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 ## 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). 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). 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: 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 ## Contact me

View File

@ -31,7 +31,8 @@ func isExported(field reflect.StructField) bool {
// Traverses recursively both values, assigning src's fields values to dst. // Traverses recursively both values, assigning src's fields values to dst.
// The map argument tracks comparisons that have already been seen, which allows // The map argument tracks comparisons that have already been seen, which allows
// short circuiting on recursive types. // 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() { if dst.CanAddr() {
addr := dst.UnsafeAddr() addr := dst.UnsafeAddr()
h := 17 * addr h := 17 * addr
@ -57,10 +58,17 @@ func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err
} }
fieldName := field.Name fieldName := field.Name
fieldName = changeInitialCase(fieldName, unicode.ToLower) 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() 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: case reflect.Struct:
srcMap := src.Interface().(map[string]interface{}) srcMap := src.Interface().(map[string]interface{})
for key := range srcMap { for key := range srcMap {
@ -85,21 +93,24 @@ func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err
srcKind = reflect.Ptr srcKind = reflect.Ptr
} }
} }
if !srcElement.IsValid() { if !srcElement.IsValid() {
continue continue
} }
if srcKind == dstKind { 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 return
} }
} else { } else {
if srcKind == reflect.Map { return fmt.Errorf("type mismatch on %s field: found %v, expected %v", fieldName, srcKind, dstKind)
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)
}
} }
} }
} }
@ -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. // doesn't apply if dst is a map.
// This is separated method from Merge because it is cleaner and it keeps sane // This is separated method from Merge because it is cleaner and it keeps sane
// semantics: merging equal types, mapping different (restricted) types. // 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 ( var (
vDst, vSrc reflect.Value vDst, vSrc reflect.Value
err error err error
) )
config := &config{}
for _, opt := range opts {
opt(config)
}
if vDst, vSrc, err = resolveValues(dst, src); err != nil { if vDst, vSrc, err = resolveValues(dst, src); err != nil {
return err return err
} }
// To be friction-less, we redirect equal-type arguments // To be friction-less, we redirect equal-type arguments
// to deepMerge. Only because arguments can be anything. // to deepMerge. Only because arguments can be anything.
if vSrc.Kind() == vDst.Kind() { 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() { switch vSrc.Kind() {
case reflect.Struct: case reflect.Struct:
@ -142,5 +170,5 @@ func Map(dst, src interface{}) error {
default: default:
return ErrNotSupported return ErrNotSupported
} }
return deepMap(vDst, vSrc, make(map[uintptr]*visit), 0) return deepMap(vDst, vSrc, make(map[uintptr]*visit), 0, config)
} }

View File

@ -8,14 +8,35 @@
package mergo package mergo
import ( import "reflect"
"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. // Traverses recursively both values, assigning src's fields values to dst.
// The map argument tracks comparisons that have already been seen, which allows // The map argument tracks comparisons that have already been seen, which allows
// short circuiting on recursive types. // 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() { if !src.IsValid() {
return return
} }
@ -32,68 +53,167 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (e
// Remember, remember... // Remember, remember...
visited[h] = &visit{addr, typ, seen} 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() { switch dst.Kind() {
case reflect.Struct: case reflect.Struct:
for i, n := 0, dst.NumField(); i < n; i++ { if hasExportedField(dst) {
if err = deepMerge(dst.Field(i), src.Field(i), visited, depth+1); err != nil { for i, n := 0, dst.NumField(); i < n; i++ {
return 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: 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() { for _, key := range src.MapKeys() {
srcElement := src.MapIndex(key) srcElement := src.MapIndex(key)
if !srcElement.IsValid() { if !srcElement.IsValid() {
continue continue
} }
dstElement := dst.MapIndex(key) dstElement := dst.MapIndex(key)
switch reflect.TypeOf(srcElement.Interface()).Kind() { switch srcElement.Kind() {
case reflect.Struct: case reflect.Chan, reflect.Func, reflect.Map, reflect.Interface, reflect.Slice:
if srcElement.IsNil() {
continue
}
fallthrough fallthrough
case reflect.Map: default:
if err = deepMerge(dstElement, srcElement, visited, depth+1); err != nil { if !srcElement.CanInterface() {
return 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) dst.SetMapIndex(key, srcElement)
} }
} }
case reflect.Slice:
dst.Set(reflect.AppendSlice(dst, src))
case reflect.Ptr: case reflect.Ptr:
fallthrough fallthrough
case reflect.Interface: case reflect.Interface:
if src.IsNil() { if src.IsNil() {
break 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) 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 return
} }
default: default:
if dst.CanSet() && !isEmptyValue(src) { if dst.CanSet() && !isEmptyValue(src) && (overwrite || isEmptyValue(dst)) {
dst.Set(src) dst.Set(src)
} }
} }
return return
} }
// Merge sets fields' values in dst from src if they have a zero // Merge will fill any empty for value type attributes on the dst struct using corresponding
// value of their type. // src attributes if they themselves are not empty. dst and src must be valid same-type structs
// dst and src must be valid same-type structs and dst must be // and dst must be a pointer to struct.
// a pointer to struct. // It won't merge unexported (private) fields and will do recursively any exported field.
// It won't merge unexported (private) fields and will do recursively func Merge(dst, src interface{}, opts ...func(*config)) error {
// any exported field. return merge(dst, src, opts...)
func Merge(dst, src interface{}) error { }
// 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 ( var (
vDst, vSrc reflect.Value vDst, vSrc reflect.Value
err error err error
) )
config := &config{}
for _, opt := range opts {
opt(config)
}
if vDst, vSrc, err = resolveValues(dst, src); err != nil { if vDst, vSrc, err = resolveValues(dst, src); err != nil {
return err return err
} }
if vDst.Type() != vSrc.Type() { if vDst.Type() != vSrc.Type() {
return ErrDifferentArgumentsTypes return ErrDifferentArgumentsTypes
} }
return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0) return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0, config)
} }

View File

@ -32,7 +32,7 @@ type visit struct {
next *visit next *visit
} }
// From src/pkg/encoding/json. // From src/pkg/encoding/json/encode.go.
func isEmptyValue(v reflect.Value) bool { func isEmptyValue(v reflect.Value) bool {
switch v.Kind() { switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String: case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
@ -45,8 +45,10 @@ func isEmptyValue(v reflect.Value) bool {
return v.Uint() == 0 return v.Uint() == 0
case reflect.Float32, reflect.Float64: case reflect.Float32, reflect.Float64:
return v.Float() == 0 return v.Float() == 0
case reflect.Interface, reflect.Ptr: case reflect.Interface, reflect.Ptr, reflect.Func:
return v.IsNil() return v.IsNil()
case reflect.Invalid:
return true
} }
return false return false
} }