From c7bafb82e7e86c3a060d5c4dc7b0862dcf7c1831 Mon Sep 17 00:00:00 2001 From: Stoica-Marcu Floris-Andrei Date: Tue, 22 Sep 2020 12:16:05 +0300 Subject: [PATCH 01/11] Add stack config command Make use of existing modules and functions in order to output the merged configs. Added skip interpolation flag of variables, so that you can pipe the output back to stack deploy without much hassle. Signed-off-by: Stoica-Marcu Floris-Andrei --- cli/command/stack/cmd.go | 1 + cli/command/stack/config.go | 62 +++++++++++++ cli/command/stack/config_test.go | 110 ++++++++++++++++++++++++ cli/command/stack/loader/loader.go | 5 +- cli/command/stack/loader/loader_test.go | 4 +- cli/command/stack/options/opts.go | 6 ++ e2e/stack/config_test.go | 12 +++ 7 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 cli/command/stack/config.go create mode 100644 cli/command/stack/config_test.go create mode 100644 e2e/stack/config_test.go diff --git a/cli/command/stack/cmd.go b/cli/command/stack/cmd.go index f4ded7716934..f2c04dde0813 100644 --- a/cli/command/stack/cmd.go +++ b/cli/command/stack/cmd.go @@ -65,6 +65,7 @@ func NewStackCommand(dockerCli command.Cli) *cobra.Command { newPsCommand(dockerCli, &opts), newRemoveCommand(dockerCli, &opts), newServicesCommand(dockerCli, &opts), + newConfigCommand(dockerCli), ) flags := cmd.PersistentFlags() flags.String("kubeconfig", "", "Kubernetes config file") diff --git a/cli/command/stack/config.go b/cli/command/stack/config.go new file mode 100644 index 000000000000..dd881d532b59 --- /dev/null +++ b/cli/command/stack/config.go @@ -0,0 +1,62 @@ +package stack + +import ( + "fmt" + + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/stack/loader" + "github.com/docker/cli/cli/command/stack/options" + composeLoader "github.com/docker/cli/cli/compose/loader" + composetypes "github.com/docker/cli/cli/compose/types" + "github.com/spf13/cobra" + yaml "gopkg.in/yaml.v2" +) + +func newConfigCommand(dockerCli command.Cli) *cobra.Command { + var opts options.Config + + cmd := &cobra.Command{ + Use: "config [OPTIONS]", + Aliases: []string{"cfg"}, + Short: "Outputs the final config file, after doing merges and interpolations", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + configDetails, err := loader.GetConfigDetails(opts.Composefiles, dockerCli.In()) + if err != nil { + return err + } + + cfg, err := OutputConfig(configDetails, opts.SkipInterpolation) + if err != nil { + return err + } + + fmt.Printf("%s", cfg) + return nil + }, + } + + flags := cmd.Flags() + flags.StringSliceVarP(&opts.Composefiles, "compose-file", "c", []string{}, `Path to a Compose file, or "-" to read from stdin`) + flags.SetAnnotation("compose-file", "version", []string{"1.25"}) + flags.BoolVar(&opts.SkipInterpolation, "skip-interpolation", false, "Skip interpolation and output only merged config") + return cmd +} + +// OutputConfig returns the merged and interpolated config file +func OutputConfig(configFiles composetypes.ConfigDetails, skipInterpolation bool) (string, error) { + optsFunc := func(options *composeLoader.Options) { + options.SkipInterpolation = skipInterpolation + } + config, err := composeLoader.Load(configFiles, optsFunc) + if err != nil { + return "", err + } + + d, err := yaml.Marshal(&config) + if err != nil { + return "", err + } + return string(d), nil +} diff --git a/cli/command/stack/config_test.go b/cli/command/stack/config_test.go new file mode 100644 index 000000000000..d53c11e5af16 --- /dev/null +++ b/cli/command/stack/config_test.go @@ -0,0 +1,110 @@ +package stack + +import ( + "io/ioutil" + "testing" + + "github.com/docker/cli/cli/compose/loader" + composetypes "github.com/docker/cli/cli/compose/types" + "github.com/docker/cli/internal/test" + "gotest.tools/v3/assert" +) + +func TestConfigWithEmptyComposeFile(t *testing.T) { + cmd := newConfigCommand(test.NewFakeCli(&fakeClient{})) + cmd.SetOut(ioutil.Discard) + + assert.ErrorContains(t, cmd.Execute(), `Please specify a Compose file`) +} + +func TestConfigMergeUsingInterpolation(t *testing.T) { + + firstConfig := []byte(` +version: "3.7" +services: + foo: + image: busybox:latest + command: cat file1.txt +`) + secondConfig := []byte(` +version: "3.7" +services: + foo: + image: busybox:${VERSION} + command: cat file2.txt +`) + + firstConfigData, err := loader.ParseYAML(firstConfig) + assert.NilError(t, err) + secondConfigData, err := loader.ParseYAML(secondConfig) + assert.NilError(t, err) + + env := map[string]string{ + "VERSION": "1.0", + } + + cfg, err := OutputConfig(composetypes.ConfigDetails{ + ConfigFiles: []composetypes.ConfigFile{ + {Config: firstConfigData, Filename: "firstConfig"}, + {Config: secondConfigData, Filename: "secondConfig"}, + }, + Environment: env, + }, false) + assert.NilError(t, err) + + var mergedConfig = `version: "3.7" +services: + foo: + command: + - cat + - file2.txt + image: busybox:1.0 +` + assert.Equal(t, cfg, mergedConfig) +} + +func TestConfigMergeSkipInterpolation(t *testing.T) { + + firstConfig := []byte(` +version: "3.7" +services: + foo: + image: busybox:latest + command: cat file1.txt +`) + secondConfig := []byte(` +version: "3.7" +services: + foo: + image: busybox:${VERSION} + command: cat file2.txt +`) + + firstConfigData, err := loader.ParseYAML(firstConfig) + assert.NilError(t, err) + secondConfigData, err := loader.ParseYAML(secondConfig) + assert.NilError(t, err) + + env := map[string]string{ + "VERSION": "1.0", + } + + cfg, err := OutputConfig(composetypes.ConfigDetails{ + ConfigFiles: []composetypes.ConfigFile{ + {Config: firstConfigData, Filename: "firstConfig"}, + {Config: secondConfigData, Filename: "secondConfig"}, + }, + Environment: env, + }, true) + assert.NilError(t, err) + + var mergedConfig = `version: "3.7" +services: + foo: + command: + - cat + - file2.txt + image: busybox:${VERSION} +` + assert.Equal(t, cfg, mergedConfig) +} diff --git a/cli/command/stack/loader/loader.go b/cli/command/stack/loader/loader.go index c4333965fae2..8ab7cbba99a6 100644 --- a/cli/command/stack/loader/loader.go +++ b/cli/command/stack/loader/loader.go @@ -19,7 +19,7 @@ import ( // LoadComposefile parse the composefile specified in the cli and returns its Config and version. func LoadComposefile(dockerCli command.Cli, opts options.Deploy) (*composetypes.Config, error) { - configDetails, err := getConfigDetails(opts.Composefiles, dockerCli.In()) + configDetails, err := GetConfigDetails(opts.Composefiles, dockerCli.In()) if err != nil { return nil, err } @@ -68,7 +68,8 @@ func propertyWarnings(properties map[string]string) string { return strings.Join(msgs, "\n\n") } -func getConfigDetails(composefiles []string, stdin io.Reader) (composetypes.ConfigDetails, error) { +// GetConfigDetails parse the composefiles specified in the cli and returns their ConfigDetails +func GetConfigDetails(composefiles []string, stdin io.Reader) (composetypes.ConfigDetails, error) { var details composetypes.ConfigDetails if len(composefiles) == 0 { diff --git a/cli/command/stack/loader/loader_test.go b/cli/command/stack/loader/loader_test.go index fd504bc9d1ff..6ddca65bb855 100644 --- a/cli/command/stack/loader/loader_test.go +++ b/cli/command/stack/loader/loader_test.go @@ -21,7 +21,7 @@ services: file := fs.NewFile(t, "test-get-config-details", fs.WithContent(content)) defer file.Remove() - details, err := getConfigDetails([]string{file.Path()}, nil) + details, err := GetConfigDetails([]string{file.Path()}, nil) assert.NilError(t, err) assert.Check(t, is.Equal(filepath.Dir(file.Path()), details.WorkingDir)) assert.Assert(t, is.Len(details.ConfigFiles, 1)) @@ -36,7 +36,7 @@ services: foo: image: alpine:3.5 ` - details, err := getConfigDetails([]string{"-"}, strings.NewReader(content)) + details, err := GetConfigDetails([]string{"-"}, strings.NewReader(content)) assert.NilError(t, err) cwd, err := os.Getwd() assert.NilError(t, err) diff --git a/cli/command/stack/options/opts.go b/cli/command/stack/options/opts.go index fb45dc45580e..9c476b523f61 100644 --- a/cli/command/stack/options/opts.go +++ b/cli/command/stack/options/opts.go @@ -11,6 +11,12 @@ type Deploy struct { Prune bool } +// Config holds docker stack config options +type Config struct { + Composefiles []string + SkipInterpolation bool +} + // List holds docker stack ls options type List struct { Format string diff --git a/e2e/stack/config_test.go b/e2e/stack/config_test.go new file mode 100644 index 000000000000..1d56ae701984 --- /dev/null +++ b/e2e/stack/config_test.go @@ -0,0 +1,12 @@ +package stack + +import ( + "testing" + + "gotest.tools/v3/icmd" +) + +func TestConfigFullStack(t *testing.T) { + result := icmd.RunCommand("docker", "stack", "config", "--compose-file=./testdata/full-stack.yml") + result.Assert(t, icmd.Success) +} From ed780e153b1fb4501530bc87890f8dbd9916b897 Mon Sep 17 00:00:00 2001 From: Stoica-Marcu Floris-Andrei Date: Thu, 24 Sep 2020 04:37:13 +0300 Subject: [PATCH 02/11] Change merge strategy for service volumes Signed-off-by: Stoica-Marcu Floris-Andrei --- cli/compose/loader/merge.go | 31 +++++++++++++ cli/compose/loader/merge_test.go | 75 ++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/cli/compose/loader/merge.go b/cli/compose/loader/merge.go index 0de8d8a1b608..a2bdabec731e 100644 --- a/cli/compose/loader/merge.go +++ b/cli/compose/loader/merge.go @@ -58,6 +58,7 @@ func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig, reflect.TypeOf([]types.ServiceSecretConfig{}): mergeSlice(toServiceSecretConfigsMap, toServiceSecretConfigsSlice), reflect.TypeOf([]types.ServiceConfigObjConfig{}): mergeSlice(toServiceConfigObjConfigsMap, toSServiceConfigObjConfigsSlice), reflect.TypeOf(&types.UlimitsConfig{}): mergeUlimitsConfig, + reflect.TypeOf([]types.ServiceVolumeConfig{}): mergeSlice(toServiceVolumeConfigsMap, toServiceVolumeConfigsSlice), reflect.TypeOf(&types.ServiceNetworkConfig{}): mergeServiceNetworkConfig, }, } @@ -116,6 +117,18 @@ func toServicePortConfigsMap(s interface{}) (map[interface{}]interface{}, error) return m, nil } +func toServiceVolumeConfigsMap(s interface{}) (map[interface{}]interface{}, error) { + volumes, ok := s.([]types.ServiceVolumeConfig) + if !ok { + return nil, errors.Errorf("not a serviceVolumeConfig slice: %v", s) + } + m := map[interface{}]interface{}{} + for _, v := range volumes { + m[v.Target] = v + } + return m, nil +} + func toServiceSecretConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error { s := []types.ServiceSecretConfig{} for _, v := range m { @@ -146,6 +159,16 @@ func toServicePortConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) return nil } +func toServiceVolumeConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error { + s := []types.ServiceVolumeConfig{} + for _, v := range m { + s = append(s, v.(types.ServiceVolumeConfig)) + } + sort.Slice(s, func(i, j int) bool { return s[i].Target < s[j].Target }) + 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 @@ -211,6 +234,14 @@ func mergeUlimitsConfig(dst, src reflect.Value) error { return nil } +//nolint: unparam +func mergeServiceVolumeConfig(dst, src reflect.Value) error { + if dst.Elem().FieldByName("target").String() == src.Elem().FieldByName("target").String() { + dst.Set(src.Elem()) + } + return nil +} + //nolint: unparam func mergeServiceNetworkConfig(dst, src reflect.Value) error { if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() { diff --git a/cli/compose/loader/merge_test.go b/cli/compose/loader/merge_test.go index 4828df85487d..c5bbf3b5de2c 100644 --- a/cli/compose/loader/merge_test.go +++ b/cli/compose/loader/merge_test.go @@ -1017,6 +1017,81 @@ func TestLoadMultipleNetworks(t *testing.T) { }, config) } +func TestLoadMultipleServiceVolumes(t *testing.T) { + base := map[string]interface{}{ + "version": "3.7", + "services": map[string]interface{}{ + "foo": map[string]interface{}{ + "image": "baz", + "volumes": []interface{}{ + map[string]interface{}{ + "type": "volume", + "source": "sourceVolume", + "target": "/var/app", + }, + }, + }, + }, + "volumes": map[string]interface{}{ + "sourceVolume": map[string]interface{}{}, + }, + "networks": map[string]interface{}{}, + "secrets": map[string]interface{}{}, + "configs": map[string]interface{}{}, + } + override := map[string]interface{}{ + "version": "3.7", + "services": map[string]interface{}{ + "foo": map[string]interface{}{ + "image": "baz", + "volumes": []interface{}{ + map[string]interface{}{ + "type": "volume", + "source": "/local", + "target": "/var/app", + }, + }, + }, + }, + "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) + assert.NilError(t, err) + assert.DeepEqual(t, &types.Config{ + Filename: "base.yml", + Version: "3.7", + Services: []types.ServiceConfig{ + { + Name: "foo", + Image: "baz", + Environment: types.MappingWithEquals{}, + Volumes: []types.ServiceVolumeConfig{ + { + Type: "volume", + Source: "/local", + Target: "/var/app", + }, + }, + }, + }, + Volumes: map[string]types.VolumeConfig{ + "sourceVolume": {}, + }, + Secrets: map[string]types.SecretConfig{}, + Configs: map[string]types.ConfigObjConfig{}, + Networks: map[string]types.NetworkConfig{}, + }, config) +} + func TestMergeUlimitsConfig(t *testing.T) { specials := &specials{ m: map[reflect.Type]func(dst, src reflect.Value) error{ From e74f0c520c0d99132521d1dafb3fa0be447f04c0 Mon Sep 17 00:00:00 2001 From: Stoica-Marcu Floris-Andrei Date: Wed, 23 Sep 2020 03:33:30 +0300 Subject: [PATCH 03/11] Add merge to ShellCommand properties in config Signed-off-by: Stoica-Marcu Floris-Andrei --- cli/compose/loader/merge.go | 7 +++-- cli/compose/loader/merge_test.go | 53 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/cli/compose/loader/merge.go b/cli/compose/loader/merge.go index a2bdabec731e..97d55ea6b882 100644 --- a/cli/compose/loader/merge.go +++ b/cli/compose/loader/merge.go @@ -59,6 +59,7 @@ func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig, reflect.TypeOf([]types.ServiceConfigObjConfig{}): mergeSlice(toServiceConfigObjConfigsMap, toSServiceConfigObjConfigsSlice), reflect.TypeOf(&types.UlimitsConfig{}): mergeUlimitsConfig, reflect.TypeOf([]types.ServiceVolumeConfig{}): mergeSlice(toServiceVolumeConfigsMap, toServiceVolumeConfigsSlice), + reflect.TypeOf(types.ShellCommand{}): mergeShellCommand, reflect.TypeOf(&types.ServiceNetworkConfig{}): mergeServiceNetworkConfig, }, } @@ -235,9 +236,9 @@ func mergeUlimitsConfig(dst, src reflect.Value) error { } //nolint: unparam -func mergeServiceVolumeConfig(dst, src reflect.Value) error { - if dst.Elem().FieldByName("target").String() == src.Elem().FieldByName("target").String() { - dst.Set(src.Elem()) +func mergeShellCommand(dst, src reflect.Value) error { + if src.Len() != 0 { + dst.Set(src) } return nil } diff --git a/cli/compose/loader/merge_test.go b/cli/compose/loader/merge_test.go index c5bbf3b5de2c..6f2262b07274 100644 --- a/cli/compose/loader/merge_test.go +++ b/cli/compose/loader/merge_test.go @@ -1017,6 +1017,59 @@ func TestLoadMultipleNetworks(t *testing.T) { }, config) } +func TestLoadMultipleServiceCommands(t *testing.T) { + base := map[string]interface{}{ + "version": "3.7", + "services": map[string]interface{}{ + "foo": map[string]interface{}{ + "image": "baz", + "command": "foo bar", + }, + }, + "volumes": map[string]interface{}{}, + "networks": map[string]interface{}{}, + "secrets": map[string]interface{}{}, + "configs": map[string]interface{}{}, + } + override := map[string]interface{}{ + "version": "3.7", + "services": map[string]interface{}{ + "foo": map[string]interface{}{ + "image": "baz", + "command": "foo baz", + }, + }, + "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) + assert.NilError(t, err) + assert.DeepEqual(t, &types.Config{ + Filename: "base.yml", + Version: "3.7", + Services: []types.ServiceConfig{ + { + Name: "foo", + Image: "baz", + Command: types.ShellCommand{"foo", "baz"}, + Environment: types.MappingWithEquals{}, + }, + }, + Volumes: map[string]types.VolumeConfig{}, + Secrets: map[string]types.SecretConfig{}, + Configs: map[string]types.ConfigObjConfig{}, + Networks: map[string]types.NetworkConfig{}, + }, config) +} + func TestLoadMultipleServiceVolumes(t *testing.T) { base := map[string]interface{}{ "version": "3.7", From 53247fe00f82693410db4894de2323ad7ea40bd9 Mon Sep 17 00:00:00 2001 From: Stoica-Marcu Floris-Andrei Date: Thu, 24 Sep 2020 03:58:30 +0300 Subject: [PATCH 04/11] Add documentation for stack config command Signed-off-by: Stoica-Marcu Floris-Andrei --- docs/reference/commandline/index.md | 1 + docs/reference/commandline/stack_config.md | 68 ++++++++++++++++++++++ docs/reference/commandline/stack_deploy.md | 1 + 3 files changed, 70 insertions(+) create mode 100644 docs/reference/commandline/stack_config.md diff --git a/docs/reference/commandline/index.md b/docs/reference/commandline/index.md index 1d4e8c72c0d4..2f093dbdb7d9 100644 --- a/docs/reference/commandline/index.md +++ b/docs/reference/commandline/index.md @@ -159,6 +159,7 @@ read the [`dockerd`](dockerd.md) reference page. | [stack ps](stack_ps.md) | List the tasks in the stack | | [stack rm](stack_rm.md) | Remove the stack from the swarm | | [stack services](stack_services.md) | List the services in the stack | +| [stack config](stack_config.md) | Output the Compose file after merges and interpolations | ### Plugin commands diff --git a/docs/reference/commandline/stack_config.md b/docs/reference/commandline/stack_config.md new file mode 100644 index 000000000000..0d3ff490ea19 --- /dev/null +++ b/docs/reference/commandline/stack_config.md @@ -0,0 +1,68 @@ +--- +title: "stack config" +description: "The stack config command description and usage" +keywords: "stack, config" +--- + +# stack config + +```markdown +Usage: docker stack config [OPTIONS] + +Outputs the final config file, after doing merges and interpolations + +Aliases: + config, cfg + +Options: + -c, --compose-file strings Path to a Compose file, or "-" to read from stdin + --orchestrator string Orchestrator to use (swarm|kubernetes|all) + --skip-interpolation Skip interpolation and output only merged config +``` + +## Description + +Outputs the final Compose file, after doing the merges and interpolations of the input Compose files. + +## Examples + +The following command outputs the result of the merge and interpolation of two Compose files. + +```bash +$ docker stack config --compose-file docker-compose.yml --compose-file docker-compose.prod.yml +``` + +The Compose file can also be provided as standard input with `--compose-file -`: + +```bash +$ cat docker-compose.yml | docker stack config --compose-file - +``` + +### Skipping interpolation + +In some cases, it might be useful to skip interpolation of environment variables. +For example, when you want to pipe the output of this command back to `stack deploy`. + +If you have a regex for a redirect route in an environment variable for your webserver you would use two `$` signs to prevent `stack deploy` from interpolating `${1}`. + +```bash + service: webserver + environment: + REDIRECT_REGEX=http://host/redirect/$${1} +``` + +With interpolation, the `stack config` command will replace the environment variable in the Compose file +with `REDIRECT_REGEX=http://host/redirect/${1}`, but then when piping it back to the `stack deploy` +command it will be interpolated again and result in undefined behavior. +That is why, when piping the output back to `stack deploy` one should always prefer the `--skip-interpolation` option. + +``` +$ docker stack config --compose-file web.yml --compose-file web.prod.yml --skip-interpolation | docker stack deploy --compose-file - +``` + +## Related commands + +* [stack deploy](stack_deploy.md) +* [stack ps](stack_ps.md) +* [stack rm](stack_rm.md) +* [stack services](stack_services.md) diff --git a/docs/reference/commandline/stack_deploy.md b/docs/reference/commandline/stack_deploy.md index 0be6dc50c08c..4dc8ae7e81e1 100644 --- a/docs/reference/commandline/stack_deploy.md +++ b/docs/reference/commandline/stack_deploy.md @@ -114,3 +114,4 @@ axqh55ipl40h vossibility_vossibility-collector replicated 1/1 icecrime/ * [stack ps](stack_ps.md) * [stack rm](stack_rm.md) * [stack services](stack_services.md) +* [stack config](stack_config.md) From a72ec7b7fbbbf8e1f823f8b17cf7200497ac09ab Mon Sep 17 00:00:00 2001 From: Stoica-Marcu Floris-Andrei Date: Wed, 7 Oct 2020 19:24:30 +0300 Subject: [PATCH 05/11] Remove alias Signed-off-by: Stoica-Marcu Floris-Andrei --- cli/command/stack/config.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cli/command/stack/config.go b/cli/command/stack/config.go index dd881d532b59..471348442b4b 100644 --- a/cli/command/stack/config.go +++ b/cli/command/stack/config.go @@ -17,10 +17,9 @@ func newConfigCommand(dockerCli command.Cli) *cobra.Command { var opts options.Config cmd := &cobra.Command{ - Use: "config [OPTIONS]", - Aliases: []string{"cfg"}, - Short: "Outputs the final config file, after doing merges and interpolations", - Args: cli.NoArgs, + Use: "config [OPTIONS]", + Short: "Outputs the final config file, after doing merges and interpolations", + Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { configDetails, err := loader.GetConfigDetails(opts.Composefiles, dockerCli.In()) if err != nil { From 5c36abb0c89d49f99024fa2b5a84c73d211a1e95 Mon Sep 17 00:00:00 2001 From: Stoica-Marcu Floris-Andrei Date: Wed, 7 Oct 2020 20:15:34 +0300 Subject: [PATCH 06/11] Use the configured cli output Signed-off-by: Stoica-Marcu Floris-Andrei --- cli/command/stack/config.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cli/command/stack/config.go b/cli/command/stack/config.go index 471348442b4b..b01ff87d297b 100644 --- a/cli/command/stack/config.go +++ b/cli/command/stack/config.go @@ -31,7 +31,11 @@ func newConfigCommand(dockerCli command.Cli) *cobra.Command { return err } - fmt.Printf("%s", cfg) + _, err = fmt.Fprintf(dockerCli.Out(), "%s", cfg) + if err != nil { + return err + } + return nil }, } From ed5d6416c6916ab630eacca906ac9a93654d0c09 Mon Sep 17 00:00:00 2001 From: Stoica-Marcu Floris-Andrei Date: Wed, 7 Oct 2020 20:23:26 +0300 Subject: [PATCH 07/11] Don't export outputConfig Signed-off-by: Stoica-Marcu Floris-Andrei --- cli/command/stack/config.go | 6 +++--- cli/command/stack/config_test.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/command/stack/config.go b/cli/command/stack/config.go index b01ff87d297b..84a921842de4 100644 --- a/cli/command/stack/config.go +++ b/cli/command/stack/config.go @@ -26,7 +26,7 @@ func newConfigCommand(dockerCli command.Cli) *cobra.Command { return err } - cfg, err := OutputConfig(configDetails, opts.SkipInterpolation) + cfg, err := outputConfig(configDetails, opts.SkipInterpolation) if err != nil { return err } @@ -47,8 +47,8 @@ func newConfigCommand(dockerCli command.Cli) *cobra.Command { return cmd } -// OutputConfig returns the merged and interpolated config file -func OutputConfig(configFiles composetypes.ConfigDetails, skipInterpolation bool) (string, error) { +// outputConfig returns the merged and interpolated config file +func outputConfig(configFiles composetypes.ConfigDetails, skipInterpolation bool) (string, error) { optsFunc := func(options *composeLoader.Options) { options.SkipInterpolation = skipInterpolation } diff --git a/cli/command/stack/config_test.go b/cli/command/stack/config_test.go index d53c11e5af16..2dbf701103c6 100644 --- a/cli/command/stack/config_test.go +++ b/cli/command/stack/config_test.go @@ -43,7 +43,7 @@ services: "VERSION": "1.0", } - cfg, err := OutputConfig(composetypes.ConfigDetails{ + cfg, err := outputConfig(composetypes.ConfigDetails{ ConfigFiles: []composetypes.ConfigFile{ {Config: firstConfigData, Filename: "firstConfig"}, {Config: secondConfigData, Filename: "secondConfig"}, @@ -89,7 +89,7 @@ services: "VERSION": "1.0", } - cfg, err := OutputConfig(composetypes.ConfigDetails{ + cfg, err := outputConfig(composetypes.ConfigDetails{ ConfigFiles: []composetypes.ConfigFile{ {Config: firstConfigData, Filename: "firstConfig"}, {Config: secondConfigData, Filename: "secondConfig"}, From ab7e34be73eb90127292e33dcaf00abbda737db2 Mon Sep 17 00:00:00 2001 From: Stoica-Marcu Floris-Andrei Date: Wed, 7 Oct 2020 21:02:07 +0300 Subject: [PATCH 08/11] Merged the two tests by using table driven test pattern Signed-off-by: Stoica-Marcu Floris-Andrei --- cli/command/stack/config_test.go | 120 +++++++++++++++---------------- 1 file changed, 58 insertions(+), 62 deletions(-) diff --git a/cli/command/stack/config_test.go b/cli/command/stack/config_test.go index 2dbf701103c6..0c67f9816197 100644 --- a/cli/command/stack/config_test.go +++ b/cli/command/stack/config_test.go @@ -17,94 +17,90 @@ func TestConfigWithEmptyComposeFile(t *testing.T) { assert.ErrorContains(t, cmd.Execute(), `Please specify a Compose file`) } -func TestConfigMergeUsingInterpolation(t *testing.T) { - - firstConfig := []byte(` -version: "3.7" +var configMergeTests = []struct { + name string + skipInterpolation bool + first string + second string + merged string +}{ + { + name: "With Interpolation", + skipInterpolation: false, + first: `version: "3.7" services: foo: image: busybox:latest command: cat file1.txt -`) - secondConfig := []byte(` -version: "3.7" +`, + second: `version: "3.7" services: foo: image: busybox:${VERSION} command: cat file2.txt -`) - - firstConfigData, err := loader.ParseYAML(firstConfig) - assert.NilError(t, err) - secondConfigData, err := loader.ParseYAML(secondConfig) - assert.NilError(t, err) - - env := map[string]string{ - "VERSION": "1.0", - } - - cfg, err := outputConfig(composetypes.ConfigDetails{ - ConfigFiles: []composetypes.ConfigFile{ - {Config: firstConfigData, Filename: "firstConfig"}, - {Config: secondConfigData, Filename: "secondConfig"}, - }, - Environment: env, - }, false) - assert.NilError(t, err) - - var mergedConfig = `version: "3.7" +`, + merged: `version: "3.7" services: foo: command: - cat - file2.txt image: busybox:1.0 -` - assert.Equal(t, cfg, mergedConfig) -} - -func TestConfigMergeSkipInterpolation(t *testing.T) { - - firstConfig := []byte(` -version: "3.7" +`, + }, + { + name: "Without Interpolation", + skipInterpolation: true, + first: `version: "3.7" services: foo: image: busybox:latest command: cat file1.txt -`) - secondConfig := []byte(` -version: "3.7" +`, + second: `version: "3.7" services: foo: image: busybox:${VERSION} command: cat file2.txt -`) - - firstConfigData, err := loader.ParseYAML(firstConfig) - assert.NilError(t, err) - secondConfigData, err := loader.ParseYAML(secondConfig) - assert.NilError(t, err) - - env := map[string]string{ - "VERSION": "1.0", - } - - cfg, err := outputConfig(composetypes.ConfigDetails{ - ConfigFiles: []composetypes.ConfigFile{ - {Config: firstConfigData, Filename: "firstConfig"}, - {Config: secondConfigData, Filename: "secondConfig"}, - }, - Environment: env, - }, true) - assert.NilError(t, err) - - var mergedConfig = `version: "3.7" +`, + merged: `version: "3.7" services: foo: command: - cat - file2.txt image: busybox:${VERSION} -` - assert.Equal(t, cfg, mergedConfig) +`, + }, +} + +func TestConfigMergeInterpolation(t *testing.T) { + + for _, tt := range configMergeTests { + t.Run(tt.name, func(t *testing.T) { + firstConfig := []byte(tt.first) + secondConfig := []byte(tt.second) + + firstConfigData, err := loader.ParseYAML(firstConfig) + assert.NilError(t, err) + secondConfigData, err := loader.ParseYAML(secondConfig) + assert.NilError(t, err) + + env := map[string]string{ + "VERSION": "1.0", + } + + cfg, err := outputConfig(composetypes.ConfigDetails{ + ConfigFiles: []composetypes.ConfigFile{ + {Config: firstConfigData, Filename: "firstConfig"}, + {Config: secondConfigData, Filename: "secondConfig"}, + }, + Environment: env, + }, tt.skipInterpolation) + assert.NilError(t, err) + + assert.Equal(t, cfg, tt.merged) + }) + } + } From 411f26c28a34edcbb731556150b17a844b8b4f98 Mon Sep 17 00:00:00 2001 From: Stoica-Marcu Floris-Andrei Date: Thu, 8 Oct 2020 18:08:13 +0300 Subject: [PATCH 09/11] Return err directly when outputting to the configured cli output Signed-off-by: Stoica-Marcu Floris-Andrei --- cli/command/stack/config.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cli/command/stack/config.go b/cli/command/stack/config.go index 84a921842de4..28797da90e6c 100644 --- a/cli/command/stack/config.go +++ b/cli/command/stack/config.go @@ -32,11 +32,7 @@ func newConfigCommand(dockerCli command.Cli) *cobra.Command { } _, err = fmt.Fprintf(dockerCli.Out(), "%s", cfg) - if err != nil { - return err - } - - return nil + return err }, } From 8674459e375ef2a943e747a71299ac6cf50ae97c Mon Sep 17 00:00:00 2001 From: Stoica-Marcu Floris-Andrei Date: Wed, 16 Dec 2020 21:20:32 +0200 Subject: [PATCH 10/11] remove composer version check since we are not calling the api Signed-off-by: Stoica-Marcu Floris-Andrei --- cli/command/stack/config.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/command/stack/config.go b/cli/command/stack/config.go index 28797da90e6c..c6bf74f1e3a3 100644 --- a/cli/command/stack/config.go +++ b/cli/command/stack/config.go @@ -38,7 +38,6 @@ func newConfigCommand(dockerCli command.Cli) *cobra.Command { flags := cmd.Flags() flags.StringSliceVarP(&opts.Composefiles, "compose-file", "c", []string{}, `Path to a Compose file, or "-" to read from stdin`) - flags.SetAnnotation("compose-file", "version", []string{"1.25"}) flags.BoolVar(&opts.SkipInterpolation, "skip-interpolation", false, "Skip interpolation and output only merged config") return cmd } From 98d5109fb99aea2943f51d0ce57c40fbfc59a481 Mon Sep 17 00:00:00 2001 From: Stoica-Marcu Floris-Andrei Date: Wed, 16 Dec 2020 23:17:59 +0200 Subject: [PATCH 11/11] add experimental flag to config command Signed-off-by: Stoica-Marcu Floris-Andrei --- cli/command/stack/config.go | 1 + e2e/stack/config_test.go | 2 ++ 2 files changed, 3 insertions(+) diff --git a/cli/command/stack/config.go b/cli/command/stack/config.go index c6bf74f1e3a3..8398805d5d57 100644 --- a/cli/command/stack/config.go +++ b/cli/command/stack/config.go @@ -34,6 +34,7 @@ func newConfigCommand(dockerCli command.Cli) *cobra.Command { _, err = fmt.Fprintf(dockerCli.Out(), "%s", cfg) return err }, + Annotations: map[string]string{"experimental": ""}, } flags := cmd.Flags() diff --git a/e2e/stack/config_test.go b/e2e/stack/config_test.go index 1d56ae701984..3c758c3aa772 100644 --- a/e2e/stack/config_test.go +++ b/e2e/stack/config_test.go @@ -3,10 +3,12 @@ package stack import ( "testing" + "github.com/docker/cli/internal/test/environment" "gotest.tools/v3/icmd" ) func TestConfigFullStack(t *testing.T) { + environment.SkipIfNotExperimentalDaemon(t) result := icmd.RunCommand("docker", "stack", "config", "--compose-file=./testdata/full-stack.yml") result.Assert(t, icmd.Success) }