preparing for v0.5

This commit is contained in:
default
2024-08-01 10:06:42 +08:00
parent 24778d1093
commit 93359110a2
115 changed files with 5153 additions and 4395 deletions

94
src/docker/client.go Normal file
View File

@@ -0,0 +1,94 @@
package docker
import (
"net/http"
"sync"
"github.com/docker/cli/cli/connhelper"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/yusing/go-proxy/common"
E "github.com/yusing/go-proxy/error"
)
type Client = *client.Client
// ConnectClient creates a new Docker client connection to the specified host.
//
// Returns existing client if available.
//
// Parameters:
// - host: the host to connect to (either a URL or "FROM_ENV").
//
// Returns:
// - Client: the Docker client connection.
// - error: an error if the connection failed.
func ConnectClient(host string) (Client, E.NestedError) {
clientMapMu.Lock()
defer clientMapMu.Unlock()
// check if client exists
if client, ok := clientMap[host]; ok {
return client, E.Nil()
}
// create client
var opt []client.Opt
switch host {
case common.DockerHostFromEnv:
opt = clientOptEnvHost
default:
helper, err := E.Check(connhelper.GetConnectionHelper(host))
if err.IsNotNil() {
logger.Fatalf("unexpected error: %s", err)
}
if helper != nil {
httpClient := &http.Client{
Transport: &http.Transport{
DialContext: helper.Dialer,
},
}
opt = []client.Opt{
client.WithHTTPClient(httpClient),
client.WithHost(helper.Host),
client.WithAPIVersionNegotiation(),
client.WithDialContext(helper.Dialer),
}
} else {
opt = []client.Opt{
client.WithHost(host),
client.WithAPIVersionNegotiation(),
}
}
}
client, err := E.Check(client.NewClientWithOpts(opt...))
if err.IsNotNil() {
return nil, err
}
clientMap[host] = client
return client, E.Nil()
}
func CloseAllClients() {
clientMapMu.Lock()
defer clientMapMu.Unlock()
for _, client := range clientMap {
client.Close()
}
clientMap = make(map[string]Client)
logger.Debug("closed all clients")
}
var clientMap map[string]Client = make(map[string]Client)
var clientMapMu sync.Mutex
var clientOptEnvHost = []client.Opt{
client.WithHostFromEnv(),
client.WithAPIVersionNegotiation(),
}
var logger = logrus.WithField("?", "docker")

48
src/docker/client_info.go Normal file
View File

