compose: fix environment interpolation from the client
| ... | ... |
@@ -15,6 +15,7 @@ import ( |
| 15 | 15 |
composetypes "github.com/docker/docker/cli/compose/types" |
| 16 | 16 |
apiclient "github.com/docker/docker/client" |
| 17 | 17 |
dockerclient "github.com/docker/docker/client" |
| 18 |
+ "github.com/pkg/errors" |
|
| 18 | 19 |
"golang.org/x/net/context" |
| 19 | 20 |
) |
| 20 | 21 |
|
| ... | ... |
@@ -122,9 +123,26 @@ func getConfigDetails(opts deployOptions) (composetypes.ConfigDetails, error) {
|
| 122 | 122 |
} |
| 123 | 123 |
// TODO: support multiple files |
| 124 | 124 |
details.ConfigFiles = []composetypes.ConfigFile{*configFile}
|
| 125 |
+ details.Environment, err = buildEnvironment(os.Environ()) |
|
| 126 |
+ if err != nil {
|
|
| 127 |
+ return details, err |
|
| 128 |
+ } |
|
| 125 | 129 |
return details, nil |
| 126 | 130 |
} |
| 127 | 131 |
|
| 132 |
+func buildEnvironment(env []string) (map[string]string, error) {
|
|
| 133 |
+ result := make(map[string]string, len(env)) |
|
| 134 |
+ for _, s := range env {
|
|
| 135 |
+ // if value is empty, s is like "K=", not "K". |
|
| 136 |
+ if !strings.Contains(s, "=") {
|
|
| 137 |
+ return result, errors.Errorf("unexpected environment %q", s)
|
|
| 138 |
+ } |
|
| 139 |
+ kv := strings.SplitN(s, "=", 2) |
|
| 140 |
+ result[kv[0]] = kv[1] |
|
| 141 |
+ } |
|
| 142 |
+ return result, nil |
|
| 143 |
+} |
|
| 144 |
+ |
|
| 128 | 145 |
func getConfigFile(filename string) (*composetypes.ConfigFile, error) {
|
| 129 | 146 |
bytes, err := ioutil.ReadFile(filename) |
| 130 | 147 |
if err != nil {
|
| ... | ... |
@@ -394,11 +394,16 @@ func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortC |
| 394 | 394 |
}, nil |
| 395 | 395 |
} |
| 396 | 396 |
|
| 397 |
-func convertEnvironment(source map[string]string) []string {
|
|
| 397 |
+func convertEnvironment(source map[string]*string) []string {
|
|
| 398 | 398 |
var output []string |
| 399 | 399 |
|
| 400 | 400 |
for name, value := range source {
|
| 401 |
- output = append(output, fmt.Sprintf("%s=%s", name, value))
|
|
| 401 |
+ switch value {
|
|
| 402 |
+ case nil: |
|
| 403 |
+ output = append(output, name) |
|
| 404 |
+ default: |
|
| 405 |
+ output = append(output, fmt.Sprintf("%s=%s", name, *value))
|
|
| 406 |
+ } |
|
| 402 | 407 |
} |
| 403 | 408 |
|
| 404 | 409 |
return output |
| ... | ... |
@@ -43,10 +43,14 @@ func TestConvertRestartPolicyFromFailure(t *testing.T) {
|
| 43 | 43 |
assert.DeepEqual(t, policy, expected) |
| 44 | 44 |
} |
| 45 | 45 |
|
| 46 |
+func strPtr(val string) *string {
|
|
| 47 |
+ return &val |
|
| 48 |
+} |
|
| 49 |
+ |
|
| 46 | 50 |
func TestConvertEnvironment(t *testing.T) {
|
| 47 |
- source := map[string]string{
|
|
| 48 |
- "foo": "bar", |
|
| 49 |
- "key": "value", |
|
| 51 |
+ source := map[string]*string{
|
|
| 52 |
+ "foo": strPtr("bar"),
|
|
| 53 |
+ "key": strPtr("value"),
|
|
| 50 | 54 |
} |
| 51 | 55 |
env := convertEnvironment(source) |
| 52 | 56 |
sort.Strings(env) |
| ... | ... |
@@ -77,10 +77,8 @@ services: |
| 77 | 77 |
# Mapping values can be strings, numbers or null |
| 78 | 78 |
# Booleans are not allowed - must be quoted |
| 79 | 79 |
environment: |
| 80 |
- RACK_ENV: development |
|
| 81 |
- SHOW: 'true' |
|
| 82 |
- SESSION_SECRET: |
|
| 83 |
- BAZ: 3 |
|
| 80 |
+ BAZ: baz_from_service_def |
|
| 81 |
+ QUX: |
|
| 84 | 82 |
# environment: |
| 85 | 83 |
# - RACK_ENV=development |
| 86 | 84 |
# - SHOW=true |
| ... | ... |
@@ -2,15 +2,16 @@ package loader |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"fmt" |
| 5 |
- "os" |
|
| 6 | 5 |
"path" |
| 7 | 6 |
"reflect" |
| 8 | 7 |
"regexp" |
| 9 | 8 |
"sort" |
| 10 | 9 |
"strings" |
| 11 | 10 |
|
| 11 |
+ "github.com/Sirupsen/logrus" |
|
| 12 | 12 |
"github.com/docker/docker/cli/compose/interpolation" |
| 13 | 13 |
"github.com/docker/docker/cli/compose/schema" |
| 14 |
+ "github.com/docker/docker/cli/compose/template" |
|
| 14 | 15 |
"github.com/docker/docker/cli/compose/types" |
| 15 | 16 |
"github.com/docker/docker/opts" |
| 16 | 17 |
runconfigopts "github.com/docker/docker/runconfig/opts" |
| ... | ... |
@@ -69,13 +70,17 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) {
|
| 69 | 69 |
} |
| 70 | 70 |
|
| 71 | 71 |
cfg := types.Config{}
|
| 72 |
+ lookupEnv := func(k string) (string, bool) {
|
|
| 73 |
+ v, ok := configDetails.Environment[k] |
|
| 74 |
+ return v, ok |
|
| 75 |
+ } |
|
| 72 | 76 |
if services, ok := configDict["services"]; ok {
|
| 73 |
- servicesConfig, err := interpolation.Interpolate(services.(types.Dict), "service", os.LookupEnv) |
|
| 77 |
+ servicesConfig, err := interpolation.Interpolate(services.(types.Dict), "service", lookupEnv) |
|
| 74 | 78 |
if err != nil {
|
| 75 | 79 |
return nil, err |
| 76 | 80 |
} |
| 77 | 81 |
|
| 78 |
- servicesList, err := LoadServices(servicesConfig, configDetails.WorkingDir) |
|
| 82 |
+ servicesList, err := LoadServices(servicesConfig, configDetails.WorkingDir, lookupEnv) |
|
| 79 | 83 |
if err != nil {
|
| 80 | 84 |
return nil, err |
| 81 | 85 |
} |
| ... | ... |
@@ -84,7 +89,7 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) {
|
| 84 | 84 |
} |
| 85 | 85 |
|
| 86 | 86 |
if networks, ok := configDict["networks"]; ok {
|
| 87 |
- networksConfig, err := interpolation.Interpolate(networks.(types.Dict), "network", os.LookupEnv) |
|
| 87 |
+ networksConfig, err := interpolation.Interpolate(networks.(types.Dict), "network", lookupEnv) |
|
| 88 | 88 |
if err != nil {
|
| 89 | 89 |
return nil, err |
| 90 | 90 |
} |
| ... | ... |
@@ -98,7 +103,7 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) {
|
| 98 | 98 |
} |
| 99 | 99 |
|
| 100 | 100 |
if volumes, ok := configDict["volumes"]; ok {
|
| 101 |
- volumesConfig, err := interpolation.Interpolate(volumes.(types.Dict), "volume", os.LookupEnv) |
|
| 101 |
+ volumesConfig, err := interpolation.Interpolate(volumes.(types.Dict), "volume", lookupEnv) |
|
| 102 | 102 |
if err != nil {
|
| 103 | 103 |
return nil, err |
| 104 | 104 |
} |
| ... | ... |
@@ -112,7 +117,7 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) {
|
| 112 | 112 |
} |
| 113 | 113 |
|
| 114 | 114 |
if secrets, ok := configDict["secrets"]; ok {
|
| 115 |
- secretsConfig, err := interpolation.Interpolate(secrets.(types.Dict), "secret", os.LookupEnv) |
|
| 115 |
+ secretsConfig, err := interpolation.Interpolate(secrets.(types.Dict), "secret", lookupEnv) |
|
| 116 | 116 |
if err != nil {
|
| 117 | 117 |
return nil, err |
| 118 | 118 |
} |
| ... | ... |
@@ -248,9 +253,11 @@ func transformHook( |
| 248 | 248 |
case reflect.TypeOf(map[string]*types.ServiceNetworkConfig{}):
|
| 249 | 249 |
return transformServiceNetworkMap(data) |
| 250 | 250 |
case reflect.TypeOf(types.MappingWithEquals{}):
|
| 251 |
- return transformMappingOrList(data, "="), nil |
|
| 251 |
+ return transformMappingOrList(data, "=", true), nil |
|
| 252 |
+ case reflect.TypeOf(types.Labels{}):
|
|
| 253 |
+ return transformMappingOrList(data, "=", false), nil |
|
| 252 | 254 |
case reflect.TypeOf(types.MappingWithColon{}):
|
| 253 |
- return transformMappingOrList(data, ":"), nil |
|
| 255 |
+ return transformMappingOrList(data, ":", false), nil |
|
| 254 | 256 |
case reflect.TypeOf(types.ServiceVolumeConfig{}):
|
| 255 | 257 |
return transformServiceVolumeConfig(data) |
| 256 | 258 |
} |
| ... | ... |
@@ -308,11 +315,11 @@ func formatInvalidKeyError(keyPrefix string, key interface{}) error {
|
| 308 | 308 |
|
| 309 | 309 |
// LoadServices produces a ServiceConfig map from a compose file Dict |
| 310 | 310 |
// the servicesDict is not validated if directly used. Use Load() to enable validation |
| 311 |
-func LoadServices(servicesDict types.Dict, workingDir string) ([]types.ServiceConfig, error) {
|
|
| 311 |
+func LoadServices(servicesDict types.Dict, workingDir string, lookupEnv template.Mapping) ([]types.ServiceConfig, error) {
|
|
| 312 | 312 |
var services []types.ServiceConfig |
| 313 | 313 |
|
| 314 | 314 |
for name, serviceDef := range servicesDict {
|
| 315 |
- serviceConfig, err := LoadService(name, serviceDef.(types.Dict), workingDir) |
|
| 315 |
+ serviceConfig, err := LoadService(name, serviceDef.(types.Dict), workingDir, lookupEnv) |
|
| 316 | 316 |
if err != nil {
|
| 317 | 317 |
return nil, err |
| 318 | 318 |
} |
| ... | ... |
@@ -324,23 +331,35 @@ func LoadServices(servicesDict types.Dict, workingDir string) ([]types.ServiceCo |
| 324 | 324 |
|
| 325 | 325 |
// LoadService produces a single ServiceConfig from a compose file Dict |
| 326 | 326 |
// the serviceDict is not validated if directly used. Use Load() to enable validation |
| 327 |
-func LoadService(name string, serviceDict types.Dict, workingDir string) (*types.ServiceConfig, error) {
|
|
| 327 |
+func LoadService(name string, serviceDict types.Dict, workingDir string, lookupEnv template.Mapping) (*types.ServiceConfig, error) {
|
|
| 328 | 328 |
serviceConfig := &types.ServiceConfig{}
|
| 329 | 329 |
if err := transform(serviceDict, serviceConfig); err != nil {
|
| 330 | 330 |
return nil, err |
| 331 | 331 |
} |
| 332 | 332 |
serviceConfig.Name = name |
| 333 | 333 |
|
| 334 |
- if err := resolveEnvironment(serviceConfig, workingDir); err != nil {
|
|
| 334 |
+ if err := resolveEnvironment(serviceConfig, workingDir, lookupEnv); err != nil {
|
|
| 335 | 335 |
return nil, err |
| 336 | 336 |
} |
| 337 | 337 |
|
| 338 |
- resolveVolumePaths(serviceConfig.Volumes, workingDir) |
|
| 338 |
+ resolveVolumePaths(serviceConfig.Volumes, workingDir, lookupEnv) |
|
| 339 | 339 |
return serviceConfig, nil |
| 340 | 340 |
} |
| 341 | 341 |
|
| 342 |
-func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string) error {
|
|
| 343 |
- environment := make(map[string]string) |
|
| 342 |
+func updateEnvironment(environment map[string]*string, vars map[string]*string, lookupEnv template.Mapping) {
|
|
| 343 |
+ for k, v := range vars {
|
|
| 344 |
+ interpolatedV, ok := lookupEnv(k) |
|
| 345 |
+ if (v == nil || *v == "") && ok {
|
|
| 346 |
+ // lookupEnv is prioritized over vars |
|
| 347 |
+ environment[k] = &interpolatedV |
|
| 348 |
+ } else {
|
|
| 349 |
+ environment[k] = v |
|
| 350 |
+ } |
|
| 351 |
+ } |
|
| 352 |
+} |
|
| 353 |
+ |
|
| 354 |
+func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, lookupEnv template.Mapping) error {
|
|
| 355 |
+ environment := make(map[string]*string) |
|
| 344 | 356 |
|
| 345 | 357 |
if len(serviceConfig.EnvFile) > 0 {
|
| 346 | 358 |
var envVars []string |
| ... | ... |
@@ -353,36 +372,35 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string) e |
| 353 | 353 |
} |
| 354 | 354 |
envVars = append(envVars, fileVars...) |
| 355 | 355 |
} |
| 356 |
- |
|
| 357 |
- for k, v := range runconfigopts.ConvertKVStringsToMap(envVars) {
|
|
| 358 |
- environment[k] = v |
|
| 359 |
- } |
|
| 360 |
- } |
|
| 361 |
- |
|
| 362 |
- for k, v := range serviceConfig.Environment {
|
|
| 363 |
- environment[k] = v |
|
| 356 |
+ updateEnvironment(environment, |
|
| 357 |
+ runconfigopts.ConvertKVStringsToMapWithNil(envVars), lookupEnv) |
|
| 364 | 358 |
} |
| 365 | 359 |
|
| 360 |
+ updateEnvironment(environment, serviceConfig.Environment, lookupEnv) |
|
| 366 | 361 |
serviceConfig.Environment = environment |
| 367 |
- |
|
| 368 | 362 |
return nil |
| 369 | 363 |
} |
| 370 | 364 |
|
| 371 |
-func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string) {
|
|
| 365 |
+func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string, lookupEnv template.Mapping) {
|
|
| 372 | 366 |
for i, volume := range volumes {
|
| 373 | 367 |
if volume.Type != "bind" {
|
| 374 | 368 |
continue |
| 375 | 369 |
} |
| 376 | 370 |
|
| 377 |
- volume.Source = absPath(workingDir, expandUser(volume.Source)) |
|
| 371 |
+ volume.Source = absPath(workingDir, expandUser(volume.Source, lookupEnv)) |
|
| 378 | 372 |
volumes[i] = volume |
| 379 | 373 |
} |
| 380 | 374 |
} |
| 381 | 375 |
|
| 382 | 376 |
// TODO: make this more robust |
| 383 |
-func expandUser(path string) string {
|
|
| 377 |
+func expandUser(path string, lookupEnv template.Mapping) string {
|
|
| 384 | 378 |
if strings.HasPrefix(path, "~") {
|
| 385 |
- return strings.Replace(path, "~", os.Getenv("HOME"), 1)
|
|
| 379 |
+ home, ok := lookupEnv("HOME")
|
|
| 380 |
+ if !ok {
|
|
| 381 |
+ logrus.Warn("cannot expand '~', because the environment lacks HOME")
|
|
| 382 |
+ return path |
|
| 383 |
+ } |
|
| 384 |
+ return strings.Replace(path, "~", home, 1) |
|
| 386 | 385 |
} |
| 387 | 386 |
return path |
| 388 | 387 |
} |
| ... | ... |
@@ -476,9 +494,9 @@ func absPath(workingDir string, filepath string) string {
|
| 476 | 476 |
func transformMapStringString(data interface{}) (interface{}, error) {
|
| 477 | 477 |
switch value := data.(type) {
|
| 478 | 478 |
case map[string]interface{}:
|
| 479 |
- return toMapStringString(value), nil |
|
| 479 |
+ return toMapStringString(value, false), nil |
|
| 480 | 480 |
case types.Dict: |
| 481 |
- return toMapStringString(value), nil |
|
| 481 |
+ return toMapStringString(value, false), nil |
|
| 482 | 482 |
case map[string]string: |
| 483 | 483 |
return value, nil |
| 484 | 484 |
default: |
| ... | ... |
@@ -592,23 +610,27 @@ func transformStringList(data interface{}) (interface{}, error) {
|
| 592 | 592 |
} |
| 593 | 593 |
} |
| 594 | 594 |
|
| 595 |
-func transformMappingOrList(mappingOrList interface{}, sep string) map[string]string {
|
|
| 596 |
- if mapping, ok := mappingOrList.(types.Dict); ok {
|
|
| 597 |
- return toMapStringString(mapping) |
|
| 598 |
- } |
|
| 599 |
- if list, ok := mappingOrList.([]interface{}); ok {
|
|
| 600 |
- result := make(map[string]string) |
|
| 601 |
- for _, value := range list {
|
|
| 595 |
+func transformMappingOrList(mappingOrList interface{}, sep string, allowNil bool) interface{} {
|
|
| 596 |
+ switch value := mappingOrList.(type) {
|
|
| 597 |
+ case types.Dict: |
|
| 598 |
+ return toMapStringString(value, allowNil) |
|
| 599 |
+ case ([]interface{}):
|
|
| 600 |
+ result := make(map[string]interface{})
|
|
| 601 |
+ for _, value := range value {
|
|
| 602 | 602 |
parts := strings.SplitN(value.(string), sep, 2) |
| 603 |
- if len(parts) == 1 {
|
|
| 604 |
- result[parts[0]] = "" |
|
| 605 |
- } else {
|
|
| 606 |
- result[parts[0]] = parts[1] |
|
| 603 |
+ key := parts[0] |
|
| 604 |
+ switch {
|
|
| 605 |
+ case len(parts) == 1 && allowNil: |
|
| 606 |
+ result[key] = nil |
|
| 607 |
+ case len(parts) == 1 && !allowNil: |
|
| 608 |
+ result[key] = "" |
|
| 609 |
+ default: |
|
| 610 |
+ result[key] = parts[1] |
|
| 607 | 611 |
} |
| 608 | 612 |
} |
| 609 | 613 |
return result |
| 610 | 614 |
} |
| 611 |
- panic(fmt.Errorf("expected a map or a slice, got: %#v", mappingOrList))
|
|
| 615 |
+ panic(fmt.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList))
|
|
| 612 | 616 |
} |
| 613 | 617 |
|
| 614 | 618 |
func transformShellCommand(value interface{}) (interface{}, error) {
|
| ... | ... |
@@ -672,17 +694,21 @@ func toServicePortConfigs(value string) ([]interface{}, error) {
|
| 672 | 672 |
return portConfigs, nil |
| 673 | 673 |
} |
| 674 | 674 |
|
| 675 |
-func toMapStringString(value map[string]interface{}) map[string]string {
|
|
| 676 |
- output := make(map[string]string) |
|
| 675 |
+func toMapStringString(value map[string]interface{}, allowNil bool) map[string]interface{} {
|
|
| 676 |
+ output := make(map[string]interface{})
|
|
| 677 | 677 |
for key, value := range value {
|
| 678 |
- output[key] = toString(value) |
|
| 678 |
+ output[key] = toString(value, allowNil) |
|
| 679 | 679 |
} |
| 680 | 680 |
return output |
| 681 | 681 |
} |
| 682 | 682 |
|
| 683 |
-func toString(value interface{}) string {
|
|
| 684 |
- if value == nil {
|
|
| 683 |
+func toString(value interface{}, allowNil bool) interface{} {
|
|
| 684 |
+ switch {
|
|
| 685 |
+ case value != nil: |
|
| 686 |
+ return fmt.Sprint(value) |
|
| 687 |
+ case allowNil: |
|
| 688 |
+ return nil |
|
| 689 |
+ default: |
|
| 685 | 690 |
return "" |
| 686 | 691 |
} |
| 687 |
- return fmt.Sprint(value) |
|
| 688 | 692 |
} |
| ... | ... |
@@ -12,7 +12,7 @@ import ( |
| 12 | 12 |
"github.com/stretchr/testify/assert" |
| 13 | 13 |
) |
| 14 | 14 |
|
| 15 |
-func buildConfigDetails(source types.Dict) types.ConfigDetails {
|
|
| 15 |
+func buildConfigDetails(source types.Dict, env map[string]string) types.ConfigDetails {
|
|
| 16 | 16 |
workingDir, err := os.Getwd() |
| 17 | 17 |
if err != nil {
|
| 18 | 18 |
panic(err) |
| ... | ... |
@@ -23,10 +23,23 @@ func buildConfigDetails(source types.Dict) types.ConfigDetails {
|
| 23 | 23 |
ConfigFiles: []types.ConfigFile{
|
| 24 | 24 |
{Filename: "filename.yml", Config: source},
|
| 25 | 25 |
}, |
| 26 |
- Environment: nil, |
|
| 26 |
+ Environment: env, |
|
| 27 | 27 |
} |
| 28 | 28 |
} |
| 29 | 29 |
|
| 30 |
+func loadYAML(yaml string) (*types.Config, error) {
|
|
| 31 |
+ return loadYAMLWithEnv(yaml, nil) |
|
| 32 |
+} |
|
| 33 |
+ |
|
| 34 |
+func loadYAMLWithEnv(yaml string, env map[string]string) (*types.Config, error) {
|
|
| 35 |
+ dict, err := ParseYAML([]byte(yaml)) |
|
| 36 |
+ if err != nil {
|
|
| 37 |
+ return nil, err |
|
| 38 |
+ } |
|
| 39 |
+ |
|
| 40 |
+ return Load(buildConfigDetails(dict, env)) |
|
| 41 |
+} |
|
| 42 |
+ |
|
| 30 | 43 |
var sampleYAML = ` |
| 31 | 44 |
version: "3" |
| 32 | 45 |
services: |
| ... | ... |
@@ -98,12 +111,16 @@ var sampleDict = types.Dict{
|
| 98 | 98 |
}, |
| 99 | 99 |
} |
| 100 | 100 |
|
| 101 |
+func strPtr(val string) *string {
|
|
| 102 |
+ return &val |
|
| 103 |
+} |
|
| 104 |
+ |
|
| 101 | 105 |
var sampleConfig = types.Config{
|
| 102 | 106 |
Services: []types.ServiceConfig{
|
| 103 | 107 |
{
|
| 104 | 108 |
Name: "foo", |
| 105 | 109 |
Image: "busybox", |
| 106 |
- Environment: map[string]string{},
|
|
| 110 |
+ Environment: map[string]*string{},
|
|
| 107 | 111 |
Networks: map[string]*types.ServiceNetworkConfig{
|
| 108 | 112 |
"with_me": nil, |
| 109 | 113 |
}, |
| ... | ... |
@@ -111,7 +128,7 @@ var sampleConfig = types.Config{
|
| 111 | 111 |
{
|
| 112 | 112 |
Name: "bar", |
| 113 | 113 |
Image: "busybox", |
| 114 |
- Environment: map[string]string{"FOO": "1"},
|
|
| 114 |
+ Environment: map[string]*string{"FOO": strPtr("1")},
|
|
| 115 | 115 |
Networks: map[string]*types.ServiceNetworkConfig{
|
| 116 | 116 |
"with_ipam": nil, |
| 117 | 117 |
}, |
| ... | ... |
@@ -154,7 +171,7 @@ func TestParseYAML(t *testing.T) {
|
| 154 | 154 |
} |
| 155 | 155 |
|
| 156 | 156 |
func TestLoad(t *testing.T) {
|
| 157 |
- actual, err := Load(buildConfigDetails(sampleDict)) |
|
| 157 |
+ actual, err := Load(buildConfigDetails(sampleDict, nil)) |
|
| 158 | 158 |
if !assert.NoError(t, err) {
|
| 159 | 159 |
return |
| 160 | 160 |
} |
| ... | ... |
@@ -373,8 +390,8 @@ services: |
| 373 | 373 |
assert.Contains(t, err.Error(), "services.foo.image must be a string") |
| 374 | 374 |
} |
| 375 | 375 |
|
| 376 |
-func TestValidEnvironment(t *testing.T) {
|
|
| 377 |
- config, err := loadYAML(` |
|
| 376 |
+func TestLoadWithEnvironment(t *testing.T) {
|
|
| 377 |
+ config, err := loadYAMLWithEnv(` |
|
| 378 | 378 |
version: "3" |
| 379 | 379 |
services: |
| 380 | 380 |
dict-env: |
| ... | ... |
@@ -383,6 +400,7 @@ services: |
| 383 | 383 |
FOO: "1" |
| 384 | 384 |
BAR: 2 |
| 385 | 385 |
BAZ: 2.5 |
| 386 |
+ QUX: |
|
| 386 | 387 |
QUUX: |
| 387 | 388 |
list-env: |
| 388 | 389 |
image: busybox |
| ... | ... |
@@ -390,15 +408,17 @@ services: |
| 390 | 390 |
- FOO=1 |
| 391 | 391 |
- BAR=2 |
| 392 | 392 |
- BAZ=2.5 |
| 393 |
- - QUUX= |
|
| 394 |
-`) |
|
| 393 |
+ - QUX= |
|
| 394 |
+ - QUUX |
|
| 395 |
+`, map[string]string{"QUX": "qux"})
|
|
| 395 | 396 |
assert.NoError(t, err) |
| 396 | 397 |
|
| 397 | 398 |
expected := types.MappingWithEquals{
|
| 398 |
- "FOO": "1", |
|
| 399 |
- "BAR": "2", |
|
| 400 |
- "BAZ": "2.5", |
|
| 401 |
- "QUUX": "", |
|
| 399 |
+ "FOO": strPtr("1"),
|
|
| 400 |
+ "BAR": strPtr("2"),
|
|
| 401 |
+ "BAZ": strPtr("2.5"),
|
|
| 402 |
+ "QUX": strPtr("qux"),
|
|
| 403 |
+ "QUUX": nil, |
|
| 402 | 404 |
} |
| 403 | 405 |
|
| 404 | 406 |
assert.Equal(t, 2, len(config.Services)) |
| ... | ... |
@@ -434,7 +454,8 @@ services: |
| 434 | 434 |
} |
| 435 | 435 |
|
| 436 | 436 |
func TestEnvironmentInterpolation(t *testing.T) {
|
| 437 |
- config, err := loadYAML(` |
|
| 437 |
+ home := "/home/foo" |
|
| 438 |
+ config, err := loadYAMLWithEnv(` |
|
| 438 | 439 |
version: "3" |
| 439 | 440 |
services: |
| 440 | 441 |
test: |
| ... | ... |
@@ -450,13 +471,14 @@ networks: |
| 450 | 450 |
volumes: |
| 451 | 451 |
test: |
| 452 | 452 |
driver: $HOME |
| 453 |
-`) |
|
| 453 |
+`, map[string]string{
|
|
| 454 |
+ "HOME": home, |
|
| 455 |
+ "FOO": "foo", |
|
| 456 |
+ }) |
|
| 454 | 457 |
|
| 455 | 458 |
assert.NoError(t, err) |
| 456 | 459 |
|
| 457 |
- home := os.Getenv("HOME")
|
|
| 458 |
- |
|
| 459 |
- expectedLabels := types.MappingWithEquals{
|
|
| 460 |
+ expectedLabels := types.Labels{
|
|
| 460 | 461 |
"home1": home, |
| 461 | 462 |
"home2": home, |
| 462 | 463 |
"nonexistent": "", |
| ... | ... |
@@ -483,7 +505,7 @@ services: |
| 483 | 483 |
`)) |
| 484 | 484 |
assert.NoError(t, err) |
| 485 | 485 |
|
| 486 |
- configDetails := buildConfigDetails(dict) |
|
| 486 |
+ configDetails := buildConfigDetails(dict, nil) |
|
| 487 | 487 |
|
| 488 | 488 |
_, err = Load(configDetails) |
| 489 | 489 |
assert.NoError(t, err) |
| ... | ... |
@@ -506,7 +528,7 @@ services: |
| 506 | 506 |
`)) |
| 507 | 507 |
assert.NoError(t, err) |
| 508 | 508 |
|
| 509 |
- configDetails := buildConfigDetails(dict) |
|
| 509 |
+ configDetails := buildConfigDetails(dict, nil) |
|
| 510 | 510 |
|
| 511 | 511 |
_, err = Load(configDetails) |
| 512 | 512 |
assert.NoError(t, err) |
| ... | ... |
@@ -601,7 +623,9 @@ func TestFullExample(t *testing.T) {
|
| 601 | 601 |
bytes, err := ioutil.ReadFile("full-example.yml")
|
| 602 | 602 |
assert.NoError(t, err) |
| 603 | 603 |
|
| 604 |
- config, err := loadYAML(string(bytes)) |
|
| 604 |
+ homeDir := "/home/foo" |
|
| 605 |
+ env := map[string]string{"HOME": homeDir, "QUX": "qux_from_environment"}
|
|
| 606 |
+ config, err := loadYAMLWithEnv(string(bytes), env) |
|
| 605 | 607 |
if !assert.NoError(t, err) {
|
| 606 | 608 |
return |
| 607 | 609 |
} |
| ... | ... |
@@ -609,7 +633,6 @@ func TestFullExample(t *testing.T) {
|
| 609 | 609 |
workingDir, err := os.Getwd() |
| 610 | 610 |
assert.NoError(t, err) |
| 611 | 611 |
|
| 612 |
- homeDir := os.Getenv("HOME")
|
|
| 613 | 612 |
stopGracePeriod := time.Duration(20 * time.Second) |
| 614 | 613 |
|
| 615 | 614 |
expectedServiceConfig := types.ServiceConfig{
|
| ... | ... |
@@ -657,13 +680,11 @@ func TestFullExample(t *testing.T) {
|
| 657 | 657 |
DNSSearch: []string{"dc1.example.com", "dc2.example.com"},
|
| 658 | 658 |
DomainName: "foo.com", |
| 659 | 659 |
Entrypoint: []string{"/code/entrypoint.sh", "-p", "3000"},
|
| 660 |
- Environment: map[string]string{
|
|
| 661 |
- "RACK_ENV": "development", |
|
| 662 |
- "SHOW": "true", |
|
| 663 |
- "SESSION_SECRET": "", |
|
| 664 |
- "FOO": "1", |
|
| 665 |
- "BAR": "2", |
|
| 666 |
- "BAZ": "3", |
|
| 660 |
+ Environment: map[string]*string{
|
|
| 661 |
+ "FOO": strPtr("foo_from_env_file"),
|
|
| 662 |
+ "BAR": strPtr("bar_from_env_file_2"),
|
|
| 663 |
+ "BAZ": strPtr("baz_from_service_def"),
|
|
| 664 |
+ "QUX": strPtr("qux_from_environment"),
|
|
| 667 | 665 |
}, |
| 668 | 666 |
EnvFile: []string{
|
| 669 | 667 |
"./example1.env", |
| ... | ... |
@@ -955,15 +976,6 @@ func TestFullExample(t *testing.T) {
|
| 955 | 955 |
assert.Equal(t, expectedVolumeConfig, config.Volumes) |
| 956 | 956 |
} |
| 957 | 957 |
|
| 958 |
-func loadYAML(yaml string) (*types.Config, error) {
|
|
| 959 |
- dict, err := ParseYAML([]byte(yaml)) |
|
| 960 |
- if err != nil {
|
|
| 961 |
- return nil, err |
|
| 962 |
- } |
|
| 963 |
- |
|
| 964 |
- return Load(buildConfigDetails(dict)) |
|
| 965 |
-} |
|
| 966 |
- |
|
| 967 | 958 |
func serviceSort(services []types.ServiceConfig) []types.ServiceConfig {
|
| 968 | 959 |
sort.Sort(servicesByName(services)) |
| 969 | 960 |
return services |
| ... | ... |
@@ -99,7 +99,7 @@ type ServiceConfig struct {
|
| 99 | 99 |
HealthCheck *HealthCheckConfig |
| 100 | 100 |
Image string |
| 101 | 101 |
Ipc string |
| 102 |
- Labels MappingWithEquals |
|
| 102 |
+ Labels Labels |
|
| 103 | 103 |
Links []string |
| 104 | 104 |
Logging *LoggingConfig |
| 105 | 105 |
MacAddress string `mapstructure:"mac_address"` |
| ... | ... |
@@ -134,8 +134,13 @@ type StringList []string |
| 134 | 134 |
type StringOrNumberList []string |
| 135 | 135 |
|
| 136 | 136 |
// MappingWithEquals is a mapping type that can be converted from a list of |
| 137 |
-// key=value strings |
|
| 138 |
-type MappingWithEquals map[string]string |
|
| 137 |
+// key[=value] strings. |
|
| 138 |
+// For the key with an empty value (`key=`), the mapped value is set to a pointer to `""`. |
|
| 139 |
+// For the key without value (`key`), the mapped value is set to nil. |
|
| 140 |
+type MappingWithEquals map[string]*string |
|
| 141 |
+ |
|
| 142 |
+// Labels is a mapping type for labels |
|
| 143 |
+type Labels map[string]string |
|
| 139 | 144 |
|
| 140 | 145 |
// MappingWithColon is a mapping type that can be converted from a list of |
| 141 | 146 |
// 'key: value' strings |
| ... | ... |
@@ -151,7 +156,7 @@ type LoggingConfig struct {
|
| 151 | 151 |
type DeployConfig struct {
|
| 152 | 152 |
Mode string |
| 153 | 153 |
Replicas *uint64 |
| 154 |
- Labels MappingWithEquals |
|
| 154 |
+ Labels Labels |
|
| 155 | 155 |
UpdateConfig *UpdateConfig `mapstructure:"update_config"` |
| 156 | 156 |
Resources Resources |
| 157 | 157 |
RestartPolicy *RestartPolicy `mapstructure:"restart_policy"` |
| ... | ... |
@@ -268,7 +273,7 @@ type NetworkConfig struct {
|
| 268 | 268 |
External External |
| 269 | 269 |
Internal bool |
| 270 | 270 |
Attachable bool |
| 271 |
- Labels MappingWithEquals |
|
| 271 |
+ Labels Labels |
|
| 272 | 272 |
} |
| 273 | 273 |
|
| 274 | 274 |
// IPAMConfig for a network |
| ... | ... |
@@ -287,7 +292,7 @@ type VolumeConfig struct {
|
| 287 | 287 |
Driver string |
| 288 | 288 |
DriverOpts map[string]string `mapstructure:"driver_opts"` |
| 289 | 289 |
External External |
| 290 |
- Labels MappingWithEquals |
|
| 290 |
+ Labels Labels |
|
| 291 | 291 |
} |
| 292 | 292 |
|
| 293 | 293 |
// External identifies a Volume or Network as a reference to a resource that is |
| ... | ... |
@@ -301,5 +306,5 @@ type External struct {
|
| 301 | 301 |
type SecretConfig struct {
|
| 302 | 302 |
File string |
| 303 | 303 |
External External |
| 304 |
- Labels MappingWithEquals |
|
| 304 |
+ Labels Labels |
|
| 305 | 305 |
} |