Browse code

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>

Daniel Nephin authored on 2017/03/15 01:39:26
Showing 9 changed files
... ...
@@ -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
 
... ...
@@ -115,17 +116,24 @@ func getConfigDetails(opts deployOptions) (composetypes.ConfigDetails, error) {
115 115
 	}
116 116
 	// TODO: support multiple files
117 117
 	details.ConfigFiles = []composetypes.ConfigFile{*configFile}
118
-	env := os.Environ()
119
-	details.Environment = make(map[string]string, len(env))
118
+	details.Environment, err = buildEnvironment(os.Environ())
119
+	if err != nil {
120
+		return details, err
121
+	}
122
+	return details, nil
123
+}
124
+
125
+func buildEnvironment(env []string) (map[string]string, error) {
126
+	result := make(map[string]string, len(env))
120 127
 	for _, s := range env {
121 128
 		// if value is empty, s is like "K=", not "K".
122 129
 		if !strings.Contains(s, "=") {
123
-			return details, fmt.Errorf("unexpected environment %q", s)
130
+			return result, errors.Errorf("unexpected environment %q", s)
124 131
 		}
125 132
 		kv := strings.SplitN(s, "=", 2)
126
-		details.Environment[kv[0]] = kv[1]
133
+		result[kv[0]] = kv[1]
127 134
 	}
128
-	return details, nil
135
+	return result, nil
129 136
 }
130 137
 
131 138
 func getConfigFile(filename string) (*composetypes.ConfigFile, error) {
... ...
@@ -393,11 +393,16 @@ func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortC
393 393
 	}, nil
394 394
 }
395 395
 
