mirror of https://github.com/docker/cli.git
Merge pull request #569 from vdemeester/compose-multiple-version-mergo
Add support for multiple composefile when deploying
This commit is contained in:
commit
25e969c854
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -45,12 +45,18 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) {
|
|||
if len(configDetails.ConfigFiles) < 1 {
|
||||
return nil, errors.Errorf("No files specified")
|
||||
}
|
||||
if len(configDetails.ConfigFiles) > 1 {
|
||||
return nil, errors.Errorf("Multiple files are not yet supported")
|
||||
}
|
||||
|
||||
configDict := getConfigDict(configDetails)
|
||||
configDetails.Version = schema.Version(configDict)
|
||||
configs := []*types.Config{}
|
||||
|
||||
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
|
||||
|
@ -65,7 +71,17 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) {
|
|||
if err := schema.Validate(configDict, configDetails.Version); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return loadSections(configDict, configDetails)
|
||||
|
||||
cfg, err := loadSections(configDict, configDetails)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.Filename = file.Filename
|
||||
|
||||
configs = append(configs, cfg)
|
||||
}
|
||||
|
||||
return merge(configs)
|
||||
}
|
||||
|
||||
func validateForbidden(configDict map[string]interface{}) error {
|
||||
|
@ -142,10 +158,11 @@ 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)) {
|
||||
for _, configDict := range configDicts {
|
||||
for _, service := range getServices(configDict) {
|
||||
serviceDict := service.(map[string]interface{})
|
||||
for _, property := range types.UnsupportedProperties {
|
||||
if _, isSet := serviceDict[property]; isSet {
|
||||
|
@ -153,6 +170,7 @@ func GetUnsupportedProperties(configDetails types.ConfigDetails) []string {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sortedKeys(unsupported)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -36,7 +36,7 @@ github.com/go-openapi/swag 1d0bd113de87027671077d3c71eb3ac5d7dbba72
|
|||
github.com/gregjones/httpcache c1f8028e62adb3d518b823a2f8e6a95c38bdd3aa
|
||||
github.com/grpc-ecosystem/grpc-gateway 1a03ca3bad1e1ebadaedd3abb76bc58d4ac8143b
|
||||
github.com/howeyc/gopass 3ca23474a7c7203e0a0a070fd33508f6efdb9b3d
|
||||
github.com/imdario/mergo 6633656539c1639d9d78127b7d47c622b5d7b6dc
|
||||
github.com/imdario/mergo ea74e0177b4df59af68c076af5008b427d00d40f
|
||||
github.com/inconshreveable/mousetrap 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
|
||||
github.com/juju/ratelimit 5b9ff866471762aa2ab2dced63c9fb6f53921342
|
||||
github.com/json-iterator/go 6240e1e7983a85228f7fd9c3e1b6932d46ec58e2
|
||||
|
@ -82,4 +82,3 @@ k8s.io/client-go kubernetes-1.8.2
|
|||
k8s.io/kubernetes v1.8.2
|
||||
k8s.io/kube-openapi 61b46af70dfed79c6d24530cd23b41440a7f22a5
|
||||
vbom.ml/util 928aaa586d7718c70f4090ddf83f2b34c16fdc8d
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,16 +93,20 @@ 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 srcKind == reflect.Map {
|
||||
if err = deepMap(dstElement, srcElement, visited, depth+1); err != nil {
|
||||
} 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 {
|
||||
|
@ -102,7 +114,6 @@ func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
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 {
|
||||
|
||||
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:
|
||||
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 srcElement.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Map, reflect.Interface, reflect.Slice:
|
||||
if srcElement.IsNil() {
|
||||
continue
|
||||
}
|
||||
fallthrough
|
||||
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); err != nil {
|
||||
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() && 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()))
|
||||
}
|
||||
if !dstElement.IsValid() {
|
||||
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 err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1); err != nil {
|
||||
} 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, 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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue