refactor(labels): refine wildcard expansion logic and tests

- 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.
This commit is contained in:
yusing
2025-12-04 16:16:43 +08:00
parent 65ee6d40bd
commit 9bb71ac76e
2 changed files with 337 additions and 105 deletions

View File

@@ -2,6 +2,7 @@ package docker
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
"github.com/goccy/go-yaml" "github.com/goccy/go-yaml"
@@ -12,6 +13,16 @@ import (
var ErrInvalidLabel = gperr.New("invalid label") 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) { func ParseLabels(labels map[string]string, aliases ...string) (types.LabelMap, gperr.Error) {
nestedMap := make(types.LabelMap) nestedMap := make(types.LabelMap)
errs := gperr.NewBuilder("labels error") errs := gperr.NewBuilder("labels error")
@@ -57,57 +68,83 @@ func ParseLabels(labels map[string]string, aliases ...string) (types.LabelMap, g
} }
func ExpandWildcard(labels map[string]string, aliases ...string) { func ExpandWildcard(labels map[string]string, aliases ...string) {
// collect all explicit aliases first aliasSet := make(map[string]int, len(aliases))
aliasSet := make(map[string]int, len(labels))
// wildcardLabels holds mapping suffix -> value derived from wildcard label definitions
wildcardLabels := make(map[string]string)
for i, alias := range aliases { for i, alias := range aliases {
aliasSet[alias] = i aliasSet[alias] = i
} }
// iterate over a copy of the keys to safely mutate the map while ranging wildcardLabels := make(map[string]string)
// First pass: collect wildcards and discover aliases
for lbl, value := range labels { for lbl, value := range labels {
parts := strings.SplitN(lbl, ".", 3) if !strings.HasPrefix(lbl, nsProxyDot) {
if len(parts) < 2 || parts[0] != NSProxy {
continue continue
} }
alias := parts[1] // lbl is "proxy.X..." where X is alias or wildcard
if alias == WildcardAlias { // "*" rest := lbl[len(nsProxyDot):] // "X..." or "X.suffix"
// remove wildcard label from original map it should not remain afterwards 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) delete(labels, lbl)
if suffix == "" || strings.Count(value, "\n") > 1 {
// value looks like YAML (multiline)
if strings.Count(value, "\n") > 1 {
expandYamlWildcard(value, wildcardLabels) expandYamlWildcard(value, wildcardLabels)
continue } else {
wildcardLabels[suffix] = value
} }
// normal wildcard label with suffix store directly
wildcardLabels[parts[2]] = value
continue continue
} }
// explicit alias label remember the alias (but not reference aliases like #1, #2)
if _, ok := aliasSet[alias]; !ok && !strings.HasPrefix(alias, "#") { if suffix == "" || alias[0] == '#' {
continue
}
if _, known := aliasSet[alias]; !known {
aliasSet[alias] = len(aliasSet) aliasSet[alias] = len(aliasSet)
} }
} }
if len(aliasSet) == 0 || len(wildcardLabels) == 0 { if len(aliasSet) == 0 || len(wildcardLabels) == 0 {
return // nothing to expand return
} }
// expand collected wildcard labels for every alias // Second pass: convert explicit labels to #N format
for suffix, v := range wildcardLabels { for lbl, value := range labels {
for alias, i := range aliasSet { if !strings.HasPrefix(lbl, nsProxyDot) {
// use numeric index instead of the alias name continue
alias = fmt.Sprintf("#%d", i+1) }
rest := lbl[len(nsProxyDot):]
dotIdx := strings.IndexByte(rest, '.')
if dotIdx == -1 {
continue
}
alias := rest[:dotIdx]
if alias[0] == '#' {
continue
}
suffix := rest[dotIdx+1:]
key := fmt.Sprintf("%s.%s.%s", NSProxy, alias, suffix) idx, known := aliasSet[alias]
if suffix == "" { // this should not happen (root wildcard handled earlier) but keep safe if !known {
key = fmt.Sprintf("%s.%s", NSProxy, alias) continue
} }
labels[key] = v
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
} }
} }
} }
@@ -139,12 +176,46 @@ func flattenMap(prefix string, src map[string]any, dest map[string]string) {
case map[string]any: case map[string]any:
flattenMap(key, vv, dest) flattenMap(key, vv, dest)
case map[any]any: case map[any]any:
// convert to map[string]any by stringifying keys flattenMapAny(key, vv, dest)
tmp := make(map[string]any, len(vv)) case string:
for kk, vvv := range vv { dest[key] = vv
tmp[fmt.Sprintf("%v", kk)] = vvv case int:
} dest[key] = strconv.Itoa(vv)
flattenMap(key, tmp, dest) 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: default:
dest[key] = fmt.Sprint(v) dest[key] = fmt.Sprint(v)
} }

View File

