mirror of https://github.com/docker/cli.git
Merge pull request #3544 from thaJeztah/carry_2740_add_config_command
Add stack config command (carry 2740)
This commit is contained in:
commit
14976338f0
|
@ -34,6 +34,7 @@ func NewStackCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
newPsCommand(dockerCli),
|
newPsCommand(dockerCli),
|
||||||
newRemoveCommand(dockerCli),
|
newRemoveCommand(dockerCli),
|
||||||
newServicesCommand(dockerCli),
|
newServicesCommand(dockerCli),
|
||||||
|
newConfigCommand(dockerCli),
|
||||||
)
|
)
|
||||||
flags := cmd.PersistentFlags()
|
flags := cmd.PersistentFlags()
|
||||||
flags.String("orchestrator", "", "Orchestrator to use (swarm|all)")
|
flags.String("orchestrator", "", "Orchestrator to use (swarm|all)")
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -18,7 +18,7 @@ import (
|
||||||
|
|
||||||
// LoadComposefile parse the composefile specified in the cli and returns its Config and version.
|
// 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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,8 @@ func propertyWarnings(properties map[string]string) string {
|
||||||
return strings.Join(msgs, "\n\n")
|
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
|
var details composetypes.ConfigDetails
|
||||||
|
|
||||||
if len(composefiles) == 0 {
|
if len(composefiles) == 0 {
|
||||||
|
|
|
@ -21,7 +21,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([]string{file.Path()}, nil)
|
details, err := GetConfigDetails([]string{file.Path()}, nil)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Check(t, is.Equal(filepath.Dir(file.Path()), details.WorkingDir))
|
assert.Check(t, is.Equal(filepath.Dir(file.Path()), details.WorkingDir))
|
||||||
assert.Assert(t, is.Len(details.ConfigFiles, 1))
|
assert.Assert(t, is.Len(details.ConfigFiles, 1))
|
||||||
|
@ -36,7 +36,7 @@ services:
|
||||||
foo:
|
foo:
|
||||||
image: alpine:3.5
|
image: alpine:3.5
|
||||||
`
|
`
|
||||||
details, err := getConfigDetails([]string{"-"}, strings.NewReader(content))
|
details, err := GetConfigDetails([]string{"-"}, strings.NewReader(content))
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
|
@ -11,6 +11,12 @@ type Deploy struct {
|
||||||
Prune bool
|
Prune bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Config holds docker stack config options
|
||||||
|
type Config struct {
|
||||||
|
Composefiles []string
|
||||||
|
SkipInterpolation bool
|
||||||
|
}
|
||||||
|
|
||||||
// List holds docker stack ls options
|
// List holds docker stack ls options
|
||||||
type List struct {
|
type List struct {
|
||||||
Format string
|
Format string
|
||||||
|
|
|
@ -58,6 +58,8 @@ func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig,
|
||||||
reflect.TypeOf([]types.ServiceSecretConfig{}): mergeSlice(toServiceSecretConfigsMap, toServiceSecretConfigsSlice),
|
reflect.TypeOf([]types.ServiceSecretConfig{}): mergeSlice(toServiceSecretConfigsMap, toServiceSecretConfigsSlice),
|
||||||
reflect.TypeOf([]types.ServiceConfigObjConfig{}): mergeSlice(toServiceConfigObjConfigsMap, toSServiceConfigObjConfigsSlice),
|
reflect.TypeOf([]types.ServiceConfigObjConfig{}): mergeSlice(toServiceConfigObjConfigsMap, toSServiceConfigObjConfigsSlice),
|
||||||
reflect.TypeOf(&types.UlimitsConfig{}): mergeUlimitsConfig,
|
reflect.TypeOf(&types.UlimitsConfig{}): mergeUlimitsConfig,
|
||||||
|
reflect.TypeOf([]types.ServiceVolumeConfig{}): mergeSlice(toServiceVolumeConfigsMap, toServiceVolumeConfigsSlice),
|
||||||
|
reflect.TypeOf(types.ShellCommand{}): mergeShellCommand,
|
||||||
reflect.TypeOf(&types.ServiceNetworkConfig{}): mergeServiceNetworkConfig,
|
reflect.TypeOf(&types.ServiceNetworkConfig{}): mergeServiceNetworkConfig,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -116,6 +118,18 @@ func toServicePortConfigsMap(s interface{}) (map[interface{}]interface{}, error)
|
||||||
return m, nil
|
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 {
|
func toServiceSecretConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error {
|
||||||
s := []types.ServiceSecretConfig{}
|
s := []types.ServiceSecretConfig{}
|
||||||
for _, v := range m {
|
for _, v := range m {
|
||||||
|
@ -146,6 +160,16 @@ func toServicePortConfigsSlice(dst reflect.Value, m map[interface{}]interface{})
|
||||||
return nil
|
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 tomapFn func(s interface{}) (map[interface{}]interface{}, error)
|
||||||
type writeValueFromMapFn func(reflect.Value, map[interface{}]interface{}) error
|
type writeValueFromMapFn func(reflect.Value, map[interface{}]interface{}) error
|
||||||
|
|
||||||
|
@ -211,6 +235,14 @@ func mergeUlimitsConfig(dst, src reflect.Value) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint: unparam
|
||||||
|
func mergeShellCommand(dst, src reflect.Value) error {
|
||||||
|
if src.Len() != 0 {
|
||||||
|
dst.Set(src)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
//nolint: unparam
|
//nolint: unparam
|
||||||
func mergeServiceNetworkConfig(dst, src reflect.Value) error {
|
func mergeServiceNetworkConfig(dst, src reflect.Value) error {
|
||||||
if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() {
|
if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() {
|
||||||
|
|
|
@ -1017,6 +1017,134 @@ func TestLoadMultipleNetworks(t *testing.T) {
|
||||||
}, config)
|
}, 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",
|
||||||
|
"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) {
|
func TestMergeUlimitsConfig(t *testing.T) {
|
||||||
specials := &specials{
|
specials := &specials{
|
||||||
m: map[reflect.Type]func(dst, src reflect.Value) error{
|
m: map[reflect.Type]func(dst, src reflect.Value) error{
|
||||||
|
|
|
@ -153,12 +153,13 @@ read the [`dockerd`](dockerd.md) reference page.
|
||||||
### Swarm stack commands
|
### Swarm stack commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|:------------------------------------|:-----------------------------------------------|
|
|:------------------------------------|:--------------------------------------------------------|
|
||||||
| [stack deploy](stack_deploy.md) | Deploy a new stack or update an existing stack |
|
| [stack deploy](stack_deploy.md) | Deploy a new stack or update an existing stack |
|
||||||
| [stack ls](stack_ls.md) | List stacks in the swarm |
|
| [stack ls](stack_ls.md) | List stacks in the swarm |
|
||||||
| [stack ps](stack_ps.md) | List the tasks in the stack |
|
| [stack ps](stack_ps.md) | List the tasks in the stack |
|
||||||
| [stack rm](stack_rm.md) | Remove the stack from the swarm |
|
| [stack rm](stack_rm.md) | Remove the stack from the swarm |
|
||||||
| [stack services](stack_services.md) | List the services in the stack |
|
| [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
|
### Plugin commands
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
@ -111,3 +111,4 @@ axqh55ipl40h vossibility_vossibility-collector replicated 1/1 icecrime/
|
||||||
* [stack ps](stack_ps.md)
|
* [stack ps](stack_ps.md)
|
||||||
* [stack rm](stack_rm.md)
|
* [stack rm](stack_rm.md)
|
||||||
* [stack services](stack_services.md)
|
* [stack services](stack_services.md)
|
||||||
|
* [stack config](stack_config.md)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
Loading…
Reference in New Issue