396
-func convertEnvironment(source map[string]string) []string {
396
+func convertEnvironment(source map[string]*string) []string {
397 397
 	var output []string
398 398
 
399 399
 	for name, value := range source {
400
-		output = append(output, fmt.Sprintf("%s=%s", name, value))
400
+		switch value {
401
+		case nil:
402
+			output = append(output, name)
403
+		default:
404
+			output = append(output, fmt.Sprintf("%s=%s", name, *value))
405
+		}
401 406
 	}
402 407
 
403 408
 	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)
... ...
@@ -1,8 +1,8 @@
1 1
 # passed through
2
-FOO=1
2
+FOO=foo_from_env_file
3 3
 
4 4
 # overridden in example2.env
5
-BAR=1
5
+BAR=bar_from_env_file
6 6
 
7 7
 # overridden in full-example.yml
8
-BAZ=1
8
+BAZ=baz_from_env_file
... ...
@@ -1,4 +1,4 @@
1
-BAR=2
1
+BAR=bar_from_env_file_2
2 2
 
3 3
 # overridden in configDetails.Environment
4
-QUX=1
4
+QUX=quz_from_env_file_2
... ...
@@ -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
... ...
@@ -253,9 +253,11 @@ func transformHook(
253 253
 	case reflect.TypeOf(map[string]*types.ServiceNetworkConfig{}):
254 254
 		return transformServiceNetworkMap(data)
255 255
 	case reflect.TypeOf(types.MappingWithEquals{}):
256
-		return transformMappingOrList(data, "="), nil
256
+		return transformMappingOrList(data, "=", true), nil
257
+	case reflect.TypeOf(types.Labels{}):
258
+		return transformMappingOrList(data, "=", false), nil
257 259
 	case reflect.TypeOf(types.MappingWithColon{}):
258
-		return transformMappingOrList(data, ":"), nil
260
+		return transformMappingOrList(data, ":", false), nil
259 261
 	case reflect.TypeOf(types.ServiceVolumeConfig{}):
260 262
 		return transformServiceVolumeConfig(data)
261 263
 	}
... ...
@@ -317,7 +319,7 @@ func LoadServices(servicesDict types.Dict, workingDir string, lookupEnv template
317 317
 	var services []types.ServiceConfig
318 318
 
319 319
 	for name, serviceDef := range servicesDict {
320
-		serviceConfig, err := loadService(name, serviceDef.(types.Dict), workingDir, lookupEnv)
320
+		serviceConfig, err := LoadService(name, serviceDef.(types.Dict), workingDir, lookupEnv)
321 321
 		if err != nil {
322 322
 			return nil, err
323 323
 		}
... ...
@@ -344,25 +346,20 @@ func LoadService(name string, serviceDict types.Dict, workingDir string, lookupE
344 344
 	return serviceConfig, nil
345 345
 }
346 346
 
347
-func updateEnvironment(environment map[string]string, vars map[string]string, lookupEnv template.Mapping) map[string]string {
348
-	result := make(map[string]string, len(environment))
349
-	for k, v := range environment {
350
-		result[k]=v
351
-	}
347
+func updateEnvironment(environment map[string]*string, vars map[string]*string, lookupEnv template.Mapping) {
352 348
 	for k, v := range vars {
353 349
 		interpolatedV, ok := lookupEnv(k)
354
-		if ok {
350
+		if (v == nil || *v == "") && ok {
355 351
 			// lookupEnv is prioritized over vars
356
-			result[k] = interpolatedV
352
+			environment[k] = &interpolatedV
357 353
 		} else {
358
-			result[k] = v
354
+			environment[k] = v
359 355
 		}
360 356
 	}
361
-	return result
362 357
 }
363 358
 
364 359
 func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, lookupEnv template.Mapping) error {
365
-	environment := make(map[string]string)
360
+	environment := make(map[string]*string)
366 361
 
367 362
 	if len(serviceConfig.EnvFile) > 0 {
368 363
 		var envVars []string
... ...
@@ -375,12 +372,12 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, l
375 375
 			}
376 376
 			envVars = append(envVars, fileVars...)
377 377
 		}
378
-		environment = updateEnvironment(environment,
379
-			runconfigopts.ConvertKVStringsToMap(envVars), lookupEnv)
378
+		updateEnvironment(environment,
379
+			runconfigopts.ConvertKVStringsToMapWithNil(envVars), lookupEnv)
380 380
 	}
381 381
 
382
-	serviceConfig.Environment = updateEnvironment(environment,
383
-		serviceConfig.Environment, lookupEnv)
382
+	updateEnvironment(environment, serviceConfig.Environment, lookupEnv)
383
+	serviceConfig.Environment = environment
384 384
 	return nil
385 385
 }
386 386
 
... ...
@@ -497,9 +494,9 @@ func absPath(workingDir string, filepath string) string {
497 497
 func transformMapStringString(data interface{}) (interface{}, error) {
498 498
 	switch value := data.(type) {
499 499
 	case map[string]interface{}:
500
-		return toMapStringString(value), nil
500
+		return toMapStringString(value, false), nil
501 501
 	case types.Dict:
502
-		return toMapStringString(value), nil
502
+		return toMapStringString(value, false), nil
503 503
 	case map[string]string:
504 504
 		return value, nil
505 505
 	default:
... ...
@@ -613,23 +610,27 @@ func transformStringList(data interface{}) (interface{}, error) {
613 613
 	}
614 614
 }
615 615
 
616
-func transformMappingOrList(mappingOrList interface{}, sep string) map[string]string {
617
-	if mapping, ok := mappingOrList.(types.Dict); ok {
618
-		return toMapStringString(mapping)
619
-	}
620
-	if list, ok := mappingOrList.([]interface{}); ok {
621
-		result := make(map[string]string)
622
-		for _, value := range list {
616
+func transformMappingOrList(mappingOrList interface{}, sep string, allowNil bool) interface{} {
617
+	switch value := mappingOrList.(type) {
618
+	case types.Dict:
619
+		return toMapStringString(value, allowNil)
620
+	case ([]interface{}):
621
+		result := make(map[string]interface{})
622
+		for _, value := range value {
623 623
 			parts := strings.SplitN(value.(string), sep, 2)
624
-			if len(parts) == 1 {
625
-				result[parts[0]] = ""
626
-			} else {
627
-				result[parts[0]] = parts[1]
624
+			key := parts[0]
625
+			switch {
626
+			case len(parts) == 1 && allowNil:
627
+				result[key] = nil
628
+			case len(parts) == 1 && !allowNil:
629
+				result[key] = ""
630
+			default:
631
+				result[key] = parts[1]
628 632
 			}
629 633
 		}
630 634
 		return result
631 635
 	}
632
-	panic(fmt.Errorf("expected a map or a slice, got: %#v", mappingOrList))
636
+	panic(fmt.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList))
633 637
 }
634 638
 
635 639
 func transformShellCommand(value interface{}) (interface{}, error) {
... ...
@@ -693,17 +694,21 @@ func toServicePortConfigs(value string) ([]interface{}, error) {
693 693
 	return portConfigs, nil
694 694
 }
695 695
 
696
-func toMapStringString(value map[string]interface{}) map[string]string {
697
-	output := make(map[string]string)
696
+func toMapStringString(value map[string]interface{}, allowNil bool) map[string]interface{} {
697
+	output := make(map[string]interface{})
698 698
 	for key, value := range value {
699
-		output[key] = toString(value)
699
+		output[key] = toString(value, allowNil)
700 700
 	}
701 701
 	return output
702 702
 }
703 703
 
704
-func toString(value interface{}) string {
705
-	if value == nil {
704
+func toString(value interface{}, allowNil bool) interface{} {
705
+	switch {
706
+	case value != nil:
707
+		return fmt.Sprint(value)
708
+	case allowNil:
709
+		return nil
710
+	default:
706 711
 		return ""
707 712
 	}
708
-	return fmt.Sprint(value)
709 713
 }
... ...
@@ -27,6 +27,19 @@ func buildConfigDetails(source types.Dict, env map[string]string) types.ConfigDe
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
 			},
... ...
@@ -173,7 +190,7 @@ services:
173 173
 secrets:
174 174
   super:
175 175
     external: true
176
-`, nil)
176
+`)
177 177
 	if !assert.NoError(t, err) {
178 178
 		return
179 179
 	}
... ...
@@ -182,7 +199,7 @@ secrets:
182 182
 }
183 183
 
184 184
 func TestParseAndLoad(t *testing.T) {
185
-	actual, err := loadYAML(sampleYAML, nil)
185
+	actual, err := loadYAML(sampleYAML)
186 186
 	if !assert.NoError(t, err) {
187 187
 		return
188 188
 	}
... ...
@@ -192,15 +209,15 @@ func TestParseAndLoad(t *testing.T) {
192 192
 }
193 193
 
194 194
 func TestInvalidTopLevelObjectType(t *testing.T) {
195
-	_, err := loadYAML("1", nil)
195
+	_, err := loadYAML("1")
196 196
 	assert.Error(t, err)
197 197
 	assert.Contains(t, err.Error(), "Top-level object must be a mapping")
198 198
 
199
-	_, err = loadYAML("\"hello\"", nil)
199
+	_, err = loadYAML("\"hello\"")
200 200
 	assert.Error(t, err)
201 201
 	assert.Contains(t, err.Error(), "Top-level object must be a mapping")
202 202
 
203
-	_, err = loadYAML("[\"hello\"]", nil)
203
+	_, err = loadYAML("[\"hello\"]")
204 204
 	assert.Error(t, err)
205 205
 	assert.Contains(t, err.Error(), "Top-level object must be a mapping")
206 206
 }
... ...
@@ -211,7 +228,7 @@ version: "3"
211 211
 123:
212 212
   foo:
213 213
     image: busybox
214
-`, nil)
214
+`)
215 215
 	assert.Error(t, err)
216 216
 	assert.Contains(t, err.Error(), "Non-string key at top level: 123")
217 217
 
... ...
@@ -222,7 +239,7 @@ services:
222 222
     image: busybox
223 223
   123:
224 224
     image: busybox
225
-`, nil)
225
+`)
226 226
 	assert.Error(t, err)
227 227
 	assert.Contains(t, err.Error(), "Non-string key in services: 123")
228 228
 
... ...
@@ -236,7 +253,7 @@ networks:
236 236
     ipam:
237 237
       config:
238 238
         - 123: oh dear
239
-`, nil)
239
+`)
240 240
 	assert.Error(t, err)
241 241
 	assert.Contains(t, err.Error(), "Non-string key in networks.default.ipam.config[0]: 123")
242 242
 
... ...
@@ -247,7 +264,7 @@ services:
247 247
     image: busybox
248 248
     environment:
249 249
       1: FOO
250
-`, nil)
250
+`)
251 251
 	assert.Error(t, err)
252 252
 	assert.Contains(t, err.Error(), "Non-string key in services.dict-env.environment: 1")
253 253
 }
... ...
@@ -258,7 +275,7 @@ version: "3"
258 258
 services:
259 259
   foo:
260 260
     image: busybox
261
-`, nil)
261
+`)
262 262
 	assert.NoError(t, err)
263 263
 
264 264
 	_, err = loadYAML(`
... ...
@@ -266,7 +283,7 @@ version: "3.0"
266 266
 services:
267 267
   foo:
268 268
     image: busybox
269
-`, nil)
269
+`)
270 270
 	assert.NoError(t, err)
271 271
 }
272 272
 
... ...
@@ -276,7 +293,7 @@ version: "2"
276 276
 services:
277 277
   foo:
278 278
     image: busybox
279
-`, nil)
279
+`)
280 280
 	assert.Error(t, err)
281 281
 	assert.Contains(t, err.Error(), "version")
282 282
 
... ...
@@ -285,7 +302,7 @@ version: "2.0"
285 285
 services:
286 286
   foo:
287 287
     image: busybox
288
-`, nil)
288
+`)
289 289
 	assert.Error(t, err)
290 290
 	assert.Contains(t, err.Error(), "version")
291 291
 }
... ...
@@ -296,7 +313,7 @@ version: 3
296 296
 services:
297 297
   foo:
298 298
     image: busybox
299
-`, nil)
299
+`)
300 300
 	assert.Error(t, err)
301 301
 	assert.Contains(t, err.Error(), "version must be a string")
302 302
 }
... ...
@@ -305,7 +322,7 @@ func TestV1Unsupported(t *testing.T) {
305 305
 	_, err := loadYAML(`