@@ -8,87 +8,248 @@ import (
) )
func TestExpandWildcard(t *testing.T) { func TestExpandWildcard(t *testing.T) {
labels := map[string]string{ t.Run("basic", func(t *testing.T) {
"proxy.a.host": "localhost", labels := map[string]string{
"proxy.b.port": "4444", "proxy.a.host": "localhost",
"proxy.b.scheme": "http", "proxy.b.port": "4444",
"proxy.*.port": "5555", "proxy.b.scheme": "http",
"proxy.*.healthcheck.disable": "true", "proxy.*.port": "5555",
} "proxy.*.healthcheck.disable": "true",
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.#1.host": "localhost",
"proxy.#1.port": "5555",
"proxy.#1.healthcheck.disable": "true",
"proxy.#2.port": "5555",
"proxy.#2.scheme": "http",
"proxy.#2.healthcheck.disable": "true",
}, labels)
})
docker.ExpandWildcard(labels) t.Run("no wildcards", func(t *testing.T) {
labels := map[string]string{
"proxy.a.host": "localhost",
"proxy.b.port": "4444",
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.a.host": "localhost",
"proxy.b.port": "4444",
}, labels)
})
require.Equal(t, map[string]string{ t.Run("no aliases", func(t *testing.T) {
"proxy.a.host": "localhost", labels := map[string]string{
"proxy.a.port": "5555", "proxy.*.port": "5555",
"proxy.a.healthcheck.disable": "true", }
"proxy.b.scheme": "http", docker.ExpandWildcard(labels)
"proxy.b.port": "5555", require.Equal(t, map[string]string{}, labels)
"proxy.b.healthcheck.disable": "true", })
}, labels)
}
func TestExpandWildcardWithFQDNAliases(t *testing.T) { t.Run("empty labels", func(t *testing.T) {
labels := map[string]string{ labels := map[string]string{}
"proxy.c.host": "localhost", docker.ExpandWildcard(labels, "a", "b")
"proxy.*.port": "5555", require.Equal(t, map[string]string{}, labels)
} })
docker.ExpandWildcard(labels, "a.example.com", "b.example.com")
require.Equal(t, map[string]string{ t.Run("only wildcards no explicit labels", func(t *testing.T) {
"proxy.#1.port": "5555", labels := map[string]string{
"proxy.#2.port": "5555", "proxy.*.port": "5555",
"proxy.c.host": "localhost", "proxy.*.scheme": "https",
"proxy.c.port": "5555", }
}, labels) docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.#1.port": "5555",
"proxy.#1.scheme": "https",
"proxy.#2.port": "5555",
"proxy.#2.scheme": "https",
}, labels)
})
t.Run("non-proxy labels unchanged", func(t *testing.T) {
labels := map[string]string{
"other.label": "value",
"proxy.*.port": "5555",
"proxy.a.scheme": "http",
}
docker.ExpandWildcard(labels, "a")
require.Equal(t, map[string]string{
"other.label": "value",
"proxy.#1.port": "5555",
"proxy.#1.scheme": "http",
}, labels)
})
t.Run("single alias multiple labels", func(t *testing.T) {
labels := map[string]string{
"proxy.a.host": "localhost",
"proxy.a.port": "8080",
"proxy.a.scheme": "https",
"proxy.*.port": "5555",
}
docker.ExpandWildcard(labels, "a")
require.Equal(t, map[string]string{
"proxy.#1.host": "localhost",
"proxy.#1.port": "5555",
"proxy.#1.scheme": "https",
}, labels)
})
t.Run("wildcard partial override", func(t *testing.T) {
labels := map[string]string{
"proxy.a.host": "localhost",
"proxy.a.port": "8080",
"proxy.a.healthcheck.path": "/health",
"proxy.*.port": "5555",
}
docker.ExpandWildcard(labels, "a")
require.Equal(t, map[string]string{
"proxy.#1.host": "localhost",
"proxy.#1.port": "5555",
"proxy.#1.healthcheck.path": "/health",
}, labels)
})
t.Run("nested suffix distinction", func(t *testing.T) {
labels := map[string]string{
"proxy.a.healthcheck.path": "/health",
"proxy.a.healthcheck.interval": "10s",
"proxy.*.healthcheck.disable": "true",
}
docker.ExpandWildcard(labels, "a")
require.Equal(t, map[string]string{
"proxy.#1.healthcheck.path": "/health",
"proxy.#1.healthcheck.interval": "10s",
"proxy.#1.healthcheck.disable": "true",
}, labels)
})
t.Run("discovered alias from explicit label", func(t *testing.T) {
labels := map[string]string{
"proxy.c.host": "localhost",
"proxy.*.port": "5555",
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.#1.port": "5555",
"proxy.#2.port": "5555",
"proxy.#3.host": "localhost",
"proxy.#3.port": "5555",
}, labels)
})
t.Run("ref alias not converted", func(t *testing.T) {
labels := map[string]string{
"proxy.#1.host": "localhost",
"proxy.#2.port": "8080",
"proxy.*.scheme": "https",
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.#1.host": "localhost",
"proxy.#1.scheme": "https",
"proxy.#2.port": "8080",
"proxy.#2.scheme": "https",
}, labels)
})
t.Run("mixed ref and named aliases", func(t *testing.T) {
labels := map[string]string{
"proxy.#1.host": "host1",
"proxy.a.host": "host2",
"proxy.*.port": "5555",
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.#1.host": "host2",
"proxy.#1.port": "5555",
"proxy.#2.port": "5555",
}, labels)
})
} }
func TestExpandWildcardYAML(t *testing.T) { func TestExpandWildcardYAML(t *testing.T) {
yaml := ` t.Run("basic yaml wildcard", func(t *testing.T) {
yaml := `
host: localhost host: localhost
port: 5555 port: 5555
healthcheck: healthcheck:
disable: true` disable: true`[1:]
labels := map[string]string{ labels := map[string]string{
"proxy.*": yaml[1:], "proxy.*": yaml,
"proxy.a.port": "4444", "proxy.a.port": "4444",
"proxy.a.healthcheck.disable": "false", "proxy.a.healthcheck.disable": "false",
"proxy.a.healthcheck.path": "/health", "proxy.a.healthcheck.path": "/health",
"proxy.b.port": "6666", "proxy.b.port": "6666",
} }
docker.ExpandWildcard(labels) docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{ require.Equal(t, map[string]string{
"proxy.a.host": "localhost", // set by wildcard "proxy.#1.host": "localhost",
"proxy.a.port": "5555", // overridden by wildcard "proxy.#1.port": "5555",
"proxy.a.healthcheck.disable": "true", // overridden by wildcard "proxy.#1.healthcheck.disable": "true",
"proxy.a.healthcheck.path": "/health", // own label "proxy.#1.healthcheck.path": "/health",
"proxy.b.host": "localhost", // set by wildcard "proxy.#2.host": "localhost",
"proxy.b.port": "5555", // overridden by wildcard "proxy.#2.port": "5555",
"proxy.b.healthcheck.disable": "true", // overridden by wildcard "proxy.#2.healthcheck.disable": "true",
}, labels) }, labels)
} })
func TestWildcardWithRefAliases(t *testing.T) { t.Run("yaml with nested maps", func(t *testing.T) {
labels := map[string]string{ yaml := `
"proxy.#1.host": "localhost", middlewares:
"proxy.#1.port": "5555", request:
"proxy.*.middlewares.request.hide_headers": "X-Header1,X-Header2", hide_headers: X-Secret
} add_headers:
docker.ExpandWildcard(labels, "a.example.com", "b.example.com") X-Custom: value`[1:]
require.Equal(t, map[string]string{ labels := map[string]string{
"proxy.#1.host": "localhost", "proxy.*": yaml,
"proxy.#1.port": "5555", "proxy.a.middlewares.request.set_headers": "X-Override: yes",
"proxy.#1.middlewares.request.hide_headers": "X-Header1,X-Header2", }
"proxy.#2.middlewares.request.hide_headers": "X-Header1,X-Header2", docker.ExpandWildcard(labels, "a")
}, labels) require.Equal(t, map[string]string{
"proxy.#1.middlewares.request.hide_headers": "X-Secret",
"proxy.#1.middlewares.request.add_headers.X-Custom": "value",
"proxy.#1.middlewares.request.set_headers": "X-Override: yes",
}, labels)
})
t.Run("yaml only no explicit labels", func(t *testing.T) {
yaml := `
host: localhost
port: 8080`[1:]
labels := map[string]string{
"proxy.*": yaml,
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.#1.host": "localhost",
"proxy.#1.port": "8080",
"proxy.#2.host": "localhost",
"proxy.#2.port": "8080",
}, labels)
})
t.Run("invalid yaml ignored", func(t *testing.T) {
labels := map[string]string{
"proxy.*": "invalid: yaml: content:\n\t\tbad",
"proxy.a.port": "8080",
}
docker.ExpandWildcard(labels, "a")
require.Equal(t, map[string]string{
"proxy.a.port": "8080",
}, labels)
})
} }
func BenchmarkParseLabels(b *testing.B) { func BenchmarkParseLabels(b *testing.B) {
m := map[string]string{
"proxy.a.host": "localhost",
"proxy.b.port": "4444",
"proxy.*.scheme": "http",
"proxy.*.middlewares.request.hide_headers": "X-Header1,X-Header2",
}
for b.Loop() { for b.Loop() {
_, _ = docker.ParseLabels(map[string]string{ _, _ = docker.ParseLabels(m, "a", "b")
"proxy.a.host": "localhost",
"proxy.b.port": "4444",
"proxy.*.scheme": "http",
"proxy.*.middlewares.request.hide_headers": "X-Header1,X-Header2",
})
} }
} }