mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-14 14:23:33 +01:00
- Added multiple test cases for the ExpandWildcard function to cover various scenarios including basic wildcards, no wildcards, empty labels, and YAML configurations. - Improved handling of nested maps and invalid YAML inputs. - Ensured that explicit labels and reference aliases are correctly processed and expanded.
224 lines
5.1 KiB
Go
224 lines
5.1 KiB
Go
package docker
|
||
|
||
import (
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/goccy/go-yaml"
|
||
"github.com/yusing/godoxy/internal/types"
|
||
gperr "github.com/yusing/goutils/errs"
|
||
strutils "github.com/yusing/goutils/strings"
|
||
)
|
||
|
||
var ErrInvalidLabel = gperr.New("invalid label")
|
||
|
||
const nsProxyDot = NSProxy + "."
|
||
|
||
var refPrefixes = func() []string {
|
||
prefixes := make([]string, 100)
|
||
for i := range prefixes {
|
||
prefixes[i] = nsProxyDot + "#" + strconv.Itoa(i+1) + "."
|
||
}
|
||
return prefixes
|
||
}()
|
||
|
||
func ParseLabels(labels map[string]string, aliases ...string) (types.LabelMap, gperr.Error) {
|
||
nestedMap := make(types.LabelMap)
|
||
errs := gperr.NewBuilder("labels error")
|
||
|
||
ExpandWildcard(labels, aliases...)
|
||
|
||
for lbl, value := range labels {
|
||
parts := strutils.SplitRune(lbl, '.')
|
||
if parts[0] != NSProxy {
|
||
continue
|
||
}
|
||
if len(parts) == 1 {
|
||
errs.Add(ErrInvalidLabel.Subject(lbl))
|
||
continue
|
||
}
|
||
parts = parts[1:]
|
||
currentMap := nestedMap
|
||
|
||
for i, k := range parts {
|
||
if i == len(parts)-1 {
|
||
// Last element, set the value
|
||
currentMap[k] = value
|
||
} else {
|
||
// If the key doesn't exist, create a new map
|
||
if _, exists := currentMap[k]; !exists {
|
||
currentMap[k] = make(types.LabelMap)
|
||
}
|
||
// Move deeper into the nested map
|
||
m, ok := currentMap[k].(types.LabelMap)
|
||
if !ok && currentMap[k] != "" {
|
||
errs.Add(gperr.Errorf("expect mapping, got %T", currentMap[k]).Subject(lbl))
|
||
continue
|
||
} else if !ok {
|
||
m = make(types.LabelMap)
|
||
currentMap[k] = m
|
||
}
|
||
currentMap = m
|
||
}
|
||
}
|
||
}
|
||
|
||
return nestedMap, errs.Error()
|
||
}
|
||
|
||
func ExpandWildcard(labels map[string]string, aliases ...string) {
|
||
aliasSet := make(map[string]int, len(aliases))
|
||
for i, alias := range aliases {
|
||
aliasSet[alias] = i
|
||
}
|
||
|
||
wildcardLabels := make(map[string]string)
|
||
|
||
// First pass: collect wildcards and discover aliases
|
||
for lbl, value := range labels {
|
||
if !strings.HasPrefix(lbl, nsProxyDot) {
|
||
continue
|
||
}
|
||
// lbl is "proxy.X..." where X is alias or wildcard
|
||
rest := lbl[len(nsProxyDot):] // "X..." or "X.suffix"
|
||
dotIdx := strings.IndexByte(rest, '.')
|
||
var alias, suffix string
|
||
if dotIdx == -1 {
|
||
alias = rest
|
||
} else {
|
||
alias = rest[:dotIdx]
|
||
suffix = rest[dotIdx+1:]
|
||
}
|
||
|
||
if alias == WildcardAlias {
|
||
delete(labels, lbl)
|
||
if suffix == "" || strings.Count(value, "\n") > 1 {
|
||
expandYamlWildcard(value, wildcardLabels)
|
||
} else {
|
||
wildcardLabels[suffix] = value
|
||
}
|
||
continue
|
||
}
|
||
|
||
if suffix == "" || alias[0] == '#' {
|
||
continue
|
||
}
|
||
|
||
if _, known := aliasSet[alias]; !known {
|
||
aliasSet[alias] = len(aliasSet)
|
||
}
|
||
}
|
||
|
||
if len(aliasSet) == 0 || len(wildcardLabels) == 0 {
|
||
return
|
||
}
|
||
|
||
// Second pass: convert explicit labels to #N format
|
||
for lbl, value := range labels {
|
||
if !strings.HasPrefix(lbl, nsProxyDot) {
|
||
continue
|
||
}
|
||
rest := lbl[len(nsProxyDot):]
|
||
dotIdx := strings.IndexByte(rest, '.')
|
||
if dotIdx == -1 {
|
||
continue
|
||
}
|
||
alias := rest[:dotIdx]
|
||
if alias[0] == '#' {
|
||
continue
|
||
}
|
||
suffix := rest[dotIdx+1:]
|
||
|
||
idx, known := aliasSet[alias]
|
||
if !known {
|
||
continue
|
||
}
|
||
|
||
delete(labels, lbl)
|
||
if _, overridden := wildcardLabels[suffix]; !overridden {
|
||
labels[refPrefixes[idx]+suffix] = value
|
||
}
|
||
}
|
||
|
||
// Expand wildcards for all aliases
|
||
for suffix, value := range wildcardLabels {
|
||
for _, idx := range aliasSet {
|
||
labels[refPrefixes[idx]+suffix] = value
|
||
}
|
||
}
|
||
}
|
||
|
||
// expandYamlWildcard parses a YAML document in value, flattens it to dot-notated keys and adds the
|
||
// results into dest map where each key is the flattened suffix and the value is the scalar string
|
||
// representation. The provided YAML is expected to be a mapping.
|
||
func expandYamlWildcard(value string, dest map[string]string) {
|
||
// replace tab indentation with spaces to make YAML parser happy
|
||
yamlStr := strings.ReplaceAll(value, "\t", " ")
|
||
|
||
raw := make(map[string]any)
|
||
if err := yaml.Unmarshal([]byte(yamlStr), &raw); err != nil {
|
||
// on parse error, ignore – treat as no-op
|
||
return
|
||
}
|
||
|
||
flattenMap("", raw, dest)
|
||
}
|
||
|
||
// flattenMap converts nested maps into a flat map with dot-delimited keys.
|
||
func flattenMap(prefix string, src map[string]any, dest map[string]string) {
|
||
for k, v := range src {
|
||
key := k
|
||
if prefix != "" {
|
||
key = prefix + "." + k
|
||
}
|
||
switch vv := v.(type) {
|
||
case map[string]any:
|
||
flattenMap(key, vv, dest)
|
||
case map[any]any:
|
||
flattenMapAny(key, vv, dest)
|
||
case string:
|
||
dest[key] = vv
|
||
case int:
|
||
dest[key] = strconv.Itoa(vv)
|
||
case bool:
|
||
dest[key] = strconv.FormatBool(vv)
|
||
case float64:
|
||
dest[key] = strconv.FormatFloat(vv, 'f', -1, 64)
|
||
default:
|
||
dest[key] = fmt.Sprint(v)
|
||
}
|
||
}
|
||
}
|
||
|
||
func flattenMapAny(prefix string, src map[any]any, dest map[string]string) {
|
||
for k, v := range src {
|
||
var key string
|
||
switch kk := k.(type) {
|
||
case string:
|
||
key = kk
|
||
default:
|
||
key = fmt.Sprint(k)
|
||
}
|
||
if prefix != "" {
|
||
key = prefix + "." + key
|
||
}
|
||
switch vv := v.(type) {
|
||
case map[string]any:
|
||
flattenMap(key, vv, dest)
|
||
case map[any]any:
|
||
flattenMapAny(key, vv, dest)
|
||
case string:
|
||
dest[key] = vv
|
||
case int:
|
||
dest[key] = strconv.Itoa(vv)
|
||
case bool:
|
||
dest[key] = strconv.FormatBool(vv)
|
||
case float64:
|
||
dest[key] = strconv.FormatFloat(vv, 'f', -1, 64)
|
||
default:
|
||
dest[key] = fmt.Sprint(v)
|
||
}
|
||
}
|
||
}
|