mirror of https://github.com/docker/cli.git
Add support for multiple composefile when deploying
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
This commit is contained in:
parent
11dfa23a5d
commit
1872bd802c
|
@ -34,7 +34,7 @@ func newDeployCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
flags.StringVar(&opts.Bundlefile, "bundle-file", "", "Path to a Distributed Application Bundle file")
|
flags.StringVar(&opts.Bundlefile, "bundle-file", "", "Path to a Distributed Application Bundle file")
|
||||||
flags.SetAnnotation("bundle-file", "experimental", nil)
|
flags.SetAnnotation("bundle-file", "experimental", nil)
|
||||||
flags.SetAnnotation("bundle-file", "swarm", nil)
|
flags.SetAnnotation("bundle-file", "swarm", nil)
|
||||||
flags.StringVarP(&opts.Composefile, "compose-file", "c", "", "Path to a Compose file")
|
flags.StringSliceVarP(&opts.Composefiles, "compose-file", "c", []string{}, "Path to a Compose file")
|
||||||
flags.SetAnnotation("compose-file", "version", []string{"1.25"})
|
flags.SetAnnotation("compose-file", "version", []string{"1.25"})
|
||||||
flags.BoolVar(&opts.SendRegistryAuth, "with-registry-auth", false, "Send registry authentication details to Swarm agents")
|
flags.BoolVar(&opts.SendRegistryAuth, "with-registry-auth", false, "Send registry authentication details to Swarm agents")
|
||||||
flags.SetAnnotation("with-registry-auth", "swarm", nil)
|
flags.SetAnnotation("with-registry-auth", "swarm", nil)
|
||||||
|
|
|
@ -16,8 +16,8 @@ import (
|
||||||
func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error {
|
func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error {
|
||||||
cmdOut := dockerCli.Out()
|
cmdOut := dockerCli.Out()
|
||||||
// Check arguments
|
// Check arguments
|
||||||
if opts.Composefile == "" {
|
if len(opts.Composefiles) == 0 {
|
||||||
return errors.Errorf("Please specify a Compose file (with --compose-file).")
|
return errors.Errorf("Please specify only one compose file (with --compose-file).")
|
||||||
}
|
}
|
||||||
// Initialize clients
|
// Initialize clients
|
||||||
stacks, err := dockerCli.stacks()
|
stacks, err := dockerCli.stacks()
|
||||||
|
@ -37,7 +37,7 @@ func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the compose file
|
// Parse the compose file
|
||||||
stack, cfg, err := LoadStack(opts.Namespace, opts.Composefile)
|
stack, cfg, err := LoadStack(opts.Namespace, opts.Composefiles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,10 +18,11 @@ import (
|
||||||
|
|
||||||
// LoadStack loads a stack from a Compose file, with a given name.
|
// LoadStack loads a stack from a Compose file, with a given name.
|
||||||
// FIXME(vdemeester) remove this and use cli/compose/loader for both swarm and kubernetes
|
// FIXME(vdemeester) remove this and use cli/compose/loader for both swarm and kubernetes
|
||||||
func LoadStack(name, composeFile string) (*apiv1beta1.Stack, *composetypes.Config, error) {
|
func LoadStack(name string, composeFiles []string) (*apiv1beta1.Stack, *composetypes.Config, error) {
|
||||||
if composeFile == "" {
|
if len(composeFiles) != 1 {
|
||||||
return nil, nil, errors.New("compose-file must be set")
|
return nil, nil, errors.New("compose-file must be set (and only one)")
|
||||||
}
|
}
|
||||||
|
composeFile := composeFiles[0]
|
||||||
|
|
||||||
workingDir, err := os.Getwd()
|
workingDir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import "github.com/docker/cli/opts"
|
||||||
// Deploy holds docker stack deploy options
|
// Deploy holds docker stack deploy options
|
||||||
type Deploy struct {
|
type Deploy struct {
|
||||||
Bundlefile string
|
Bundlefile string
|
||||||
Composefile string
|
Composefiles []string
|
||||||
Namespace string
|
Namespace string
|
||||||
ResolveImage string
|
ResolveImage string
|
||||||
SendRegistryAuth bool
|
SendRegistryAuth bool
|
||||||
|
|
|
@ -29,9 +29,9 @@ func RunDeploy(dockerCli command.Cli, opts options.Deploy) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case opts.Bundlefile == "" && opts.Composefile == "":
|
case opts.Bundlefile == "" && len(opts.Composefiles) == 0:
|
||||||
return errors.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).")
|
return errors.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).")
|
||||||
case opts.Bundlefile != "" && opts.Composefile != "":
|
case opts.Bundlefile != "" && len(opts.Composefiles) != 0:
|
||||||
return errors.Errorf("You cannot specify both a bundle file and a Compose file.")
|
return errors.Errorf("You cannot specify both a bundle file and a Compose file.")
|
||||||
case opts.Bundlefile != "":
|
case opts.Bundlefile != "":
|
||||||
return deployBundle(ctx, dockerCli, opts)
|
return deployBundle(ctx, dockerCli, opts)
|
||||||
|
|
|
@ -24,7 +24,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error {
|
func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error {
|
||||||
configDetails, err := getConfigDetails(opts.Composefile, dockerCli.In())
|
configDetails, err := getConfigDetails(opts.Composefiles, dockerCli.In())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -39,13 +39,14 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Depl
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
unsupportedProperties := loader.GetUnsupportedProperties(configDetails)
|
dicts := getDictsFrom(configDetails.ConfigFiles)
|
||||||
|
unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
|
||||||
if len(unsupportedProperties) > 0 {
|
if len(unsupportedProperties) > 0 {
|
||||||
fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n",
|
fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n",
|
||||||
strings.Join(unsupportedProperties, ", "))
|
strings.Join(unsupportedProperties, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
deprecatedProperties := loader.GetDeprecatedProperties(configDetails)
|
deprecatedProperties := loader.GetDeprecatedProperties(dicts...)
|
||||||
if len(deprecatedProperties) > 0 {
|
if len(deprecatedProperties) > 0 {
|
||||||
fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n",
|
fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n",
|
||||||
propertyWarnings(deprecatedProperties))
|
propertyWarnings(deprecatedProperties))
|
||||||
|
@ -97,6 +98,16 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Depl
|
||||||
return deployServices(ctx, dockerCli, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
|
return deployServices(ctx, dockerCli, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getDictsFrom(configFiles []composetypes.ConfigFile) []map[string]interface{} {
|
||||||
|
dicts := []map[string]interface{}{}
|
||||||
|
|
||||||
|
for _, configFile := range configFiles {
|
||||||
|
dicts = append(dicts, configFile.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dicts
|
||||||
|
}
|
||||||
|
|
||||||
func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
|
func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
|
||||||
serviceNetworks := map[string]struct{}{}
|
serviceNetworks := map[string]struct{}{}
|
||||||
for _, serviceConfig := range serviceConfigs {
|
for _, serviceConfig := range serviceConfigs {
|
||||||
|
@ -120,29 +131,32 @@ func propertyWarnings(properties map[string]string) string {
|
||||||
return strings.Join(msgs, "\n\n")
|
return strings.Join(msgs, "\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConfigDetails(composefile string, stdin io.Reader) (composetypes.ConfigDetails, error) {
|
func getConfigDetails(composefiles []string, stdin io.Reader) (composetypes.ConfigDetails, error) {
|
||||||
var details composetypes.ConfigDetails
|
var details composetypes.ConfigDetails
|
||||||
|
|
||||||
if composefile == "-" {
|
if len(composefiles) == 0 {
|
||||||
|
return details, errors.New("no composefile(s)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if composefiles[0] == "-" && len(composefiles) == 1 {
|
||||||
workingDir, err := os.Getwd()
|
workingDir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return details, err
|
return details, err
|
||||||
}
|
}
|
||||||
details.WorkingDir = workingDir
|
details.WorkingDir = workingDir
|
||||||
} else {
|
} else {
|
||||||
absPath, err := filepath.Abs(composefile)
|
absPath, err := filepath.Abs(composefiles[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return details, err
|
return details, err
|
||||||
}
|
}
|
||||||
details.WorkingDir = filepath.Dir(absPath)
|
details.WorkingDir = filepath.Dir(absPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
configFile, err := getConfigFile(composefile, stdin)
|
var err error
|
||||||
|
details.ConfigFiles, err = loadConfigFiles(composefiles, stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return details, err
|
return details, err
|
||||||
}
|
}
|
||||||
// TODO: support multiple files
|
|
||||||
details.ConfigFiles = []composetypes.ConfigFile{*configFile}
|
|
||||||
details.Environment, err = buildEnvironment(os.Environ())
|
details.Environment, err = buildEnvironment(os.Environ())
|
||||||
return details, err
|
return details, err
|
||||||
}
|
}
|
||||||
|
@ -160,7 +174,21 @@ func buildEnvironment(env []string) (map[string]string, error) {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConfigFile(filename string, stdin io.Reader) (*composetypes.ConfigFile, error) {
|
func loadConfigFiles(filenames []string, stdin io.Reader) ([]composetypes.ConfigFile, error) {
|
||||||
|
var configFiles []composetypes.ConfigFile
|
||||||
|
|
||||||
|
for _, filename := range filenames {
|
||||||
|
configFile, err := loadConfigFile(filename, stdin)
|
||||||
|
if err != nil {
|
||||||
|
return configFiles, err
|
||||||
|
}
|
||||||
|
configFiles = append(configFiles, *configFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return configFiles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfigFile(filename string, stdin io.Reader) (*composetypes.ConfigFile, error) {
|
||||||
var bytes []byte
|
var bytes []byte
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ services:
|
||||||
file := fs.NewFile(t, "test-get-config-details", fs.WithContent(content))
|
file := fs.NewFile(t, "test-get-config-details", fs.WithContent(content))
|
||||||
defer file.Remove()
|
defer file.Remove()
|
||||||
|
|
||||||
details, err := getConfigDetails(file.Path(), nil)
|
details, err := getConfigDetails([]string{file.Path()}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, filepath.Dir(file.Path()), details.WorkingDir)
|
assert.Equal(t, filepath.Dir(file.Path()), details.WorkingDir)
|
||||||
require.Len(t, details.ConfigFiles, 1)
|
require.Len(t, details.ConfigFiles, 1)
|
||||||
|
@ -41,7 +41,7 @@ services:
|
||||||
foo:
|
foo:
|
||||||
image: alpine:3.5
|
image: alpine:3.5
|
||||||
`
|
`
|
||||||
details, err := getConfigDetails("-", strings.NewReader(content))
|
details, err := getConfigDetails([]string{"-"}, strings.NewReader(content))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -45,27 +45,43 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) {
|
||||||
if len(configDetails.ConfigFiles) < 1 {
|
if len(configDetails.ConfigFiles) < 1 {
|
||||||
return nil, errors.Errorf("No files specified")
|
return nil, errors.Errorf("No files specified")
|
||||||
}
|
}
|
||||||
if len(configDetails.ConfigFiles) > 1 {
|
|
||||||
return nil, errors.Errorf("Multiple files are not yet supported")
|
configs := []*types.Config{}
|
||||||
|
|
||||||
|
for _, file := range configDetails.ConfigFiles {
|
||||||
|
configDict := file.Config
|
||||||
|
version := schema.Version(configDict)
|
||||||
|
if configDetails.Version == "" {
|
||||||
|
configDetails.Version = version
|
||||||
|
}
|
||||||
|
if configDetails.Version != version {
|
||||||
|
return nil, errors.Errorf("version mismatched between two composefiles : %v and %v", configDetails.Version, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateForbidden(configDict); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
configDict, err = interpolateConfig(configDict, configDetails.LookupEnv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := schema.Validate(configDict, configDetails.Version); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := loadSections(configDict, configDetails)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cfg.Filename = file.Filename
|
||||||
|
|
||||||
|
configs = append(configs, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
configDict := getConfigDict(configDetails)
|
return merge(configs)
|
||||||
configDetails.Version = schema.Version(configDict)
|
|
||||||
|
|
||||||
if err := validateForbidden(configDict); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
configDict, err = interpolateConfig(configDict, configDetails.LookupEnv)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := schema.Validate(configDict, configDetails.Version); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return loadSections(configDict, configDetails)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateForbidden(configDict map[string]interface{}) error {
|
func validateForbidden(configDict map[string]interface{}) error {
|
||||||
|
@ -142,14 +158,16 @@ func getSection(config map[string]interface{}, key string) map[string]interface{
|
||||||
|
|
||||||
// GetUnsupportedProperties returns the list of any unsupported properties that are
|
// GetUnsupportedProperties returns the list of any unsupported properties that are
|
||||||
// used in the Compose files.
|
// used in the Compose files.
|
||||||
func GetUnsupportedProperties(configDetails types.ConfigDetails) []string {
|
func GetUnsupportedProperties(configDicts ...map[string]interface{}) []string {
|
||||||
unsupported := map[string]bool{}
|
unsupported := map[string]bool{}
|
||||||
|
|
||||||
for _, service := range getServices(getConfigDict(configDetails)) {
|
for _, configDict := range configDicts {
|
||||||
serviceDict := service.(map[string]interface{})
|
for _, service := range getServices(configDict) {
|
||||||
for _, property := range types.UnsupportedProperties {
|
serviceDict := service.(map[string]interface{})
|
||||||
if _, isSet := serviceDict[property]; isSet {
|
for _, property := range types.UnsupportedProperties {
|
||||||
unsupported[property] = true
|
if _, isSet := serviceDict[property]; isSet {
|
||||||
|
unsupported[property] = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -168,8 +186,17 @@ func sortedKeys(set map[string]bool) []string {
|
||||||
|
|
||||||
// GetDeprecatedProperties returns the list of any deprecated properties that
|
// GetDeprecatedProperties returns the list of any deprecated properties that
|
||||||
// are used in the compose files.
|
// are used in the compose files.
|
||||||
func GetDeprecatedProperties(configDetails types.ConfigDetails) map[string]string {
|
func GetDeprecatedProperties(configDicts ...map[string]interface{}) map[string]string {
|
||||||
return getProperties(getServices(getConfigDict(configDetails)), types.DeprecatedProperties)
|
deprecated := map[string]string{}
|
||||||
|
|
||||||
|
for _, configDict := range configDicts {
|
||||||
|
deprecatedProperties := getProperties(getServices(configDict), types.DeprecatedProperties)
|
||||||
|
for key, value := range deprecatedProperties {
|
||||||
|
deprecated[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deprecated
|
||||||
}
|
}
|
||||||
|
|
||||||
func getProperties(services map[string]interface{}, propertyMap map[string]string) map[string]string {
|
func getProperties(services map[string]interface{}, propertyMap map[string]string) map[string]string {
|
||||||
|
@ -198,11 +225,6 @@ func (e *ForbiddenPropertiesError) Error() string {
|
||||||
return "Configuration contains forbidden properties"
|
return "Configuration contains forbidden properties"
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: resolve multiple files into a single config
|
|
||||||
func getConfigDict(configDetails types.ConfigDetails) map[string]interface{} {
|
|
||||||
return configDetails.ConfigFiles[0].Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func getServices(configDict map[string]interface{}) map[string]interface{} {
|
func getServices(configDict map[string]interface{}) map[string]interface{} {
|
||||||
if services, ok := configDict["services"]; ok {
|
if services, ok := configDict["services"]; ok {
|
||||||
if servicesDict, ok := services.(map[string]interface{}); ok {
|
if servicesDict, ok := services.(map[string]interface{}); ok {
|
||||||
|
|
|
@ -572,6 +572,7 @@ networks:
|
||||||
config, err := Load(buildConfigDetails(dict, env))
|
config, err := Load(buildConfigDetails(dict, env))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
expected := &types.Config{
|
expected := &types.Config{
|
||||||
|
Filename: "filename.yml",
|
||||||
Services: []types.ServiceConfig{
|
Services: []types.ServiceConfig{
|
||||||
{
|
{
|
||||||
Name: "web",
|
Name: "web",
|
||||||
|
@ -670,7 +671,7 @@ services:
|
||||||
_, err = Load(configDetails)
|
_, err = Load(configDetails)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
unsupported := GetUnsupportedProperties(configDetails)
|
unsupported := GetUnsupportedProperties(dict)
|
||||||
assert.Equal(t, []string{"build", "links", "pid"}, unsupported)
|
assert.Equal(t, []string{"build", "links", "pid"}, unsupported)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -713,7 +714,7 @@ services:
|
||||||
_, err = Load(configDetails)
|
_, err = Load(configDetails)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
deprecated := GetDeprecatedProperties(configDetails)
|
deprecated := GetDeprecatedProperties(dict)
|
||||||
assert.Len(t, deprecated, 2)
|
assert.Len(t, deprecated, 2)
|
||||||
assert.Contains(t, deprecated, "container_name")
|
assert.Contains(t, deprecated, "container_name")
|
||||||
assert.Contains(t, deprecated, "expose")
|
assert.Contains(t, deprecated, "expose")
|
||||||
|
|
|
@ -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
|
// Config is a full compose file configuration
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
Filename string
|
||||||
Services []ServiceConfig
|
Services []ServiceConfig
|
||||||
Networks map[string]NetworkConfig
|
Networks map[string]NetworkConfig
|
||||||
Volumes map[string]VolumeConfig
|
Volumes map[string]VolumeConfig
|
||||||
|
|
|
@ -36,7 +36,7 @@ github.com/go-openapi/swag 1d0bd113de87027671077d3c71eb3ac5d7dbba72
|
||||||
github.com/gregjones/httpcache c1f8028e62adb3d518b823a2f8e6a95c38bdd3aa
|
github.com/gregjones/httpcache c1f8028e62adb3d518b823a2f8e6a95c38bdd3aa
|
||||||
github.com/grpc-ecosystem/grpc-gateway 1a03ca3bad1e1ebadaedd3abb76bc58d4ac8143b
|
github.com/grpc-ecosystem/grpc-gateway 1a03ca3bad1e1ebadaedd3abb76bc58d4ac8143b
|
||||||
github.com/howeyc/gopass 3ca23474a7c7203e0a0a070fd33508f6efdb9b3d
|
github.com/howeyc/gopass 3ca23474a7c7203e0a0a070fd33508f6efdb9b3d
|
||||||
github.com/imdario/mergo 6633656539c1639d9d78127b7d47c622b5d7b6dc
|
github.com/imdario/mergo ea74e0177b4df59af68c076af5008b427d00d40f
|
||||||
github.com/inconshreveable/mousetrap 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
|
github.com/inconshreveable/mousetrap 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
|
||||||
github.com/juju/ratelimit 5b9ff866471762aa2ab2dced63c9fb6f53921342
|
github.com/juju/ratelimit 5b9ff866471762aa2ab2dced63c9fb6f53921342
|
||||||
github.com/json-iterator/go 6240e1e7983a85228f7fd9c3e1b6932d46ec58e2
|
github.com/json-iterator/go 6240e1e7983a85228f7fd9c3e1b6932d46ec58e2
|
||||||
|
@ -81,4 +81,3 @@ k8s.io/client-go kubernetes-1.8.2
|
||||||
k8s.io/kubernetes v1.8.2
|
k8s.io/kubernetes v1.8.2
|
||||||
k8s.io/kube-openapi 61b46af70dfed79c6d24530cd23b41440a7f22a5
|
k8s.io/kube-openapi 61b46af70dfed79c6d24530cd23b41440a7f22a5
|
||||||
vbom.ml/util 928aaa586d7718c70f4090ddf83f2b34c16fdc8d
|
vbom.ml/util 928aaa586d7718c70f4090ddf83f2b34c16fdc8d
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,67 @@
|
||||||
|
|
||||||
A helper to merge structs and maps in Golang. Useful for configuration default values, avoiding messy if-statements.
|
A helper to merge structs and maps in Golang. Useful for configuration default values, avoiding messy if-statements.
|
||||||
|
|
||||||
Also a lovely [comune](http://en.wikipedia.org/wiki/Mergo) (municipality) in the Province of Ancona in the Italian region Marche.
|
Also a lovely [comune](http://en.wikipedia.org/wiki/Mergo) (municipality) in the Province of Ancona in the Italian region of Marche.
|
||||||
|
|
||||||
![Mergo dall'alto](http://www.comune.mergo.an.it/Siti/Mergo/Immagini/Foto/mergo_dall_alto.jpg)
|
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
It is ready for production use. It works fine although it may use more of testing. Here some projects in the wild using Mergo:
|
It is ready for production use. [It is used in several projects by Docker, Google, The Linux Foundation, VMWare, Shopify, etc](https://github.com/imdario/mergo#mergo-in-the-wild).
|
||||||
|
|
||||||
|
[![Build Status][1]][2]
|
||||||
|
[![GoDoc][3]][4]
|
||||||
|
[![GoCard][5]][6]
|
||||||
|
[![Coverage Status][7]][8]
|
||||||
|
|
||||||
|
[1]: https://travis-ci.org/imdario/mergo.png
|
||||||
|
[2]: https://travis-ci.org/imdario/mergo
|
||||||
|
[3]: https://godoc.org/github.com/imdario/mergo?status.svg
|
||||||
|
[4]: https://godoc.org/github.com/imdario/mergo
|
||||||
|
[5]: https://goreportcard.com/badge/imdario/mergo
|
||||||
|
[6]: https://goreportcard.com/report/github.com/imdario/mergo
|
||||||
|
[7]: https://coveralls.io/repos/github/imdario/mergo/badge.svg?branch=master
|
||||||
|
[8]: https://coveralls.io/github/imdario/mergo?branch=master
|
||||||
|
|
||||||
|
### Latest release
|
||||||
|
|
||||||
|
[Release 0.3.2](https://github.com/imdario/mergo/releases/tag/0.3.2) is an important release because it changes `Merge()`and `Map()` signatures to support [transformers](#transformers). An optional/variadic argument has been added, so it won't break existing code.
|
||||||
|
|
||||||
|
### Important note
|
||||||
|
|
||||||
|
If you were using Mergo **before** April 6th 2015, please check your project works as intended after updating your local copy with ```go get -u github.com/imdario/mergo```. I apologize for any issue caused by its previous behavior and any future bug that Mergo could cause (I hope it won't!) in existing projects after the change (release 0.2.0).
|
||||||
|
|
||||||
|
### Donations
|
||||||
|
|
||||||
|
If Mergo is useful to you, consider buying me a coffe, a beer or making a monthly donation so I can keep building great free software. :heart_eyes:
|
||||||
|
|
||||||
|
<a href='https://ko-fi.com/B0B58839' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://az743702.vo.msecnd.net/cdn/kofi1.png?v=0' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
|
||||||
|
[![Beerpay](https://beerpay.io/imdario/mergo/badge.svg)](https://beerpay.io/imdario/mergo)
|
||||||
|
[![Beerpay](https://beerpay.io/imdario/mergo/make-wish.svg)](https://beerpay.io/imdario/mergo)
|
||||||
|
<a href="https://liberapay.com/dario/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a>
|
||||||
|
|
||||||
|
### Mergo in the wild
|
||||||
|
|
||||||
|
- [moby/moby](https://github.com/moby/moby)
|
||||||
|
- [kubernetes/kubernetes](https://github.com/kubernetes/kubernetes)
|
||||||
|
- [vmware/dispatch](https://github.com/vmware/dispatch)
|
||||||
|
- [Shopify/themekit](https://github.com/Shopify/themekit)
|
||||||
|
- [imdario/zas](https://github.com/imdario/zas)
|
||||||
|
- [matcornic/hermes](https://github.com/matcornic/hermes)
|
||||||
|
- [OpenBazaar/openbazaar-go](https://github.com/OpenBazaar/openbazaar-go)
|
||||||
|
- [kataras/iris](https://github.com/kataras/iris)
|
||||||
|
- [michaelsauter/crane](https://github.com/michaelsauter/crane)
|
||||||
|
- [go-task/task](https://github.com/go-task/task)
|
||||||
|
- [sensu/uchiwa](https://github.com/sensu/uchiwa)
|
||||||
|
- [ory/hydra](https://github.com/ory/hydra)
|
||||||
|
- [sisatech/vcli](https://github.com/sisatech/vcli)
|
||||||
|
- [dairycart/dairycart](https://github.com/dairycart/dairycart)
|
||||||
|
- [projectcalico/felix](https://github.com/projectcalico/felix)
|
||||||
|
- [resin-os/balena](https://github.com/resin-os/balena)
|
||||||
|
- [go-kivik/kivik](https://github.com/go-kivik/kivik)
|
||||||
|
- [Telefonica/govice](https://github.com/Telefonica/govice)
|
||||||
|
- [supergiant/supergiant](supergiant/supergiant)
|
||||||
|
- [SergeyTsalkov/brooce](https://github.com/SergeyTsalkov/brooce)
|
||||||
|
- [soniah/dnsmadeeasy](https://github.com/soniah/dnsmadeeasy)
|
||||||
|
- [ohsu-comp-bio/funnel](https://github.com/ohsu-comp-bio/funnel)
|
||||||
- [EagerIO/Stout](https://github.com/EagerIO/Stout)
|
- [EagerIO/Stout](https://github.com/EagerIO/Stout)
|
||||||
- [lynndylanhurley/defsynth-api](https://github.com/lynndylanhurley/defsynth-api)
|
- [lynndylanhurley/defsynth-api](https://github.com/lynndylanhurley/defsynth-api)
|
||||||
- [russross/canvasassignments](https://github.com/russross/canvasassignments)
|
- [russross/canvasassignments](https://github.com/russross/canvasassignments)
|
||||||
|
@ -17,12 +70,17 @@ It is ready for production use. It works fine although it may use more of testin
|
||||||
- [casualjim/exeggutor](https://github.com/casualjim/exeggutor)
|
- [casualjim/exeggutor](https://github.com/casualjim/exeggutor)
|
||||||
- [divshot/gitling](https://github.com/divshot/gitling)
|
- [divshot/gitling](https://github.com/divshot/gitling)
|
||||||
- [RWJMurphy/gorl](https://github.com/RWJMurphy/gorl)
|
- [RWJMurphy/gorl](https://github.com/RWJMurphy/gorl)
|
||||||
|
- [andrerocker/deploy42](https://github.com/andrerocker/deploy42)
|
||||||
[![Build Status][1]][2]
|
- [elwinar/rambler](https://github.com/elwinar/rambler)
|
||||||
[![GoDoc](https://godoc.org/github.com/imdario/mergo?status.svg)](https://godoc.org/github.com/imdario/mergo)
|
- [tmaiaroto/gopartman](https://github.com/tmaiaroto/gopartman)
|
||||||
|
- [jfbus/impressionist](https://github.com/jfbus/impressionist)
|
||||||
[1]: https://travis-ci.org/imdario/mergo.png
|
- [Jmeyering/zealot](https://github.com/Jmeyering/zealot)
|
||||||
[2]: https://travis-ci.org/imdario/mergo
|
- [godep-migrator/rigger-host](https://github.com/godep-migrator/rigger-host)
|
||||||
|
- [Dronevery/MultiwaySwitch-Go](https://github.com/Dronevery/MultiwaySwitch-Go)
|
||||||
|
- [thoas/picfit](https://github.com/thoas/picfit)
|
||||||
|
- [mantasmatelis/whooplist-server](https://github.com/mantasmatelis/whooplist-server)
|
||||||
|
- [jnuthong/item_search](https://github.com/jnuthong/item_search)
|
||||||
|
- [bukalapak/snowboard](https://github.com/bukalapak/snowboard)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -37,23 +95,113 @@ It is ready for production use. It works fine although it may use more of testin
|
||||||
|
|
||||||
You can only merge same-type structs with exported fields initialized as zero value of their type and same-types maps. Mergo won't merge unexported (private) fields but will do recursively any exported one. Also maps will be merged recursively except for structs inside maps (because they are not addressable using Go reflection).
|
You can only merge same-type structs with exported fields initialized as zero value of their type and same-types maps. Mergo won't merge unexported (private) fields but will do recursively any exported one. Also maps will be merged recursively except for structs inside maps (because they are not addressable using Go reflection).
|
||||||
|
|
||||||
if err := mergo.Merge(&dst, src); err != nil {
|
```go
|
||||||
// ...
|
if err := mergo.Merge(&dst, src); err != nil {
|
||||||
}
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Additionally, you can map a map[string]interface{} to a struct (and otherwise, from struct to map), following the same restrictions as in Merge(). Keys are capitalized to find each corresponding exported field.
|
Also, you can merge overwriting values using the transformer `WithOverride`.
|
||||||
|
|
||||||
if err := mergo.Map(&dst, srcMap); err != nil {
|
```go
|
||||||
// ...
|
if err := mergo.Merge(&dst, src, WithOverride); err != nil {
|
||||||
}
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Warning: if you map a struct to map, it won't do it recursively. Don't expect Mergo to map struct members of your struct as map[string]interface{}. They will be just assigned as values.
|
Additionally, you can map a `map[string]interface{}` to a struct (and otherwise, from struct to map), following the same restrictions as in `Merge()`. Keys are capitalized to find each corresponding exported field.
|
||||||
|
|
||||||
|
```go
|
||||||
|
if err := mergo.Map(&dst, srcMap); err != nil {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Warning: if you map a struct to map, it won't do it recursively. Don't expect Mergo to map struct members of your struct as `map[string]interface{}`. They will be just assigned as values.
|
||||||
|
|
||||||
More information and examples in [godoc documentation](http://godoc.org/github.com/imdario/mergo).
|
More information and examples in [godoc documentation](http://godoc.org/github.com/imdario/mergo).
|
||||||
|
|
||||||
|
### Nice example
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/imdario/mergo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Foo struct {
|
||||||
|
A string
|
||||||
|
B int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
src := Foo{
|
||||||
|
A: "one",
|
||||||
|
B: 2,
|
||||||
|
}
|
||||||
|
dest := Foo{
|
||||||
|
A: "two",
|
||||||
|
}
|
||||||
|
mergo.Merge(&dest, src)
|
||||||
|
fmt.Println(dest)
|
||||||
|
// Will print
|
||||||
|
// {two 2}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Note: if test are failing due missing package, please execute:
|
Note: if test are failing due missing package, please execute:
|
||||||
|
|
||||||
go get gopkg.in/yaml.v1
|
go get gopkg.in/yaml.v2
|
||||||
|
|
||||||
|
### Transformers
|
||||||
|
|
||||||
|
Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, `time.Time` is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero `time.Time`?
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type timeTransfomer struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t timeTransfomer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error {
|
||||||
|
if typ == reflect.TypeOf(time.Time{}) {
|
||||||
|
return func(dst, src reflect.Value) error {
|
||||||
|
if dst.CanSet() {
|
||||||
|
isZero := dst.MethodByName("IsZero")
|
||||||
|
result := isZero.Call([]reflect.Value{})
|
||||||
|
if result[0].Bool() {
|
||||||
|
dst.Set(src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Snapshot struct {
|
||||||
|
Time time.Time
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
src := Snapshot{time.Now()}
|
||||||
|
dest := Snapshot{}
|
||||||
|
mergo.Merge(&dest, src, WithTransformers(timeTransfomer{}))
|
||||||
|
fmt.Println(dest)
|
||||||
|
// Will print
|
||||||
|
// { 2018-01-12 01:15:00 +0000 UTC m=+0.000000001 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Contact me
|
## Contact me
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,8 @@ func isExported(field reflect.StructField) bool {
|
||||||
// Traverses recursively both values, assigning src's fields values to dst.
|
// Traverses recursively both values, assigning src's fields values to dst.
|
||||||
// The map argument tracks comparisons that have already been seen, which allows
|
// The map argument tracks comparisons that have already been seen, which allows
|
||||||
// short circuiting on recursive types.
|
// short circuiting on recursive types.
|
||||||
func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err error) {
|
func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int, config *config) (err error) {
|
||||||
|
overwrite := config.overwrite
|
||||||
if dst.CanAddr() {
|
if dst.CanAddr() {
|
||||||
addr := dst.UnsafeAddr()
|
addr := dst.UnsafeAddr()
|
||||||
h := 17 * addr
|
h := 17 * addr
|
||||||
|
@ -57,10 +58,17 @@ func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err
|
||||||
}
|
}
|
||||||
fieldName := field.Name
|
fieldName := field.Name
|
||||||
fieldName = changeInitialCase(fieldName, unicode.ToLower)
|
fieldName = changeInitialCase(fieldName, unicode.ToLower)
|
||||||
if v, ok := dstMap[fieldName]; !ok || isEmptyValue(reflect.ValueOf(v)) {
|
if v, ok := dstMap[fieldName]; !ok || (isEmptyValue(reflect.ValueOf(v)) || overwrite) {
|
||||||
dstMap[fieldName] = src.Field(i).Interface()
|
dstMap[fieldName] = src.Field(i).Interface()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case reflect.Ptr:
|
||||||
|
if dst.IsNil() {
|
||||||
|
v := reflect.New(dst.Type().Elem())
|
||||||
|
dst.Set(v)
|
||||||
|
}
|
||||||
|
dst = dst.Elem()
|
||||||
|
fallthrough
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
srcMap := src.Interface().(map[string]interface{})
|
srcMap := src.Interface().(map[string]interface{})
|
||||||
for key := range srcMap {
|
for key := range srcMap {
|
||||||
|
@ -85,21 +93,24 @@ func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err
|
||||||
srcKind = reflect.Ptr
|
srcKind = reflect.Ptr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !srcElement.IsValid() {
|
if !srcElement.IsValid() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if srcKind == dstKind {
|
if srcKind == dstKind {
|
||||||
if err = deepMerge(dstElement, srcElement, visited, depth+1); err != nil {
|
if err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if dstKind == reflect.Interface && dstElement.Kind() == reflect.Interface {
|
||||||
|
if err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if srcKind == reflect.Map {
|
||||||
|
if err = deepMap(dstElement, srcElement, visited, depth+1, config); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if srcKind == reflect.Map {
|
return fmt.Errorf("type mismatch on %s field: found %v, expected %v", fieldName, srcKind, dstKind)
|
||||||
if err = deepMap(dstElement, srcElement, visited, depth+1); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("type mismatch on %s field: found %v, expected %v", fieldName, srcKind, dstKind)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,18 +128,35 @@ func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err
|
||||||
// doesn't apply if dst is a map.
|
// doesn't apply if dst is a map.
|
||||||
// This is separated method from Merge because it is cleaner and it keeps sane
|
// This is separated method from Merge because it is cleaner and it keeps sane
|
||||||
// semantics: merging equal types, mapping different (restricted) types.
|
// semantics: merging equal types, mapping different (restricted) types.
|
||||||
func Map(dst, src interface{}) error {
|
func Map(dst, src interface{}, opts ...func(*config)) error {
|
||||||
|
return _map(dst, src, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapWithOverwrite will do the same as Map except that non-empty dst attributes will be overriden by
|
||||||
|
// non-empty src attribute values.
|
||||||
|
// Deprecated: Use Map(…) with WithOverride
|
||||||
|
func MapWithOverwrite(dst, src interface{}, opts ...func(*config)) error {
|
||||||
|
return _map(dst, src, append(opts, WithOverride)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _map(dst, src interface{}, opts ...func(*config)) error {
|
||||||
var (
|
var (
|
||||||
vDst, vSrc reflect.Value
|
vDst, vSrc reflect.Value
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
config := &config{}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(config)
|
||||||
|
}
|
||||||
|
|
||||||
if vDst, vSrc, err = resolveValues(dst, src); err != nil {
|
if vDst, vSrc, err = resolveValues(dst, src); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// To be friction-less, we redirect equal-type arguments
|
// To be friction-less, we redirect equal-type arguments
|
||||||
// to deepMerge. Only because arguments can be anything.
|
// to deepMerge. Only because arguments can be anything.
|
||||||
if vSrc.Kind() == vDst.Kind() {
|
if vSrc.Kind() == vDst.Kind() {
|
||||||
return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0)
|
return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0, config)
|
||||||
}
|
}
|
||||||
switch vSrc.Kind() {
|
switch vSrc.Kind() {
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
|
@ -142,5 +170,5 @@ func Map(dst, src interface{}) error {
|
||||||
default:
|
default:
|
||||||
return ErrNotSupported
|
return ErrNotSupported
|
||||||
}
|
}
|
||||||
return deepMap(vDst, vSrc, make(map[uintptr]*visit), 0)
|
return deepMap(vDst, vSrc, make(map[uintptr]*visit), 0, config)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,35 @@
|
||||||
|
|
||||||
package mergo
|
package mergo
|
||||||
|
|
||||||
import (
|
import "reflect"
|
||||||
"reflect"
|
|
||||||
)
|
func hasExportedField(dst reflect.Value) (exported bool) {
|
||||||
|
for i, n := 0, dst.NumField(); i < n; i++ {
|
||||||
|
field := dst.Type().Field(i)
|
||||||
|
if field.Anonymous && dst.Field(i).Kind() == reflect.Struct {
|
||||||
|
exported = exported || hasExportedField(dst.Field(i))
|
||||||
|
} else {
|
||||||
|
exported = exported || len(field.PkgPath) == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
overwrite bool
|
||||||
|
transformers transformers
|
||||||
|
}
|
||||||
|
|
||||||
|
type transformers interface {
|
||||||
|
Transformer(reflect.Type) func(dst, src reflect.Value) error
|
||||||
|
}
|
||||||
|
|
||||||
// Traverses recursively both values, assigning src's fields values to dst.
|
// Traverses recursively both values, assigning src's fields values to dst.
|
||||||
// The map argument tracks comparisons that have already been seen, which allows
|
// The map argument tracks comparisons that have already been seen, which allows
|
||||||
// short circuiting on recursive types.
|
// short circuiting on recursive types.
|
||||||
func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err error) {
|
func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, config *config) (err error) {
|
||||||
|
overwrite := config.overwrite
|
||||||
|
|
||||||
if !src.IsValid() {
|
if !src.IsValid() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -32,68 +53,167 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (e
|
||||||
// Remember, remember...
|
// Remember, remember...
|
||||||
visited[h] = &visit{addr, typ, seen}
|
visited[h] = &visit{addr, typ, seen}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.transformers != nil && !isEmptyValue(dst) {
|
||||||
|
if fn := config.transformers.Transformer(dst.Type()); fn != nil {
|
||||||
|
err = fn(dst, src)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch dst.Kind() {
|
switch dst.Kind() {
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
for i, n := 0, dst.NumField(); i < n; i++ {
|
if hasExportedField(dst) {
|
||||||
if err = deepMerge(dst.Field(i), src.Field(i), visited, depth+1); err != nil {
|
for i, n := 0, dst.NumField(); i < n; i++ {
|
||||||
return
|
if err = deepMerge(dst.Field(i), src.Field(i), visited, depth+1, config); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if dst.CanSet() && !isEmptyValue(src) && (overwrite || isEmptyValue(dst)) {
|
||||||
|
dst.Set(src)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
|
if len(src.MapKeys()) == 0 && !src.IsNil() && len(dst.MapKeys()) == 0 {
|
||||||
|
dst.Set(reflect.MakeMap(dst.Type()))
|
||||||
|
return
|
||||||
|
}
|
||||||
for _, key := range src.MapKeys() {
|
for _, key := range src.MapKeys() {
|
||||||
srcElement := src.MapIndex(key)
|
srcElement := src.MapIndex(key)
|
||||||
if !srcElement.IsValid() {
|
if !srcElement.IsValid() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dstElement := dst.MapIndex(key)
|
dstElement := dst.MapIndex(key)
|
||||||
switch reflect.TypeOf(srcElement.Interface()).Kind() {
|
switch srcElement.Kind() {
|
||||||
case reflect.Struct:
|
case reflect.Chan, reflect.Func, reflect.Map, reflect.Interface, reflect.Slice:
|
||||||
|
if srcElement.IsNil() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
fallthrough
|
fallthrough
|
||||||
case reflect.Map:
|
default:
|
||||||
if err = deepMerge(dstElement, srcElement, visited, depth+1); err != nil {
|
if !srcElement.CanInterface() {
|
||||||
return
|
continue
|
||||||
|
}
|
||||||
|
switch reflect.TypeOf(srcElement.Interface()).Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Ptr:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Map:
|
||||||
|
if err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case reflect.Slice:
|
||||||
|
srcSlice := reflect.ValueOf(srcElement.Interface())
|
||||||
|
|
||||||
|
var dstSlice reflect.Value
|
||||||
|
if !dstElement.IsValid() || dstElement.IsNil() {
|
||||||
|
dstSlice = reflect.MakeSlice(srcSlice.Type(), 0, srcSlice.Len())
|
||||||
|
} else {
|
||||||
|
dstSlice = reflect.ValueOf(dstElement.Interface())
|
||||||
|
}
|
||||||
|
|
||||||
|
dstSlice = reflect.AppendSlice(dstSlice, srcSlice)
|
||||||
|
dst.SetMapIndex(key, dstSlice)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !dstElement.IsValid() {
|
if dstElement.IsValid() && reflect.TypeOf(srcElement.Interface()).Kind() == reflect.Map {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if srcElement.IsValid() && (overwrite || (!dstElement.IsValid() || isEmptyValue(dst))) {
|
||||||
|
if dst.IsNil() {
|
||||||
|
dst.Set(reflect.MakeMap(dst.Type()))
|
||||||
|
}
|
||||||
dst.SetMapIndex(key, srcElement)
|
dst.SetMapIndex(key, srcElement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case reflect.Slice:
|
||||||
|
dst.Set(reflect.AppendSlice(dst, src))
|
||||||
case reflect.Ptr:
|
case reflect.Ptr:
|
||||||
fallthrough
|
fallthrough
|
||||||
case reflect.Interface:
|
case reflect.Interface:
|
||||||
if src.IsNil() {
|
if src.IsNil() {
|
||||||
break
|
break
|
||||||
} else if dst.IsNil() {
|
}
|
||||||
if dst.CanSet() && isEmptyValue(dst) {
|
if src.Kind() != reflect.Interface {
|
||||||
|
if dst.IsNil() || overwrite {
|
||||||
|
if dst.CanSet() && (overwrite || isEmptyValue(dst)) {
|
||||||
|
dst.Set(src)
|
||||||
|
}
|
||||||
|
} else if src.Kind() == reflect.Ptr {
|
||||||
|
if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1, config); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if dst.Elem().Type() == src.Type() {
|
||||||
|
if err = deepMerge(dst.Elem(), src, visited, depth+1, config); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return ErrDifferentArgumentsTypes
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if dst.IsNil() || overwrite {
|
||||||
|
if dst.CanSet() && (overwrite || isEmptyValue(dst)) {
|
||||||
dst.Set(src)
|
dst.Set(src)
|
||||||
}
|
}
|
||||||
} else if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1); err != nil {
|
} else if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1, config); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if dst.CanSet() && !isEmptyValue(src) {
|
if dst.CanSet() && !isEmptyValue(src) && (overwrite || isEmptyValue(dst)) {
|
||||||
dst.Set(src)
|
dst.Set(src)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge sets fields' values in dst from src if they have a zero
|
// Merge will fill any empty for value type attributes on the dst struct using corresponding
|
||||||
// value of their type.
|
// src attributes if they themselves are not empty. dst and src must be valid same-type structs
|
||||||
// dst and src must be valid same-type structs and dst must be
|
// and dst must be a pointer to struct.
|
||||||
// a pointer to struct.
|
// It won't merge unexported (private) fields and will do recursively any exported field.
|
||||||
// It won't merge unexported (private) fields and will do recursively
|
func Merge(dst, src interface{}, opts ...func(*config)) error {
|
||||||
// any exported field.
|
return merge(dst, src, opts...)
|
||||||
func Merge(dst, src interface{}) error {
|
}
|
||||||
|
|
||||||
|
// MergeWithOverwrite will do the same as Merge except that non-empty dst attributes will be overriden by
|
||||||
|
// non-empty src attribute values.
|
||||||
|
// Deprecated: use Merge(…) with WithOverride
|
||||||
|
func MergeWithOverwrite(dst, src interface{}, opts ...func(*config)) error {
|
||||||
|
return merge(dst, src, append(opts, WithOverride)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTransformers adds transformers to merge, allowing to customize the merging of some types.
|
||||||
|
func WithTransformers(transformers transformers) func(*config) {
|
||||||
|
return func(config *config) {
|
||||||
|
config.transformers = transformers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithOverride will make merge override non-empty dst attributes with non-empty src attributes values.
|
||||||
|
func WithOverride(config *config) {
|
||||||
|
config.overwrite = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func merge(dst, src interface{}, opts ...func(*config)) error {
|
||||||
var (
|
var (
|
||||||
vDst, vSrc reflect.Value
|
vDst, vSrc reflect.Value
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
|
config := &config{}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(config)
|
||||||
|
}
|
||||||
|
|
||||||
if vDst, vSrc, err = resolveValues(dst, src); err != nil {
|
if vDst, vSrc, err = resolveValues(dst, src); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if vDst.Type() != vSrc.Type() {
|
if vDst.Type() != vSrc.Type() {
|
||||||
return ErrDifferentArgumentsTypes
|
return ErrDifferentArgumentsTypes
|
||||||
}
|
}
|
||||||
return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0)
|
return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0, config)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ type visit struct {
|
||||||
next *visit
|
next *visit
|
||||||
}
|
}
|
||||||
|
|
||||||
// From src/pkg/encoding/json.
|
// From src/pkg/encoding/json/encode.go.
|
||||||
func isEmptyValue(v reflect.Value) bool {
|
func isEmptyValue(v reflect.Value) bool {
|
||||||
switch v.Kind() {
|
switch v.Kind() {
|
||||||
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
|
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
|
||||||
|
@ -45,8 +45,10 @@ func isEmptyValue(v reflect.Value) bool {
|
||||||
return v.Uint() == 0
|
return v.Uint() == 0
|
||||||
case reflect.Float32, reflect.Float64:
|
case reflect.Float32, reflect.Float64:
|
||||||
return v.Float() == 0
|
return v.Float() == 0
|
||||||
case reflect.Interface, reflect.Ptr:
|
case reflect.Interface, reflect.Ptr, reflect.Func:
|
||||||
return v.IsNil()
|
return v.IsNil()
|
||||||
|
case reflect.Invalid:
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue