Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/command/stack/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func NewStackCommand(dockerCli command.Cli) *cobra.Command {
newPsCommand(dockerCli),
newRemoveCommand(dockerCli),
newServicesCommand(dockerCli),
newConfigCommand(dockerCli),
)
flags := cmd.PersistentFlags()
flags.String("orchestrator", "", "Orchestrator to use (swarm|all)")
Expand Down
60 changes: 60 additions & 0 deletions cli/command/stack/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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]",
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
}

_, err = fmt.Fprintf(dockerCli.Out(), "%s", cfg)
return err
},
}

flags := cmd.Flags()
flags.StringSliceVarP(&opts.Composefiles, "compose-file", "c", []string{}, `Path to a Compose file, or "-" to read from stdin`)
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
}
106 changes: 106 additions & 0 deletions cli/command/stack/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package stack

import (
"io"
"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(io.Discard)

assert.ErrorContains(t, cmd.Execute(), `Please specify a Compose file`)
}

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
`,
second: `version: "3.7"
services:
foo:
image: busybox:${VERSION}
command: cat file2.txt
`,
merged: `version: "3.7"
services:
foo:
command:
- cat
- file2.txt
image: busybox:1.0
`,
},
{
name: "Without Interpolation",
skipInterpolation: true,
first: `version: "3.7"
services:
foo:
image: busybox:latest
command: cat file1.txt
`,
second: `version: "3.7"
services:
foo:
image: busybox:${VERSION}
command: cat file2.txt
`,
merged: `version: "3.7"
services:
foo:
command:
- cat
- file2.txt
image: busybox:${VERSION}
`,
},
}

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

}
5 changes: 3 additions & 2 deletions cli/command/stack/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,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
}
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions cli/command/stack/loader/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions cli/command/stack/options/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions cli/compose/loader/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ 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.ShellCommand{}): mergeShellCommand,
reflect.TypeOf(&types.ServiceNetworkConfig{}): mergeServiceNetworkConfig,
},
}
Expand Down Expand Up @@ -116,6 +118,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 {
Expand Down Expand Up @@ -146,6 +160,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

Expand Down Expand Up @@ -211,6 +235,14 @@ func mergeUlimitsConfig(dst, src reflect.Value) error {
return nil
}

//nolint: unparam
func mergeShellCommand(dst, src reflect.Value) error {
if src.Len() != 0 {
dst.Set(src)
}
return nil
}

//nolint: unparam
func mergeServiceNetworkConfig(dst, src reflect.Value) error {
if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() {
Expand Down
Loading