Add support for multiple composefile when deploying

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
This commit is contained in:
Vincent Demeester 2017-09-29 14:21:40 +02:00
parent 11dfa23a5d
commit 1872bd802c
No known key found for this signature in database
GPG Key ID: 083CC6FD6EB699A3
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.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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
type Config struct {
Filename string
Services []ServiceConfig
Networks map[string]NetworkConfig
Volumes map[string]VolumeConfig

View File

@ -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
@ -81,4 +81,3 @@ k8s.io/client-go kubernetes-1.8.2
k8s.io/kubernetes v1.8.2
k8s.io/kube-openapi 61b46af70dfed79c6d24530cd23b41440a7f22a5
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.
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:
<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)
- [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

View File

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

View File

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

View File

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