mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-20 12:42:34 +02:00
support inline yaml for docker labels, serveral minor fixes
This commit is contained in:
parent
6e30d39b78
commit
b38bff41d8
7 changed files with 248 additions and 42 deletions
|
@ -7,6 +7,8 @@ import (
|
||||||
|
|
||||||
type LabelMap = map[string]any
|
type LabelMap = map[string]any
|
||||||
|
|
||||||
|
var ErrInvalidLabel = E.New("invalid label")
|
||||||
|
|
||||||
func ParseLabels(labels map[string]string) (LabelMap, E.Error) {
|
func ParseLabels(labels map[string]string) (LabelMap, E.Error) {
|
||||||
nestedMap := make(LabelMap)
|
nestedMap := make(LabelMap)
|
||||||
errs := E.NewBuilder("labels error")
|
errs := E.NewBuilder("labels error")
|
||||||
|
@ -17,7 +19,7 @@ func ParseLabels(labels map[string]string) (LabelMap, E.Error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(parts) == 1 {
|
if len(parts) == 1 {
|
||||||
errs.Add(E.Errorf("invalid label %s", lbl).Subject(lbl))
|
errs.Add(ErrInvalidLabel.Subject(lbl))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
parts = parts[1:]
|
parts = parts[1:]
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package provider
|
package provider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -13,6 +14,7 @@ import (
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
U "github.com/yusing/go-proxy/internal/utils"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
"github.com/yusing/go-proxy/internal/watcher"
|
"github.com/yusing/go-proxy/internal/watcher"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DockerProvider struct {
|
type DockerProvider struct {
|
||||||
|
@ -125,12 +127,20 @@ func (p *DockerProvider) entriesFromContainerLabels(container *docker.Container)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var ok bool
|
|
||||||
entryMap, ok := entryMapAny.(docker.LabelMap)
|
entryMap, ok := entryMapAny.(docker.LabelMap)
|
||||||
if !ok {
|
if !ok {
|
||||||
errs.Add(E.Errorf("expect mapping, got %T", entryMap).Subject(alias))
|
// 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
|
continue
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if alias == docker.WildcardAlias {
|
if alias == docker.WildcardAlias {
|
||||||
wildcardProps = entryMap
|
wildcardProps = entryMap
|
||||||
|
@ -153,8 +163,8 @@ func (p *DockerProvider) entriesFromContainerLabels(container *docker.Container)
|
||||||
}
|
}
|
||||||
|
|
||||||
// init entry if not exist
|
// init entry if not exist
|
||||||
var en *route.RawEntry
|
en, ok := entries.Load(alias)
|
||||||
if en, ok = entries.Load(alias); !ok {
|
if !ok {
|
||||||
en = &route.RawEntry{
|
en = &route.RawEntry{
|
||||||
Alias: alias,
|
Alias: alias,
|
||||||
Container: container,
|
Container: container,
|
||||||
|
@ -171,10 +181,12 @@ func (p *DockerProvider) entriesFromContainerLabels(container *docker.Container)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if wildcardProps != nil {
|
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 {
|
if err := U.Deserialize(wildcardProps, re); err != nil {
|
||||||
errs.Add(err.Subject(alias))
|
errs.Add(err.Subject(docker.WildcardAlias))
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
128
internal/route/provider/docker_labels.yaml
Normal file
128
internal/route/provider/docker_labels.yaml
Normal 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
|
36
internal/route/provider/docker_labels_test.go
Normal file
36
internal/route/provider/docker_labels_test.go
Normal 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"))
|
||||||
|
}
|
|
@ -9,9 +9,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed all_fields.yaml
|
//go:embed all_fields.yaml
|
||||||
var yaml []byte
|
var testAllFieldsYAML []byte
|
||||||
|
|
||||||
func TestFile(t *testing.T) {
|
func TestFile(t *testing.T) {
|
||||||
_, err := validate(yaml)
|
_, err := validate(testAllFieldsYAML)
|
||||||
ExpectNoError(t, err)
|
ExpectNoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,8 +24,6 @@ var (
|
||||||
ErrNilValue = E.New("nil")
|
ErrNilValue = E.New("nil")
|
||||||
ErrUnsettable = E.New("unsettable")
|
ErrUnsettable = E.New("unsettable")
|
||||||
ErrUnsupportedConversion = E.New("unsupported conversion")
|
ErrUnsupportedConversion = E.New("unsupported conversion")
|
||||||
ErrMapMissingColon = E.New("map missing colon")
|
|
||||||
ErrMapTooManyColons = E.New("map too many colons")
|
|
||||||
ErrUnknownField = E.New("unknown field")
|
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))
|
return true, E.From(parser.Parse(src))
|
||||||
}
|
}
|
||||||
// yaml like
|
// yaml like
|
||||||
lines := []string{}
|
isMultiline := strings.ContainsRune(src, '\n')
|
||||||
src = strings.TrimSpace(src)
|
|
||||||
if src != "" {
|
|
||||||
lines = strutils.SplitLine(src)
|
|
||||||
for i := range lines {
|
|
||||||
lines[i] = strings.TrimSpace(lines[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var tmp any
|
var tmp any
|
||||||
switch dst.Kind() {
|
switch dst.Kind() {
|
||||||
case reflect.Slice:
|
case reflect.Slice:
|
||||||
// one liner is comma separated list
|
// one liner is comma separated list
|
||||||
if len(lines) == 1 {
|
if !isMultiline {
|
||||||
values := strutils.CommaSeperatedList(src)
|
values := strutils.CommaSeperatedList(src)
|
||||||
dst.Set(reflect.MakeSlice(dst.Type(), len(values), len(values)))
|
dst.Set(reflect.MakeSlice(dst.Type(), len(values), len(values)))
|
||||||
errs := E.NewBuilder("invalid slice values")
|
errs := E.NewBuilder("invalid slice values")
|
||||||
|
@ -449,38 +440,25 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr E.E
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
lines := strutils.SplitLine(src)
|
||||||
sl := make([]string, 0, len(lines))
|
sl := make([]string, 0, len(lines))
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
line = strings.TrimLeftFunc(line, func(r rune) bool {
|
line = strings.TrimLeftFunc(line, func(r rune) bool {
|
||||||
return r == '-' || unicode.IsSpace(r)
|
return r == '-' || unicode.IsSpace(r)
|
||||||
})
|
})
|
||||||
if line == "" {
|
if line == "" || line[0] == '#' {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sl = append(sl, line)
|
sl = append(sl, line)
|
||||||
}
|
}
|
||||||
tmp = sl
|
tmp = sl
|
||||||
case reflect.Map:
|
case reflect.Map, reflect.Struct:
|
||||||
m := make(map[string]string, len(lines))
|
rawMap := make(SerializedObject)
|
||||||
errs := E.NewBuilder("invalid map")
|
err := yaml.Unmarshal([]byte(src), &rawMap)
|
||||||
for i, line := range lines {
|
if err != nil {
|
||||||
parts := strings.Split(line, ":")
|
return true, E.From(err)
|
||||||
if len(parts) < 2 {
|
|
||||||
errs.Add(ErrMapMissingColon.Subjectf("line %d", i+1))
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if len(parts) > 2 {
|
tmp = rawMap
|
||||||
errs.Add(ErrMapTooManyColons.Subjectf("line %d", i+1))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
k := strings.TrimSpace(parts[0])
|
|
||||||
v := strings.TrimSpace(parts[1])
|
|
||||||
m[k] = v
|
|
||||||
}
|
|
||||||
if errs.HasError() {
|
|
||||||
return true, errs.Error()
|
|
||||||
}
|
|
||||||
tmp = m
|
|
||||||
default:
|
default:
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -163,3 +163,53 @@ func TestConvertor(t *testing.T) {
|
||||||
ExpectError(t, ErrUnsupportedConversion, Deserialize(map[string]any{"Test": struct{}{}}, m))
|
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})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue