support inline yaml for docker labels, serveral minor fixes

This commit is contained in:
yusing 2025-01-03 15:35:40 +08:00
parent 6e30d39b78
commit b38bff41d8
7 changed files with 248 additions and 42 deletions

View file

@ -7,6 +7,8 @@ import (
type LabelMap = map[string]any
var ErrInvalidLabel = E.New("invalid label")
func ParseLabels(labels map[string]string) (LabelMap, E.Error) {
nestedMap := make(LabelMap)
errs := E.NewBuilder("labels error")
@ -17,7 +19,7 @@ func ParseLabels(labels map[string]string) (LabelMap, E.Error) {
continue
}
if len(parts) == 1 {
errs.Add(E.Errorf("invalid label %s", lbl).Subject(lbl))
errs.Add(ErrInvalidLabel.Subject(lbl))
continue
}
parts = parts[1:]

View file

@ -1,6 +1,7 @@
package provider
import (
"fmt"
"strconv"
"strings"
@ -13,6 +14,7 @@ import (
U "github.com/yusing/go-proxy/internal/utils"
"github.com/yusing/go-proxy/internal/utils/strutils"
"github.com/yusing/go-proxy/internal/watcher"
"gopkg.in/yaml.v3"
)
type DockerProvider struct {
@ -125,11 +127,19 @@ func (p *DockerProvider) entriesFromContainerLabels(container *docker.Container)
continue
}
var ok bool
entryMap, ok := entryMapAny.(docker.LabelMap)
if !ok {
errs.Add(E.Errorf("expect mapping, got %T", entryMap).Subject(alias))
continue
// try to deserialize to map
entryMap = make(docker.LabelMap)
yamlStr, ok := entryMapAny.(string)
if !ok {
// should not happen
panic(fmt.Errorf("invalid entry map type %T", entryMapAny))
}
if err := yaml.Unmarshal([]byte(yamlStr), &entryMap); err != nil {
errs.Add(E.From(err).Subject(alias))
continue
}
}
if alias == docker.WildcardAlias {
@ -153,8 +163,8 @@ func (p *DockerProvider) entriesFromContainerLabels(container *docker.Container)
}
// init entry if not exist
var en *route.RawEntry
if en, ok = entries.Load(alias); !ok {
en, ok := entries.Load(alias)
if !ok {
en = &route.RawEntry{
Alias: alias,
Container: container,
@ -171,10 +181,12 @@ func (p *DockerProvider) entriesFromContainerLabels(container *docker.Container)
}
}
if wildcardProps != nil {
entries.RangeAll(func(alias string, re *route.RawEntry) {
entries.Range(func(alias string, re *route.RawEntry) bool {
if err := U.Deserialize(wildcardProps, re); err != nil {
errs.Add(err.Subject(alias))
errs.Add(err.Subject(docker.WildcardAlias))
return false
}
return true
})
}

View file

@ -0,0 +1,128 @@
proxy.aliases: app,app1
# style 1 - inline yaml
proxy.app: |
scheme: http
host: 10.0.0.254
port: 80
path_patterns: # Check https://pkg.go.dev/net/http#hdr-Patterns-ServeMux for syntax
- GET / # accept any GET request
- POST /auth # for /auth and /auth/* accept only POST
- GET /home/{$} # for exactly /home
healthcheck:
disabled: false
path: /
interval: 5s
load_balance:
link: app
mode: ip_hash
options:
header: X-Forwarded-For
middlewares:
cidr_whitelist:
allow:
- 127.0.0.1
- 10.0.0.0/8
status_code: 403
message: IP not allowed
hideXForwarded:
homepage:
name: Example App
icon: png/example.png
description: An example app
category: example
access_log:
buffer_size: 100
path: /var/log/example.log
filters:
status_codes:
values:
- 200-299
- 101
method:
values:
- GET
host:
values:
- example.y.z
headers:
negative: true
values:
- foo=bar
- baz
cidr:
values:
- 192.168.10.0/24
fields:
headers:
default: keep
config:
foo: redact
query:
default: drop
config:
foo: keep
cookies:
default: redact
config:
foo: keep
# style 2 - full labels and mixed
proxy.app1.scheme: http
proxy.app1.host: 10.0.0.254
proxy.app1.port: 80
proxy.app1.path_patterns:
| # Check https://pkg.go.dev/net/http#hdr-Patterns-ServeMux for syntax
GET / # accept any GET request
POST /auth # for /auth and /auth/* accept only POST
GET /home/{$} # for exactly /home
proxy.app1.healthcheck.disabled: false
proxy.app1.healthcheck.path: /
proxy.app1.healthcheck.interval: 5s
proxy.app1.load_balance.link: app
proxy.app1.load_balance.mode: ip_hash
proxy.app1.load_balance.options.header: X-Forwarded-For
proxy.app1.middlewares.cidr_whitelist: |
allow:
- 127.0.0.1
- 10.0.0.0/8
status_code: 403
message: IP not allowed
proxy.app1.middlewares.hideXForwarded:
proxy.app1.homepage.name: Example App
proxy.app1.homepage.icon: png/example.png
proxy.app1.homepage.description: An example app
proxy.app1.homepage.category: example
proxy.app1.access_log.buffer_size: 100
proxy.app1.access_log.path: /var/log/example.log
proxy.app1.access_log.filters: |
status_codes:
values:
- 200-299
- 101
method:
values:
- GET
host:
values:
- example.y.z
headers:
negative: true
values:
- foo=bar
- baz
cidr:
values:
- 192.168.10.0/24
proxy.app1.access_log.fields: |
headers:
default: keep
config:
foo: redact
query:
default: drop
config:
foo: keep
cookies:
default: redact
config:
foo: keep

View file

@ -0,0 +1,36 @@
package provider
import (
"testing"
"github.com/docker/docker/api/types"
"github.com/yusing/go-proxy/internal/docker"
. "github.com/yusing/go-proxy/internal/utils/testing"
"gopkg.in/yaml.v3"
_ "embed"
)
//go:embed docker_labels.yaml
var testDockerLabelsYAML []byte
func TestParseDockerLabels(t *testing.T) {
var provider DockerProvider
labels := make(map[string]string)
ExpectNoError(t, yaml.Unmarshal(testDockerLabelsYAML, &labels))
routes, err := provider.entriesFromContainerLabels(
docker.FromDocker(&types.Container{
Names: []string{"container"},
Labels: labels,
State: "running",
Ports: []types.Port{
{Type: "tcp", PrivatePort: 1234, PublicPort: 1234},
},
}, "/var/run/docker.sock"),
)
ExpectNoError(t, err)
ExpectTrue(t, routes.Has("app"))
ExpectTrue(t, routes.Has("app1"))
}

View file

@ -9,9 +9,9 @@ import (
)
//go:embed all_fields.yaml
var yaml []byte
var testAllFieldsYAML []byte
func TestFile(t *testing.T) {
_, err := validate(yaml)
_, err := validate(testAllFieldsYAML)
ExpectNoError(t, err)
}

View file

@ -24,8 +24,6 @@ var (
ErrNilValue = E.New("nil")
ErrUnsettable = E.New("unsettable")
ErrUnsupportedConversion = E.New("unsupported conversion")
ErrMapMissingColon = E.New("map missing colon")
ErrMapTooManyColons = E.New("map too many colons")
ErrUnknownField = E.New("unknown field")
)
@ -422,19 +420,12 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr E.E
return true, E.From(parser.Parse(src))
}
// yaml like
lines := []string{}
src = strings.TrimSpace(src)
if src != "" {
lines = strutils.SplitLine(src)
for i := range lines {
lines[i] = strings.TrimSpace(lines[i])
}
}
isMultiline := strings.ContainsRune(src, '\n')
var tmp any
switch dst.Kind() {
case reflect.Slice:
// one liner is comma separated list
if len(lines) == 1 {
if !isMultiline {
values := strutils.CommaSeperatedList(src)
dst.Set(reflect.MakeSlice(dst.Type(), len(values), len(values)))
errs := E.NewBuilder("invalid slice values")
@ -449,38 +440,25 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr E.E
}
return
}
lines := strutils.SplitLine(src)
sl := make([]string, 0, len(lines))
for _, line := range lines {
line = strings.TrimLeftFunc(line, func(r rune) bool {
return r == '-' || unicode.IsSpace(r)
})
if line == "" {
if line == "" || line[0] == '#' {
continue
}
sl = append(sl, line)
}
tmp = sl
case reflect.Map:
m := make(map[string]string, len(lines))
errs := E.NewBuilder("invalid map")
for i, line := range lines {
parts := strings.Split(line, ":")
if len(parts) < 2 {
errs.Add(ErrMapMissingColon.Subjectf("line %d", i+1))
continue
}
if len(parts) > 2 {
errs.Add(ErrMapTooManyColons.Subjectf("line %d", i+1))
continue
}
k := strings.TrimSpace(parts[0])
v := strings.TrimSpace(parts[1])
m[k] = v
case reflect.Map, reflect.Struct:
rawMap := make(SerializedObject)
err := yaml.Unmarshal([]byte(src), &rawMap)
if err != nil {
return true, E.From(err)
}
if errs.HasError() {
return true, errs.Error()
}
tmp = m
tmp = rawMap
default:
return false, nil
}

View file

@ -163,3 +163,53 @@ func TestConvertor(t *testing.T) {
ExpectError(t, ErrUnsupportedConversion, Deserialize(map[string]any{"Test": struct{}{}}, m))
})
}
func TestStringToSlice(t *testing.T) {
t.Run("comma_separated", func(t *testing.T) {
dst := make([]string, 0)
convertible, err := ConvertString("a,b,c", reflect.ValueOf(&dst))
ExpectTrue(t, convertible)
ExpectNoError(t, err)
ExpectDeepEqual(t, dst, []string{"a", "b", "c"})
})
t.Run("multiline", func(t *testing.T) {
dst := make([]string, 0)
convertible, err := ConvertString(" a\n b\n c", reflect.ValueOf(&dst))
ExpectTrue(t, convertible)
ExpectNoError(t, err)
ExpectDeepEqual(t, dst, []string{"a", "b", "c"})
})
t.Run("yaml-like", func(t *testing.T) {
dst := make([]string, 0)
convertible, err := ConvertString(" - a\n - b\n - c", reflect.ValueOf(&dst))
ExpectTrue(t, convertible)
ExpectNoError(t, err)
ExpectDeepEqual(t, dst, []string{"a", "b", "c"})
})
}
func TestStringToMap(t *testing.T) {
t.Run("yaml-like", func(t *testing.T) {
dst := make(map[string]string)
convertible, err := ConvertString(" a: b\n c: d", reflect.ValueOf(&dst))
ExpectTrue(t, convertible)
ExpectNoError(t, err)
ExpectDeepEqual(t, dst, map[string]string{"a": "b", "c": "d"})
})
}
func TestStringToStruct(t *testing.T) {
t.Run("yaml-like", func(t *testing.T) {
dst := struct {
A string
B int
}{}
convertible, err := ConvertString(" A: a\n B: 123", reflect.ValueOf(&dst))
ExpectTrue(t, convertible)
ExpectNoError(t, err)
ExpectDeepEqual(t, dst, struct {
A string
B int
}{"a", 123})
})
}