mirror of https://github.com/docker/cli.git
Fix environment resolving.
Load from env should only happen if the value is unset. Extract a buildEnvironment function and revert some changes to tests. Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
parent
b7ffa960bf
commit
146d3eb304
|
@ -15,6 +15,7 @@ import (
|
||||||
composetypes "github.com/docker/docker/cli/compose/types"
|
composetypes "github.com/docker/docker/cli/compose/types"
|
||||||
apiclient "github.com/docker/docker/client"
|
apiclient "github.com/docker/docker/client"
|
||||||
dockerclient "github.com/docker/docker/client"
|
dockerclient "github.com/docker/docker/client"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -115,17 +116,24 @@ func getConfigDetails(opts deployOptions) (composetypes.ConfigDetails, error) {
|
||||||
}
|
}
|
||||||
// TODO: support multiple files
|
// TODO: support multiple files
|
||||||
details.ConfigFiles = []composetypes.ConfigFile{*configFile}
|
details.ConfigFiles = []composetypes.ConfigFile{*configFile}
|
||||||
env := os.Environ()
|
details.Environment, err = buildEnvironment(os.Environ())
|
||||||
details.Environment = make(map[string]string, len(env))
|
if err != nil {
|
||||||
|
return details, err
|
||||||
|
}
|
||||||
|
return details, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildEnvironment(env []string) (map[string]string, error) {
|
||||||
|
result := make(map[string]string, len(env))
|
||||||
for _, s := range env {
|
for _, s := range env {
|
||||||
// if value is empty, s is like "K=", not "K".
|
// if value is empty, s is like "K=", not "K".
|
||||||
if !strings.Contains(s, "=") {
|
if !strings.Contains(s, "=") {
|
||||||
return details, fmt.Errorf("unexpected environment %q", s)
|
return result, errors.Errorf("unexpected environment %q", s)
|
||||||
}
|
}
|
||||||
kv := strings.SplitN(s, "=", 2)
|
kv := strings.SplitN(s, "=", 2)
|
||||||
details.Environment[kv[0]] = kv[1]
|
result[kv[0]] = kv[1]
|
||||||
}
|
}
|
||||||
return details, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConfigFile(filename string) (*composetypes.ConfigFile, error) {
|
func getConfigFile(filename string) (*composetypes.ConfigFile, error) {
|
||||||
|
|
|
@ -393,11 +393,16 @@ func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortC
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertEnvironment(source map[string]string) []string {
|
func convertEnvironment(source map[string]*string) []string {
|
||||||
var output []string
|
var output []string
|
||||||
|
|
||||||
for name, value := range source {
|
for name, value := range source {
|
||||||
output = append(output, fmt.Sprintf("%s=%s", name, value))
|
switch value {
|
||||||
|
case nil:
|
||||||
|
output = append(output, name)
|
||||||
|
default:
|
||||||
|
output = append(output, fmt.Sprintf("%s=%s", name, *value))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
|
@ -43,10 +43,14 @@ func TestConvertRestartPolicyFromFailure(t *testing.T) {
|
||||||
assert.DeepEqual(t, policy, expected)
|
assert.DeepEqual(t, policy, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func strPtr(val string) *string {
|
||||||
|
return &val
|
||||||
|
}
|
||||||
|
|
||||||
func TestConvertEnvironment(t *testing.T) {
|
func TestConvertEnvironment(t *testing.T) {
|
||||||
source := map[string]string{
|
source := map[string]*string{
|
||||||
"foo": "bar",
|
"foo": strPtr("bar"),
|
||||||
"key": "value",
|
"key": strPtr("value"),
|
||||||
}
|
}
|
||||||
env := convertEnvironment(source)
|
env := convertEnvironment(source)
|
||||||
sort.Strings(env)
|
sort.Strings(env)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# passed through
|
# passed through
|
||||||
FOO=1
|
FOO=foo_from_env_file
|
||||||
|
|
||||||
# overridden in example2.env
|
# overridden in example2.env
|
||||||
BAR=1
|
BAR=bar_from_env_file
|
||||||
|
|
||||||
# overridden in full-example.yml
|
# overridden in full-example.yml
|
||||||
BAZ=1
|
BAZ=baz_from_env_file
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
BAR=2
|
BAR=bar_from_env_file_2
|
||||||
|
|
||||||
# overridden in configDetails.Environment
|
# overridden in configDetails.Environment
|
||||||
QUX=1
|
QUX=quz_from_env_file_2
|
||||||
|
|
|
@ -77,10 +77,8 @@ services:
|
||||||
# Mapping values can be strings, numbers or null
|
# Mapping values can be strings, numbers or null
|
||||||
# Booleans are not allowed - must be quoted
|
# Booleans are not allowed - must be quoted
|
||||||
environment:
|
environment:
|
||||||
RACK_ENV: development
|
BAZ: baz_from_service_def
|
||||||
SHOW: 'true'
|
QUX:
|
||||||
SESSION_SECRET:
|
|
||||||
BAZ: 3
|
|
||||||
# environment:
|
# environment:
|
||||||
# - RACK_ENV=development
|
# - RACK_ENV=development
|
||||||
# - SHOW=true
|
# - SHOW=true
|
||||||
|
|
|
@ -253,9 +253,11 @@ func transformHook(
|
||||||
case reflect.TypeOf(map[string]*types.ServiceNetworkConfig{}):
|
case reflect.TypeOf(map[string]*types.ServiceNetworkConfig{}):
|
||||||
return transformServiceNetworkMap(data)
|
return transformServiceNetworkMap(data)
|
||||||
case reflect.TypeOf(types.MappingWithEquals{}):
|
case reflect.TypeOf(types.MappingWithEquals{}):
|
||||||
return transformMappingOrList(data, "="), nil
|
return transformMappingOrList(data, "=", true), nil
|
||||||
|
case reflect.TypeOf(types.Labels{}):
|
||||||
|
return transformMappingOrList(data, "=", false), nil
|
||||||
case reflect.TypeOf(types.MappingWithColon{}):
|
case reflect.TypeOf(types.MappingWithColon{}):
|
||||||
return transformMappingOrList(data, ":"), nil
|
return transformMappingOrList(data, ":", false), nil
|
||||||
case reflect.TypeOf(types.ServiceVolumeConfig{}):
|
case reflect.TypeOf(types.ServiceVolumeConfig{}):
|
||||||
return transformServiceVolumeConfig(data)
|
return transformServiceVolumeConfig(data)
|
||||||
}
|
}
|
||||||
|
@ -317,7 +319,7 @@ func LoadServices(servicesDict types.Dict, workingDir string, lookupEnv template
|
||||||
var services []types.ServiceConfig
|
var services []types.ServiceConfig
|
||||||
|
|
||||||
for name, serviceDef := range servicesDict {
|
for name, serviceDef := range servicesDict {
|
||||||
serviceConfig, err := loadService(name, serviceDef.(types.Dict), workingDir, lookupEnv)
|
serviceConfig, err := LoadService(name, serviceDef.(types.Dict), workingDir, lookupEnv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -344,25 +346,20 @@ func LoadService(name string, serviceDict types.Dict, workingDir string, lookupE
|
||||||
return serviceConfig, nil
|
return serviceConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateEnvironment(environment map[string]string, vars map[string]string, lookupEnv template.Mapping) map[string]string {
|
func updateEnvironment(environment map[string]*string, vars map[string]*string, lookupEnv template.Mapping) {
|
||||||
result := make(map[string]string, len(environment))
|
|
||||||
for k, v := range environment {
|
|
||||||
result[k]=v
|
|
||||||
}
|
|
||||||
for k, v := range vars {
|
for k, v := range vars {
|
||||||
interpolatedV, ok := lookupEnv(k)
|
interpolatedV, ok := lookupEnv(k)
|
||||||
if ok {
|
if (v == nil || *v == "") && ok {
|
||||||
// lookupEnv is prioritized over vars
|
// lookupEnv is prioritized over vars
|
||||||
result[k] = interpolatedV
|
environment[k] = &interpolatedV
|
||||||
} else {
|
} else {
|
||||||
result[k] = v
|
environment[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, lookupEnv template.Mapping) error {
|
func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, lookupEnv template.Mapping) error {
|
||||||
environment := make(map[string]string)
|
environment := make(map[string]*string)
|
||||||
|
|
||||||
if len(serviceConfig.EnvFile) > 0 {
|
if len(serviceConfig.EnvFile) > 0 {
|
||||||
var envVars []string
|
var envVars []string
|
||||||
|
@ -375,12 +372,12 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, l
|
||||||
}
|
}
|
||||||
envVars = append(envVars, fileVars...)
|
envVars = append(envVars, fileVars...)
|
||||||
}
|
}
|
||||||
environment = updateEnvironment(environment,
|
updateEnvironment(environment,
|
||||||
runconfigopts.ConvertKVStringsToMap(envVars), lookupEnv)
|
runconfigopts.ConvertKVStringsToMapWithNil(envVars), lookupEnv)
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceConfig.Environment = updateEnvironment(environment,
|
updateEnvironment(environment, serviceConfig.Environment, lookupEnv)
|
||||||
serviceConfig.Environment, lookupEnv)
|
serviceConfig.Environment = environment
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -497,9 +494,9 @@ func absPath(workingDir string, filepath string) string {
|
||||||
func transformMapStringString(data interface{}) (interface{}, error) {
|
func transformMapStringString(data interface{}) (interface{}, error) {
|
||||||
switch value := data.(type) {
|
switch value := data.(type) {
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
return toMapStringString(value), nil
|
return toMapStringString(value, false), nil
|
||||||
case types.Dict:
|
case types.Dict:
|
||||||
return toMapStringString(value), nil
|
return toMapStringString(value, false), nil
|
||||||
case map[string]string:
|
case map[string]string:
|
||||||
return value, nil
|
return value, nil
|
||||||
default:
|
default:
|
||||||
|
@ -613,23 +610,27 @@ func transformStringList(data interface{}) (interface{}, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func transformMappingOrList(mappingOrList interface{}, sep string) map[string]string {
|
func transformMappingOrList(mappingOrList interface{}, sep string, allowNil bool) interface{} {
|
||||||
if mapping, ok := mappingOrList.(types.Dict); ok {
|
switch value := mappingOrList.(type) {
|
||||||
return toMapStringString(mapping)
|
case types.Dict:
|
||||||
}
|
return toMapStringString(value, allowNil)
|
||||||
if list, ok := mappingOrList.([]interface{}); ok {
|
case ([]interface{}):
|
||||||
result := make(map[string]string)
|
result := make(map[string]interface{})
|
||||||
for _, value := range list {
|
for _, value := range value {
|
||||||
parts := strings.SplitN(value.(string), sep, 2)
|
parts := strings.SplitN(value.(string), sep, 2)
|
||||||
if len(parts) == 1 {
|
key := parts[0]
|
||||||
result[parts[0]] = ""
|
switch {
|
||||||
} else {
|
case len(parts) == 1 && allowNil:
|
||||||
result[parts[0]] = parts[1]
|
result[key] = nil
|
||||||
|
case len(parts) == 1 && !allowNil:
|
||||||
|
result[key] = ""
|
||||||
|
default:
|
||||||
|
result[key] = parts[1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
panic(fmt.Errorf("expected a map or a slice, got: %#v", mappingOrList))
|
panic(fmt.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList))
|
||||||
}
|
}
|
||||||
|
|
||||||
func transformShellCommand(value interface{}) (interface{}, error) {
|
func transformShellCommand(value interface{}) (interface{}, error) {
|
||||||
|
@ -693,17 +694,21 @@ func toServicePortConfigs(value string) ([]interface{}, error) {
|
||||||
return portConfigs, nil
|
return portConfigs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func toMapStringString(value map[string]interface{}) map[string]string {
|
func toMapStringString(value map[string]interface{}, allowNil bool) map[string]interface{} {
|
||||||
output := make(map[string]string)
|
output := make(map[string]interface{})
|
||||||
for key, value := range value {
|
for key, value := range value {
|
||||||
output[key] = toString(value)
|
output[key] = toString(value, allowNil)
|
||||||
}
|
}
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
func toString(value interface{}) string {
|
func toString(value interface{}, allowNil bool) interface{} {
|
||||||
if value == nil {
|
switch {
|
||||||
|
case value != nil:
|
||||||
|
return fmt.Sprint(value)
|
||||||
|
case allowNil:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return fmt.Sprint(value)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,19 @@ func buildConfigDetails(source types.Dict, env map[string]string) types.ConfigDe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadYAML(yaml string) (*types.Config, error) {
|
||||||
|
return loadYAMLWithEnv(yaml, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadYAMLWithEnv(yaml string, env map[string]string) (*types.Config, error) {
|
||||||
|
dict, err := ParseYAML([]byte(yaml))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Load(buildConfigDetails(dict, env))
|
||||||
|
}
|
||||||
|
|
||||||
var sampleYAML = `
|
var sampleYAML = `
|
||||||
version: "3"
|
version: "3"
|
||||||
services:
|
services:
|
||||||
|
@ -98,12 +111,16 @@ var sampleDict = types.Dict{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func strPtr(val string) *string {
|
||||||
|
return &val
|
||||||
|
}
|
||||||
|
|
||||||
var sampleConfig = types.Config{
|
var sampleConfig = types.Config{
|
||||||
Services: []types.ServiceConfig{
|
Services: []types.ServiceConfig{
|
||||||
{
|
{
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Image: "busybox",
|
Image: "busybox",
|
||||||
Environment: map[string]string{},
|
Environment: map[string]*string{},
|
||||||
Networks: map[string]*types.ServiceNetworkConfig{
|
Networks: map[string]*types.ServiceNetworkConfig{
|
||||||
"with_me": nil,
|
"with_me": nil,
|
||||||
},
|
},
|
||||||
|
@ -111,7 +128,7 @@ var sampleConfig = types.Config{
|
||||||
{
|
{
|
||||||
Name: "bar",
|
Name: "bar",
|
||||||
Image: "busybox",
|
Image: "busybox",
|
||||||
Environment: map[string]string{"FOO": "1"},
|
Environment: map[string]*string{"FOO": strPtr("1")},
|
||||||
Networks: map[string]*types.ServiceNetworkConfig{
|
Networks: map[string]*types.ServiceNetworkConfig{
|
||||||
"with_ipam": nil,
|
"with_ipam": nil,
|
||||||
},
|
},
|
||||||
|
@ -173,7 +190,7 @@ services:
|
||||||
secrets:
|
secrets:
|
||||||
super:
|
super:
|
||||||
external: true
|
external: true
|
||||||
`, nil)
|
`)
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -182,7 +199,7 @@ secrets:
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseAndLoad(t *testing.T) {
|
func TestParseAndLoad(t *testing.T) {
|
||||||
actual, err := loadYAML(sampleYAML, nil)
|
actual, err := loadYAML(sampleYAML)
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -192,15 +209,15 @@ func TestParseAndLoad(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInvalidTopLevelObjectType(t *testing.T) {
|
func TestInvalidTopLevelObjectType(t *testing.T) {
|
||||||
_, err := loadYAML("1", nil)
|
_, err := loadYAML("1")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "Top-level object must be a mapping")
|
assert.Contains(t, err.Error(), "Top-level object must be a mapping")
|
||||||
|
|
||||||
_, err = loadYAML("\"hello\"", nil)
|
_, err = loadYAML("\"hello\"")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "Top-level object must be a mapping")
|
assert.Contains(t, err.Error(), "Top-level object must be a mapping")
|
||||||
|
|
||||||
_, err = loadYAML("[\"hello\"]", nil)
|
_, err = loadYAML("[\"hello\"]")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "Top-level object must be a mapping")
|
assert.Contains(t, err.Error(), "Top-level object must be a mapping")
|
||||||
}
|
}
|
||||||
|
@ -211,7 +228,7 @@ version: "3"
|
||||||
123:
|
123:
|
||||||
foo:
|
foo:
|
||||||
image: busybox
|
image: busybox
|
||||||
`, nil)
|
`)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "Non-string key at top level: 123")
|
assert.Contains(t, err.Error(), "Non-string key at top level: 123")
|
||||||
|
|
||||||
|
@ -222,7 +239,7 @@ services:
|
||||||
image: busybox
|
image: busybox
|
||||||
123:
|
123:
|
||||||
image: busybox
|
image: busybox
|
||||||
`, nil)
|
`)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "Non-string key in services: 123")
|
assert.Contains(t, err.Error(), "Non-string key in services: 123")
|
||||||
|
|
||||||
|
@ -236,7 +253,7 @@ networks:
|
||||||
ipam:
|
ipam:
|
||||||
config:
|
config:
|
||||||
- 123: oh dear
|
- 123: oh dear
|
||||||
`, nil)
|
`)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "Non-string key in networks.default.ipam.config[0]: 123")
|
assert.Contains(t, err.Error(), "Non-string key in networks.default.ipam.config[0]: 123")
|
||||||
|
|
||||||
|
@ -247,7 +264,7 @@ services:
|
||||||
image: busybox
|
image: busybox
|
||||||
environment:
|
environment:
|
||||||
1: FOO
|
1: FOO
|
||||||
`, nil)
|
`)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "Non-string key in services.dict-env.environment: 1")
|
assert.Contains(t, err.Error(), "Non-string key in services.dict-env.environment: 1")
|
||||||
}
|
}
|
||||||
|
@ -258,7 +275,7 @@ version: "3"
|
||||||
services:
|
services:
|
||||||
foo:
|
foo:
|
||||||
image: busybox
|
image: busybox
|
||||||
`, nil)
|
`)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
_, err = loadYAML(`
|
_, err = loadYAML(`
|
||||||
|
@ -266,7 +283,7 @@ version: "3.0"
|
||||||
services:
|
services:
|
||||||
foo:
|
foo:
|
||||||
image: busybox
|
image: busybox
|
||||||
`, nil)
|
`)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,7 +293,7 @@ version: "2"
|
||||||
services:
|
services:
|
||||||
foo:
|
foo:
|
||||||
image: busybox
|
image: busybox
|
||||||
`, nil)
|
`)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "version")
|
assert.Contains(t, err.Error(), "version")
|
||||||
|
|
||||||
|
@ -285,7 +302,7 @@ version: "2.0"
|
||||||
services:
|
services:
|
||||||
foo:
|
foo:
|
||||||
image: busybox
|
image: busybox
|
||||||
`, nil)
|
`)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "version")
|
assert.Contains(t, err.Error(), "version")
|
||||||
}
|
}
|
||||||
|
@ -296,7 +313,7 @@ version: 3
|
||||||
services:
|
services:
|
||||||
foo:
|
foo:
|
||||||
image: busybox
|
image: busybox
|
||||||
`, nil)
|
`)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "version must be a string")
|
assert.Contains(t, err.Error(), "version must be a string")
|
||||||
}
|
}
|
||||||
|
@ -305,7 +322,7 @@ func TestV1Unsupported(t *testing.T) {
|
||||||
_, err := loadYAML(`
|
_, err := loadYAML(`
|
||||||
foo:
|
foo:
|
||||||
image: busybox
|
image: busybox
|
||||||
`, nil)
|
`)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,7 +332,7 @@ version: "3"
|
||||||
services:
|
services:
|
||||||
- foo:
|
- foo:
|
||||||
image: busybox
|
image: busybox
|
||||||
`, nil)
|
`)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "services must be a mapping")
|
assert.Contains(t, err.Error(), "services must be a mapping")
|
||||||
|
|
||||||
|
@ -323,7 +340,7 @@ services:
|
||||||
version: "3"
|
version: "3"
|
||||||
services:
|
services:
|
||||||
foo: busybox
|
foo: busybox
|
||||||
`, nil)
|
`)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "services.foo must be a mapping")
|
assert.Contains(t, err.Error(), "services.foo must be a mapping")
|
||||||
|
|
||||||
|
@ -332,7 +349,7 @@ version: "3"
|
||||||
networks:
|
networks:
|
||||||
- default:
|
- default:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
`, nil)
|
`)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "networks must be a mapping")
|
assert.Contains(t, err.Error(), "networks must be a mapping")
|
||||||
|
|
||||||
|
@ -340,7 +357,7 @@ networks:
|
||||||
version: "3"
|
version: "3"
|
||||||
networks:
|
networks:
|
||||||
default: bridge
|
default: bridge
|
||||||
`, nil)
|
`)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "networks.default must be a mapping")
|
assert.Contains(t, err.Error(), "networks.default must be a mapping")
|
||||||
|
|
||||||
|
@ -349,7 +366,7 @@ version: "3"
|
||||||
volumes:
|
volumes:
|
||||||
- data:
|
- data:
|
||||||
driver: local
|
driver: local
|
||||||
`, nil)
|
`)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "volumes must be a mapping")
|
assert.Contains(t, err.Error(), "volumes must be a mapping")
|
||||||
|
|
||||||
|
@ -357,7 +374,7 @@ volumes:
|
||||||
version: "3"
|
version: "3"
|
||||||
volumes:
|
volumes:
|
||||||
data: local
|
data: local
|
||||||
`, nil)
|
`)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "volumes.data must be a mapping")
|
assert.Contains(t, err.Error(), "volumes.data must be a mapping")
|
||||||
}
|
}
|
||||||
|
@ -368,13 +385,13 @@ version: "3"
|
||||||
services:
|
services:
|
||||||
foo:
|
foo:
|
||||||
image: ["busybox", "latest"]
|
image: ["busybox", "latest"]
|
||||||
`, nil)
|
`)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "services.foo.image must be a string")
|
assert.Contains(t, err.Error(), "services.foo.image must be a string")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidEnvironment(t *testing.T) {
|
func TestLoadWithEnvironment(t *testing.T) {
|
||||||
config, err := loadYAML(`
|
config, err := loadYAMLWithEnv(`
|
||||||
version: "3"
|
version: "3"
|
||||||
services:
|
services:
|
||||||
dict-env:
|
dict-env:
|
||||||
|
@ -391,17 +408,17 @@ services:
|
||||||
- FOO=1
|
- FOO=1
|
||||||
- BAR=2
|
- BAR=2
|
||||||
- BAZ=2.5
|
- BAZ=2.5
|
||||||
- QUX
|
- QUX=
|
||||||
- QUUX=
|
- QUUX
|
||||||
`, map[string]string{"QUX": "qux"})
|
`, map[string]string{"QUX": "qux"})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
expected := types.MappingWithEquals{
|
expected := types.MappingWithEquals{
|
||||||
"FOO": "1",
|
"FOO": strPtr("1"),
|
||||||
"BAR": "2",
|
"BAR": strPtr("2"),
|
||||||
"BAZ": "2.5",
|
"BAZ": strPtr("2.5"),
|
||||||
"QUX": "qux",
|
"QUX": strPtr("qux"),
|
||||||
"QUUX": "",
|
"QUUX": nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, 2, len(config.Services))
|
assert.Equal(t, 2, len(config.Services))
|
||||||
|
@ -419,7 +436,7 @@ services:
|
||||||
image: busybox
|
image: busybox
|
||||||
environment:
|
environment:
|
||||||
FOO: ["1"]
|
FOO: ["1"]
|
||||||
`, nil)
|
`)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "services.dict-env.environment.FOO must be a string, number or null")
|
assert.Contains(t, err.Error(), "services.dict-env.environment.FOO must be a string, number or null")
|
||||||
}
|
}
|
||||||
|
@ -431,14 +448,14 @@ services:
|
||||||
dict-env:
|
dict-env:
|
||||||
image: busybox
|
image: busybox
|
||||||
environment: "FOO=1"
|
environment: "FOO=1"
|
||||||
`, nil)
|
`)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "services.dict-env.environment must be a mapping")
|
assert.Contains(t, err.Error(), "services.dict-env.environment must be a mapping")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEnvironmentInterpolation(t *testing.T) {
|
func TestEnvironmentInterpolation(t *testing.T) {
|
||||||
home := "/home/foo"
|
home := "/home/foo"
|
||||||
config, err := loadYAML(`
|
config, err := loadYAMLWithEnv(`
|
||||||
version: "3"
|
version: "3"
|
||||||
services:
|
services:
|
||||||
test:
|
test:
|
||||||
|
@ -461,7 +478,7 @@ volumes:
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
expectedLabels := types.MappingWithEquals{
|
expectedLabels := types.Labels{
|
||||||
"home1": home,
|
"home1": home,
|
||||||
"home2": home,
|
"home2": home,
|
||||||
"nonexistent": "",
|
"nonexistent": "",
|
||||||
|
@ -534,7 +551,7 @@ services:
|
||||||
bar:
|
bar:
|
||||||
extends:
|
extends:
|
||||||
service: foo
|
service: foo
|
||||||
`, nil)
|
`)
|
||||||
|
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.IsType(t, &ForbiddenPropertiesError{}, err)
|
assert.IsType(t, &ForbiddenPropertiesError{}, err)
|
||||||
|
@ -607,7 +624,8 @@ func TestFullExample(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
homeDir := "/home/foo"
|
homeDir := "/home/foo"
|
||||||
config, err := loadYAML(string(bytes), map[string]string{"HOME": homeDir, "QUX": "2"})
|
env := map[string]string{"HOME": homeDir, "QUX": "qux_from_environment"}
|
||||||
|
config, err := loadYAMLWithEnv(string(bytes), env)
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -662,14 +680,11 @@ func TestFullExample(t *testing.T) {
|
||||||
DNSSearch: []string{"dc1.example.com", "dc2.example.com"},
|
DNSSearch: []string{"dc1.example.com", "dc2.example.com"},
|
||||||
DomainName: "foo.com",
|
DomainName: "foo.com",
|
||||||
Entrypoint: []string{"/code/entrypoint.sh", "-p", "3000"},
|
Entrypoint: []string{"/code/entrypoint.sh", "-p", "3000"},
|
||||||
Environment: map[string]string{
|
Environment: map[string]*string{
|
||||||
"RACK_ENV": "development",
|
"FOO": strPtr("foo_from_env_file"),
|
||||||
"SHOW": "true",
|
"BAR": strPtr("bar_from_env_file_2"),
|
||||||
"SESSION_SECRET": "",
|
"BAZ": strPtr("baz_from_service_def"),
|
||||||
"FOO": "1",
|
"QUX": strPtr("qux_from_environment"),
|
||||||
"BAR": "2",
|
|
||||||
"BAZ": "3",
|
|
||||||
"QUX": "2",
|
|
||||||
},
|
},
|
||||||
EnvFile: []string{
|
EnvFile: []string{
|
||||||
"./example1.env",
|
"./example1.env",
|
||||||
|
@ -961,15 +976,6 @@ func TestFullExample(t *testing.T) {
|
||||||
assert.Equal(t, expectedVolumeConfig, config.Volumes)
|
assert.Equal(t, expectedVolumeConfig, config.Volumes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadYAML(yaml string, env map[string]string) (*types.Config, error) {
|
|
||||||
dict, err := ParseYAML([]byte(yaml))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return Load(buildConfigDetails(dict, env))
|
|
||||||
}
|
|
||||||
|
|
||||||
func serviceSort(services []types.ServiceConfig) []types.ServiceConfig {
|
func serviceSort(services []types.ServiceConfig) []types.ServiceConfig {
|
||||||
sort.Sort(servicesByName(services))
|
sort.Sort(servicesByName(services))
|
||||||
return services
|
return services
|
||||||
|
|
|
@ -99,7 +99,7 @@ type ServiceConfig struct {
|
||||||
HealthCheck *HealthCheckConfig
|
HealthCheck *HealthCheckConfig
|
||||||
Image string
|
Image string
|
||||||
Ipc string
|
Ipc string
|
||||||
Labels MappingWithEquals
|
Labels Labels
|
||||||
Links []string
|
Links []string
|
||||||
Logging *LoggingConfig
|
Logging *LoggingConfig
|
||||||
MacAddress string `mapstructure:"mac_address"`
|
MacAddress string `mapstructure:"mac_address"`
|
||||||
|
@ -135,7 +135,10 @@ type StringOrNumberList []string
|
||||||
|
|
||||||
// MappingWithEquals is a mapping type that can be converted from a list of
|
// MappingWithEquals is a mapping type that can be converted from a list of
|
||||||
// key=value strings
|
// key=value strings
|
||||||
type MappingWithEquals map[string]string
|
type MappingWithEquals map[string]*string
|
||||||
|
|
||||||
|
// Labels is a mapping type for labels
|
||||||
|
type Labels map[string]string
|
||||||
|
|
||||||
// MappingWithColon is a mapping type that can be converted from a list of
|
// MappingWithColon is a mapping type that can be converted from a list of
|
||||||
// 'key: value' strings
|
// 'key: value' strings
|
||||||
|
@ -151,7 +154,7 @@ type LoggingConfig struct {
|
||||||
type DeployConfig struct {
|
type DeployConfig struct {
|
||||||
Mode string
|
Mode string
|
||||||
Replicas *uint64
|
Replicas *uint64
|
||||||
Labels MappingWithEquals
|
Labels Labels
|
||||||
UpdateConfig *UpdateConfig `mapstructure:"update_config"`
|
UpdateConfig *UpdateConfig `mapstructure:"update_config"`
|
||||||
Resources Resources
|
Resources Resources
|
||||||
RestartPolicy *RestartPolicy `mapstructure:"restart_policy"`
|
RestartPolicy *RestartPolicy `mapstructure:"restart_policy"`
|
||||||
|
@ -268,7 +271,7 @@ type NetworkConfig struct {
|
||||||
External External
|
External External
|
||||||
Internal bool
|
Internal bool
|
||||||
Attachable bool
|
Attachable bool
|
||||||
Labels MappingWithEquals
|
Labels Labels
|
||||||
}
|
}
|
||||||
|
|
||||||
// IPAMConfig for a network
|
// IPAMConfig for a network
|
||||||
|
@ -287,7 +290,7 @@ type VolumeConfig struct {
|
||||||
Driver string
|
Driver string
|
||||||
DriverOpts map[string]string `mapstructure:"driver_opts"`
|
DriverOpts map[string]string `mapstructure:"driver_opts"`
|
||||||
External External
|
External External
|
||||||
Labels MappingWithEquals
|
Labels Labels
|
||||||
}
|
}
|
||||||
|
|
||||||
// External identifies a Volume or Network as a reference to a resource that is
|
// External identifies a Volume or Network as a reference to a resource that is
|
||||||
|
@ -301,5 +304,5 @@ type External struct {
|
||||||
type SecretConfig struct {
|
type SecretConfig struct {
|
||||||
File string
|
File string
|
||||||
External External
|
External External
|
||||||
Labels MappingWithEquals
|
Labels Labels
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue