mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-20 04:42:33 +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
|
||||
|
||||
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:]
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
|
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
|
||||
var yaml []byte
|
||||
var testAllFieldsYAML []byte
|
||||
|
||||
func TestFile(t *testing.T) {
|
||||
_, err := validate(yaml)
|
||||
_, err := validate(testAllFieldsYAML)
|
||||
ExpectNoError(t, err)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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})
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue