Skip to content

Commit c7bafb8

Browse files
committed
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 <floris.sm@gmail.com>
1 parent 6916b42 commit c7bafb8

7 files changed

Lines changed: 196 additions & 4 deletions

File tree

cli/command/stack/cmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ func NewStackCommand(dockerCli command.Cli) *cobra.Command {
6565
newPsCommand(dockerCli, &opts),
6666
newRemoveCommand(dockerCli, &opts),
6767
newServicesCommand(dockerCli, &opts),
68+
newConfigCommand(dockerCli),
6869
)
6970
flags := cmd.PersistentFlags()
7071
flags.String("kubeconfig", "", "Kubernetes config file")

cli/command/stack/config.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package stack
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/docker/cli/cli"
7+
"github.com/docker/cli/cli/command"
8+
"github.com/docker/cli/cli/command/stack/loader"
9+
"github.com/docker/cli/cli/command/stack/options"
10+
composeLoader "github.com/docker/cli/cli/compose/loader"
11+
composetypes "github.com/docker/cli/cli/compose/types"
12+
"github.com/spf13/cobra"
13+
yaml "gopkg.in/yaml.v2"
14+
)
15+
16+
func newConfigCommand(dockerCli command.Cli) *cobra.Command {
17+
var opts options.Config
18+
19+
cmd := &cobra.Command{
20+
Use: "config [OPTIONS]",
21+
Aliases: []string{"cfg"},
22+
Short: "Outputs the final config file, after doing merges and interpolations",
23+
Args: cli.NoArgs,
24+
RunE: func(cmd *cobra.Command, args []string) error {
25+
configDetails, err := loader.GetConfigDetails(opts.Composefiles, dockerCli.In())
26+
if err != nil {
27+
return err
28+
}
29+
30+
cfg, err := OutputConfig(configDetails, opts.SkipInterpolation)
31+
if err != nil {
32+
return err
33+
}
34+
35+
fmt.Printf("%s", cfg)
36+
return nil
37+
},
38+
}
39+
40+
flags := cmd.Flags()
41+
flags.StringSliceVarP(&opts.Composefiles, "compose-file", "c", []string{}, `Path to a Compose file, or "-" to read from stdin`)
42+
flags.SetAnnotation("compose-file", "version", []string{"1.25"})
43+
flags.BoolVar(&opts.SkipInterpolation, "skip-interpolation", false, "Skip interpolation and output only merged config")
44+
return cmd
45+
}
46+
47+
// OutputConfig returns the merged and interpolated config file
48+
func OutputConfig(configFiles composetypes.ConfigDetails, skipInterpolation bool) (string, error) {
49+
optsFunc := func(options *composeLoader.Options) {
50+
options.SkipInterpolation = skipInterpolation
51+
}
52+
config, err := composeLoader.Load(configFiles, optsFunc)
53+
if err != nil {
54+
return "", err
55+
}
56+
57+
d, err := yaml.Marshal(&config)
58+
if err != nil {
59+
return "", err
60+
}
61+
return string(d), nil
62+
}

