diff --git a/staging/src/k8s.io/component-base/cli/flag/bracket_separated_slice_map_string_string.go b/staging/src/k8s.io/component-base/cli/flag/bracket_separated_slice_map_string_string.go new file mode 100644 index 00000000000..c21780d01b2 --- /dev/null +++ b/staging/src/k8s.io/component-base/cli/flag/bracket_separated_slice_map_string_string.go @@ -0,0 +1,118 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package flag + +import ( + "fmt" + "sort" + "strings" +) + +// BracketSeparatedSliceMapStringString can be set from the command line with the format `--flag {key=value, ...}, {...}`. +// Multiple comma-separated key-value pairs in a braket(`{}`) in a single invocation are supported. For example: `--flag {key=value, key=value, ...}`. +// Multiple braket-separated list of key-value pairs in a single invocation are supported. For example: `--flag {key=value, key=value}, {key=value, key=value}`. +type BracketSeparatedSliceMapStringString struct { + Value *[]map[string]string + initialized bool // set to true after the first Set call +} + +// NewBracketSeparatedSliceMapStringString takes a pointer to a []map[string]string and returns the +// BracketSeparatedSliceMapStringString flag parsing shim for that map +func NewBracketSeparatedSliceMapStringString(m *[]map[string]string) *BracketSeparatedSliceMapStringString { + return &BracketSeparatedSliceMapStringString{Value: m} +} + + +// Set implements github.com/spf13/pflag.Value +func (m *BracketSeparatedSliceMapStringString) Set(value string) error { + if m.Value == nil { + return fmt.Errorf("no target (nil pointer to []map[string]string)") + } + if !m.initialized || *m.Value == nil { + *m.Value = make([]map[string]string, 0) + m.initialized = true + } + + value = strings.TrimSpace(value) + + // split here + //{numa-node=0,memory-type=memory,limit=1Gi},{numa-node=1,memory-type=memory,limit=1Gi},{numa-node=1,memory-type=memory,limit=1Gi} +// for _, split := range strings.Split(value, "{") { +// split = strings.TrimRight(split, ",") +// split = strings.TrimRight(split, "}") + for _, split := range strings.Split(value, ",{") { + //split = strings.TrimRight(split, ",") + split = strings.TrimLeft(split, "{") + split = strings.TrimRight(split, "}") + + if len(split) == 0 { + continue + } + + // now we have "numa-node=1,memory-type=memory,limit=1Gi" + tmpRawMap := make(map[string]string) + + tmpMap:= NewMapStringString(&tmpRawMap) + + if err := tmpMap.Set(split); err != nil { + return fmt.Errorf("could not parse String: (%s): %v", value, err) + } + + *m.Value = append(*m.Value, tmpRawMap) + } + + return nil +} + +// String implements github.com/spf13/pflag.Value +func (m *BracketSeparatedSliceMapStringString) String() string { + if m == nil || m.Value == nil { + return "" + } + + var slices []string + + for _, configMap := range *m.Value { + var tmpPairs []string + + var keys []string + for key := range configMap { + keys = append(keys, key) + } + sort.Strings(keys) + + for _, key := range keys { + tmpPairs = append(tmpPairs, fmt.Sprintf("%s=%s", key, configMap[key])) + } + + if len(tmpPairs) != 0 { + slices = append(slices, "{" + strings.Join(tmpPairs, ",") + "}") + } + } + sort.Strings(slices) + return strings.Join(slices, ",") +} + +// Type implements github.com/spf13/pflag.Value +func (*BracketSeparatedSliceMapStringString) Type() string { + return "BracketSeparatedSliceMapStringString" +} + +// Empty implements OmitEmpty +func (m *BracketSeparatedSliceMapStringString) Empty() bool { + return !m.initialized || m.Value == nil || len(*m.Value) == 0 +} diff --git a/staging/src/k8s.io/component-base/cli/flag/bracket_separated_slice_map_string_string_test.go b/staging/src/k8s.io/component-base/cli/flag/bracket_separated_slice_map_string_string_test.go new file mode 100644 index 00000000000..caea52c8761 --- /dev/null +++ b/staging/src/k8s.io/component-base/cli/flag/bracket_separated_slice_map_string_string_test.go @@ -0,0 +1,178 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package flag + +import ( + "reflect" + "testing" +) + +func TestStringBracketSeparatedSliceMapStringString(t *testing.T) { + var nilSliceMap []map[string]string + testCases := []struct { + desc string + m *BracketSeparatedSliceMapStringString + expect string + }{ + {"nill", NewBracketSeparatedSliceMapStringString(&nilSliceMap), ""}, + {"empty", NewBracketSeparatedSliceMapStringString(&[]map[string]string{}), ""}, + {"one key", NewBracketSeparatedSliceMapStringString(&[]map[string]string{{"a": "string"}}), "{a=string}"}, + {"two keys", NewBracketSeparatedSliceMapStringString(&[]map[string]string{{"a": "string", "b": "string"}}), "{a=string,b=string}"}, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + str := tc.m.String() + if tc.expect != str { + t.Fatalf("expect %q but got %q", tc.expect, str) + } + }) + } +} + +func TestSetBracketSeparatedSliceMapStringString(t *testing.T) { + var nilMap []map[string]string + testCases := []struct { + desc string + vals []string + start *BracketSeparatedSliceMapStringString + expect *BracketSeparatedSliceMapStringString + err string + }{ + // we initialize the map with a default key that should be cleared by Set + {"clears defaults", []string{""}, + NewBracketSeparatedSliceMapStringString(&[]map[string]string{{"default": ""}}), + &BracketSeparatedSliceMapStringString{ + initialized: true, + Value: &[]map[string]string{}, + }, ""}, + // make sure we still allocate for "initialized" multimaps where Multimap was initially set to a nil map + {"allocates map if currently nil", []string{""}, + &BracketSeparatedSliceMapStringString{initialized: true, Value: &nilMap}, + &BracketSeparatedSliceMapStringString{ + initialized: true, + Value: &[]map[string]string{}, + }, ""}, + // for most cases, we just reuse nilMap, which should be allocated by Set, and is reset before each test case + {"empty", []string{""}, + NewBracketSeparatedSliceMapStringString(&nilMap), + &BracketSeparatedSliceMapStringString{ + initialized: true, + Value: &[]map[string]string{}, + }, ""}, + {"empty braket", []string{"{}"}, + NewBracketSeparatedSliceMapStringString(&nilMap), + &BracketSeparatedSliceMapStringString{ + initialized: true, + Value: &[]map[string]string{}, + }, ""}, + {"missing braket", []string{"a=string, b=string"}, + NewBracketSeparatedSliceMapStringString(&nilMap), + &BracketSeparatedSliceMapStringString{ + initialized: true, + Value: &[]map[string]string{{"a": "string", "b": "string"}}, + }, ""}, + {"empty key", []string{"{=string}"}, + NewBracketSeparatedSliceMapStringString(&nilMap), + &BracketSeparatedSliceMapStringString{ + initialized: true, + Value: &[]map[string]string{{"": "string"}}, + }, ""}, + {"one key", []string{"{a=string}"}, + NewBracketSeparatedSliceMapStringString(&nilMap), + &BracketSeparatedSliceMapStringString{ + initialized: true, + Value: &[]map[string]string{{"a": "string"}}, + }, ""}, + {"two keys", []string{"{a=string,b=string}"}, + NewBracketSeparatedSliceMapStringString(&nilMap), + &BracketSeparatedSliceMapStringString{ + initialized: true, + Value: &[]map[string]string{{"a": "string", "b": "string"}}, + }, ""}, + {"two duplecated keys", []string{"{a=string,a=string}"}, + NewBracketSeparatedSliceMapStringString(&nilMap), + &BracketSeparatedSliceMapStringString{ + initialized: true, + Value: &[]map[string]string{{"a": "string"}}, + }, ""}, + {"two keys with space", []string{"{a = string, b = string}"}, + NewBracketSeparatedSliceMapStringString(&nilMap), + &BracketSeparatedSliceMapStringString{ + initialized: true, + Value: &[]map[string]string{{"a": "string", "b": "string"}}, + }, ""}, + {"two keys, multiple Set invocations", []string{"{a=string, b=string}", "{a=string, b=string}"}, + NewBracketSeparatedSliceMapStringString(&nilMap), + &BracketSeparatedSliceMapStringString{ + initialized: true, + Value: &[]map[string]string{{"a": "string", "b": "string"}, {"a": "string", "b": "string"}}, + }, ""}, + {"no target", []string{""}, + NewBracketSeparatedSliceMapStringString(nil), + nil, + "no target (nil pointer to []map[string]string)"}, + } + for _, tc := range testCases { + nilMap = nil + t.Run(tc.desc, func(t *testing.T) { + var err error + for _, val := range tc.vals { + err = tc.start.Set(val) + if err != nil { + break + } + } + if tc.err != "" { + if err == nil || err.Error() != tc.err { + t.Fatalf("expect error %s but got %v", tc.err, err) + } + return + } else if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(tc.expect, tc.start) { + t.Fatalf("expect %#v but got %#v", tc.expect, tc.start) + } + }) + } +} + +func TestEmptyBracketSeparatedSliceMapStringString(t *testing.T) { + var nilSliceMap []map[string]string + notEmpty := &BracketSeparatedSliceMapStringString{ + Value: &[]map[string]string{{"a": "int", "b": "string", "c": "string"}}, + initialized: true, + } + + testCases := []struct { + desc string + m *BracketSeparatedSliceMapStringString + expect bool + }{ + {"nil", NewBracketSeparatedSliceMapStringString(&nilSliceMap), true}, + {"empty", NewBracketSeparatedSliceMapStringString(&[]map[string]string{}), true}, + {"populated", notEmpty, false}, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + ret := tc.m.Empty() + if ret != tc.expect { + t.Fatalf("expect %t but got %t", tc.expect, ret) + } + }) + } +}