Skip to content
Open
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
7 changes: 5 additions & 2 deletions cli/command/stack/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
type configOptions struct {
composeFiles []string
skipInterpolation bool
profiles []string
}

func newConfigCommand(dockerCLI command.Cli) *cobra.Command {
Expand All @@ -31,7 +32,7 @@ func newConfigCommand(dockerCLI command.Cli) *cobra.Command {
return err
}

cfg, err := outputConfig(configDetails, opts.skipInterpolation)
cfg, err := outputConfig(configDetails, opts.skipInterpolation, opts.profiles)
if err != nil {
return err
}
Expand All @@ -46,13 +47,15 @@ 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.BoolVar(&opts.skipInterpolation, "skip-interpolation", false, "Skip interpolation and output only merged config")
flags.StringArrayVar(&opts.profiles, "profile", []string{}, "Specify a profile to enable")
return cmd
}

// outputConfig returns the merged and interpolated config file
func outputConfig(configFiles composetypes.ConfigDetails, skipInterpolation bool) (string, error) {
func outputConfig(configFiles composetypes.ConfigDetails, skipInterpolation bool, profiles []string) (string, error) {
optsFunc := func(opts *composeLoader.Options) {
opts.SkipInterpolation = skipInterpolation
opts.Profiles = profiles
}
config, err := composeLoader.Load(configFiles, optsFunc)
if err != nil {
Expand Down
31 changes: 30 additions & 1 deletion cli/command/stack/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package stack

import (
"io"
"strings"
"testing"

"github.com/docker/cli/cli/compose/loader"
Expand Down Expand Up @@ -91,9 +92,37 @@ services:
Environment: map[string]string{
"VERSION": "1.0",
},
}, tc.skipInterpolation)
}, tc.skipInterpolation, nil)
assert.Check(t, err)
assert.Equal(t, tc.expected, actual)
})
}
}

func TestConfigProfiles(t *testing.T) {
dict, err := loader.ParseYAML([]byte(`version: "3.8"
services:
web:
image: busybox:latest
debug:
image: busybox:latest
profiles:
- debug
`))
assert.NilError(t, err)
details := composetypes.ConfigDetails{
ConfigFiles: []composetypes.ConfigFile{
{Config: dict, Filename: "compose.yaml"},
},
}

withoutProfile, err := outputConfig(details, false, nil)
assert.NilError(t, err)
assert.Check(t, !strings.Contains(withoutProfile, "debug:"))
assert.Check(t, strings.Contains(withoutProfile, "web:"))

withProfile, err := outputConfig(details, false, []string{"debug"})
assert.NilError(t, err)
assert.Check(t, strings.Contains(withProfile, "debug:"))
assert.Check(t, strings.Contains(withProfile, "web:"))
}
2 changes: 2 additions & 0 deletions cli/command/stack/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type deployOptions struct {
prune bool
detach bool
quiet bool
profiles []string
}

