operator/pkg/values/map.go (351 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 values import ( "encoding/json" "fmt" "github.com/apache/dubbo-kubernetes/operator/pkg/util/ptr" "path/filepath" "reflect" "sigs.k8s.io/yaml" "strconv" "strings" ) // Map is a wrapper around an untyped map, used throughout the operator codebase for generic access. type Map map[string]any // JSON serializes a Map to a JSON string. func (m Map) JSON() string { bytes, err := json.Marshal(m) if err != nil { panic(fmt.Sprintf("json Marshal: %v", err)) } return string(bytes) } // YAML serializes a Map to a YAML string. func (m Map) YAML() string { bytes, err := yaml.Marshal(m) if err != nil { panic(fmt.Sprintf("yaml Marshal: %v", err)) } return string(bytes) } // MapFromJSON constructs a Map from JSON func MapFromJSON(input []byte) (Map, error) { m := make(Map) err := json.Unmarshal(input, &m) if err != nil { return nil, err } return m, nil } // MapFromYAML constructs a Map from YAML func MapFromYAML(input []byte) (Map, error) { m := make(Map) err := yaml.Unmarshal(input, &m) if err != nil { return nil, err } return m, nil } func tableLookup(m Map, simple string) (Map, bool) { v, ok := m[simple] if !ok { return nil, false } if vv, ok := v.(map[string]interface{}); ok { return vv, true } // This catches a case where a value is of type Values, but doesn't (for some // reason) match the map[string]interface{}. This has been observed in the // wild, and might be a result of a nil map of type Values. if vv, ok := v.(Map); ok { return vv, true } return nil, false } func parsePath(key string) []string { return strings.Split(key, ".") } func (m Map) GetPathMap(s string) (Map, bool) { current := m for _, n := range parsePath(s) { subkey, ok := tableLookup(current, n) if !ok { return nil, false } current = subkey } return current, true } func splitEscaped(s string, r rune) []string { var prev rune if len(s) == 0 { return []string{} } prevIndex := 0 var out []string for i, c := range s { if c == r && (i == 0 || i > 0 && prev != '\\') { out = append(out, s[prevIndex:i]) prevIndex = i + 1 } prev = c } out = append(out, s[prevIndex:]) return out } func splitPath(path string) []string { path = filepath.Clean(path) path = strings.TrimPrefix(path, ".") path = strings.TrimSuffix(path, ".") pv := splitEscaped(path, '.') var r []string for _, str := range pv { if str != "" { str = strings.ReplaceAll(str, "\\.", ".") // Is str of the form node[expr], convert to "node", "[expr]"? nBracket := strings.IndexRune(str, '[') if nBracket > 0 { r = append(r, str[:nBracket], str[nBracket:]) } else { // str is "[expr]" or "node" r = append(r, str) } } } return r } // GetPathAs is a helper function to get a patch value and cast it to a specified type. // If the path is not found, or the cast fails, the zero value is returned. func GetPathAs[T any](m Map, name string) T { v, ok := m.GetPath(name) if !ok { return ptr.Empty[T]() } t, _ := v.(T) return t } // GetPathString is a helper around TryGetPathAs[string] to allow usage as a method (otherwise impossible with generics) func (m Map) GetPathString(s string) string { return GetPathAs[string](m, s) } // GetPathStringOr is a helper around TryGetPathAs[string] to allow usage as a method (otherwise impossible with generics), // with an allowance for a default value if it is not found/not set. func (m Map) GetPathStringOr(s string, def string) string { return ptr.NonEmptyOrDefault(m.GetPathString(s), def) } func (m Map) GetPath(name string) (any, bool) { current := any(m) paths := splitPath(name) for _, n := range paths { if idx, ok := extractIndex(n); ok { a, ok := current.([]any) if !ok { return nil, false } if idx >= 0 && idx < len(a) { current = a[idx] } else { return nil, false } } else if k, v, ok := extractKeyValue(n); ok { a, ok := current.([]any) if !ok { return nil, false } index := -1 for idx, cm := range a { if MustCastAsMap(cm)[k] == v { index = idx break } } if index == -1 { return nil, false } current = a[idx] } else { cm, ok := CastAsMap(current) if !ok { return nil, false } subKey, ok := cm[n] if !ok { return nil, false } current = subKey } } if p, ok := current.(*any); ok { return *p, true } return current, true } // MustCastAsMap casts a value to a Map; if the value is not a map, it will panic.. func MustCastAsMap(current any) Map { m, ok := CastAsMap(current) if !ok { if !reflect.ValueOf(current).IsValid() { return Map{} } panic(fmt.Sprintf("not a map, got %T: %v %v", current, current, reflect.ValueOf(current).Kind())) } return m } // CastAsMap casts a value to a Map, if possible. func CastAsMap(current any) (Map, bool) { if m, ok := current.(Map); ok { return m, true } if m, ok := current.(map[string]any); ok { return m, true } return nil, false } // ConvertMap translates a Map to a T, via JSON func ConvertMap[T any](m Map) (T, error) { return fromJSON[T]([]byte(m.JSON())) } func fromJSON[T any](overlay []byte) (T, error) { v := new(T) err := json.Unmarshal(overlay, &v) if err != nil { return ptr.Empty[T](), err } return *v, nil } // getPV returns the path and value components for the given set flag string, which must be in path=value format. func getPV(setFlag string) (path string, value string) { pv := strings.Split(setFlag, "=") if len(pv) != 2 { return setFlag, "" } path, value = strings.TrimSpace(pv[0]), strings.TrimSpace(pv[1]) return } // SetPath applies values from a path like `key.subkey`, `key.[0].var`, or `key.[name:foo]`. func (m Map) SetPath(paths string, value any) error { path := splitPath(paths) base := m if err := setPathRecurse(base, path, value); err != nil { return err } return nil } // SetPaths applies values from input like `key.subkey=val` func (m Map) SetPaths(paths ...string) error { for _, sf := range paths { p, v := getPV(sf) // input value type is always string, transform it to correct type before setting. var val any = v if !isAlwaysString(p) { val = parseValue(v) } if err := m.SetPath(p, val); err != nil { return err } } return nil } // SetSpecPaths applies values from input like `key.subkey=val`, and applies them under 'spec' func (m Map) SetSpecPaths(paths ...string) error { for _, path := range paths { if err := m.SetPaths("spec." + path); err != nil { return err } } return nil } func setPathRecurse(base map[string]any, paths []string, value any) error { seg := paths[0] last := len(paths) == 1 nextIsArray := len(paths) >= 2 && strings.HasPrefix(paths[1], "[") if nextIsArray { last = len(paths) == 2 // Find or create target list if _, f := base[seg]; !f { base[seg] = []any{} } var index int if k, v, ok := extractKV(paths[1]); ok { index = -1 for idx, cm := range base[seg].([]any) { if MustCastAsMap(cm)[k] == v { index = idx break } } if index == -1 { return fmt.Errorf("element %v not found", paths[1]) } } else if idx, ok := extractIndex(paths[1]); ok { index = idx } else { return fmt.Errorf("unknown segment %v", paths[1]) } l := base[seg].([]any) if index < 0 || index >= len(l) { // Index is greater, we need to append if last { l = append(l, value) } else { nm := Map{} if err := setPathRecurse(nm, paths[2:], value); err != nil { return err } l = append(l, nm) } base[seg] = l } else { v := MustCastAsMap(l[index]) if err := setPathRecurse(v, paths[2:], value); err != nil { return err } l[index] = v } } else { if _, f := base[seg]; !f { base[seg] = map[string]any{} } if last { base[seg] = value } else { return setPathRecurse(MustCastAsMap(base[seg]), paths[1:], value) } } return nil } func extractKV(seg string) (string, string, bool) { if !strings.HasPrefix(seg, "[") || !strings.HasSuffix(seg, "]") { return "", "", false } sanitized := seg[1 : len(seg)-1] return strings.Cut(sanitized, ":") } func extractIndex(seg string) (int, bool) { if !strings.HasPrefix(seg, "[") || !strings.HasSuffix(seg, "]") { return 0, false } sanitized := seg[1 : len(seg)-1] v, err := strconv.Atoi(sanitized) if err != nil { return 0, false } return v, true } func extractKeyValue(seg string) (string, string, bool) { if !strings.HasPrefix(seg, "[") || !strings.HasSuffix(seg, "]") { return "", "", false } sanitized := seg[1 : len(seg)-1] return strings.Cut(sanitized, ":") } // alwaysString represents types that should always be decoded as strings var alwaysString = []string{} func isAlwaysString(s string) bool { for _, a := range alwaysString { if strings.HasPrefix(s, a) { return true } } return false } // parseValue parses string into a value. func parseValue(valueStr string) any { var value any if v, err := strconv.Atoi(valueStr); err == nil { value = v } else if v, err := strconv.ParseFloat(valueStr, 64); err == nil { value = v } else if v, err := strconv.ParseBool(valueStr); err == nil { value = v } else { value = strings.ReplaceAll(valueStr, "\\,", ",") } return value } // MergeFrom does a key-wise merge between the current map and the passed in map. // The other map has precedence, and the result will modify the current map. func (m Map) MergeFrom(other Map) { for k, v := range other { if vm, ok := v.(Map); ok { v = map[string]any(vm) } if v, ok := v.(map[string]any); ok { if bv, ok := m[k]; ok { if bv, ok := bv.(map[string]any); ok { Map(bv).MergeFrom(v) continue } } } m[k] = v } }