@@ -0,0 +1,48 @@
package docker
import (
"context"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
E "github.com/yusing/go-proxy/error"
)
type ClientInfo struct {
Host string
Containers []types.Container
}
func GetClientInfo(clientHost string) (*ClientInfo, E.NestedError) {
dockerClient, err := ConnectClient(clientHost)
if err.IsNotNil() {
return nil, E.Failure("create docker client").With(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
containers, err := E.Check(dockerClient.ContainerList(ctx, container.ListOptions{All: true}))
if err.IsNotNil() {
return nil, E.Failure("list containers").With(err)
}
// extract host from docker client url
// since the services being proxied to
// should have the same IP as the docker client
url, err := E.Check(client.ParseHostURL(dockerClient.DaemonHost()))
if err.IsNotNil() {
return nil, E.Invalid("host url", dockerClient.DaemonHost()).With(err)
}
if url.Scheme == "unix" {
return &ClientInfo{Host: "localhost", Containers: containers}, E.Nil()
}
return &ClientInfo{Host: url.Hostname(), Containers: containers}, E.Nil()
}
func IsErrConnectionFailed(err error) bool {
return client.IsErrConnectionFailed(err)
}

View File

@@ -0,0 +1,32 @@
package docker
type (
HomePageConfig struct{ m map[string]HomePageCategory }
HomePageCategory []HomePageItem
HomePageItem struct {
Name string
Icon string
Category string
Description string
WidgetConfig map[string]interface{}
}
)
func NewHomePageConfig() *HomePageConfig {
return &HomePageConfig{m: make(map[string]HomePageCategory)}
}
func NewHomePageItem() *HomePageItem {
return &HomePageItem{}
}
func (c *HomePageConfig) Clear() {
c.m = make(map[string]HomePageCategory)
}
func (c *HomePageConfig) Add(item HomePageItem) {
c.m[item.Category] = HomePageCategory{item}
}
const NSHomePage = "homepage"

81
src/docker/label.go Normal file
View File

@@ -0,0 +1,81 @@
package docker
import (
"errors"
"strings"
E "github.com/yusing/go-proxy/error"
U "github.com/yusing/go-proxy/utils"
)
type Label struct {
Namespace string
Target string
Attribute string
Value any
}
// Apply applies the value of a Label to the corresponding field in the given object.
//
// Parameters:
// - obj: a pointer to the object to which the Label value will be applied.
// - l: a pointer to the Label containing the attribute and value to be applied.
//
// Returns:
// - error: an error if the field does not exist.
func ApplyLabel[T any](obj *T, l *Label) E.NestedError {
return U.SetFieldFromSnake(obj, l.Attribute, l.Value)
}
type ValueParser func(string) (any, E.NestedError)
type ValueParserMap map[string]ValueParser
func ParseLabel(label string, value string) (*Label, E.NestedError) {
parts := strings.Split(label, ".")
if len(parts) < 2 {
return &Label{
Namespace: label,
Value: value,
}, E.Nil()
}
l := &Label{
Namespace: parts[0],
Target: parts[1],
Value: value,
}
if len(parts) == 3 {
l.Attribute = parts[2]
} else {
l.Attribute = l.Target
}
// find if namespace has value parser
pm, ok := labelValueParserMap[l.Namespace]
if !ok {
return l, E.Nil()
}
// find if attribute has value parser
p, ok := pm[l.Attribute]
if !ok {
return l, E.Nil()
}
// try to parse value
v, err := p(value)
if err.IsNotNil() {
return nil, err
}
l.Value = v
return l, E.Nil()
}
func RegisterNamespace(namespace string, pm ValueParserMap) {
labelValueParserMap[namespace] = pm
}
// namespace:target.attribute -> func(string) (any, error)
var labelValueParserMap = make(map[string]ValueParserMap)
var ErrInvalidLabel = errors.New("invalid label")

55
src/docker/proxy_label.go Normal file
View File

@@ -0,0 +1,55 @@
package docker
import (
"net/http"
"strings"
E "github.com/yusing/go-proxy/error"
)
func setHeadersParser(value string) (any, E.NestedError) {
value = strings.TrimSpace(value)
lines := strings.Split(value, "\n")
h := make(http.Header)
for _, line := range lines {
parts := strings.SplitN(line, ":", 2)
if len(parts) != 2 {
return nil, E.Invalid("set header statement", line)
}
key := strings.TrimSpace(parts[0])
val := strings.TrimSpace(parts[1])
h.Add(key, val)
}
return h, E.Nil()
}
func commaSepParser(value string) (any, E.NestedError) {
v := strings.Split(value, ",")
for i := range v {
v[i] = strings.TrimSpace(v[i])
}
return v, E.Nil()
}
func boolParser(value string) (any, E.NestedError) {
switch strings.ToLower(value) {
case "true", "yes", "1":
return true, E.Nil()
case "false", "no", "0":
return false, E.Nil()
default:
return nil, E.Invalid("boolean value", value)
}
}
const NSProxy = "proxy"
var _ = func() int {
RegisterNamespace(NSProxy, ValueParserMap{
"aliases": commaSepParser,
"set_headers": setHeadersParser,
"hide_headers": commaSepParser,
"no_tls_verify": boolParser,
})
return 0
}()

View File

@@ -0,0 +1,207 @@
package docker
import (
"errors"
"fmt"
"net/http"
"reflect"
"testing"
E "github.com/yusing/go-proxy/error"
)
func makeLabel(namespace string, alias string, field string) string {
return fmt.Sprintf("%s.%s.%s", namespace, alias, field)
}
func TestInvalidLabel(t *testing.T) {
pl, err := ParseLabel("foo.bar", "1234")
if !errors.Is(err, ErrInvalidLabel) {
t.Errorf("expected errInvalidLabel, got %s", err)
}
if pl != nil {
t.Errorf("expected nil, got %v", pl)
}
_, err = ParseLabel("proxy.foo", "bar")
if !errors.Is(err, ErrInvalidLabel) {
t.Errorf("expected errInvalidLabel, got %s", err)
}
}
func TestHomePageLabel(t *testing.T) {
alias := "foo"
field := "ip"
v := "bar"
pl, err := ParseLabel(makeLabel(NSHomePage, alias, field), v)
if err.IsNotNil() {
t.Errorf("expected err=nil, got %s", err)
}
if pl.Target != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Target)
}
if pl.Attribute != field {
t.Errorf("expected field=%s, got %s", field, pl.Target)
}
if pl.Value != v {
t.Errorf("expected value=%q, got %s", v, pl.Value)
}
}
func TestStringProxyLabel(t *testing.T) {
alias := "foo"
field := "ip"
v := "bar"
pl, err := ParseLabel(makeLabel(NSProxy, alias, field), v)
if err.IsNotNil() {
t.Errorf("expected err=nil, got %s", err)
}
if pl.Target != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Target)
}
if pl.Attribute != field {
t.Errorf("expected field=%s, got %s", field, pl.Target)
}
if pl.Value != v {
t.Errorf("expected value=%q, got %s", v, pl.Value)
}
}
func TestBoolProxyLabelValid(t *testing.T) {
alias := "foo"
field := "no_tls_verify"
tests := map[string]bool{
"true": true,
"TRUE": true,
"yes": true,
"1": true,
"false": false,
"FALSE": false,
"no": false,
"0": false,
}
for k, v := range tests {
pl, err := ParseLabel(makeLabel(NSProxy, alias, field), k)
if err.IsNotNil() {
t.Errorf("expected err=nil, got %s", err)
}
if pl.Target != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Target)
}
if pl.Attribute != field {
t.Errorf("expected field=%s, got %s", field, pl.Attribute)
}
if pl.Value != v {
t.Errorf("expected value=%v, got %v", v, pl.Value)
}
}
}
func TestBoolProxyLabelInvalid(t *testing.T) {
alias := "foo"
field := "no_tls_verify"
_, err := ParseLabel(makeLabel(NSProxy, alias, field), "invalid")
if !errors.Is(err, E.ErrInvalid) {
t.Errorf("expected err InvalidProxyLabel, got %s", err)
}
}
func TestHeaderProxyLabelValid(t *testing.T) {
alias := "foo"
field := "set_headers"
v := `
X-Custom-Header1: foo
X-Custom-Header1: bar
X-Custom-Header2: baz
`
h := make(http.Header, 0)
h.Set("X-Custom-Header1", "foo")
h.Add("X-Custom-Header1", "bar")
h.Set("X-Custom-Header2", "baz")
pl, err := ParseLabel(makeLabel(NSProxy, alias, field), v)
if err.IsNotNil() {
t.Errorf("expected err=nil, got %s", err)
}
if pl.Target != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Target)
}
if pl.Attribute != field {
t.Errorf("expected field=%s, got %s", field, pl.Attribute)
}
hGot, ok := pl.Value.(http.Header)
if !ok {
t.Error("value is not http.Header")
return
}
for k, vWant := range h {
vGot := hGot[k]
if !reflect.DeepEqual(vGot, vWant) {
t.Errorf("expected %s=%q, got %q", k, vWant, vGot)
}
}
}
func TestHeaderProxyLabelInvalid(t *testing.T) {
alias := "foo"
field := "set_headers"
tests := []string{
"X-Custom-Header1 = bar",
"X-Custom-Header1",
}
for _, v := range tests {
_, err := ParseLabel(makeLabel(NSProxy, alias, field), v)
if !errors.Is(err, E.ErrInvalid) {
t.Errorf("expected err InvalidProxyLabel for %q, got %v", v, err)
}
}
}
func TestCommaSepProxyLabelSingle(t *testing.T) {
alias := "foo"
field := "hide_headers"
v := "X-Custom-Header1"
pl, err := ParseLabel(makeLabel(NSProxy, alias, field), v)
if err.IsNotNil() {
t.Errorf("expected err=nil, got %s", err)
}
if pl.Target != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Target)
}
if pl.Attribute != field {
t.Errorf("expected field=%s, got %s", field, pl.Attribute)
}
sGot, ok := pl.Value.([]string)
sWant := []string{"X-Custom-Header1"}
if !ok {
t.Error("value is not []string")
}
if !reflect.DeepEqual(sGot, sWant) {
t.Errorf("expected %q, got %q", sWant, sGot)
}
}
func TestCommaSepProxyLabelMulti(t *testing.T) {
alias := "foo"
field := "hide_headers"
v := "X-Custom-Header1, X-Custom-Header2,X-Custom-Header3"
pl, err := ParseLabel(makeLabel(NSProxy, alias, field), v)
if err.IsNotNil() {
t.Errorf("expected err=nil, got %s", err)
}
if pl.Target != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Target)
}
if pl.Attribute != field {
t.Errorf("expected field=%s, got %s", field, pl.Attribute)
}
sGot, ok := pl.Value.([]string)
sWant := []string{"X-Custom-Header1", "X-Custom-Header2", "X-Custom-Header3"}
if !ok {
t.Error("value is not []string")
}
if !reflect.DeepEqual(sGot, sWant) {
t.Errorf("expected %q, got %q", sWant, sGot)
}
}