func newDeployCommand(dockerCLI command.Cli) *cobra.Command {
Expand Down Expand Up @@ -65,6 +66,7 @@ func newDeployCommand(dockerCLI command.Cli) *cobra.Command {
flags.SetAnnotation("resolve-image", "version", []string{"1.30"})
flags.BoolVarP(&opts.detach, "detach", "d", true, "Exit immediately instead of waiting for the stack services to converge")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress progress output")
flags.StringArrayVar(&opts.profiles, "profile", []string{}, "Specify a profile to enable")
return cmd
}

Expand Down
4 changes: 3 additions & 1 deletion cli/command/stack/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ func loadComposeFile(streams command.Streams, opts deployOptions) (*composetypes
return nil, err
}

config, err := loader.Load(configDetails)
config, err := loader.Load(configDetails, func(o *loader.Options) {
o.Profiles = opts.profiles
})
if err != nil {
if fpe, ok := errors.AsType[*loader.ForbiddenPropertiesError](err); ok {
// this error is intentionally formatted multi-line
Expand Down
60 changes: 59 additions & 1 deletion cli/compose/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ type Options struct {
Interpolate *interp.Options
// Discard 'env_file' entries after resolving to 'environment' section
discardEnvFiles bool
// Profiles to enable, in addition to COMPOSE_PROFILES from the environment.
Profiles []string
}

// ParseVolume parses a volume spec without any knowledge of the target platform.
Expand Down Expand Up @@ -137,7 +139,63 @@ func Load(configDetails types.ConfigDetails, opt ...func(*Options)) (*types.Conf
configs = append(configs, cfg)
}

return merge(configs)
cfg, err := merge(configs)
if err != nil {
return nil, err
}
cfg.Services = filterServicesByProfile(cfg.Services, profilesFrom(configDetails, options))
return cfg, nil
}

func profilesFrom(configDetails types.ConfigDetails, options *Options) []string {
var out []string
seen := map[string]struct{}{}
add := func(p string) {
p = strings.TrimSpace(p)
if p == "" {
return
}
if _, ok := seen[p]; ok {
return
}
seen[p] = struct{}{}
out = append(out, p)
}
if v := configDetails.Environment["COMPOSE_PROFILES"]; v != "" {
for _, p := range strings.Split(v, ",") {
add(p)
}
}
for _, p := range options.Profiles {
add(p)
}
return out
}

func filterServicesByProfile(services []types.ServiceConfig, enabled []string) []types.ServiceConfig {
active := make(map[string]struct{}, len(enabled))
for _, p := range enabled {
active[p] = struct{}{}
}
out := make([]types.ServiceConfig, 0, len(services))
for _, svc := range services {
if serviceEnabledForProfiles(svc, active) {
out = append(out, svc)
}
}
return out
}

func serviceEnabledForProfiles(svc types.ServiceConfig, active map[string]struct{}) bool {
if len(svc.Profiles) == 0 {
return true
}
for _, p := range svc.Profiles {
if _, ok := active[p]; ok {
return true
}
}
return false
}

func validateForbidden(configDict map[string]any) error {
Expand Down
34 changes: 34 additions & 0 deletions cli/compose/loader/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,40 @@ func TestInvalidResource(t *testing.T) {
assert.Check(t, is.ErrorContains(err, "additional property 'impossible' is not allowed"))
}

func TestLoadProfiles(t *testing.T) {
yaml := `
version: "3.8"
services:
web:
image: busybox
debug:
image: busybox
profiles:
- debug
`
config, err := loadYAML(yaml)
assert.NilError(t, err)
assert.Equal(t, len(config.Services), 1)
assert.Equal(t, config.Services[0].Name, "web")

config, err = loadYAMLWithEnv(yaml, map[string]string{"COMPOSE_PROFILES": "debug"})
assert.NilError(t, err)
assert.Equal(t, len(config.Services), 2)
byName := map[string]types.ServiceConfig{}
for _, svc := range config.Services {
byName[svc.Name] = svc
}
assert.Check(t, is.DeepEqual(byName["debug"].Profiles, []string{"debug"}))

dict, err := ParseYAML([]byte(yaml))
assert.NilError(t, err)
config, err = Load(buildConfigDetails(dict, nil), func(o *Options) {
o.Profiles = []string{"debug"}
})
assert.NilError(t, err)
assert.Equal(t, len(config.Services), 2)
}

func TestInvalidExternalAndDriverCombination(t *testing.T) {
_, err := loadYAML(`
version: "3"
Expand Down
1 change: 1 addition & 0 deletions cli/compose/schema/data/config_schema_v3.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
},

"privileged": {"type": "boolean"},
"profiles": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
Expand Down
1 change: 1 addition & 0 deletions cli/compose/schema/data/config_schema_v3.1.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@
},

"privileged": {"type": "boolean"},
"profiles": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
Expand Down
1 change: 1 addition & 0 deletions cli/compose/schema/data/config_schema_v3.10.json
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@
},

"privileged": {"type": "boolean"},
"profiles": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
Expand Down
1 change: 1 addition & 0 deletions cli/compose/schema/data/config_schema_v3.11.json
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@
},

"privileged": {"type": "boolean"},
"profiles": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
Expand Down
1 change: 1 addition & 0 deletions cli/compose/schema/data/config_schema_v3.12.json
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@
},

"privileged": {"type": "boolean"},
"profiles": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
Expand Down
1 change: 1 addition & 0 deletions cli/compose/schema/data/config_schema_v3.13.json
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@
},

"privileged": {"type": "boolean"},
"profiles": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
Expand Down
1 change: 1 addition & 0 deletions cli/compose/schema/data/config_schema_v3.2.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
},

"privileged": {"type": "boolean"},
"profiles": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
Expand Down
1 change: 1 addition & 0 deletions cli/compose/schema/data/config_schema_v3.3.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@
},

"privileged": {"type": "boolean"},
"profiles": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
Expand Down
1 change: 1 addition & 0 deletions cli/compose/schema/data/config_schema_v3.4.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@
},

"privileged": {"type": "boolean"},
"profiles": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
Expand Down
1 change: 1 addition & 0 deletions cli/compose/schema/data/config_schema_v3.5.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
},

"privileged": {"type": "boolean"},
"profiles": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
Expand Down
1 change: 1 addition & 0 deletions cli/compose/schema/data/config_schema_v3.6.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
},

"privileged": {"type": "boolean"},
"profiles": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
Expand Down
1 change: 1 addition & 0 deletions cli/compose/schema/data/config_schema_v3.7.json
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@
},

"privileged": {"type": "boolean"},
"profiles": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
Expand Down
1 change: 1 addition & 0 deletions cli/compose/schema/data/config_schema_v3.8.json
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@
},

"privileged": {"type": "boolean"},
"profiles": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
Expand Down
1 change: 1 addition & 0 deletions cli/compose/schema/data/config_schema_v3.9.json
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@
},

"privileged": {"type": "boolean"},
"profiles": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
Expand Down
14 changes: 14 additions & 0 deletions cli/compose/schema/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,20 @@ func TestValidatePorts(t *testing.T) {
}
}

func TestValidateProfiles(t *testing.T) {
config := dict{
"version": "3.8",
"services": dict{
"foo": dict{
"image": "busybox",
"profiles": []any{"debug", "dev"},
},
},
}
assert.NilError(t, Validate(config, "3.8"))
assert.NilError(t, Validate(config, "3"))
}

func TestValidateUndefinedTopLevelOption(t *testing.T) {
config := dict{
"version": "3.0",
Expand Down
1 change: 1 addition & 0 deletions cli/compose/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ type ServiceConfig struct {
Pid string `yaml:",omitempty" json:"pid,omitempty"`
Ports []ServicePortConfig `yaml:",omitempty" json:"ports,omitempty"`
Privileged bool `yaml:",omitempty" json:"privileged,omitempty"`
Profiles []string `yaml:",omitempty" json:"profiles,omitempty"`
ReadOnly bool `mapstructure:"read_only" yaml:"read_only,omitempty" json:"read_only,omitempty"`
Restart string `yaml:",omitempty" json:"restart,omitempty"`
Secrets []ServiceSecretConfig `yaml:",omitempty" json:"secrets,omitempty"`
Expand Down
1 change: 1 addition & 0 deletions docs/reference/commandline/stack_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Outputs the final config file, after doing merges and interpolations
| Name | Type | Default | Description |
|:-----------------------|:--------------|:--------|:--------------------------------------------------|
| `-c`, `--compose-file` | `stringSlice` | | Path to a Compose file, or `-` to read from stdin |
| `--profile` | `stringArray` | | Specify a profile to enable |
| `--skip-interpolation` | `bool` | | Skip interpolation and output only merged config |


Expand Down
1 change: 1 addition & 0 deletions docs/reference/commandline/stack_deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Deploy a new stack or update an existing stack
|:---------------------------------------------------------|:--------------|:---------|:--------------------------------------------------------------------------------------------------|
| [`-c`](#compose-file), [`--compose-file`](#compose-file) | `stringSlice` | | Path to a Compose file, or `-` to read from stdin |
| `-d`, `--detach` | `bool` | `true` | Exit immediately instead of waiting for the stack services to converge |
| `--profile` | `stringArray` | | Specify a profile to enable |
| `--prune` | `bool` | | Prune services that are no longer referenced |
| `-q`, `--quiet` | `bool` | | Suppress progress output |
| `--resolve-image` | `string` | `always` | Query the registry to resolve image digest and supported platforms (`always`, `changed`, `never`) |
Expand Down
1 change: 1 addition & 0 deletions e2e/stack/testdata/stack-deploy-help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Options:
from stdin
-d, --detach Exit immediately instead of waiting for
the stack services to converge (default true)
--profile stringArray Specify a profile to enable
--prune Prune services that are no longer referenced
-q, --quiet Suppress progress output
--resolve-image string Query the registry to resolve image digest
Expand Down
Loading