306 306
 foo:
307 307
   image: busybox
308
-`, nil)
308
+`)
309 309
 	assert.Error(t, err)
310 310
 }
311 311
 
... ...
@@ -315,7 +332,7 @@ version: "3"
315 315
 services:
316 316
   - foo:
317 317
       image: busybox
318
-`, nil)
318
+`)
319 319
 	assert.Error(t, err)
320 320
 	assert.Contains(t, err.Error(), "services must be a mapping")
321 321
 
... ...
@@ -323,7 +340,7 @@ services:
323 323
 version: "3"
324 324
 services:
325 325
   foo: busybox
326
-`, nil)
326
+`)
327 327
 	assert.Error(t, err)
328 328
 	assert.Contains(t, err.Error(), "services.foo must be a mapping")
329 329
 
... ...
@@ -332,7 +349,7 @@ version: "3"
332 332
 networks:
333 333
   - default:
334 334
       driver: bridge
335
-`, nil)
335
+`)
336 336
 	assert.Error(t, err)
337 337
 	assert.Contains(t, err.Error(), "networks must be a mapping")
338 338
 
... ...
@@ -340,7 +357,7 @@ networks:
340 340
 version: "3"
341 341
 networks:
342 342
   default: bridge
343
-`, nil)
343
+`)
344 344
 	assert.Error(t, err)
