mirror of
https://github.com/yusing/godoxy.git
synced 2025-06-15 14:36:48 +02:00
fix(docker): wildcard labels not applying properly for YAML style values and alias without labels
This commit is contained in:
parent
880d66c75e
commit
f8c57d930f
4 changed files with 113 additions and 27 deletions
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
)
|
)
|
||||||
|
@ -12,11 +13,11 @@ type LabelMap = map[string]any
|
||||||
|
|
||||||
var ErrInvalidLabel = gperr.New("invalid label")
|
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)
|
nestedMap := make(LabelMap)
|
||||||
errs := gperr.NewBuilder("labels error")
|
errs := gperr.NewBuilder("labels error")
|
||||||
|
|
||||||
ExpandWildcard(labels)
|
ExpandWildcard(labels, aliases...)
|
||||||
|
|
||||||
for lbl, value := range labels {
|
for lbl, value := range labels {
|
||||||
parts := strutils.SplitRune(lbl, '.')
|
parts := strutils.SplitRune(lbl, '.')
|
||||||
|
@ -56,31 +57,92 @@ func ParseLabels(labels map[string]string) (LabelMap, gperr.Error) {
|
||||||
return nestedMap, errs.Error()
|
return nestedMap, errs.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExpandWildcard(labels map[string]string) {
|
func ExpandWildcard(labels map[string]string, aliases ...string) {
|
||||||
aliasLabels := make([]string, 0, len(labels))
|
// 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)
|
wildcardLabels := make(map[string]string)
|
||||||
|
|
||||||
for lbl, value := range labels {
|
for _, alias := range aliases {
|
||||||
parts := strings.SplitN(lbl, ".", 3) // Split into proxy, alias, rest
|
aliasSet[alias] = struct{}{}
|
||||||
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 lbl, v := range wildcardLabels {
|
// iterate over a copy of the keys to safely mutate the map while ranging
|
||||||
for _, alias := range aliasLabels {
|
for lbl, value := range labels {
|
||||||
labels[fmt.Sprintf("%s.%s.%s", NSProxy, alias, lbl)] = v
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,31 @@ func TestExpandWildcard(t *testing.T) {
|
||||||
}, labels)
|
}, 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) {
|
func BenchmarkParseLabels(b *testing.B) {
|
||||||
for b.Loop() {
|
for b.Loop() {
|
||||||
_, _ = docker.ParseLabels(map[string]string{
|
_, _ = docker.ParseLabels(map[string]string{
|
||||||
|
|
|
@ -124,7 +124,7 @@ func (p *DockerProvider) routesFromContainerLabels(container *docker.Container)
|
||||||
|
|
||||||
errs := gperr.NewBuilder("label errors")
|
errs := gperr.NewBuilder("label errors")
|
||||||
|
|
||||||
m, err := docker.ParseLabels(container.Labels)
|
m, err := docker.ParseLabels(container.Labels, container.Aliases...)
|
||||||
errs.Add(err)
|
errs.Add(err)
|
||||||
|
|
||||||
for alias, entryMapAny := range m {
|
for alias, entryMapAny := range m {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
D "github.com/yusing/go-proxy/internal/docker"
|
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},
|
{Type: "udp", PrivatePort: privPort, PublicPort: pubPort},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -372,7 +371,7 @@ func TestImplicitExcludeDatabase(t *testing.T) {
|
||||||
t.Run("exposed port detection", func(t *testing.T) {
|
t.Run("exposed port detection", func(t *testing.T) {
|
||||||
r, ok := makeRoutes(&container.SummaryTrimmed{
|
r, ok := makeRoutes(&container.SummaryTrimmed{
|
||||||
Names: dummyNames,
|
Names: dummyNames,
|
||||||
Ports: []types.Port{
|
Ports: []container.Port{
|
||||||
{Type: "tcp", PrivatePort: 5432, PublicPort: 5432},
|
{Type: "tcp", PrivatePort: 5432, PublicPort: 5432},
|
||||||
},
|
},
|
||||||
})["a"]
|
})["a"]
|
||||||
|
|
Loading…
Add table
Reference in a new issue