mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-25 20:53:33 +00:00
Merge pull request #4296 from deads2k/deads-allow-dots
allow dots in config set
This commit is contained in:
commit
b23230e616
148
pkg/kubectl/cmd/config/navigation_step_parser.go
Normal file
148
pkg/kubectl/cmd/config/navigation_step_parser.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
|
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 config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type navigationSteps struct {
|
||||||
|
steps []navigationStep
|
||||||
|
currentStepIndex int
|
||||||
|
}
|
||||||
|
|
||||||
|
type navigationStep struct {
|
||||||
|
stepValue string
|
||||||
|
stepType reflect.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNavigationSteps(path string) (*navigationSteps, error) {
|
||||||
|
steps := []navigationStep{}
|
||||||
|
individualParts := strings.Split(path, ".")
|
||||||
|
|
||||||
|
currType := reflect.TypeOf(clientcmdapi.Config{})
|
||||||
|
currPartIndex := 0
|
||||||
|
for currPartIndex < len(individualParts) {
|
||||||
|
switch currType.Kind() {
|
||||||
|
case reflect.Map:
|
||||||
|
// if we're in a map, we need to locate a name. That name may contain dots, so we need to know what tokens are legal for the map's value type
|
||||||
|
// for example, we could have a set request like: `set clusters.10.10.12.56.insecure-skip-tls-verify true`. We enter this case with
|
||||||
|
// steps representing 10, 10, 12, 56, insecure-skip-tls-verify. The name is "10.10.12.56", so we want to collect all those parts together and
|
||||||
|
// store them as a single step. In order to do that, we need to determine what set of tokens is a legal step AFTER the name of the map key
|
||||||
|
// This set of reflective code pulls the type of the map values, uses that type to look up the set of legal tags. Those legal tags are used to
|
||||||
|
// walk the list of remaining parts until we find a match to a legal tag or the end of the string. That name is used to burn all the used parts.
|
||||||
|
mapValueType := currType.Elem()
|
||||||
|
mapValueOptions, err := getPotentialTypeValues(mapValueType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nextPart := findNameStep(individualParts[currPartIndex:], util.KeySet(reflect.ValueOf(mapValueOptions)))
|
||||||
|
|
||||||
|
steps = append(steps, navigationStep{nextPart, mapValueType})
|
||||||
|
currPartIndex += len(strings.Split(nextPart, "."))
|
||||||
|
currType = mapValueType
|
||||||
|
|
||||||
|
case reflect.Struct:
|
||||||
|
nextPart := individualParts[currPartIndex]
|
||||||
|
|
||||||
|
options, err := getPotentialTypeValues(currType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fieldType, exists := options[nextPart]
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("unable to parse %v after %v at %v", path, steps, currType)
|
||||||
|
}
|
||||||
|
|
||||||
|
steps = append(steps, navigationStep{nextPart, fieldType})
|
||||||
|
currPartIndex += len(strings.Split(nextPart, "."))
|
||||||
|
currType = fieldType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &navigationSteps{steps, 0}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *navigationSteps) pop() navigationStep {
|
||||||
|
if s.moreStepsRemaining() {
|
||||||
|
s.currentStepIndex++
|
||||||
|
return s.steps[s.currentStepIndex-1]
|
||||||
|
}
|
||||||
|
return navigationStep{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *navigationSteps) peek() navigationStep {
|
||||||
|
if s.moreStepsRemaining() {
|
||||||
|
return s.steps[s.currentStepIndex]
|
||||||
|
}
|
||||||
|
return navigationStep{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *navigationSteps) moreStepsRemaining() bool {
|
||||||
|
return len(s.steps) > s.currentStepIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
// findNameStep takes the list of parts and a set of valid tags that can be used after the name. It then walks the list of parts
|
||||||
|
// until it find a valid "next" tag or until it reaches the end of the parts and then builds the name back up out of the individual parts
|
||||||
|
func findNameStep(parts []string, typeOptions util.StringSet) string {
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfPartsInStep := findKnownValue(parts[1:], typeOptions) + 1
|
||||||
|
// if we didn't find a known value, then the entire thing must be a name
|
||||||
|
if numberOfPartsInStep == 0 {
|
||||||
|
numberOfPartsInStep = len(parts)
|
||||||
|
}
|
||||||
|
nextParts := parts[0:numberOfPartsInStep]
|
||||||
|
|
||||||
|
return strings.Join(nextParts, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPotentialTypeValues takes a type and looks up the tags used to represent its fields when serialized.
|
||||||
|
func getPotentialTypeValues(typeValue reflect.Type) (map[string]reflect.Type, error) {
|
||||||
|
if typeValue.Kind() != reflect.Struct {
|
||||||
|
return nil, fmt.Errorf("%v is not of type struct", typeValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := make(map[string]reflect.Type)
|
||||||
|
|
||||||
|
for fieldIndex := 0; fieldIndex < typeValue.NumField(); fieldIndex++ {
|
||||||
|
fieldType := typeValue.Field(fieldIndex)
|
||||||
|
yamlTag := fieldType.Tag.Get("json")
|
||||||
|
yamlTagName := strings.Split(yamlTag, ",")[0]
|
||||||
|
|
||||||
|
ret[yamlTagName] = fieldType.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findKnownValue(parts []string, valueOptions util.StringSet) int {
|
||||||
|
for i := range parts {
|
||||||
|
if valueOptions.Has(parts[i]) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1
|
||||||
|
}
|
95
pkg/kubectl/cmd/config/navigation_step_parser_test.go
Normal file
95
pkg/kubectl/cmd/config/navigation_step_parser_test.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
|
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 config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepParserTest struct {
|
||||||
|
path string
|
||||||
|
expectedNavigationSteps navigationSteps
|
||||||
|
expectedError string
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseWithDots(t *testing.T) {
|
||||||
|
test := stepParserTest{
|
||||||
|
path: "clusters.my.dot.delimited.name.server",
|
||||||
|
expectedNavigationSteps: navigationSteps{
|
||||||
|
steps: []navigationStep{
|
||||||
|
{"clusters", reflect.TypeOf(make(map[string]clientcmdapi.Cluster))},
|
||||||
|
{"my.dot.delimited.name", reflect.TypeOf(clientcmdapi.Cluster{})},
|
||||||
|
{"server", reflect.TypeOf("")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
test.run(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseWithDotsEndingWithName(t *testing.T) {
|
||||||
|
test := stepParserTest{
|
||||||
|
path: "contexts.10.12.12.12",
|
||||||
|
expectedNavigationSteps: navigationSteps{
|
||||||
|
steps: []navigationStep{
|
||||||
|
{"contexts", reflect.TypeOf(make(map[string]clientcmdapi.Context))},
|
||||||
|
{"10.12.12.12", reflect.TypeOf(clientcmdapi.Context{})},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
test.run(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseWithBadValue(t *testing.T) {
|
||||||
|
test := stepParserTest{
|
||||||
|
path: "user.bad",
|
||||||
|
expectedNavigationSteps: navigationSteps{
|
||||||
|
steps: []navigationStep{},
|
||||||
|
},
|
||||||
|
expectedError: "unable to parse user.bad after [] at api.Config",
|
||||||
|
}
|
||||||
|
|
||||||
|
test.run(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (test stepParserTest) run(t *testing.T) {
|
||||||
|
actualSteps, err := newNavigationSteps(test.path)
|
||||||
|
if len(test.expectedError) != 0 {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Did not get %v", test.expectedError)
|
||||||
|
} else {
|
||||||
|
if !strings.Contains(err.Error(), test.expectedError) {
|
||||||
|
t.Errorf("Expected %v, but got %v", test.expectedError, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(test.expectedNavigationSteps, *actualSteps) {
|
||||||
|
t.Errorf("diff: %v", util.ObjectDiff(test.expectedNavigationSteps, *actualSteps))
|
||||||
|
}
|
||||||
|
}
|
@ -33,8 +33,6 @@ const (
|
|||||||
additionStepRequiredUnlessUnsettingError = "Must have additional steps after %v unless you are unsetting it"
|
additionStepRequiredUnlessUnsettingError = "Must have additional steps after %v unless you are unsetting it"
|
||||||
)
|
)
|
||||||
|
|
||||||
type navigationSteps []string
|
|
||||||
|
|
||||||
type setOptions struct {
|
type setOptions struct {
|
||||||
pathOptions *pathOptions
|
pathOptions *pathOptions
|
||||||
propertyName string
|
propertyName string
|
||||||
@ -83,8 +81,11 @@ func (o setOptions) run() error {
|
|||||||
return errors.New("cannot set property without using a specific file")
|
return errors.New("cannot set property without using a specific file")
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(o.propertyName, ".")
|
steps, err := newNavigationSteps(o.propertyName)
|
||||||
err = modifyConfig(reflect.ValueOf(config), parts, o.propertyValue, false)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = modifyConfig(reflect.ValueOf(config), steps, o.propertyValue, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -121,28 +122,8 @@ func (o setOptions) validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// moreStepsRemaining just makes code read cleaner
|
func modifyConfig(curr reflect.Value, steps *navigationSteps, propertyValue string, unset bool) error {
|
||||||
func moreStepsRemaining(remainder []string) bool {
|
currStep := steps.pop()
|
||||||
return len(remainder) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s navigationSteps) nextSteps() navigationSteps {
|
|
||||||
if len(s) < 2 {
|
|
||||||
return make([]string, 0, 0)
|
|
||||||
} else {
|
|
||||||
return s[1:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (s navigationSteps) moreStepsRemaining() bool {
|
|
||||||
return len(s) != 0
|
|
||||||
}
|
|
||||||
func (s navigationSteps) nextStep() string {
|
|
||||||
return s[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func modifyConfig(curr reflect.Value, steps navigationSteps, propertyValue string, unset bool) error {
|
|
||||||
shouldUnsetNextField := !steps.nextSteps().moreStepsRemaining() && unset
|
|
||||||
shouldSetThisField := !steps.moreStepsRemaining() && !unset
|
|
||||||
|
|
||||||
actualCurrValue := curr
|
actualCurrValue := curr
|
||||||
if curr.Kind() == reflect.Ptr {
|
if curr.Kind() == reflect.Ptr {
|
||||||
@ -151,14 +132,14 @@ func modifyConfig(curr reflect.Value, steps navigationSteps, propertyValue strin
|
|||||||
|
|
||||||
switch actualCurrValue.Kind() {
|
switch actualCurrValue.Kind() {
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
if shouldSetThisField {
|
if !steps.moreStepsRemaining() && !unset {
|
||||||
return fmt.Errorf("Can't set a map to a value: %v", actualCurrValue)
|
return fmt.Errorf("Can't set a map to a value: %v", actualCurrValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
mapKey := reflect.ValueOf(steps.nextStep())
|
mapKey := reflect.ValueOf(currStep.stepValue)
|
||||||
mapValueType := curr.Type().Elem().Elem()
|
mapValueType := curr.Type().Elem().Elem()
|
||||||
|
|
||||||
if shouldUnsetNextField {
|
if !steps.moreStepsRemaining() && unset {
|
||||||
actualCurrValue.SetMapIndex(mapKey, reflect.Value{})
|
actualCurrValue.SetMapIndex(mapKey, reflect.Value{})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -181,7 +162,7 @@ func modifyConfig(curr reflect.Value, steps navigationSteps, propertyValue strin
|
|||||||
if modifiableMapValue.Kind() == reflect.Struct {
|
if modifiableMapValue.Kind() == reflect.Struct {
|
||||||
modifiableMapValue = modifiableMapValue.Addr()
|
modifiableMapValue = modifiableMapValue.Addr()
|
||||||
}
|
}
|
||||||
err := modifyConfig(modifiableMapValue, steps.nextSteps(), propertyValue, unset)
|
err := modifyConfig(modifiableMapValue, steps, propertyValue, unset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -208,40 +189,36 @@ func modifyConfig(curr reflect.Value, steps navigationSteps, propertyValue strin
|
|||||||
return nil
|
return nil
|
||||||
|
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
if !steps.moreStepsRemaining() {
|
|
||||||
return fmt.Errorf("Can't set a struct to a value: %v", actualCurrValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
for fieldIndex := 0; fieldIndex < actualCurrValue.NumField(); fieldIndex++ {
|
for fieldIndex := 0; fieldIndex < actualCurrValue.NumField(); fieldIndex++ {
|
||||||
currFieldValue := actualCurrValue.Field(fieldIndex)
|
currFieldValue := actualCurrValue.Field(fieldIndex)
|
||||||
currFieldType := actualCurrValue.Type().Field(fieldIndex)
|
currFieldType := actualCurrValue.Type().Field(fieldIndex)
|
||||||
currYamlTag := currFieldType.Tag.Get("json")
|
currYamlTag := currFieldType.Tag.Get("json")
|
||||||
currFieldTypeYamlName := strings.Split(currYamlTag, ",")[0]
|
currFieldTypeYamlName := strings.Split(currYamlTag, ",")[0]
|
||||||
|
|
||||||
if currFieldTypeYamlName == steps.nextStep() {
|
if currFieldTypeYamlName == currStep.stepValue {
|
||||||
thisMapHasNoValue := (currFieldValue.Kind() == reflect.Map && currFieldValue.IsNil())
|
thisMapHasNoValue := (currFieldValue.Kind() == reflect.Map && currFieldValue.IsNil())
|
||||||
|
|
||||||
if thisMapHasNoValue {
|
if thisMapHasNoValue {
|
||||||
newValue := reflect.MakeMap(currFieldValue.Type())
|
newValue := reflect.MakeMap(currFieldValue.Type())
|
||||||
currFieldValue.Set(newValue)
|
currFieldValue.Set(newValue)
|
||||||
|
|
||||||
if shouldUnsetNextField {
|
if !steps.moreStepsRemaining() && unset {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if shouldUnsetNextField {
|
if !steps.moreStepsRemaining() && unset {
|
||||||
// if we're supposed to unset the value or if the value is a map that doesn't exist, create a new value and overwrite
|
// if we're supposed to unset the value or if the value is a map that doesn't exist, create a new value and overwrite
|
||||||
newValue := reflect.New(currFieldValue.Type()).Elem()
|
newValue := reflect.New(currFieldValue.Type()).Elem()
|
||||||
currFieldValue.Set(newValue)
|
currFieldValue.Set(newValue)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return modifyConfig(currFieldValue.Addr(), steps.nextSteps(), propertyValue, unset)
|
return modifyConfig(currFieldValue.Addr(), steps, propertyValue, unset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("Unable to locate path %v under %v", steps, actualCurrValue)
|
return fmt.Errorf("Unable to locate path %#v under %v", currStep, actualCurrValue)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
@ -73,8 +72,11 @@ func (o unsetOptions) run() error {
|
|||||||
return errors.New("cannot set property without using a specific file")
|
return errors.New("cannot set property without using a specific file")
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(o.propertyName, ".")
|
steps, err := newNavigationSteps(o.propertyName)
|
||||||
err = modifyConfig(reflect.ValueOf(config), parts, "", true)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = modifyConfig(reflect.ValueOf(config), steps, "", true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,6 +33,18 @@ func NewStringSet(items ...string) StringSet {
|
|||||||
return ss
|
return ss
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KeySet creates a StringSet from a keys of a map[string](? extends interface{}). Since you can't describe that map type in the Go type system
|
||||||
|
// the reflected value is required.
|
||||||
|
func KeySet(theMap reflect.Value) StringSet {
|
||||||
|
ret := StringSet{}
|
||||||
|
|
||||||
|
for _, keyValue := range theMap.MapKeys() {
|
||||||
|
ret.Insert(keyValue.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
// Insert adds items to the set.
|
// Insert adds items to the set.
|
||||||
func (s StringSet) Insert(items ...string) {
|
func (s StringSet) Insert(items ...string) {
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
|
Loading…
Reference in New Issue
Block a user