Files
godoxy/internal/docker/label.go
yusing 1579f490c0 refactor: replace gperr with standard errors package and simplify string parsing
- Replace gperr.Error return types with standard error across test files
- Replace gperr.New with errors.New in validation and serialization tests
- Update API documentation in README files to use error instead of gperr.Error
- Simplify string parsing using strings.Cut in docker/label.go
- Update benchmarks to use NewTestEntrypoint and remove task package dependency
2026-02-10 16:59:19 +08:00

212 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package docker
import (
"errors"
"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 = errors.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, 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.AddSubject(ErrInvalidLabel, 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.AddSubject(fmt.Errorf("expect mapping, got %T", currentMap[k]), 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"
alias, suffix, _ := strings.Cut(rest, ".")
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):]
alias, suffix, ok := strings.Cut(rest, ".")
if !ok || alias == "" || alias[0] == '#' {
continue
}
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)
}
}
}