cli/command/stack/config_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package stack
2+
3+
import (
4+
"io/ioutil"
5+
"testing"
6+
7+
"github.com/docker/cli/cli/compose/loader"
8+
composetypes "github.com/docker/cli/cli/compose/types"
9+
"github.com/docker/cli/internal/test"
10+
"gotest.tools/v3/assert"
11+
)
12+
13+
func TestConfigWithEmptyComposeFile(t *testing.T) {
14+
cmd := newConfigCommand(test.NewFakeCli(&fakeClient{}))
15+
cmd.SetOut(ioutil.Discard)
16+
17+
assert.ErrorContains(t, cmd.Execute(), `Please specify a Compose file`)
18+
}
19+
20+
func TestConfigMergeUsingInterpolation(t *testing.T) {
21+
22+
firstConfig := []byte(`
23+
version: "3.7"
24+
services:
25+
foo:
26+
image: busybox:latest
27+
command: cat file1.txt
28+
`)
29+
secondConfig := []byte(`
30+
version: "3.7"
31+
services:
32+
foo:
33+
image: busybox:${VERSION}
34+
command: cat file2.txt
35+
`)
36+
37+
firstConfigData, err := loader.ParseYAML(firstConfig)
38+
assert.NilError(t, err)
39+
secondConfigData, err := loader.ParseYAML(secondConfig)
40+
assert.NilError(t, err)
41+
42+
env := map[string]string{
43+
"VERSION": "1.0",
44+
}
45+
46+
cfg, err := OutputConfig(composetypes.ConfigDetails{
47+
ConfigFiles: []composetypes.ConfigFile{
48+
{Config: firstConfigData, Filename: "firstConfig"},
49+
{Config: secondConfigData, Filename: "secondConfig"},
50+
},
51+
Environment: env,
52+
}, false)
53+
assert.NilError(t, err)
54+
55+
var mergedConfig = `version: "3.7"
56+
services:
57+
foo:
58+
command:
59+
- cat
60+
- file2.txt
61+
image: busybox:1.0
62+
`
63+
assert.Equal(t, cfg, mergedConfig)
64+
}
65+
66+
func TestConfigMergeSkipInterpolation(t *testing.T) {
67+
68+
firstConfig := []byte(`
69+
version: "3.7"
70+
services:
71+
foo:
72+
image: busybox:latest
73+
command: cat file1.txt
74+
`)
75+
secondConfig := []byte(`
76+
version: "3.7"
77+
services:
78+
foo:
79+
image: busybox:${VERSION}
80+
command: cat file2.txt
81+
`)
82+
83+
firstConfigData, err := loader.ParseYAML(firstConfig)
84+
assert.NilError(t, err)
85+
secondConfigData, err := loader.ParseYAML(secondConfig)
86+
assert.NilError(t, err)
87+
88+
env := map[string]string{
89+
"VERSION": "1.0",
90+
}
91+
92+
cfg, err := OutputConfig(composetypes.ConfigDetails{
93+
ConfigFiles: []composetypes.ConfigFile{
94+
{Config: firstConfigData, Filename: "firstConfig"},
95+
{Config: secondConfigData, Filename: "secondConfig"},
96+
},
97+
Environment: env,
98+
}, true)
99+
assert.NilError(t, err)
100+
101+
var mergedConfig = `version: "3.7"
102+
services:
103+
foo:
104+
command:
105+
- cat
106+
- file2.txt
107+
image: busybox:${VERSION}
108+
`
109+
assert.Equal(t, cfg, mergedConfig)
110+
}

cli/command/stack/loader/loader.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import (
1919

2020
// LoadComposefile parse the composefile specified in the cli and returns its Config and version.
2121
func LoadComposefile(dockerCli command.Cli, opts options.Deploy) (*composetypes.Config, error) {
22-
configDetails, err := getConfigDetails(opts.Composefiles, dockerCli.In())
22+
configDetails, err := GetConfigDetails(opts.Composefiles, dockerCli.In())
2323
if err != nil {
2424
return nil, err
2525
}
@@ -68,7 +68,8 @@ func propertyWarnings(properties map[string]string) string {
6868
return strings.Join(msgs, "\n\n")
6969
}
7070

71-
func getConfigDetails(composefiles []string, stdin io.Reader) (composetypes.ConfigDetails, error) {
71+
// GetConfigDetails parse the composefiles specified in the cli and returns their ConfigDetails
72+
func GetConfigDetails(composefiles []string, stdin io.Reader) (composetypes.ConfigDetails, error) {
7273
var details composetypes.ConfigDetails
7374

7475
if len(composefiles) == 0 {

cli/command/stack/loader/loader_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ services:
2121
file := fs.NewFile(t, "test-get-config-details", fs.WithContent(content))
2222
defer file.Remove()
2323

24-
details, err := getConfigDetails([]string{file.Path()}, nil)
24+
details, err := GetConfigDetails([]string{file.Path()}, nil)
2525
assert.NilError(t, err)
2626
assert.Check(t, is.Equal(filepath.Dir(file.Path()), details.WorkingDir))
2727
assert.Assert(t, is.Len(details.ConfigFiles, 1))
@@ -36,7 +36,7 @@ services:
3636
foo:
3737
image: alpine:3.5
3838
`
39-
details, err := getConfigDetails([]string{"-"}, strings.NewReader(content))
39+
details, err := GetConfigDetails([]string{"-"}, strings.NewReader(content))
4040
assert.NilError(t, err)
4141
cwd, err := os.Getwd()
4242
assert.NilError(t, err)

cli/command/stack/options/opts.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ type Deploy struct {
1111
Prune bool
1212
}
1313

14+
// Config holds docker stack config options
15+
type Config struct {
16+
Composefiles []string
17+
SkipInterpolation bool
18+
}
19+
1420
// List holds docker stack ls options
1521
type List struct {
1622
Format string

e2e/stack/config_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package stack
2+
3+
import (
4+
"testing"
5+
6+
"gotest.tools/v3/icmd"
7+
)
8+
9+
func TestConfigFullStack(t *testing.T) {
10+
result := icmd.RunCommand("docker", "stack", "config", "--compose-file=./testdata/full-stack.yml")
11+
result.Assert(t, icmd.Success)
12+
}

0 commit comments

Comments
 (0)