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
}
}