From 9bb71ac76e9acaf4db4b90f1f18f4ce3f79749e5 Mon Sep 17 00:00:00 2001 From: yusing Date: Thu, 4 Dec 2025 16:16:43 +0800 Subject: [PATCH] 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. --- internal/docker/label.go | 145 ++++++++++++----- internal/docker/label_test.go | 297 ++++++++++++++++++++++++++-------- 2 files changed, 337 insertions(+), 105 deletions(-) diff --git a/internal/docker/label.go b/internal/docker/label.go index c45d5c2c..baf86d71 100644 --- a/internal/docker/label.go +++ b/internal/docker/label.go @@ -2,6 +2,7 @@ package docker import ( "fmt" + "strconv" "strings" "github.com/goccy/go-yaml" @@ -12,6 +13,16 @@ import ( 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") @@ -57,57 +68,83 @@ func ParseLabels(labels map[string]string, aliases ...string) (types.LabelMap, g } func ExpandWildcard(labels map[string]string, aliases ...string) { - // collect all explicit aliases first - aliasSet := make(map[string]int, len(labels)) - // wildcardLabels holds mapping suffix -> value derived from wildcard label definitions - wildcardLabels := make(map[string]string) - + aliasSet := make(map[string]int, len(aliases)) for i, alias := range aliases { 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 { - parts := strings.SplitN(lbl, ".", 3) - if len(parts) < 2 || parts[0] != NSProxy { + if !strings.HasPrefix(lbl, nsProxyDot) { continue } - alias := parts[1] - if alias == WildcardAlias { // "*" - // remove wildcard label from original map – it should not remain afterwards + // 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) - - // value looks like YAML (multiline) - if strings.Count(value, "\n") > 1 { + if suffix == "" || strings.Count(value, "\n") > 1 { expandYamlWildcard(value, wildcardLabels) - continue + } else { + wildcardLabels[suffix] = value } - - // normal wildcard label with suffix – store directly - wildcardLabels[parts[2]] = value 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) } } if len(aliasSet) == 0 || len(wildcardLabels) == 0 { - return // nothing to expand + return } - // expand collected wildcard labels for every alias - for suffix, v := range wildcardLabels { - for alias, i := range aliasSet { - // use numeric index instead of the alias name - alias = fmt.Sprintf("#%d", i+1) + // 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:] - key := fmt.Sprintf("%s.%s.%s", NSProxy, alias, suffix) - if suffix == "" { // this should not happen (root wildcard handled earlier) but keep safe - key = fmt.Sprintf("%s.%s", NSProxy, alias) - } - labels[key] = v + 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 } } } @@ -139,12 +176,46 @@ func flattenMap(prefix string, src map[string]any, dest map[string]string) { case map[string]any: flattenMap(key, vv, dest) case map[any]any: - // convert to map[string]any by stringifying keys - tmp := make(map[string]any, len(vv)) - for kk, vvv := range vv { - tmp[fmt.Sprintf("%v", kk)] = vvv - } - flattenMap(key, tmp, dest) + 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) } diff --git a/internal/docker/label_test.go b/internal/docker/label_test.go index 83b6fd19..6a7a3848 100644 --- a/internal/docker/label_test.go +++ b/internal/docker/label_test.go @@ -8,87 +8,248 @@ import ( ) func TestExpandWildcard(t *testing.T) { - labels := map[string]string{ - "proxy.a.host": "localhost", - "proxy.b.port": "4444", - "proxy.b.scheme": "http", - "proxy.*.port": "5555", - "proxy.*.healthcheck.disable": "true", - } + t.Run("basic", func(t *testing.T) { + labels := map[string]string{ + "proxy.a.host": "localhost", + "proxy.b.port": "4444", + "proxy.b.scheme": "http", + "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{ - "proxy.a.host": "localhost", - "proxy.a.port": "5555", - "proxy.a.healthcheck.disable": "true", - "proxy.b.scheme": "http", - "proxy.b.port": "5555", - "proxy.b.healthcheck.disable": "true", - }, labels) -} + t.Run("no aliases", func(t *testing.T) { + labels := map[string]string{ + "proxy.*.port": "5555", + } + docker.ExpandWildcard(labels) + require.Equal(t, map[string]string{}, labels) + }) -func TestExpandWildcardWithFQDNAliases(t *testing.T) { - labels := map[string]string{ - "proxy.c.host": "localhost", - "proxy.*.port": "5555", - } - docker.ExpandWildcard(labels, "a.example.com", "b.example.com") - require.Equal(t, map[string]string{ - "proxy.#1.port": "5555", - "proxy.#2.port": "5555", - "proxy.c.host": "localhost", - "proxy.c.port": "5555", - }, labels) + t.Run("empty labels", func(t *testing.T) { + labels := map[string]string{} + docker.ExpandWildcard(labels, "a", "b") + require.Equal(t, map[string]string{}, labels) + }) + + t.Run("only wildcards no explicit labels", func(t *testing.T) { + labels := map[string]string{ + "proxy.*.port": "5555", + "proxy.*.scheme": "https", + } + 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) { - yaml := ` + t.Run("basic yaml wildcard", func(t *testing.T) { + yaml := ` host: localhost port: 5555 healthcheck: - disable: true` - labels := map[string]string{ - "proxy.*": yaml[1:], - "proxy.a.port": "4444", - "proxy.a.healthcheck.disable": "false", - "proxy.a.healthcheck.path": "/health", - "proxy.b.port": "6666", - } - docker.ExpandWildcard(labels) - require.Equal(t, map[string]string{ - "proxy.a.host": "localhost", // set by wildcard - "proxy.a.port": "5555", // overridden by wildcard - "proxy.a.healthcheck.disable": "true", // overridden by wildcard - "proxy.a.healthcheck.path": "/health", // own label - "proxy.b.host": "localhost", // set by wildcard - "proxy.b.port": "5555", // overridden by wildcard - "proxy.b.healthcheck.disable": "true", // overridden by wildcard - }, labels) -} + disable: true`[1:] + labels := map[string]string{ + "proxy.*": yaml, + "proxy.a.port": "4444", + "proxy.a.healthcheck.disable": "false", + "proxy.a.healthcheck.path": "/health", + "proxy.b.port": "6666", + } + 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.#1.healthcheck.path": "/health", + "proxy.#2.host": "localhost", + "proxy.#2.port": "5555", + "proxy.#2.healthcheck.disable": "true", + }, labels) + }) -func TestWildcardWithRefAliases(t *testing.T) { - labels := map[string]string{ - "proxy.#1.host": "localhost", - "proxy.#1.port": "5555", - "proxy.*.middlewares.request.hide_headers": "X-Header1,X-Header2", - } - docker.ExpandWildcard(labels, "a.example.com", "b.example.com") - require.Equal(t, map[string]string{ - "proxy.#1.host": "localhost", - "proxy.#1.port": "5555", - "proxy.#1.middlewares.request.hide_headers": "X-Header1,X-Header2", - "proxy.#2.middlewares.request.hide_headers": "X-Header1,X-Header2", - }, labels) + t.Run("yaml with nested maps", func(t *testing.T) { + yaml := ` +middlewares: + request: + hide_headers: X-Secret + add_headers: + X-Custom: value`[1:] + labels := map[string]string{ + "proxy.*": yaml, + "proxy.a.middlewares.request.set_headers": "X-Override: yes", + } + docker.ExpandWildcard(labels, "a") + 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) { + 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() { - _, _ = docker.ParseLabels(map[string]string{ - "proxy.a.host": "localhost", - "proxy.b.port": "4444", - "proxy.*.scheme": "http", - "proxy.*.middlewares.request.hide_headers": "X-Header1,X-Header2", - }) + _, _ = docker.ParseLabels(m, "a", "b") } }