Skip to content

Commit a6b86f6

Browse files
committed
feature: make env support inputting empty value when creation
Signed-off-by: Allen Sun <[email protected]>
1 parent ac1a952 commit a6b86f6

File tree

9 files changed

+224
-104
lines changed

9 files changed

+224
-104
lines changed

apis/opts/env.go

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,60 @@ import (
55
"strings"
66
)
77

8-
// ParseEnv parses the env param of container.
9-
func ParseEnv(env []string) (map[string]string, error) {
10-
results := make(map[string]string)
11-
for _, e := range env {
12-
fields := strings.SplitN(e, "=", 2)
13-
if len(fields) != 2 {
14-
return nil, fmt.Errorf("invalid env %s: env must be in format of key=value", e)
8+
// ParseEnvs parses the env slice in container's config.
9+
func ParseEnvs(envs []string) ([]string, error) {
10+
results := []string{}
11+
for _, env := range envs {
12+
result, err := parseEnv(env)
13+
if err != nil {
14+
return nil, err
1515
}
16-
results[fields[0]] = fields[1]
16+
results = append(results, result)
1717
}
1818

1919
return results, nil
2020
}
2121

22+
// parseEnv parses single elements of env slice.
23+
func parseEnv(env string) (string, error) {
24+
env = strings.TrimSpace(env)
25+
if len(env) == 0 {
26+
return "", fmt.Errorf("invalid env: env cannot be empty")
27+
}
28+
29+
arr := strings.SplitN(env, "=", 2)
30+
if arr[0] == "" {
31+
return "", fmt.Errorf("invalid env %s: key of env cannot be empty", env)
32+
}
33+
34+
if len(arr) == 1 {
35+
// no matter it is "KEY=" or just "KEY", both are valid.
36+
return env, nil
37+
}
38+
39+
return fmt.Sprintf("%s=%s", arr[0], arr[1]), nil
40+
}
41+
42+
// ValidSliceEnvsToMap converts slice envs to be map
43+
// assuming that the input are always valid with a char of '='.
44+
func ValidSliceEnvsToMap(envs []string) map[string]string {
45+
results := make(map[string]string)
46+
for _, env := range envs {
47+
arr := strings.SplitN(env, "=", 2)
48+
results[arr[0]] = arr[1]
49+
}
50+
return results
51+
}
52+
53+
// ValidMapEnvsToSlice converts valid map envs to slice.
54+
func ValidMapEnvsToSlice(envs map[string]string) []string {
55+
results := make([]string, 0)
56+
for key, value := range envs {
57+
results = append(results, fmt.Sprintf("%s=%s", key, value))
58+
}
59+
return results
60+
}
61+
2262
// ValidateEnv verifies the correct of env
2363
func ValidateEnv(map[string]string) error {
2464
// TODO

apis/opts/env_test.go

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,29 @@ import (
66
)
77

88
func TestParseEnv(t *testing.T) {
9-
type args struct {
10-
env []string
11-
}
129
tests := []struct {
1310
name string
14-
args args
15-
want map[string]string
11+
args string
12+
want string
1613
wantErr bool
1714
}{
18-
{"test single env", args{env: []string{"foo=bar"}}, map[string]string{"foo": "bar"}, false},
19-
{"test error env format", args{env: []string{"ErrorInfo"}}, nil, true},
20-
{"test multiple envs", args{env: []string{"foo=bar", "A=a"}}, map[string]string{"foo": "bar", "A": "a"}, false},
21-
{"test multiple '=' envs", args{env: []string{"A=1=2"}}, map[string]string{"A": "1=2"}, false},
22-
{"test nil env", args{env: nil}, map[string]string{}, false}, // empty map
23-
{"test empty env", args{env: []string{""}}, nil, true},
24-
{"test empty blank", args{env: []string{" "}}, nil, true},
15+
{"test single env", "foo=bar", "foo=bar", false},
16+
{"test str with no =", "NOEQUALMARK", "NOEQUALMARK", false},
17+
{"test multiple '=' envs", "A=1=2", "A=1=2", false},
18+
{"test empty blank in value", "A=B C", "A=B C", false},
19+
{"test only =", "=", "", true},
20+
{"test empty env", "", "", true}, // empty map
21+
{"test empty blank", " ", "", true},
2522
}
2623
for _, tt := range tests {
2724
t.Run(tt.name, func(t *testing.T) {
28-
got, err := ParseEnv(tt.args.env)
25+
got, err := parseEnv(tt.args)
2926
if (err != nil) != tt.wantErr {
30-
t.Errorf("ParseEnv() error = %v, wantErr %v", err, tt.wantErr)
27+
t.Errorf("parseEnv() error = %v, wantErr %v", err, tt.wantErr)
3128
return
3229
}
3330
if !reflect.DeepEqual(got, tt.want) {
34-
t.Errorf("ParseEnv() = %v, want %v", got, tt.want)
31+
t.Errorf("parseEnv() = %v, want %v", got, tt.want)
3532
}
3633
})
3734
}

apis/swagger.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2149,7 +2149,9 @@ definitions:
21492149
x-nullable: false
21502150
Env:
21512151
description: |
2152-
A list of environment variables to set inside the container in the form `["VAR=value", ...]`. A variable without `=` is removed from the environment, rather than to have an empty value.
2152+
A list of environment variables to set inside the container in the form `["VAR=value", ...]`.
2153+
A variable like "A=" means setting env A in container to be empty value.
2154+
And a variable without `=` is removed from the environment, rather than to have an empty value.
21532155
type: "array"
21542156
items:
21552157
type: "string"
@@ -2505,7 +2507,9 @@ definitions:
25052507
$ref: "#/definitions/RestartPolicy"
25062508
Env:
25072509
description: |
2508-
A list of environment variables to set inside the container in the form `["VAR=value", ...]`. A variable without `=` is removed from the environment, rather than to have an empty value.
2510+
A list of environment variables to set inside the container in the form `["VAR=value", ...]`.
2511+
A variable like "A=" means updating env A in container to be empty value.
2512+
A variable without `=` is removed from the environment, rather than to have an empty value.
25092513
type: "array"
25102514
items:
25112515
type: "string"

cli/common_flags.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func addCommonFlags(flagSet *pflag.FlagSet) *container {
3939

4040
flagSet.BoolVar(&c.enableLxcfs, "enableLxcfs", false, "Enable lxcfs for the container, only effective when enable-lxcfs switched on in Pouchd")
4141
flagSet.StringVar(&c.entrypoint, "entrypoint", "", "Overwrite the default ENTRYPOINT of the image")
42-
flagSet.StringArrayVarP(&c.env, "env", "e", nil, "Set environment variables for container")
42+
flagSet.StringArrayVarP(&c.env, "env", "e", nil, "Set environment variables for container('--env A=' means setting env A to empty, '--env B' means removing env B from container env inherited from image)")
4343
flagSet.StringVar(&c.hostname, "hostname", "", "Set container's hostname")
4444
flagSet.BoolVar(&c.disableNetworkFiles, "disable-network-files", false, "Disable the generation of network files(/etc/hostname, /etc/hosts and /etc/resolv.conf) for container. If true, no network files will be generated. Default false")
4545

cli/update.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ func (uc *UpdateCommand) addFlags() {
5151
flagSet.StringVar(&uc.cpusetmems, "cpuset-mems", "", "MEMs in cpuset which to allow execution (0-3, 0, 1)")
5252
flagSet.StringVarP(&uc.memory, "memory", "m", "", "Container memory limit")
5353
flagSet.StringVar(&uc.memorySwap, "memory-swap", "", "Container swap limit")
54-
flagSet.StringSliceVarP(&uc.env, "env", "e", nil, "Set environment variables for container")
55-
flagSet.StringSliceVarP(&uc.labels, "label", "l", nil, "Set label for container")
54+
flagSet.StringSliceVarP(&uc.env, "env", "e", nil, "Update environment variables for container('--env A=' means updating env A to be empty and '--env A' means removing env A)")
55+
flagSet.StringSliceVarP(&uc.labels, "label", "l", nil, "Update labels for container")
5656
flagSet.StringVar(&uc.restartPolicy, "restart", "", "Restart policy to apply when container exits")
5757
flagSet.StringSliceVar(&uc.diskQuota, "disk-quota", nil, "Update disk quota for container(/=10g)")
5858
flagSet.StringSliceVar(&uc.specAnnotation, "annotation", nil, "Update annotation for runtime spec")

daemon/mgr/container_utils.go

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -269,46 +269,49 @@ func amendContainerSettings(config *types.ContainerConfig, hostConfig *types.Hos
269269
}
270270
}
271271

272-
func mergeEnvSlice(newEnv, oldEnv []string) ([]string, error) {
272+
// mergeEnvSlice merges two parts into a singe one.
273+
// Here are some cases:
274+
// 1. container creation needs to merge user input envs and envs inherited from image;
275+
// 2. update action with env needs to merge original envs and the user input envs;
276+
// 3. exec action needs to merge container's original envs and user input envs.
277+
func mergeEnvSlice(new, old []string) ([]string, error) {
273278
// if newEnv is empty, return old env slice
274-
if len(newEnv) == 0 {
275-
return oldEnv, nil
279+
if len(new) == 0 {
280+
return old, nil
276281
}
277282

278-
newEnvMap, err := opts.ParseEnv(newEnv)
283+
newEnvs, err := opts.ParseEnvs(new)
279284
if err != nil {
280285
return nil, errors.Wrapf(err, "failed to parse new env")
281286
}
282287

283-
oldEnvMap, err := opts.ParseEnv(oldEnv)
288+
// TODO: do we need to valid the old one?
289+
oldEnvs, err := opts.ParseEnvs(old)
284290
if err != nil {
285291
return nil, errors.Wrapf(err, "failed to parse old env")
286292
}
287293

288-
for k, v := range newEnvMap {
289-
// key should not be empty
290-
if k == "" {
291-
continue
292-
}
293-
294-
// add or change an env
295-
if v != "" {
296-
oldEnvMap[k] = v
297-
continue
298-
}
299-
300-
// value is empty, we need delete the env
301-
if _, exists := oldEnvMap[k]; exists {
302-
delete(oldEnvMap, k)
294+
oldEnvsMap := opts.ValidSliceEnvsToMap(oldEnvs)
295+
296+
for _, env := range newEnvs {
297+
arr := strings.SplitN(env, "=", 2)
298+
if len(arr) == 1 {
299+
// there are two cases, the first is 'KEY=', the second is just 'KEY'
300+
if len(env) == len(arr[0]) {
301+
// the case of 'KEY'. It is valid to remove the env from original one.
302+
if _, exits := oldEnvsMap[arr[0]]; exits {
303+
delete(oldEnvsMap, arr[0])
304+
}
305+
} else {
306+
// the case of 'KEY='. It is valid to set env value empty.
307+
oldEnvsMap[arr[0]] = ""
308+
}
309+
} else {
310+
oldEnvsMap[arr[0]] = arr[1]
303311
}
304312
}
305313

306-
newEnvSlice := []string{}
307-
for k, v := range oldEnvMap {
308-
newEnvSlice = append(newEnvSlice, fmt.Sprintf("%s=%s", k, v))
309-
}
310-
311-
return newEnvSlice, nil
314+
return opts.ValidMapEnvsToSlice(oldEnvsMap), nil
312315
}
313316

314317
func mergeAnnotation(newAnnotation, oldAnnotation map[string]string) map[string]string {

daemon/mgr/container_utils_test.go

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -200,9 +200,8 @@ func Test_mergeEnvSlice(t *testing.T) {
200200
want []string
201201
wantErr bool
202202
}{
203-
// TODO: Add test cases.
204203
{
205-
name: "test1",
204+
name: "normal merge with adding ones",
206205
args: args{
207206
newEnv: []string{"a=b"},
208207
oldEnv: []string{"c=d"},
@@ -211,7 +210,7 @@ func Test_mergeEnvSlice(t *testing.T) {
211210
wantErr: false,
212211
},
213212
{
214-
name: "test2",
213+
name: "normal merge with updating existing one",
215214
args: args{
216215
newEnv: []string{"test=false"},
217216
oldEnv: []string{"test=true"},
@@ -220,16 +219,16 @@ func Test_mergeEnvSlice(t *testing.T) {
220219
wantErr: false,
221220
},
222221
{
223-
name: "test3",
222+
name: "normal merge with removing one",
224223
args: args{
225-
newEnv: []string{"wrong-format"},
226-
oldEnv: []string{"c=d"},
224+
newEnv: []string{"JUST_KEY"},
225+
oldEnv: []string{"c=d", "JUST_KEY=VALUE"},
227226
},
228-
want: nil,
229-
wantErr: true,
227+
want: []string{"c=d"},
228+
wantErr: false,
230229
},
231230
{
232-
name: "test4",
231+
name: "normal merge with nothing in new",
233232
args: args{
234233
newEnv: []string{},
235234
oldEnv: []string{"c=d"},
@@ -238,23 +237,41 @@ func Test_mergeEnvSlice(t *testing.T) {
238237
wantErr: false,
239238
},
240239
{
241-
name: "test5",
240+
name: "normal merge with updating existing key to empty",
242241
args: args{
243-
newEnv: []string{"a=b"},
244-
oldEnv: []string{},
242+
newEnv: []string{"a="},
243+
oldEnv: []string{"a=b"},
245244
},
246-
want: []string{"a=b"},
245+
want: []string{"a="},
247246
wantErr: false,
248247
},
249248
{
250-
name: "test6",
249+
name: "normal merge with adding env with whitespace in value",
251250
args: args{
252-
newEnv: []string{"a="},
253-
oldEnv: []string{"a=b"},
251+
newEnv: []string{"a=b c d "},
252+
oldEnv: []string{"c=b"},
254253
},
255-
want: []string{},
254+
want: []string{"a=b c d", "c=b"},
256255
wantErr: false,
257256
},
257+
{
258+
name: "illegal merge with invalid in new ones",
259+
args: args{
260+
newEnv: []string{"="},
261+
oldEnv: []string{"a=b"},
262+
},
263+
want: nil,
264+
wantErr: true,
265+
},
266+
{
267+
name: "illegal merge with empty element in new ones",
268+
args: args{
269+
newEnv: []string{"", ""},
270+
oldEnv: []string{"a=b"},
271+
},
272+
want: nil,
273+
wantErr: true,
274+
},
258275
}
259276
for _, tt := range tests {
260277
t.Run(tt.name, func(t *testing.T) {

0 commit comments

Comments
 (0)