From f8c57d930f029d4d4923bf9c61bb858e7ee51631 Mon Sep 17 00:00:00 2001 From: yusing Date: Fri, 13 Jun 2025 23:02:25 +0800 Subject: [PATCH] fix(docker): wildcard labels not applying properly for YAML style values and alias without labels --- internal/docker/label.go | 108 +++++++++++++++++++------ internal/docker/label_test.go | 25 ++++++ internal/route/provider/docker.go | 2 +- internal/route/provider/docker_test.go | 5 +- 4 files changed, 113 insertions(+), 27 deletions(-) diff --git a/internal/docker/label.go b/internal/docker/label.go index 5a8ba6b..1adf393 100644 --- a/internal/docker/label.go +++ b/internal/docker/label.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/goccy/go-yaml" "github.com/yusing/go-proxy/internal/gperr" "github.com/yusing/go-proxy/internal/utils/strutils" ) @@ -12,11 +13,11 @@ type LabelMap = map[string]any var ErrInvalidLabel = gperr.New("invalid label") -func ParseLabels(labels map[string]string) (LabelMap, gperr.Error) { +func ParseLabels(labels map[string]string, aliases ...string) (LabelMap, gperr.Error) { nestedMap := make(LabelMap) errs := gperr.NewBuilder("labels error") - ExpandWildcard(labels) + ExpandWildcard(labels, aliases...) for lbl, value := range labels { parts := strutils.SplitRune(lbl, '.') @@ -56,31 +57,92 @@ func ParseLabels(labels map[string]string) (LabelMap, gperr.Error) { return nestedMap, errs.Error() } -func ExpandWildcard(labels map[string]string) { - aliasLabels := make([]string, 0, len(labels)) +func ExpandWildcard(labels map[string]string, aliases ...string) { + // collect all explicit aliases first + aliasSet := make(map[string]struct{}, len(labels)) + // wildcardLabels holds mapping suffix -> value derived from wildcard label definitions wildcardLabels := make(map[string]string) - for lbl, value := range labels { - parts := strings.SplitN(lbl, ".", 3) // Split into proxy, alias, rest - if parts[0] != NSProxy || len(parts) < 2 { - continue - } - alias := parts[1] // alias or wildcard alias - if alias == WildcardAlias { - delete(labels, lbl) - if len(parts) < 3 { // invalid wildcard label (no suffix) - continue - } - wildcardLabels[parts[2]] = value - } else { - // Extract just the alias part (first segment after proxy) - aliasLabels = append(aliasLabels, alias) - } + for _, alias := range aliases { + aliasSet[alias] = struct{}{} } - for lbl, v := range wildcardLabels { - for _, alias := range aliasLabels { - labels[fmt.Sprintf("%s.%s.%s", NSProxy, alias, lbl)] = v + // iterate over a copy of the keys to safely mutate the map while ranging + for lbl, value := range labels { + parts := strings.SplitN(lbl, ".", 3) + if len(parts) < 2 || parts[0] != NSProxy { + continue + } + alias := parts[1] + if alias == WildcardAlias { // "*" + // remove wildcard label from original map – it should not remain afterwards + delete(labels, lbl) + + // value looks like YAML (multiline) + if strings.Count(value, "\n") > 1 { + expandYamlWildcard(value, wildcardLabels) + continue + } + + // normal wildcard label with suffix – store directly + wildcardLabels[parts[2]] = value + continue + } + // explicit alias label – remember the alias + aliasSet[alias] = struct{}{} + } + + if len(aliasSet) == 0 || len(wildcardLabels) == 0 { + return // nothing to expand + } + + // expand collected wildcard labels for every alias + for suffix, v := range wildcardLabels { + for alias := range aliasSet { + 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 + } + } +} + +// 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: + // 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) + default: + dest[key] = fmt.Sprint(v) } } } diff --git a/internal/docker/label_test.go b/internal/docker/label_test.go index e8174e8..cfc48ca 100644 --- a/internal/docker/label_test.go +++ b/internal/docker/label_test.go @@ -28,6 +28,31 @@ func TestExpandWildcard(t *testing.T) { }, labels) } +func TestExpandWildcardYAML(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) +} + func BenchmarkParseLabels(b *testing.B) { for b.Loop() { _, _ = docker.ParseLabels(map[string]string{ diff --git a/internal/route/provider/docker.go b/internal/route/provider/docker.go index 7804b16..c36d3d1 100755 --- a/internal/route/provider/docker.go +++ b/internal/route/provider/docker.go @@ -124,7 +124,7 @@ func (p *DockerProvider) routesFromContainerLabels(container *docker.Container) errs := gperr.NewBuilder("label errors") - m, err := docker.ParseLabels(container.Labels) + m, err := docker.ParseLabels(container.Labels, container.Aliases...) errs.Add(err) for alias, entryMapAny := range m { diff --git a/internal/route/provider/docker_test.go b/internal/route/provider/docker_test.go index 8c919cf..b8cc5f5 100644 --- a/internal/route/provider/docker_test.go +++ b/internal/route/provider/docker_test.go @@ -4,7 +4,6 @@ import ( "testing" "time" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" D "github.com/yusing/go-proxy/internal/docker" @@ -319,7 +318,7 @@ func TestStreamDefaultValues(t *testing.T) { }, }, }, - Ports: []types.Port{ + Ports: []container.Port{ {Type: "udp", PrivatePort: privPort, PublicPort: pubPort}, }, } @@ -372,7 +371,7 @@ func TestImplicitExcludeDatabase(t *testing.T) { t.Run("exposed port detection", func(t *testing.T) { r, ok := makeRoutes(&container.SummaryTrimmed{ Names: dummyNames, - Ports: []types.Port{ + Ports: []container.Port{ {Type: "tcp", PrivatePort: 5432, PublicPort: 5432}, }, })["a"]