345 345
 	assert.Contains(t, err.Error(), "networks.default must be a mapping")
346 346
 
... ...
@@ -349,7 +366,7 @@ version: "3"
349 349
 volumes:
350 350
   - data:
351 351
       driver: local
352
-`, nil)
352
+`)
353 353
 	assert.Error(t, err)
354 354
 	assert.Contains(t, err.Error(), "volumes must be a mapping")
355 355
 
... ...
@@ -357,7 +374,7 @@ volumes:
357 357
 version: "3"
358 358
 volumes:
359 359
   data: local
360
-`, nil)
360
+`)
361 361
 	assert.Error(t, err)
362 362
 	assert.Contains(t, err.Error(), "volumes.data must be a mapping")
363 363
 }
... ...
@@ -368,13 +385,13 @@ version: "3"
368 368
 services:
369 369
   foo:
370 370
     image: ["busybox", "latest"]
371
-`, nil)
371
+`)
372 372
 	assert.Error(t, err)
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:
... ...
@@ -391,17 +408,17 @@ services:
391 391
       - FOO=1
392 392
       - BAR=2
393 393
       - BAZ=2.5
394
-      - QUX
395
-      - QUUX=
394
+      - QUX=
395
+      - QUUX
396 396
 `, map[string]string{"QUX": "qux"})
397 397
 	assert.NoError(t, err)
398 398
 
399 399
 	expected := types.MappingWithEquals{
400
-		"FOO":  "1",
401
-		"BAR":  "2",
402
-		"BAZ":  "2.5",
403
-		"QUX":  "qux",
404
-		"QUUX": "",
400
+		"FOO":  strPtr("1"),
401
+		"BAR":  strPtr("2"),
402
+		"BAZ":  strPtr("2.5"),
403
+		"QUX":  strPtr("qux"),
404
+		"QUUX": nil,
405 405
 	}
406 406
 
407 407
 	assert.Equal(t, 2, len(config.Services))
... ...
@@ -419,7 +436,7 @@ services:
419 419
     image: busybox
420 420
     environment:
421 421
       FOO: ["1"]
422
-`, nil)
422
+`)
423 423
 	assert.Error(t, err)
424 424
 	assert.Contains(t, err.Error(), "services.dict-env.environment.FOO must be a string, number or null")
425 425
 }
... ...
@@ -431,14 +448,14 @@ services:
431 431
   dict-env:
432 432
     image: busybox
433 433
     environment: "FOO=1"
434
-`, nil)
434
+`)
435 435
 	assert.Error(t, err)
436 436
 	assert.Contains(t, err.Error(), "services.dict-env.environment must be a mapping")
437 437
 }
438 438
 
439 439
 func TestEnvironmentInterpolation(t *testing.T) {
440 440
 	home := "/home/foo"
441
-	config, err := loadYAML(`
441
+	config, err := loadYAMLWithEnv(`
442 442
 version: "3"
443 443
 services:
444 444
   test:
... ...
@@ -461,7 +478,7 @@ volumes:
461 461
 
462 462
 	assert.NoError(t, err)
463 463
 
464
-	expectedLabels := types.MappingWithEquals{
464
+	expectedLabels := types.Labels{
465 465
 		"home1":       home,
466 466
 		"home2":       home,
467 467
 		"nonexistent": "",
... ...
@@ -534,7 +551,7 @@ services:
534 534
   bar:
535 535
     extends:
536 536
       service: foo
537
-`, nil)
537
+`)
538 538
 
539 539
 	assert.Error(t, err)
540 540
 	assert.IsType(t, &ForbiddenPropertiesError{}, err)
... ...
@@ -607,7 +624,8 @@ func TestFullExample(t *testing.T) {
607 607
 	assert.NoError(t, err)
608 608
 
609 609
 	homeDir := "/home/foo"
610
-	config, err := loadYAML(string(bytes), map[string]string{"HOME": homeDir, "QUX": "2"})
610
+	env := map[string]string{"HOME": homeDir, "QUX": "qux_from_environment"}
611
+	config, err := loadYAMLWithEnv(string(bytes), env)
611 612
 	if !assert.NoError(t, err) {
612 613
 		return
613 614
 	}
... ...
@@ -662,14 +680,11 @@ func TestFullExample(t *testing.T) {
662 662
 		DNSSearch:  []string{"dc1.example.com", "dc2.example.com"},
663 663
 		DomainName: "foo.com",
664 664
 		Entrypoint: []string{"/code/entrypoint.sh", "-p", "3000"},
665
-		Environment: map[string]string{
666
-			"RACK_ENV":       "development",
667
-			"SHOW":           "true",
668
-			"SESSION_SECRET": "",
669
-			"FOO":            "1",
670
-			"BAR":            "2",
671
-			"BAZ":            "3",
672
-			"QUX":            "2",
665
+		Environment: map[string]*string{
666
+			"FOO": strPtr("foo_from_env_file"),
667
+			"BAR": strPtr("bar_from_env_file_2"),
668
+			"BAZ": strPtr("baz_from_service_def"),
669
+			"QUX": strPtr("qux_from_environment"),
673 670
 		},
674 671
 		EnvFile: []string{
675 672
 			"./example1.env",
... ...
@@ -961,15 +976,6 @@ func TestFullExample(t *testing.T) {
961 961
 	assert.Equal(t, expectedVolumeConfig, config.Volumes)
962 962
 }
963 963
 
964
-func loadYAML(yaml string, env map[string]string) (*types.Config, error) {
965
-	dict, err := ParseYAML([]byte(yaml))
966
-	if err != nil {
967
-		return nil, err
968
-	}
969
-
970
-	return Load(buildConfigDetails(dict, env))
971
-}
972
-
973 964
 func serviceSort(services []types.ServiceConfig) []types.ServiceConfig {
974 965
 	sort.Sort(servicesByName(services))
975 966
 	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"`
... ...
@@ -135,7 +135,10 @@ type StringOrNumberList []string
135 135
 
136 136
 // MappingWithEquals is a mapping type that can be converted from a list of
137 137
 // key=value strings
138
-type MappingWithEquals map[string]string
138
+type MappingWithEquals map[string]*string
139
+
140
+// Labels is a mapping type for labels
141
+type Labels map[string]string
139 142
 
140 143
 // MappingWithColon is a mapping type that can be converted from a list of
141 144
 // 'key: value' strings
... ...
@@ -151,7 +154,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 +271,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 +290,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 +304,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
 }