diff --git a/cmd/dgate-cli/commands/collection.go b/cmd/dgate-cli/commands/collection_commands.go similarity index 90% rename from cmd/dgate-cli/commands/collection.go rename to cmd/dgate-cli/commands/collection_commands.go index c4f237d..37e190b 100644 --- a/cmd/dgate-cli/commands/collection.go +++ b/cmd/dgate-cli/commands/collection_commands.go @@ -57,7 +57,13 @@ func CollectionCommand(client *dgclient.DGateClient) *cli.Command { Aliases: []string{"ls"}, Usage: "list collections", Action: func(ctx *cli.Context) error { - col, err := client.ListCollection() + nsp, err := createMapFromArgs[dgclient.NamespacePayload]( + ctx.Args().Slice(), + ) + if err != nil { + return err + } + col, err := client.ListCollection(nsp.Namespace) if err != nil { return err } diff --git a/cmd/dgate-cli/commands/document.go b/cmd/dgate-cli/commands/document_commands.go similarity index 62% rename from cmd/dgate-cli/commands/document.go rename to cmd/dgate-cli/commands/document_commands.go index c66c6a7..80e15af 100644 --- a/cmd/dgate-cli/commands/document.go +++ b/cmd/dgate-cli/commands/document_commands.go @@ -37,18 +37,37 @@ func DocumentCommand(client *dgclient.DGateClient) *cli.Command { Name: "delete", Aliases: []string{"rm"}, Usage: "delete a document", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "all", + Usage: "delete all documents", + }, + }, Action: func(ctx *cli.Context) error { - doc, err := createMapFromArgs[spec.Document]( - ctx.Args().Slice(), "id", - ) - if err != nil { - return err - } - err = client.DeleteDocument( - doc.ID, doc.NamespaceName, - ) - if err != nil { - return err + if ctx.Bool("all") { + doc, err := createMapFromArgs[spec.Document]( + ctx.Args().Slice()) + if err != nil { + return err + } + err = client.DeleteAllDocument( + doc.NamespaceName, doc.CollectionName) + if err != nil { + return err + } + return nil + } else { + doc, err := createMapFromArgs[spec.Document]( + ctx.Args().Slice(), "id", + ) + if err != nil { + return err + } + err = client.DeleteDocument(doc.ID, + doc.NamespaceName, doc.CollectionName) + if err != nil { + return err + } } return nil }, @@ -58,7 +77,14 @@ func DocumentCommand(client *dgclient.DGateClient) *cli.Command { Aliases: []string{"ls"}, Usage: "list documents", Action: func(ctx *cli.Context) error { - doc, err := client.ListDocument() + d, err := createMapFromArgs[spec.Document]( + ctx.Args().Slice(), "collection", + ) + if err != nil { + return err + } + doc, err := client.ListDocument( + d.NamespaceName, d.CollectionName) if err != nil { return err } @@ -75,9 +101,8 @@ func DocumentCommand(client *dgclient.DGateClient) *cli.Command { if err != nil { return err } - doc, err = client.GetDocument( - doc.ID, doc.NamespaceName, - ) + doc, err = client.GetDocument(doc.ID, + doc.NamespaceName, doc.CollectionName) if err != nil { return err } diff --git a/cmd/dgate-cli/commands/domain.go b/cmd/dgate-cli/commands/domain_commands.go similarity index 90% rename from cmd/dgate-cli/commands/domain.go rename to cmd/dgate-cli/commands/domain_commands.go index 825fcfd..eeaa5ef 100644 --- a/cmd/dgate-cli/commands/domain.go +++ b/cmd/dgate-cli/commands/domain_commands.go @@ -57,7 +57,13 @@ func DomainCommand(client *dgclient.DGateClient) *cli.Command { Aliases: []string{"ls"}, Usage: "list domains", Action: func(ctx *cli.Context) error { - dom, err := client.ListDomain() + nsp, err := createMapFromArgs[dgclient.NamespacePayload]( + ctx.Args().Slice(), + ) + if err != nil { + return err + } + dom, err := client.ListDomain(nsp.Namespace) if err != nil { return err } diff --git a/cmd/dgate-cli/commands/module.go b/cmd/dgate-cli/commands/module_commands.go similarity index 90% rename from cmd/dgate-cli/commands/module.go rename to cmd/dgate-cli/commands/module_commands.go index fdd0859..9ac6f18 100644 --- a/cmd/dgate-cli/commands/module.go +++ b/cmd/dgate-cli/commands/module_commands.go @@ -57,7 +57,13 @@ func ModuleCommand(client *dgclient.DGateClient) *cli.Command { Aliases: []string{"ls"}, Usage: "list modules", Action: func(ctx *cli.Context) error { - mod, err := client.ListModule() + nsp, err := createMapFromArgs[dgclient.NamespacePayload]( + ctx.Args().Slice(), + ) + if err != nil { + return err + } + mod, err := client.ListModule(nsp.Namespace) if err != nil { return err } diff --git a/cmd/dgate-cli/commands/namespace.go b/cmd/dgate-cli/commands/namespace_commands.go similarity index 100% rename from cmd/dgate-cli/commands/namespace.go rename to cmd/dgate-cli/commands/namespace_commands.go diff --git a/cmd/dgate-cli/commands/route.go b/cmd/dgate-cli/commands/route_commands.go similarity index 90% rename from cmd/dgate-cli/commands/route.go rename to cmd/dgate-cli/commands/route_commands.go index 272062f..8cdb10b 100644 --- a/cmd/dgate-cli/commands/route.go +++ b/cmd/dgate-cli/commands/route_commands.go @@ -58,7 +58,13 @@ func RouteCommand(client *dgclient.DGateClient) *cli.Command { Aliases: []string{"ls"}, Usage: "list routes", Action: func(ctx *cli.Context) error { - rt, err := client.ListRoute() + nsp, err := createMapFromArgs[dgclient.NamespacePayload]( + ctx.Args().Slice(), + ) + if err != nil { + return err + } + rt, err := client.ListRoute(nsp.Namespace) if err != nil { return err } diff --git a/cmd/dgate-cli/commands/secret_commands.go b/cmd/dgate-cli/commands/secret_commands.go new file mode 100644 index 0000000..02afd3e --- /dev/null +++ b/cmd/dgate-cli/commands/secret_commands.go @@ -0,0 +1,95 @@ +package commands + +import ( + "github.com/dgate-io/dgate/pkg/dgclient" + "github.com/dgate-io/dgate/pkg/spec" + "github.com/urfave/cli/v2" +) + +func SecretCommand(client *dgclient.DGateClient) *cli.Command { + return &cli.Command{ + Name: "secret", + Aliases: []string{"sec"}, + Args: true, + ArgsUsage: " ", + Usage: "secret commands", + Subcommands: []*cli.Command{ + { + Name: "create", + Aliases: []string{"mk"}, + Usage: "create a secret", + Action: func(ctx *cli.Context) error { + sec, err := createMapFromArgs[spec.Secret]( + ctx.Args().Slice(), "name", "data", + ) + if err != nil { + return err + } + err = client.CreateSecret(sec) + if err != nil { + return err + } + // redact the data field + sec.Data = "**redacted**" + return jsonPrettyPrint(sec) + }, + }, + { + Name: "delete", + Aliases: []string{"rm"}, + Usage: "delete a secret", + Action: func(ctx *cli.Context) error { + sec, err := createMapFromArgs[spec.Secret]( + ctx.Args().Slice(), "name", + ) + if err != nil { + return err + } + err = client.DeleteSecret( + sec.Name, sec.NamespaceName) + if err != nil { + return err + } + return nil + }, + }, + { + Name: "list", + Aliases: []string{"ls"}, + Usage: "list services", + Action: func(ctx *cli.Context) error { + nsp, err := createMapFromArgs[dgclient.NamespacePayload]( + ctx.Args().Slice(), + ) + if err != nil { + return err + } + sec, err := client.ListSecret(nsp.Namespace) + if err != nil { + return err + } + return jsonPrettyPrint(sec) + }, + }, + { + Name: "get", + Usage: "get a secret", + Action: func(ctx *cli.Context) error { + s, err := createMapFromArgs[spec.Secret]( + ctx.Args().Slice(), "name", + ) + if err != nil { + return err + } + sec, err := client.GetSecret( + s.Name, s.NamespaceName, + ) + if err != nil { + return err + } + return jsonPrettyPrint(sec) + }, + }, + }, + } +} diff --git a/cmd/dgate-cli/commands/service.go b/cmd/dgate-cli/commands/service_commands.go similarity index 90% rename from cmd/dgate-cli/commands/service.go rename to cmd/dgate-cli/commands/service_commands.go index 1c4ac3f..84bdda8 100644 --- a/cmd/dgate-cli/commands/service.go +++ b/cmd/dgate-cli/commands/service_commands.go @@ -57,7 +57,13 @@ func ServiceCommand(client *dgclient.DGateClient) *cli.Command { Aliases: []string{"ls"}, Usage: "list services", Action: func(ctx *cli.Context) error { - svc, err := client.ListService() + nsp, err := createMapFromArgs[dgclient.NamespacePayload]( + ctx.Args().Slice(), + ) + if err != nil { + return err + } + svc, err := client.ListService(nsp.Namespace) if err != nil { return err } diff --git a/cmd/dgate-cli/main.go b/cmd/dgate-cli/main.go index 1ed1632..dd5456f 100644 --- a/cmd/dgate-cli/main.go +++ b/cmd/dgate-cli/main.go @@ -43,6 +43,19 @@ func main() { EnvVars: []string{"DGATE_ADMIN_AUTH"}, Usage: "basic auth username:password; or just username for password prompt", }, + &cli.BoolFlag{ + Name: "follow", + DefaultText: "false", + Aliases: []string{"f"}, + EnvVars: []string{"DGATE_FOLLOW_REDIRECTS"}, + Usage: "follows redirects, useful for raft leader changes", + }, + &cli.BoolFlag{ + Name: "verbose", + DefaultText: "false", + Aliases: []string{"V"}, + Usage: "enable verbose logging", + }, }, Before: func(ctx *cli.Context) (err error) { var authOption dgclient.Options = func(dc *dgclient.DGateClient) {} @@ -70,11 +83,17 @@ func main() { return client.Init( ctx.String("admin"), authOption, + dgclient.WithFollowRedirect( + ctx.Bool("follow"), + ), dgclient.WithUserAgent( "DGate CLI "+version+ ";os="+runtime.GOOS+ ";arch="+runtime.GOARCH, ), + dgclient.WithVerboseLogging( + ctx.Bool("verbose"), + ), ) }, Action: func(ctx *cli.Context) error { @@ -88,6 +107,7 @@ func main() { commands.DomainCommand(client), commands.CollectionCommand(client), commands.DocumentCommand(client), + commands.SecretCommand(client), }, } diff --git a/cmd/dgate-server/main.go b/cmd/dgate-server/main.go index b0a5b8f..34cbaef 100644 --- a/cmd/dgate-server/main.go +++ b/cmd/dgate-server/main.go @@ -3,6 +3,9 @@ package main import ( "fmt" "os" + "os/signal" + "runtime" + "syscall" "runtime/debug" @@ -34,7 +37,8 @@ func main() { } if version == "dev" { - version = fmt.Sprintf("dev/PID:%d", os.Getpid()) + fmt.Printf("PID:%d\n", os.Getpid()) + fmt.Printf("GOMAXPROCS:%d\n", runtime.GOMAXPROCS(0)) } if *showVersion { @@ -52,16 +56,26 @@ func main() { "-----------------------------------\n", ) } - dgateConfig, err := config.LoadConfig(*configPath) - if err != nil { - panic(err) - } + if dgateConfig, err := config.LoadConfig(*configPath); err != nil { + fmt.Printf("Error loading config: %s\n", err) + os.Exit(1) + } else { + proxyState := proxy.StartProxyGateway(version, dgateConfig) + admin.StartAdminAPI(dgateConfig, proxyState) + if err := proxyState.Start(); err != nil { + fmt.Printf("Error loading config: %s\n", err) + os.Exit(1) + } - proxyState, err := proxy.StartProxyGateway(dgateConfig) - if err != nil { - panic(err) + sigchan := make(chan os.Signal, 1) + signal.Notify(sigchan, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, + ) + <-sigchan + proxyState.Stop() + os.Exit(1) } - - admin.StartAdminAPI(dgateConfig, proxyState) } } diff --git a/config.dgate.yaml b/config.dgate.yaml index daffd24..fa97514 100644 --- a/config.dgate.yaml +++ b/config.dgate.yaml @@ -37,24 +37,8 @@ proxy: tls: port: ${PORT_SSL:-443} auto_generate: true - cert_file: .lego/certificates/ufosoup.com.crt - key_file: .lego/certificates/ufosoup.com.key - client_transport: - dns_server: 8.8.8.8:53 - dns_timeout: 10s - max_conns_per_host: 1000 - max_idle_conns: 5000 - max_idle_conns_per_host: 1000 - idle_conn_timeout: 60s - tls_handshake_timeout: 10s - expect_continue_timeout: 5s - max_response_header_bytes: 4096 - write_buffer_size: 4096 - read_buffer_size: 4096 - max_conns_per_client: 1000 - disable_keep_alives: false - response_header_timeout: 10s - dial_timeout: 10s + cert_file: internal/proxy/testdata/server.crt + key_file: internal/proxy/testdata/server.key admin: port: 9080 host: 0.0.0.0 diff --git a/functional-tests/raft_tests/raft_test.sh b/functional-tests/raft_tests/raft_test.sh new file mode 100755 index 0000000..19f734c --- /dev/null +++ b/functional-tests/raft_tests/raft_test.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +set -eo xtrace + +ADMIN_URL1=${ADMIN_URL1:-"http://localhost:9081"} +PROXY_URL1=${PROXY_URL1:-"http://localhost:81"} + +ADMIN_URL2=${ADMIN_URL2:-"http://localhost:9082"} +PROXY_URL2=${PROXY_URL2:-"http://localhost:82"} + +ADMIN_URL3=${ADMIN_URL3:-"http://localhost:9083"} +PROXY_URL3=${PROXY_URL3:-"http://localhost:83"} + +ADMIN_URL4=${ADMIN_URL4:-"http://localhost:9084"} +PROXY_URL4=${PROXY_URL4:-"http://localhost:84"} + +ADMIN_URL5=${ADMIN_URL5:-"http://localhost:9085"} +PROXY_URL5=${PROXY_URL5:-"http://localhost:85"} + + +DIR="$( cd "$( dirname "$0" )" && pwd )" + +# domain setup + +export DGATE_ADMIN_API=$ADMIN_URL1 + + +id=$(uuid) + +dgate-cli -f namespace create name=ns-$id + +dgate-cli -f domain create name=dm-$id \ + namespace=ns-$id priority:=$RANDOM patterns="$id.example.com" + +dgate-cli -f service create \ + name=svc-$id namespace=ns-$id \ + urls="http://localhost:8888/$RANDOM" + +dgate-cli -f route create \ + name=rt-$id \ + service=svc-$id \ + namespace=ns-$id \ + paths="/$id/{id}" \ + methods:='["GET"]' \ + preserveHost:=false \ + stripPath:=true + +for i in {1..5}; do + curl -f $PROXY_URL1/$id/$i -H Host:$id.example.com + curl -f $PROXY_URL2/$id/$i -H Host:$id.example.com + curl -f $PROXY_URL3/$id/$i -H Host:$id.example.com + curl -f $PROXY_URL4/$id/$i -H Host:$id.example.com + curl -f $PROXY_URL5/$id/$i -H Host:$id.example.com +done + +dgate-cli -V --admin $ADMIN_URL1 route get name=rt-$id namespace=ns-$id +dgate-cli -V --admin $ADMIN_URL2 route get name=rt-$id namespace=ns-$id +dgate-cli -V --admin $ADMIN_URL3 route get name=rt-$id namespace=ns-$id +dgate-cli -V --admin $ADMIN_URL4 route get name=rt-$id namespace=ns-$id +dgate-cli -V --admin $ADMIN_URL5 route get name=rt-$id namespace=ns-$id + + +if dgate-cli --admin $ADMIN_URL4 namespace create name=0; then + echo "Expected error when creating namespace" + exit 1 +fi + +export DGATE_ADMIN_API=$ADMIN_URL5 + +if dgate-cli --admin $ADMIN_URL5 namespace create name=0; then + echo "Expected error when creating namespace" + exit 1 +fi + +echo "Raft Test Succeeded" diff --git a/functional-tests/raft_tests/test1.yaml b/functional-tests/raft_tests/test1.yaml index e77e83d..be95b88 100644 --- a/functional-tests/raft_tests/test1.yaml +++ b/functional-tests/raft_tests/test1.yaml @@ -1,8 +1,7 @@ version: v1 debug: true -log_level: trace +log_level: info -# read_only: false tags: - "dev" - "internal" @@ -17,9 +16,8 @@ stats?: enable_cache_stats: true enable_stream_stats: true enable_cluster_stats: true -# read_only must be true test_server: - port: 8080 + port: 8081 host: 0.0.0.0 proxy: port: 81 diff --git a/functional-tests/raft_tests/test2.yaml b/functional-tests/raft_tests/test2.yaml index aa54d36..ca654e8 100644 --- a/functional-tests/raft_tests/test2.yaml +++ b/functional-tests/raft_tests/test2.yaml @@ -1,6 +1,6 @@ version: v1 debug: true -log_level: trace +log_level: info tags: - "dev" - "internal" diff --git a/functional-tests/raft_tests/test3.yaml b/functional-tests/raft_tests/test3.yaml index 6237fb9..4c847af 100644 --- a/functional-tests/raft_tests/test3.yaml +++ b/functional-tests/raft_tests/test3.yaml @@ -1,6 +1,6 @@ version: v1 debug: true -log_level: trace +log_level: info # read_only: false tags: - "dev" diff --git a/functional-tests/raft_tests/test4_watch.yaml b/functional-tests/raft_tests/test4_watch.yaml index 232e4ae..209a3e3 100644 --- a/functional-tests/raft_tests/test4_watch.yaml +++ b/functional-tests/raft_tests/test4_watch.yaml @@ -47,11 +47,10 @@ proxy: admin: port: 9084 host: 0.0.0.0 - # read_only: true + watch_only: true replication: bootstrap_cluster: false id: "test4" - watch_only: true advert_address: "localhost:9084" cluster_address: - "localhost:9081" diff --git a/functional-tests/raft_tests/test5_watch.yaml b/functional-tests/raft_tests/test5_watch.yaml index 753fc41..74ab421 100644 --- a/functional-tests/raft_tests/test5_watch.yaml +++ b/functional-tests/raft_tests/test5_watch.yaml @@ -1,7 +1,7 @@ version: v1 debug: true log_level: info -# read_only: false + tags: - "dev" - "internal" @@ -16,7 +16,6 @@ stats?: enable_cache_stats: true enable_stream_stats: true enable_cluster_stats: true -# read_only must be true proxy: port: 85 host: 0.0.0.0 @@ -48,11 +47,10 @@ proxy: admin: port: 9085 host: 0.0.0.0 - # read_only: true + watch_only: true replication: bootstrap_cluster: false id: "test5" - watch_only: true advert_address: "localhost:9085" cluster_address: - "localhost:9081" diff --git a/go.mod b/go.mod index c56b14d..3b2c600 100644 --- a/go.mod +++ b/go.mod @@ -20,13 +20,17 @@ require ( github.com/knadh/koanf/providers/rawbytes v0.1.0 github.com/knadh/koanf/v2 v2.0.1 github.com/mitchellh/mapstructure v1.5.0 - github.com/montanaflynn/stats v0.7.1 + github.com/prometheus/client_golang v1.19.0 github.com/rs/zerolog v1.31.0 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/spf13/pflag v1.0.5 github.com/stoewer/go-strcase v1.3.0 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 github.com/urfave/cli/v2 v2.27.1 + go.opentelemetry.io/otel v1.26.0 + go.opentelemetry.io/otel/exporters/prometheus v0.48.0 + go.opentelemetry.io/otel/metric v1.26.0 + go.opentelemetry.io/otel/sdk/metric v1.26.0 golang.org/x/net v0.21.0 golang.org/x/sync v0.6.0 golang.org/x/term v0.19.0 @@ -34,6 +38,7 @@ require ( require ( github.com/armon/go-metrics v0.4.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -43,6 +48,8 @@ require ( github.com/dustin/go-humanize v1.0.0 // indirect github.com/fatih/color v1.14.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.1.2 // indirect @@ -56,7 +63,6 @@ require ( github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/klauspost/compress v1.17.0 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect - github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect @@ -64,13 +70,18 @@ require ( github.com/pelletier/go-toml v1.9.5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/stretchr/objx v0.5.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/otel/sdk v1.26.0 // indirect + go.opentelemetry.io/otel/trace v1.26.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a977d83..8ba5aae 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,7 @@ github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJ github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -69,6 +70,11 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -107,8 +113,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 h1:pUa4ghanp6q4IJHwE9RwLgmVFfReJN+KbQ8ExNEUUoQ= @@ -187,14 +193,11 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= -github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -205,17 +208,24 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c h1:fPpdjePK1atuOg28PXfNSqgwf9I/qD1Hlo39JFwKBXk= github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= @@ -234,8 +244,9 @@ github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8w github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -243,8 +254,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= @@ -255,6 +266,18 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= +go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= +go.opentelemetry.io/otel/exporters/prometheus v0.48.0 h1:sBQe3VNGUjY9IKWQC6z2lNqa5iGbDSxhs60ABwK4y0s= +go.opentelemetry.io/otel/exporters/prometheus v0.48.0/go.mod h1:DtrbMzoZWwQHyrQmCfLam5DZbnmorsGbOtTbYHycU5o= +go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= +go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= +go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8= +go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs= +go.opentelemetry.io/otel/sdk/metric v1.26.0 h1:cWSks5tfriHPdWFnl+qpX3P681aAYqlZHcAyHw5aU9Y= +go.opentelemetry.io/otel/sdk/metric v1.26.0/go.mod h1:ClMFFknnThJCksebJwz7KIyEDHO+nTB6gK8obLy8RyE= +go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= +go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -361,8 +384,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/admin/admin_api.go b/internal/admin/admin_api.go index 8c8cd73..4bb79fb 100644 --- a/internal/admin/admin_api.go +++ b/internal/admin/admin_api.go @@ -6,6 +6,7 @@ import ( "net/http" "os" "strings" + "time" "github.com/dgate-io/chi-router" "github.com/dgate-io/chi-router/middleware" @@ -21,8 +22,7 @@ func StartAdminAPI(conf *config.DGateConfig, proxyState *proxy.ProxyState) { if conf.AdminConfig == nil { proxyState.Logger().Warn(). Msg("Admin API is disabled") - // wait forever - select {} + return } // Start HTTP Server @@ -59,79 +59,93 @@ func StartAdminAPI(conf *config.DGateConfig, proxyState *proxy.ProxyState) { }() // Start Test Server - if conf.TestServerConfig != nil && !conf.Debug { - proxyState.Logger().Warn(). - Msg("Test server is disabled in non-debug mode") - } else if conf.Debug && conf.TestServerConfig != nil { - go func() { - testHostPort := fmt.Sprintf("%s:%d", - conf.TestServerConfig.Host, conf.TestServerConfig.Port) - mux := chi.NewRouter() - mux.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { - if strings.HasPrefix(r.URL.Path, "/debug") { - // strip /debug prefix - r.URL.Path = strings.TrimPrefix(r.URL.Path, "/debug") - middleware.Profiler().ServeHTTP(w, r) - return - } - respMap := map[string]any{} - respMap["method"] = r.Method - respMap["path"] = r.URL.String() - respMap["remote_addr"] = r.RemoteAddr - respMap["host"] = r.Host - respMap["req_headers"] = r.Header - if conf.TestServerConfig.EnableEnvVars { - respMap["env"] = os.Environ() + if conf.TestServerConfig != nil { + if !conf.Debug { + proxyState.Logger().Warn(). + Msg("Test server is disabled in non-debug mode") + } else { + go func() { + testHostPort := fmt.Sprintf("%s:%d", + conf.TestServerConfig.Host, conf.TestServerConfig.Port) + mux := chi.NewRouter() + mux.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/debug") { + // strip /debug prefix + r.URL.Path = strings.TrimPrefix(r.URL.Path, "/debug") + middleware.Profiler().ServeHTTP(w, r) + return + } + respMap := map[string]any{} + if waitStr := r.URL.Query().Get("wait"); waitStr != "" { + if waitTime, err := time.ParseDuration(waitStr); err != nil { + util.JsonResponse(w, http.StatusBadRequest, map[string]string{ + "error": fmt.Sprintf("Invalid wait time: %s", waitStr), + }) + return + } else { + respMap["waited"] = waitTime.String() + time.Sleep(waitTime) + } + } + respMap["method"] = r.Method + respMap["path"] = r.URL.String() + respMap["remote_addr"] = r.RemoteAddr + respMap["host"] = r.Host + respMap["req_headers"] = r.Header + if conf.TestServerConfig.EnableEnvVars { + respMap["env"] = os.Environ() + } + respMap["global_headers"] = conf.TestServerConfig.GlobalHeaders + for k, v := range conf.TestServerConfig.GlobalHeaders { + w.Header().Set(k, v) + } + util.JsonResponse(w, http.StatusOK, respMap) + }) + + testServerLogger := proxyState.Logger( + proxy.WithComponentLogger("test-server-http"), + proxy.WithDefaultLevel(zerolog.InfoLevel), + ) + testServer := &http.Server{ + Addr: testHostPort, + Handler: mux, + ErrorLog: log.New(testServerLogger, "", 0), } - respMap["global_headers"] = conf.TestServerConfig.GlobalHeaders - for k, v := range conf.TestServerConfig.GlobalHeaders { - w.Header().Set(k, v) + if conf.TestServerConfig.EnableHTTP2 { + h2Server := &http2.Server{} + err := http2.ConfigureServer(testServer, h2Server) + if err != nil { + panic(err) + } + if conf.TestServerConfig.EnableH2C { + testServer.Handler = h2c.NewHandler(mux, h2Server) + } } - util.JsonResponse(w, http.StatusOK, respMap) - }) + proxyState.Logger().Info(). + Msgf("Starting test server on %s", testHostPort) - testServerLogger := proxyState.Logger( - proxy.WithComponentLogger("test-server-http"), - proxy.WithDefaultLevel(zerolog.InfoLevel), - ) - testServer := &http.Server{ - Addr: testHostPort, - Handler: mux, - ErrorLog: log.New(testServerLogger, "", 0), - } - if conf.TestServerConfig.EnableHTTP2 { - h2Server := &http2.Server{} - err := http2.ConfigureServer(testServer, h2Server) - if err != nil { + if err := testServer.ListenAndServe(); err != nil { panic(err) } - if conf.TestServerConfig.EnableH2C { - testServer.Handler = h2c.NewHandler(mux, h2Server) - } - } - proxyState.Logger().Info(). - Msgf("Starting test server on %s", testHostPort) - - if err := testServer.ListenAndServe(); err != nil { - panic(err) - } - }() - } - - adminHttpLogger := proxyState.Logger( - proxy.WithComponentLogger("admin-http"), - proxy.WithDefaultLevel(zerolog.InfoLevel), - ) - hostPort := fmt.Sprintf("%s:%d", - conf.AdminConfig.Host, conf.AdminConfig.Port) - proxyState.Logger().Info(). - Msgf("Starting admin api on %s", hostPort) - server := &http.Server{ - Addr: hostPort, - Handler: mux, - ErrorLog: log.New(adminHttpLogger, "", 0), - } - if err := server.ListenAndServe(); err != nil { - panic(err) + }() + } } + go func() { + adminHttpLogger := proxyState.Logger( + proxy.WithComponentLogger("admin-http"), + proxy.WithDefaultLevel(zerolog.InfoLevel), + ) + hostPort := fmt.Sprintf("%s:%d", + conf.AdminConfig.Host, conf.AdminConfig.Port) + proxyState.Logger().Info(). + Msgf("Starting admin api on %s", hostPort) + server := &http.Server{ + Addr: hostPort, + Handler: mux, + ErrorLog: log.New(adminHttpLogger, "", 0), + } + if err := server.ListenAndServe(); err != nil { + panic(err) + } + }() } diff --git a/internal/admin/admin_fsm.go b/internal/admin/admin_fsm.go index 1058b3a..c8b71ee 100644 --- a/internal/admin/admin_fsm.go +++ b/internal/admin/admin_fsm.go @@ -27,23 +27,51 @@ func newDGateAdminFSM(ps *proxy.ProxyState) *dgateAdminFSM { } } -func (fsm *dgateAdminFSM) Apply(log *raft.Log) interface{} { +func (fsm *dgateAdminFSM) isReplay(log *raft.Log) bool { + return !fsm.ps.Ready() && + log.Index+1 >= fsm.ps.Raft().LastIndex() && + log.Index+1 >= fsm.ps.Raft().AppliedIndex() +} + +func (fsm *dgateAdminFSM) checkLast(log *raft.Log) { + rft := fsm.ps.Raft() + if !fsm.ps.Ready() && fsm.isReplay(log) { + fsm.logger.Info(). + Msgf("FSM is not ready, setting ready @ Index: %d, AIndex: %d, LIndex: %d", + log.Index, rft.AppliedIndex(), rft.LastIndex()) + defer func() { + if err := fsm.ps.ReloadState(false); err != nil { + fsm.logger.Error().Err(err). + Msg("Error processing change log in FSM") + } else { + fsm.ps.SetReady() + } + }() + } +} + +func (fsm *dgateAdminFSM) applyLog(log *raft.Log) (*spec.ChangeLog, error) { + rft := fsm.ps.Raft() switch log.Type { case raft.LogCommand: - rft := fsm.ps.Raft() fsm.logger.Debug(). Msgf("log cmd: %d, %v, %s - applied: %v, latest: %v", log.Index, log.Type, log.Data, rft.AppliedIndex(), rft.LastIndex()) var cl spec.ChangeLog - err := json.Unmarshal(log.Data, &cl) - if err != nil { + if err := json.Unmarshal(log.Data, &cl); err != nil { fsm.logger.Error().Err(err). Msg("Error unmarshalling change log") - return err + return nil, err + } else if cl.Cmd.IsNoop() { + return nil, nil + } else if cl.ID == "" { + fsm.logger.Error(). + Msg("Change log ID is empty") + panic("change log ID is empty") } // find a way to apply only if latest index to save time - return fsm.ps.ProcessChangeLog(&cl, false) + return &cl, fsm.ps.ProcessChangeLog(&cl, false) case raft.LogNoop: fsm.logger.Debug().Msg("Noop Log - current leader is still leader") case raft.LogConfiguration: @@ -58,47 +86,51 @@ func (fsm *dgateAdminFSM) Apply(log *raft.Log) interface{} { fsm.ps.Logger().Error(). Msg("Unknown log type in FSM Apply") } - return nil + return nil, nil } -func (fsm *dgateAdminFSM) ApplyBatch(logs []*raft.Log) []interface{} { - fsm.logger.Debug(). - Msgf("applying log batch of %d logs", len(logs)) - results := make([]interface{}, len(logs)) - for i, log := range logs { - results[i] = fsm.Apply(log) +func (fsm *dgateAdminFSM) Apply(log *raft.Log) any { + defer fsm.checkLast(log) + _, err := fsm.applyLog(log) + return err +} + +func (fsm *dgateAdminFSM) ApplyBatch(logs []*raft.Log) []any { + lastLog := logs[len(logs)-1] + if fsm.isReplay(lastLog) { + rft := fsm.ps.Raft() + fsm.logger.Info(). + Msgf("applying log batch of %d logs - current:%d applied:%d commit:%d last:%d", + len(logs), lastLog.Index, rft.AppliedIndex(), rft.CommitIndex(), rft.LastIndex()) } + cls := make([]*spec.ChangeLog, 0, len(logs)) defer func() { - if err := fsm.ps.ReloadState(); err != nil { + if !fsm.ps.Ready() { + fsm.checkLast(logs[len(logs)-1]) + return + } + + if err := fsm.ps.ReloadState(true, cls...); err != nil { fsm.logger.Error().Err(err). - Msg("Error reloading state") + Msg("Error reloading state @ FSM ApplyBatch") } }() + + results := make([]any, len(logs)) + for i, log := range logs { + var cl *spec.ChangeLog + cl, results[i] = fsm.applyLog(log) + if cl != nil { + cls = append(cls, cl) + } + } return results } func (fsm *dgateAdminFSM) Snapshot() (raft.FSMSnapshot, error) { - return &dgateAdminState{ - snap: fsm.ps.Snapshot(), - }, nil + panic("snapshots not supported") } func (fsm *dgateAdminFSM) Restore(rc io.ReadCloser) error { - defer rc.Close() - return fsm.ps.RestoreState(rc) -} - -type dgateAdminState struct { - snap *proxy.ProxySnapshot - psRef *proxy.ProxyState -} - -func (state *dgateAdminState) Persist(sink raft.SnapshotSink) error { - defer sink.Close() - return state.snap.PersistState(sink) -} - -func (state *dgateAdminState) Release() { - state.psRef = nil - state.snap = nil + panic("snapshots not supported") } diff --git a/internal/admin/admin_raft.go b/internal/admin/admin_raft.go index cbab821..0d29699 100644 --- a/internal/admin/admin_raft.go +++ b/internal/admin/admin_raft.go @@ -50,14 +50,15 @@ func setupRaft(conf *config.DGateConfig, server *chi.Mux, ps *proxy.ProxyState) &raft.Config{ ProtocolVersion: raft.ProtocolVersionMax, LocalID: raft.ServerID(raftId), - HeartbeatTimeout: time.Millisecond * 8000, - ElectionTimeout: time.Second * 10, - CommitTimeout: time.Second * 5, + HeartbeatTimeout: time.Second * 4, + ElectionTimeout: time.Second * 5, + CommitTimeout: time.Second * 4, BatchApplyCh: true, MaxAppendEntries: 16, - SnapshotInterval: time.Hour * 72, - LeaderLeaseTimeout: time.Millisecond * 4000, - SnapshotThreshold: 8192, + LeaderLeaseTimeout: time.Second * 4, + // TODO: Support snapshots + SnapshotInterval: time.Hour * 24, + SnapshotThreshold: ^uint64(0), Logger: logger.NewZeroHCLogger( ps.Logger().With(). Str("component", "raft"). @@ -87,7 +88,7 @@ func setupRaft(conf *config.DGateConfig, server *chi.Mux, ps *proxy.ProxyState) panic(err) } - ps.EnableRaft(raftNode, raftConfig) + ps.SetupRaft(raftNode, raftConfig) // Setup raft handler server.Handle("/raft/*", trans) @@ -192,12 +193,15 @@ func setupRaft(conf *config.DGateConfig, server *chi.Mux, ps *proxy.ProxyState) } // If this node is watch only, add it as a non-voter node, otherwise add it as a voter node if adminConfig.WatchOnly { - ps.Logger().Debug(). + ps.Logger().Info(). Msgf("Adding non-voter: %s", url) - resp, err := adminClient.AddNonvoter(context.Background(), raft.ServerAddress(url), &raftadmin.AddNonvoterRequest{ - ID: raftId, - Address: adminConfig.Replication.AdvertAddr, - }) + resp, err := adminClient.AddNonvoter( + context.Background(), raft.ServerAddress(url), + &raftadmin.AddNonvoterRequest{ + ID: raftId, + Address: adminConfig.Replication.AdvertAddr, + }, + ) if err != nil { panic(err) } @@ -205,7 +209,7 @@ func setupRaft(conf *config.DGateConfig, server *chi.Mux, ps *proxy.ProxyState) panic(resp.Error) } } else { - ps.Logger().Debug(). + ps.Logger().Info(). Msgf("Adding voter: %s - leader: %s", adminConfig.Replication.AdvertAddr, url) resp, err := adminClient.AddVoter(context.Background(), raft.ServerAddress(url), &raftadmin.AddVoterRequest{ diff --git a/internal/admin/admin_routes.go b/internal/admin/admin_routes.go index da55e65..aad8145 100644 --- a/internal/admin/admin_routes.go +++ b/internal/admin/admin_routes.go @@ -1,12 +1,10 @@ package admin import ( - "encoding/json" "fmt" - "math" + "log" "net" "net/http" - "runtime" "strings" "github.com/dgate-io/chi-router" @@ -16,6 +14,12 @@ import ( "github.com/dgate-io/dgate/pkg/util" "github.com/dgate-io/dgate/pkg/util/iplist" "github.com/hashicorp/raft" + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/prometheus" + api "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/metric/noop" + "go.opentelemetry.io/otel/sdk/metric" ) func configureRoutes(server *chi.Mux, proxyState *proxy.ProxyState, conf *config.DGateConfig) { @@ -165,7 +169,7 @@ func configureRoutes(server *chi.Mux, proxyState *proxy.ProxyState, conf *config if adminConfig.Replication != nil { setupRaft(conf, server, proxyState) } - if adminConfig != nil && !adminConfig.WatchOnly { + if adminConfig != nil { server.Route("/api/v1", func(api chi.Router) { routes.ConfigureRouteAPI(api, proxyState, conf) routes.ConfigureModuleAPI(api, proxyState, conf) @@ -173,6 +177,7 @@ func configureRoutes(server *chi.Mux, proxyState *proxy.ProxyState, conf *config routes.ConfigureNamespaceAPI(api, proxyState, conf) routes.ConfigureDomainAPI(api, proxyState, conf) routes.ConfigureCollectionAPI(api, proxyState, conf) + routes.ConfigureSecretAPI(api, proxyState, conf) }) } @@ -180,41 +185,26 @@ func configureRoutes(server *chi.Mux, proxyState *proxy.ProxyState, conf *config routes.ConfigureChangeLogAPI(misc, proxyState, conf) routes.ConfigureHealthAPI(misc, proxyState, conf) - misc.Get("/stats", func(w http.ResponseWriter, r *http.Request) { - snapshot := proxyState.Stats().Snapshot() - b, err := json.Marshal(snapshot) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("Error getting stats: " + err.Error())) - return - } - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(b)) - }) - - misc.Get("/system", func(w http.ResponseWriter, r *http.Request) { - var m runtime.MemStats - runtime.ReadMemStats(&m) - systemStats := map[string]any{ - "goroutines": runtime.NumGoroutine(), - "mem_alloc_mb": bToMb(m.Alloc), - "mem_total_alloc_mb": bToMb(m.TotalAlloc), - "mem_sys_mb": bToMb(m.Sys), - "mem_num_gc": m.NumGC, - } - b, err := json.Marshal(systemStats) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("Error getting stats: " + err.Error())) - return - } - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(b)) + setupMetricProvider(conf, func() { + misc.Handle("/metrics", promhttp.Handler()) }) }) } -func bToMb(b uint64) float64 { - v := float64(b) / 1048576 - return math.Round(v*100) / 100 +func setupMetricProvider( + config *config.DGateConfig, + callback func(), +) { + var provider api.MeterProvider + if !config.DisableMetrics { + defer callback() + exporter, err := prometheus.New() + if err != nil { + log.Fatal(err) + } + provider = metric.NewMeterProvider(metric.WithReader(exporter)) + } else { + provider = noop.NewMeterProvider() + } + otel.SetMeterProvider(provider) } diff --git a/internal/admin/routes/collection_routes.go b/internal/admin/routes/collection_routes.go index 28324cd..48fe818 100644 --- a/internal/admin/routes/collection_routes.go +++ b/internal/admin/routes/collection_routes.go @@ -88,13 +88,19 @@ func ConfigureCollectionAPI(server chi.Router, proxyState *proxy.ProxyState, app namespace = spec.DefaultNamespace.Name } collections := rm.GetCollectionsByNamespace(namespace) - b, err := json.Marshal(spec.TransformDGateCollections(collections...)) - if err != nil { - util.JsonError(w, http.StatusInternalServerError, err.Error()) + util.JsonResponse(w, http.StatusOK, + spec.TransformDGateCollections(collections...)) + }) + + server.Get("/collection/{name}", func(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "name") + nsName := r.URL.Query().Get("namespace") + col, ok := rm.GetCollection(name, nsName) + if !ok { + util.JsonError(w, http.StatusNotFound, "collection not found") return } - w.Header().Set("Content-Type", "application/json") - w.Write(b) + util.JsonResponse(w, http.StatusOK, col) }) server.Get("/document", func(w http.ResponseWriter, r *http.Request) { @@ -139,6 +145,7 @@ func ConfigureCollectionAPI(server chi.Router, proxyState *proxy.ProxyState, app util.JsonError(w, http.StatusInternalServerError, err.Error()) return } + b, err := json.Marshal(map[string]any{ "documents": docs, "limit": limit, @@ -178,7 +185,6 @@ func ConfigureCollectionAPI(server chi.Router, proxyState *proxy.ProxyState, app util.JsonError(w, http.StatusNotFound, "collection not found") return } else { - if collection.Type != spec.CollectionTypeDocument { util.JsonError(w, http.StatusBadRequest, "collection is not a document collection") return diff --git a/internal/admin/routes/domain_routes.go b/internal/admin/routes/domain_routes.go index 081cf4d..847810d 100644 --- a/internal/admin/routes/domain_routes.go +++ b/internal/admin/routes/domain_routes.go @@ -54,7 +54,8 @@ func ConfigureDomainAPI(server chi.Router, proxyState *proxy.ProxyState, appConf return } } - util.JsonResponse(w, http.StatusCreated, spec.TransformDGateDomains(rm.GetDomainsByNamespace(domain.NamespaceName)...)) + util.JsonResponse(w, http.StatusCreated, spec.TransformDGateDomains( + rm.GetDomainsByNamespace(domain.NamespaceName)...)) }) server.Delete("/domain", func(w http.ResponseWriter, r *http.Request) { @@ -91,8 +92,7 @@ func ConfigureDomainAPI(server chi.Router, proxyState *proxy.ProxyState, appConf } cl := spec.NewChangeLog(&domain, domain.NamespaceName, spec.DeleteDomainCommand) - err = proxyState.ApplyChangeLog(cl) - if err != nil { + if err = proxyState.ApplyChangeLog(cl); err != nil { util.JsonError(w, http.StatusBadRequest, err.Error()) return } @@ -119,4 +119,22 @@ func ConfigureDomainAPI(server chi.Router, proxyState *proxy.ProxyState, appConf } util.JsonResponse(w, http.StatusOK, spec.TransformDGateDomains(dgateDomains...)) }) + + server.Get("/domain/{name}", func(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "name") + nsName := r.URL.Query().Get("namespace") + if nsName == "" { + if appConfig.DisableDefaultNamespace { + util.JsonError(w, http.StatusBadRequest, "namespace is required") + return + } + nsName = spec.DefaultNamespace.Name + } + dom, ok := rm.GetDomain(name, nsName) + if !ok { + util.JsonError(w, http.StatusNotFound, "domain not found") + return + } + util.JsonResponse(w, http.StatusOK, dom) + }) } diff --git a/internal/admin/routes/misc_routes.go b/internal/admin/routes/misc_routes.go index 3c01593..1ac0c20 100644 --- a/internal/admin/routes/misc_routes.go +++ b/internal/admin/routes/misc_routes.go @@ -19,34 +19,52 @@ func ConfigureChangeLogAPI(server chi.Router, proxyState *proxy.ProxyState, appC util.JsonError(w, http.StatusInternalServerError, err.Error()) return } + // TODO: find a way to get the raft log hash + // perhaps generate based on current log commands and computed hash } - b, err := json.Marshal(map[string]any{ + + if b, err := json.Marshal(map[string]any{ "hash": proxyState.ChangeHash(), - }) - if err != nil { + }); err != nil { util.JsonError(w, http.StatusInternalServerError, err.Error()) - return + } else { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(b)) + } + }) + server.Get("/changelog/rm", func(w http.ResponseWriter, r *http.Request) { + if b, err := json.Marshal(proxyState.ResourceManager()); err != nil { + util.JsonError(w, http.StatusInternalServerError, err.Error()) + } else { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(b)) } - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(b)) }) } func ConfigureHealthAPI(server chi.Router, ps *proxy.ProxyState, _ *config.DGateConfig) { + healthlyResp := []byte( + `{"status":"ok","version":"` + + ps.Version() + `"}`, + ) server.Get("/healthz", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{"status":"ok"}`)) + w.Write(healthlyResp) }) server.Get("/readyz", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") if r := ps.Raft(); r != nil { if r.Leader() == "" { w.WriteHeader(http.StatusServiceUnavailable) w.Write([]byte(`{"status":"no leader"}`)) return + } else if !ps.Ready() { + w.WriteHeader(http.StatusServiceUnavailable) + w.Write([]byte(`{"status":"not ready"}`)) + return } } - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{"status":"ok"}`)) + w.Write(healthlyResp) }) } diff --git a/internal/admin/routes/module_routes.go b/internal/admin/routes/module_routes.go index 74b549b..02f09b6 100644 --- a/internal/admin/routes/module_routes.go +++ b/internal/admin/routes/module_routes.go @@ -104,4 +104,22 @@ func ConfigureModuleAPI(server chi.Router, proxyState *proxy.ProxyState, appConf util.JsonResponse(w, http.StatusCreated, spec.TransformDGateModules( rm.GetModulesByNamespace(nsName)...)) }) + + server.Get("/module/{name}", func(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "name") + nsName := r.URL.Query().Get("namespace") + if nsName == "" { + if appConfig.DisableDefaultNamespace { + util.JsonError(w, http.StatusBadRequest, "namespace is required") + return + } + nsName = spec.DefaultNamespace.Name + } + mod, ok := rm.GetModule(name, nsName) + if !ok { + util.JsonError(w, http.StatusNotFound, "module not found") + return + } + util.JsonResponse(w, http.StatusOK, mod) + }) } diff --git a/internal/admin/routes/namespace_routes.go b/internal/admin/routes/namespace_routes.go index 10ab8bc..60488e6 100644 --- a/internal/admin/routes/namespace_routes.go +++ b/internal/admin/routes/namespace_routes.go @@ -81,7 +81,8 @@ func ConfigureNamespaceAPI(server chi.Router, proxyState *proxy.ProxyState, _ *c }) server.Get("/namespace", func(w http.ResponseWriter, r *http.Request) { - util.JsonResponse(w, http.StatusOK, spec.TransformDGateNamespaces(rm.GetNamespaces()...)) + util.JsonResponse(w, http.StatusOK, + spec.TransformDGateNamespaces(rm.GetNamespaces()...)) }) server.Get("/namespace/{name}", func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/admin/routes/route_routes.go b/internal/admin/routes/route_routes.go index 3527f8e..0fc58f5 100644 --- a/internal/admin/routes/route_routes.go +++ b/internal/admin/routes/route_routes.go @@ -110,12 +110,27 @@ func ConfigureRouteAPI(server chi.Router, proxyState *proxy.ProxyState, appConfi return } } - b, err := json.Marshal(spec.TransformDGateRoutes(rm.GetRoutesByNamespace(nsName)...)) - if err != nil { - util.JsonError(w, http.StatusInternalServerError, err.Error()) + routes := rm.GetRoutesByNamespace(nsName) + util.JsonResponse(w, http.StatusCreated, + spec.TransformDGateRoutes(routes...)) + }) + + server.Get("/route/{name}", func(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "name") + nsName := r.URL.Query().Get("namespace") + if nsName == "" { + if appConfig.DisableDefaultNamespace { + util.JsonError(w, http.StatusBadRequest, "namespace is required") + return + } + nsName = spec.DefaultNamespace.Name + } + rt, ok := rm.GetRoute(name, nsName) + if !ok { + util.JsonError(w, http.StatusNotFound, "route not found") return } - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(b)) + util.JsonResponse(w, http.StatusOK, + spec.TransformDGateRoute(rt)) }) } diff --git a/internal/admin/routes/secret_routes.go b/internal/admin/routes/secret_routes.go index 3014206..a6a131d 100644 --- a/internal/admin/routes/secret_routes.go +++ b/internal/admin/routes/secret_routes.go @@ -1,6 +1,7 @@ package routes import ( + "encoding/base64" "encoding/json" "io" "net/http" @@ -22,24 +23,26 @@ func ConfigureSecretAPI(server chi.Router, proxyState *proxy.ProxyState, appConf util.JsonError(w, http.StatusBadRequest, "error reading body") return } - scrt := spec.Secret{} - err = json.Unmarshal(eb, &scrt) + sec := spec.Secret{} + err = json.Unmarshal(eb, &sec) if err != nil { util.JsonError(w, http.StatusBadRequest, "error unmarshalling body") return } - if scrt.Data == "" { + if sec.Data == "" { util.JsonError(w, http.StatusBadRequest, "payload is required") return + } else { + sec.Data = base64.RawStdEncoding.EncodeToString([]byte(sec.Data)) } - if scrt.NamespaceName == "" { + if sec.NamespaceName == "" { if appConfig.DisableDefaultNamespace { util.JsonError(w, http.StatusBadRequest, "namespace is required") return } - scrt.NamespaceName = spec.DefaultNamespace.Name + sec.NamespaceName = spec.DefaultNamespace.Name } - cl := spec.NewChangeLog(&scrt, scrt.NamespaceName, spec.AddSecretCommand) + cl := spec.NewChangeLog(&sec, sec.NamespaceName, spec.AddSecretCommand) if err = proxyState.ApplyChangeLog(cl); err != nil { util.JsonError(w, http.StatusBadRequest, err.Error()) return @@ -51,9 +54,9 @@ func ConfigureSecretAPI(server chi.Router, proxyState *proxy.ProxyState, appConf return } } + secrets := rm.GetSecretsByNamespace(sec.NamespaceName) util.JsonResponse(w, http.StatusCreated, - spec.TransformDGateSecrets(true, - rm.GetSecretsByNamespace(scrt.NamespaceName)...)) + spec.TransformDGateSecrets(secrets...)) }) server.Delete("/secret", func(w http.ResponseWriter, r *http.Request) { @@ -63,20 +66,20 @@ func ConfigureSecretAPI(server chi.Router, proxyState *proxy.ProxyState, appConf util.JsonError(w, http.StatusBadRequest, "error reading body") return } - scrt := spec.Secret{} - err = json.Unmarshal(eb, &scrt) + sec := spec.Secret{} + err = json.Unmarshal(eb, &sec) if err != nil { util.JsonError(w, http.StatusBadRequest, "error unmarshalling body") return } - if scrt.NamespaceName == "" { + if sec.NamespaceName == "" { if appConfig.DisableDefaultNamespace { util.JsonError(w, http.StatusBadRequest, "namespace is required") return } - scrt.NamespaceName = spec.DefaultNamespace.Name + sec.NamespaceName = spec.DefaultNamespace.Name } - cl := spec.NewChangeLog(&scrt, scrt.NamespaceName, spec.DeleteSecretCommand) + cl := spec.NewChangeLog(&sec, sec.NamespaceName, spec.DeleteSecretCommand) if err = proxyState.ApplyChangeLog(cl); err != nil { util.JsonError(w, http.StatusBadRequest, err.Error()) return @@ -92,14 +95,30 @@ func ConfigureSecretAPI(server chi.Router, proxyState *proxy.ProxyState, appConf return } nsName = spec.DefaultNamespace.Name - } else { - if _, ok := rm.GetNamespace(nsName); !ok { - util.JsonError(w, http.StatusBadRequest, "namespace not found: "+nsName) + } else if _, ok := rm.GetNamespace(nsName); !ok { + util.JsonError(w, http.StatusBadRequest, "namespace not found: "+nsName) + return + } + secrets := rm.GetSecretsByNamespace(nsName) + util.JsonResponse(w, http.StatusOK, + spec.TransformDGateSecrets(secrets...)) + }) + + server.Get("/secret/{name}", func(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "name") + nsName := r.URL.Query().Get("namespace") + if nsName == "" { + if appConfig.DisableDefaultNamespace { + util.JsonError(w, http.StatusBadRequest, "namespace is required") return } + nsName = spec.DefaultNamespace.Name + } + if sec, ok := rm.GetSecret(name, nsName); !ok { + util.JsonError(w, http.StatusNotFound, "secret not found") + } else { + util.JsonResponse(w, http.StatusOK, + spec.TransformDGateSecret(sec)) } - util.JsonResponse(w, http.StatusCreated, - spec.TransformDGateSecrets(true, - rm.GetSecretsByNamespace(nsName)...)) }) } diff --git a/internal/admin/routes/service_routes.go b/internal/admin/routes/service_routes.go index 66ad23a..87ad29b 100644 --- a/internal/admin/routes/service_routes.go +++ b/internal/admin/routes/service_routes.go @@ -75,7 +75,9 @@ func ConfigureServiceAPI(server chi.Router, proxyState *proxy.ProxyState, appCon return } } - util.JsonResponse(w, http.StatusCreated, spec.TransformDGateServices(rm.GetServicesByNamespace(svc.NamespaceName)...)) + svcs := rm.GetServicesByNamespace(svc.NamespaceName) + util.JsonResponse(w, http.StatusCreated, + spec.TransformDGateServices(svcs...)) }) server.Delete("/service", func(w http.ResponseWriter, r *http.Request) { @@ -128,6 +130,13 @@ func ConfigureServiceAPI(server chi.Router, proxyState *proxy.ProxyState, appCon server.Get("/service/{name}", func(w http.ResponseWriter, r *http.Request) { name := chi.URLParam(r, "name") nsName := r.URL.Query().Get("namespace") + if nsName == "" { + if appConfig.DisableDefaultNamespace { + util.JsonError(w, http.StatusBadRequest, "namespace is required") + return + } + nsName = spec.DefaultNamespace.Name + } svc, ok := rm.GetService(name, nsName) if !ok { util.JsonError(w, http.StatusNotFound, "service not found") diff --git a/internal/config/config.go b/internal/config/config.go index c192200..d4331c6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -8,30 +8,33 @@ import ( type ( DGateConfig struct { - Version string `koanf:"version"` - LogLevel string `koanf:"log_level"` - Debug bool `koanf:"debug"` - Tags []string `koanf:"tags"` - Storage DGateStorageConfig `koanf:"storage"` - ProxyConfig DGateProxyConfig `koanf:"proxy"` - AdminConfig *DGateAdminConfig `koanf:"admin"` - TestServerConfig *DGateTestServerConfig `koanf:"test_server"` - DisableDefaultNamespace bool `koanf:"disable_default_namespace"` + Version string `koanf:"version"` + LogLevel string `koanf:"log_level"` + Debug bool `koanf:"debug"` + Tags []string `koanf:"tags"` + Storage DGateStorageConfig `koanf:"storage"` + ProxyConfig DGateProxyConfig `koanf:"proxy"` + AdminConfig *DGateAdminConfig `koanf:"admin"` + TestServerConfig *DGateTestServerConfig `koanf:"test_server"` + + DisableMetrics bool `koanf:"disable_metrics"` + DisableDefaultNamespace bool `koanf:"disable_default_namespace"` } DGateProxyConfig struct { - Host string `koanf:"host"` - Port int `koanf:"port"` - TLS *DGateTLSConfig `koanf:"tls"` - EnableH2C bool `koanf:"enable_h2c"` - EnableHTTP2 bool `koanf:"enable_http2"` - EnableConsoleLogger bool `koanf:"enable_console_logger"` - RedirectHttpsDomains []string `koanf:"redirect_https"` - AllowedDomains []string `koanf:"allowed_domains"` - GlobalHeaders map[string]string `koanf:"global_headers"` - Transport DGateHttpTransportConfig `koanf:"client_transport"` - InitResources *DGateResources `koanf:"init_resources"` - DisableXForwardedHeaders bool `koanf:"disable_x_forwarded_headers"` + Host string `koanf:"host"` + Port int `koanf:"port"` + TLS *DGateTLSConfig `koanf:"tls"` + EnableH2C bool `koanf:"enable_h2c"` + EnableHTTP2 bool `koanf:"enable_http2"` + EnableConsoleLogger bool `koanf:"enable_console_logger"` + RedirectHttpsDomains []string `koanf:"redirect_https"` + AllowedDomains []string `koanf:"allowed_domains"` + GlobalHeaders map[string]string `koanf:"global_headers"` + Transport DGateHttpTransportConfig `koanf:"client_transport"` + // WARN: debug use only + InitResources *DGateResources `koanf:"init_resources"` + DisableXForwardedHeaders bool `koanf:"disable_x_forwarded_headers"` } DGateTestServerConfig struct { @@ -135,6 +138,7 @@ type ( } DGateHttpTransportConfig struct { + // DNSServer string `koanf:"dns_server"` DNSTimeout time.Duration `koanf:"dns_timeout"` DNSPreferGo bool `koanf:"dns_prefer_go"` diff --git a/internal/config/store_config.go b/internal/config/store_config.go index d4e74a8..7253c1c 100644 --- a/internal/config/store_config.go +++ b/internal/config/store_config.go @@ -7,6 +7,7 @@ import ( type StorageType string const ( + StorageTypeDebug StorageType = "debug" StorageTypeMemory StorageType = "memory" StorageTypeFile StorageType = "file" ) diff --git a/internal/proxy/change_log.go b/internal/proxy/change_log.go index 1085453..2e72599 100644 --- a/internal/proxy/change_log.go +++ b/internal/proxy/change_log.go @@ -85,35 +85,37 @@ func (ps *ProxyState) processChangeLog( return } } - if reload && (cl.Cmd.Resource().IsRelatedTo(spec.Routes) || cl.Cmd.IsNoop()) { - ps.logger.Trace().Msgf("Registering change log: %s", cl.Cmd) - errChan := ps.applyChange(cl) - select { - case err = <-errChan: - break - case <-time.After(time.Second * 15): - err = errors.New("timeout applying change log") - } - if err != nil { - ps.logger.Err(err).Msg("Error registering change log") - return - } - // update change log hash only when the change is successfully applied - // even if the change is a noop, we still need to update the hash - changeHash, err := HashAny[*spec.ChangeLog](ps.changeHash, cl) - if err != nil { - if !ps.config.Debug { - return err + if reload { + if cl.Cmd.Resource().IsRelatedTo(spec.Routes) || cl.Cmd.IsNoop() { + ps.logger.Trace().Msgf("Registering change log: %s", cl.Cmd) + select { + case err = <-ps.applyChange(cl): + break + case <-time.After(time.Second * 15): + err = errors.New("timeout applying change log") + } + if err != nil { + ps.logger.Err(err).Msg("Error registering change log") + return + } + // update change log hash only when the change is successfully applied + // even if the change is a noop, we still need to update the hash + changeHash, err := HashAny(ps.changeHash, cl) + if err != nil { + if !ps.config.Debug { + return err + } + ps.logger.Error().Err(err). + Msg("error updating change log hash") + } else { + ps.changeHash = changeHash } - ps.logger.Error().Err(err). - Msg("error updating change log hash") - } else { - ps.changeHash = changeHash } } if store { if err = ps.store.StoreChangeLog(cl); err != nil { - // TODO: revert change here on error ?? + // TODO: find a way to revert the change and reload the state + // TODO: OR add flag in config to ignore storage errors ps.logger.Err(err).Msg("Error storing change log") return } @@ -272,10 +274,7 @@ func (ps *ProxyState) applyChange(changeLog *spec.ChangeLog) <-chan error { } func (ps *ProxyState) rollbackChange(changeLog *spec.ChangeLog) { - if changeLog == nil { - return - } - ps.changeChan <- changeLog + panic("not implemented") } func (ps *ProxyState) restoreFromChangeLogs() error { @@ -298,8 +297,12 @@ func (ps *ProxyState) restoreFromChangeLogs() error { return err } } - ps.processChangeLog(nil, true, false) - // TODO: change to configurable variable + + if err = ps.processChangeLog(nil, true, false); err != nil { + return err + } + + // TODO: optionally compact change logs through a flag in config? if len(logs) > 1 { removed, err := ps.compactChangeLogs(logs) if err != nil { diff --git a/internal/proxy/dynamic_proxy.go b/internal/proxy/dynamic_proxy.go index ec2de6c..c2a0f4d 100644 --- a/internal/proxy/dynamic_proxy.go +++ b/internal/proxy/dynamic_proxy.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "log" - "net" "net/http" "os" "time" @@ -24,20 +23,22 @@ import ( "golang.org/x/sync/errgroup" ) -func (state *ProxyState) reconfigureState(log *spec.ChangeLog) error { +func (state *ProxyState) reconfigureState(init bool, log *spec.ChangeLog) error { start := time.Now() - err := state.setupModules() - if err != nil { + if err := state.setupModules(); err != nil { return err } - err = state.setupRoutes() - if err != nil { + if err := state.setupRoutes(); err != nil { return err } - if log != nil { - state.logger.Info(). - Msgf("State reloaded in %s: %s", - time.Since(start), log.Cmd) + if !init && log != nil { + state.logger.Debug().Msgf( + "State reloaded in %s", + time.Since(start)) + } else if init { + state.logger.Info().Msgf( + "State initialized in %s", + time.Since(start)) } return nil } @@ -106,12 +107,12 @@ func (ps *ProxyState) setupRoutes() (err error) { for namespaceName, routes := range ps.rm.GetRouteNamespaceMap() { mux := router.NewMux() for _, r := range routes { - reqCtxProvider := NewRequestContextProvider(r) + reqCtxProvider := NewRequestContextProvider(r, ps) reqCtxProviders.Insert(r.Namespace.Name+"/"+r.Name, reqCtxProvider) if len(r.Modules) > 0 { modBuf, err := NewModuleBuffer( 256, 1024, reqCtxProvider, - createModuleExtractorFunc(ps, r), + ps.createModuleExtractorFunc(r), ) if err != nil { ps.logger.Err(err).Msg("Error creating module buffer") @@ -134,8 +135,9 @@ func (ps *ProxyState) setupRoutes() (err error) { } else { if len(r.Methods) == 0 { return errors.New("route must have at least one method") + } else if err = ValidateMethods(r.Methods); err != nil { + return err } - // TODO: validate methods for _, method := range r.Methods { mux.Method(method, path, ps.HandleRoute(reqCtxProvider, path)) } @@ -145,7 +147,7 @@ func (ps *ProxyState) setupRoutes() (err error) { }() } - ps.logger.Info().Msg("Routes have changed, reloading") + ps.logger.Trace().Msg("Routes have changed, reloading") if dr, ok := ps.routers.Find(namespaceName); ok { dr.ReplaceMux(mux) } else { @@ -156,7 +158,7 @@ func (ps *ProxyState) setupRoutes() (err error) { return } -func createModuleExtractorFunc(ps *ProxyState, r *spec.DGateRoute) ModuleExtractorFunc { +func (ps *ProxyState) createModuleExtractorFunc(r *spec.DGateRoute) ModuleExtractorFunc { return func(reqCtx *RequestContextProvider) ModuleExtractor { programs := sliceutil.SliceMapper(r.Modules, func(m *spec.DGateModule) *goja.Program { program, ok := ps.modPrograms.Find(m.Name + "/" + r.Namespace.Name) @@ -205,142 +207,152 @@ func createModuleExtractorFunc(ps *ProxyState, r *spec.DGateRoute) ModuleExtract } } -func StartProxyGateway(conf *config.DGateConfig) (*ProxyState, error) { - ps := NewProxyState(conf) +func (ps *ProxyState) startChangeLoop() { + ps.proxyLock.Lock() + if err := ps.reconfigureState(true, nil); err != nil { + ps.logger.Err(err).Msg("Error initiating state") + os.Exit(1) + } + ps.proxyLock.Unlock() - go func() { - ps.proxyLock.Lock() - err := ps.reconfigureState(nil) - if err != nil { - ps.logger.Err(err).Msg("Error initiating state") - os.Exit(1) + for { + log := <-ps.changeChan + if log.Cmd == spec.StopCommand { + ps.logger.Warn(). + Msg("Stop command received, closing change loop") + log.PushError(nil) + return } - ps.proxyLock.Unlock() - for { - ps.status = ProxyStatusRunning - var log = <-ps.changeChan - if ps.status == ProxyStatusStopping || ps.status == ProxyStatusClosed { - ps.logger.Info().Msg("Stopping proxy gracefully :D") - os.Exit(0) - return + func() { + ps.proxyLock.Lock() + defer ps.proxyLock.Unlock() + err := ps.reconfigureState(false, log) + defer log.PushError(err) + if err != nil { + ps.logger.Err(err). + Msgf("Error reconfiguring state @namespace:%s", log.Namespace) + // ps.rollbackChange(log) } - func() { - ps.proxyLock.Lock() - defer ps.proxyLock.Unlock() - ps.status = ProxyStatusModifying - var err error - defer func() { log.PushError(err) }() - err = ps.reconfigureState(log) - if err != nil { - ps.logger.Err(err).Msg("Error reconfiguring state") - ps.rollbackChange(log) - } - }() - } - }() + }() + } +} - err := ps.store.InitStore() - if err != nil { - return nil, err +func (ps *ProxyState) startProxyServer() { + cfg := ps.config.ProxyConfig + hostPort := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) + ps.logger.Info(). + Msgf("Starting proxy server on %s", hostPort) + proxyHttpLogger := Logger(&ps.logger, + WithComponentLogger("proxy-http"), + ) + server := &http.Server{ + Addr: hostPort, + Handler: ps, + ErrorLog: log.New(proxyHttpLogger, "", 0), } - if !ps.replicationEnabled { - err = ps.restoreFromChangeLogs() + if cfg.EnableHTTP2 { + h2Server := &http2.Server{} + err := http2.ConfigureServer(server, h2Server) if err != nil { - return nil, err + panic(err) + } + if cfg.EnableH2C { + server.Handler = h2c.NewHandler(ps, h2Server) } } + if err := server.ListenAndServe(); err != nil { + ps.logger.Err(err).Msg("Error starting proxy server") + os.Exit(1) + } +} - go func() { - config := conf.ProxyConfig - hostPort := fmt.Sprintf("%s:%d", config.Host, config.Port) - ps.logger.Info(). - Msgf("Starting proxy server on %s", hostPort) - proxyHttpLogger := ps.Logger( - WithComponentLogger("proxy-http"), - WithDefaultLevel(zerolog.InfoLevel), - ) - server := &http.Server{ - Addr: hostPort, - Handler: ps, - ErrorLog: log.New(proxyHttpLogger, "", 0), +func (ps *ProxyState) startProxyServerTLS() { + cfg := ps.config.ProxyConfig + if cfg.TLS == nil { + return + } + hostPort := fmt.Sprintf("%s:%d", cfg.Host, cfg.TLS.Port) + ps.logger.Info(). + Msgf("Starting secure proxy server on %s", hostPort) + proxyHttpsLogger := Logger(&ps.logger, + WithComponentLogger("proxy-https"), + WithDefaultLevel(zerolog.InfoLevel), + ) + secureServer := &http.Server{ + Addr: hostPort, + Handler: ps, + ErrorLog: log.New(proxyHttpsLogger, "", 0), + TLSConfig: ps.DynamicTLSConfig( + cfg.TLS.CertFile, + cfg.TLS.KeyFile, + ), + } + if cfg.EnableHTTP2 { + h2Server := &http2.Server{} + err := http2.ConfigureServer(secureServer, h2Server) + if err != nil { + panic(err) } - if config.EnableHTTP2 { - h2Server := &http2.Server{} - err := http2.ConfigureServer(server, h2Server) - if err != nil { - panic(err) - } - if config.EnableH2C { - server.Handler = h2c.NewHandler(ps, h2Server) - } + if cfg.EnableH2C { + secureServer.Handler = h2c.NewHandler(ps, h2Server) } - if err := server.ListenAndServe(); err != nil { - ps.logger.Err(err).Msg("Error starting proxy server") - os.Exit(1) + } + if err := secureServer.ListenAndServeTLS("", ""); err != nil { + ps.logger.Err(err).Msg("Error starting secure proxy server") + os.Exit(1) + } +} + +func StartProxyGateway(version string, conf *config.DGateConfig) *ProxyState { + ps := NewProxyState(conf) + ps.version = version + + return ps +} + +func (ps *ProxyState) Start() (err error) { + defer func() { + if err != nil { + ps.Stop() } }() - if conf.ProxyConfig.TLS != nil { - go func() { - config := conf.ProxyConfig - hostPort := fmt.Sprintf("%s:%d", config.Host, config.TLS.Port) - ps.logger.Info(). - Msgf("Starting secure proxy server on %s", hostPort) - proxyHttpsLogger := ps.Logger( - WithComponentLogger("proxy-https"), - WithDefaultLevel(zerolog.InfoLevel), - ) - secureServer := &http.Server{ - Addr: hostPort, - Handler: ps, - ErrorLog: log.New(proxyHttpsLogger, "", 0), - TLSConfig: ps.DynamicTLSConfig( - conf.ProxyConfig.TLS.CertFile, - conf.ProxyConfig.TLS.KeyFile, - ), - } - if config.EnableHTTP2 { - h2Server := &http2.Server{} - err := http2.ConfigureServer(secureServer, h2Server) - if err != nil { - panic(err) - } - if config.EnableH2C { - secureServer.Handler = h2c.NewHandler(ps, h2Server) - } - } - if err := secureServer.ListenAndServeTLS("", ""); err != nil { - ps.logger.Err(err).Msg("Error starting secure proxy server") - os.Exit(1) - } - }() - } + go ps.startChangeLoop() + go ps.startProxyServer() + go ps.startProxyServerTLS() - return ps, nil -} + ps.metrics.Setup(ps.config) + if err = ps.store.InitStore(); err != nil { + return err + } -func (ps *ProxyState) HandleRoute(requestCtxProvider *RequestContextProvider, pattern string) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, cancel := context.WithCancel(requestCtxProvider.ctx) - defer cancel() - reqCtx := requestCtxProvider. - CreateRequestContext(ctx, w, r, pattern) - ps.ProxyHandlerFunc(ps, reqCtx) + if !ps.replicationEnabled { + if err = ps.restoreFromChangeLogs(); err != nil { + return err + } } + + return nil } -func TimeoutDialer(connTimeout time.Duration) func(net, addr string) (c net.Conn, err error) { - return func(_net, _addr string) (net.Conn, error) { - conn, err := net.DialTimeout(_net, _addr, connTimeout) - if err != nil { - return nil, err - } - return conn, nil +func (ps *ProxyState) Stop() { + cl := &spec.ChangeLog{ + Cmd: spec.StopCommand, } + done := make(chan error, 1) + cl.SetErrorChan(done) + // push change to change loop + ps.changeChan <- cl + // wait for change loop to stop + <-done } -func RouteHash(routes ...*spec.DGateRoute) (hash uint32, err error) { - hash, err = HashAny[*spec.Route](0, spec.TransformDGateRoutes(routes...)) - return +func (ps *ProxyState) HandleRoute(requestCtxProvider *RequestContextProvider, pattern string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // ctx, cancel := context.WithCancel(requestCtxProvider.ctx) + // defer cancel() + ps.ProxyHandlerFunc(ps, requestCtxProvider. + CreateRequestContext(requestCtxProvider.ctx, w, r, pattern)) + } } diff --git a/internal/proxy/module_executor.go b/internal/proxy/module_executor.go index 1567bf1..9c58222 100644 --- a/internal/proxy/module_executor.go +++ b/internal/proxy/module_executor.go @@ -3,11 +3,10 @@ package proxy import ( "context" "errors" - "time" ) type ModuleBuffer interface { - Load(cb func()) + // Load(cb func()) Borrow() (ModuleExtractor, bool) Return(me ModuleExtractor) Close() @@ -51,20 +50,20 @@ func NewModuleBuffer( return mb, nil } -func (mb *moduleBuffer) Load(cb func()) { - go func() { - for i := 0; i < mb.min; i++ { - me := mb.createModuleExtract() - if me == nil { - panic("could not load moduleExtract") - } - mb.modExtBuffer <- me - } - if cb != nil { - cb() - } - }() -} +// func (mb *moduleBuffer) Load(cb func()) { +// go func() { +// for i := 0; i < mb.min; i++ { +// me := mb.createModuleExtract() +// if me == nil { +// panic("could not load moduleExtract") +// } +// mb.modExtBuffer <- me +// } +// if cb != nil { +// cb() +// } +// }() +// } func (mb *moduleBuffer) Borrow() (ModuleExtractor, bool) { if mb == nil || mb.ctx == nil || mb.ctx.Err() != nil { @@ -75,26 +74,24 @@ func (mb *moduleBuffer) Borrow() (ModuleExtractor, bool) { case me = <-mb.modExtBuffer: break // NOTE: important for performance - case <-time.After(2 * time.Millisecond): default: me = mb.createModuleExtract() - break } return me, true } func (mb *moduleBuffer) Return(me ModuleExtractor) { - me.SetModuleContext(nil) - if mb.ctx == nil || mb.ctx.Err() != nil { - return - } - - select { - case mb.modExtBuffer <- me: - default: - // if buffer is full, discard module extract - me.Stop(true) + defer me.SetModuleContext(nil) + // if context is canceled, do not return module extract + if mb.ctx != nil && mb.ctx.Err() == nil { + select { + case mb.modExtBuffer <- me: + return + default: + // if buffer is full, discard module extract + } } + me.Stop(true) } func (mb *moduleBuffer) Close() { diff --git a/internal/proxy/module_extractor.go b/internal/proxy/module_extractor.go index e1b3349..bd52c9c 100644 --- a/internal/proxy/module_extractor.go +++ b/internal/proxy/module_extractor.go @@ -11,15 +11,12 @@ type ModuleExtractor interface { Start() // Stop stops the event loop for the module extractor Stop(wait bool) - // RuntimeContext returns the runtime context for the module extractor RuntimeContext() modules.RuntimeContext - // SetModuleContext sets the module context for the module extractor SetModuleContext(*types.ModuleContext) // ModuleContext returns the module context for the module extractor ModuleContext() *types.ModuleContext - // ModHash returns the hash of the module ModHash() uint32 diff --git a/internal/proxy/proxy_handler.go b/internal/proxy/proxy_handler.go index 2cf2a5d..2fc25e1 100644 --- a/internal/proxy/proxy_handler.go +++ b/internal/proxy/proxy_handler.go @@ -1,9 +1,7 @@ package proxy import ( - "crypto/tls" "io" - "net" "net/http" "net/url" "time" @@ -16,34 +14,24 @@ import ( type ProxyHandlerFunc func(ps *ProxyState, reqCtx *RequestContext) func proxyHandler(ps *ProxyState, reqCtx *RequestContext) { - rs := NewRequestStats(reqCtx.route) - var err error + defer ps.metrics.MeasureProxyRequest(time.Now(), reqCtx) + defer func() { + if reqCtx.req.Body != nil { + // Ensure that the request body is drained/closed, so the connection can be reused + io.Copy(io.Discard, reqCtx.req.Body) + reqCtx.req.Body.Close() + } + event := ps.logger.Debug(). Str("route", reqCtx.route.Name). Str("namespace", reqCtx.route.Namespace.Name) - if err != nil { - event = event.Err(err) - } + if reqCtx.route.Service != nil { event = event. Str("service", reqCtx.route.Service.Name) - event = event. - Stringer("upstream", rs.UpstreamRequestDur) - } - for k, v := range rs.MiscDurs { - event = event.Stringer(k, v) - } - event.Msg("[STATS] Request Latency") - ps.stats.AddRequestStats(rs) - }() - - defer func() { - if reqCtx.req.Body != nil { - // Ensure that the request body is drained/closed, so the connection can be reused - io.Copy(io.Discard, reqCtx.req.Body) - reqCtx.req.Body.Close() } + event.Msg("Request") }() var modExt ModuleExtractor @@ -80,6 +68,7 @@ func proxyHandler(ps *ProxyState, reqCtx *RequestContext) { ) // TODO: consider passing context to properly close modExt.Start() + defer func() { rtCtx.Clean() modExt.Stop(true) @@ -91,23 +80,19 @@ func proxyHandler(ps *ProxyState, reqCtx *RequestContext) { return } - runtimeElapsed := time.Since(runtimeStart) - rs.AddMiscDuration("moduleExtract", runtimeElapsed) - ps.logger.Trace(). - Str("duration", runtimeElapsed.String()). - Msg("[STATS] Runtime Created") + ps.metrics.MeasureModuleDuration("module_extract", runtimeStart, reqCtx) } else { modExt = NewEmptyModuleExtractor() } if reqCtx.route.Service != nil { - handleServiceProxy(ps, reqCtx, modExt, rs) + handleServiceProxy(ps, reqCtx, modExt) } else { - requestHandlerModule(ps, reqCtx, modExt, rs) + requestHandlerModule(ps, reqCtx, modExt) } } -func handleServiceProxy(ps *ProxyState, reqCtx *RequestContext, modExt ModuleExtractor, rs *RequestStats) { +func handleServiceProxy(ps *ProxyState, reqCtx *RequestContext, modExt ModuleExtractor) { var host string if fetchUpstreamUrl, ok := modExt.FetchUpstreamUrlFunc(); ok { fetchUpstreamStart := time.Now() @@ -118,11 +103,9 @@ func handleServiceProxy(ps *ProxyState, reqCtx *RequestContext, modExt ModuleExt return } host = hostUrl.String() - fetchUpstreamElapsed := time.Since(fetchUpstreamStart) - rs.AddMiscDuration("fetchUpstreamUrl", fetchUpstreamElapsed) - ps.logger.Trace(). - Str("duration", fetchUpstreamElapsed.String()). - Msg("[STATS] fetch upstream module") + ps.metrics.MeasureModuleDuration( + "fetch_upstream", fetchUpstreamStart, reqCtx, + ) } else { if reqCtx.route.Service.URLs == nil || len(reqCtx.route.Service.URLs) == 0 { ps.logger.Error().Msg("Error getting service urls") @@ -149,71 +132,40 @@ func handleServiceProxy(ps *ProxyState, reqCtx *RequestContext, modExt ModuleExt util.WriteStatusCodeError(reqCtx.rw, http.StatusBadGateway) return } - proxyTransport := setupTranportFromConfig( - ps.config.ProxyConfig.Transport, - func(dialer *net.Dialer, t *http.Transport) { - t.TLSClientConfig = &tls.Config{ - InsecureSkipVerify: reqCtx.route.Service.TLSSkipVerify, - } - dialer.Timeout = reqCtx.route.Service.ConnectTimeout - t.ForceAttemptHTTP2 = reqCtx.route.Service.HTTP2Only - }, - ) - - ptb := ps.ProxyTransportBuilder.Clone(). - Transport(proxyTransport). - Retries(reqCtx.route.Service.Retries). - RetryTimeout(reqCtx.route.Service.RetryTimeout). - RequestTimeout(reqCtx.route.Service.RequestTimeout) - proxy, err := ptb.Build() - if err != nil { - ps.logger.Err(err).Msg("Error creating proxy transport") - util.WriteStatusCodeError(reqCtx.rw, http.StatusInternalServerError) - return - } - - rpb := ps.ReverseProxyBuilder.Clone(). - Transport(proxy).FlushInterval(-1). - ProxyRewrite( - reqCtx.route.StripPath, - reqCtx.route.PreserveHost, - reqCtx.route.Service.DisableQueryParams, - ps.config.ProxyConfig.DisableXForwardedHeaders, - ). + rpb := reqCtx.provider.rpb.Clone(). ModifyResponse(func(res *http.Response) error { + defer ps.metrics.MeasureModuleDuration( + "response_modifier", time.Now(), reqCtx, + ) if reqCtx.route.Service.HideDGateHeaders { res.Header.Set("Via", "DGate Proxy") } if responseModifier, ok := modExt.ResponseModifierFunc(); ok { resModifierStart := time.Now() err = responseModifier(modExt.ModuleContext(), res) - resModifierElapsed := time.Since(resModifierStart) if err != nil { ps.logger.Err(err).Msg("Error modifying response") return err } - rs.AddMiscDuration("responseModifier", resModifierElapsed) - ps.logger.Trace(). - Str("duration", resModifierElapsed.String()). - Msg("[STATS] respond modifier module") + ps.metrics.MeasureModuleDuration( + "response_modifier", resModifierStart, reqCtx, + ) } return nil }). ErrorHandler(func(w http.ResponseWriter, r *http.Request, reqErr error) { ps.logger.Debug().Err(reqErr).Msg("Error proxying request") + // TODO: add metric for error if reqCtx.rw.HeadersSent() { return } if errorHandler, ok := modExt.ErrorHandlerFunc(); ok { errorHandlerStart := time.Now() err = errorHandler(modExt.ModuleContext(), reqErr) - errorHandlerElapsed := time.Since(errorHandlerStart) - rs.AddMiscDuration("errorHandler", errorHandlerElapsed) - ps.logger.Trace(). - Str("duration", errorHandlerElapsed.String()). - Msg("[STATS] error handler module") - ps.logger.Trace().Err(reqErr).Msg("Error proxying request") + ps.metrics.MeasureModuleDuration( + "error_handler", errorHandlerStart, reqCtx, + ) if err != nil { ps.logger.Err(err).Msg("Error handling error") util.WriteStatusCodeError(reqCtx.rw, http.StatusInternalServerError) @@ -234,14 +186,11 @@ func handleServiceProxy(ps *ProxyState, reqCtx *RequestContext, modExt ModuleExt util.WriteStatusCodeError(reqCtx.rw, http.StatusInternalServerError) return } - reqModifierElapsed := time.Since(reqModifierStart) - rs.AddMiscDuration("requestModifier", reqModifierElapsed) - ps.logger.Trace(). - Str("duration", reqModifierElapsed.String()). - Msg("[STATS] request modifier module") + ps.metrics.MeasureModuleDuration( + "request_modifier", reqModifierStart, reqCtx, + ) } - upstreamStart := time.Now() rp, err := rpb.Build(upstreamUrl, reqCtx.pattern) if err != nil { ps.logger.Err(err).Msg("Error creating reverse proxy") @@ -253,16 +202,14 @@ func handleServiceProxy(ps *ProxyState, reqCtx *RequestContext, modExt ModuleExt reqCtx.rw.Header().Set(k, v) } + upstreamStart := time.Now() rp.ServeHTTP(reqCtx.rw, reqCtx.req) - - upstreamElapsed := time.Since(upstreamStart) - rs.AddUpstreamRequestDuration(upstreamElapsed) - ps.logger.Trace(). - Str("duration", upstreamElapsed.String()). - Msg("[STATS] upstream") + ps.metrics.MeasureUpstreamDuration( + upstreamStart, upstreamUrl.String(), reqCtx, + ) } -func requestHandlerModule(ps *ProxyState, reqCtx *RequestContext, modExt ModuleExtractor, rs *RequestStats) { +func requestHandlerModule(ps *ProxyState, reqCtx *RequestContext, modExt ModuleExtractor) { var err error if requestModifier, ok := modExt.RequestModifierFunc(); ok { // extract request modifier function from module @@ -273,16 +220,17 @@ func requestHandlerModule(ps *ProxyState, reqCtx *RequestContext, modExt ModuleE util.WriteStatusCodeError(reqCtx.rw, http.StatusInternalServerError) return } - reqModifierElapsed := time.Since(reqModifierStart) - rs.AddMiscDuration("requestModifier", reqModifierElapsed) - ps.logger.Trace(). - Str("duration", reqModifierElapsed.String()). - Msg("[STATS] request modifier module") + ps.metrics.MeasureModuleDuration( + "request_modifier", reqModifierStart, reqCtx, + ) } if requestHandler, ok := modExt.RequestHandlerFunc(); ok { requestHandlerStart := time.Now() + defer ps.metrics.MeasureModuleDuration( + "request_handler", requestHandlerStart, reqCtx, + ) if err := requestHandler(modExt.ModuleContext()); err != nil { - ps.logger.Error().Err(err).Msg("Error handling request") + ps.logger.Error().Err(err).Msg("Error @ request_handler module") if errorHandler, ok := modExt.ErrorHandlerFunc(); ok { // extract error handler function from module errorHandlerStart := time.Now() @@ -291,22 +239,15 @@ func requestHandlerModule(ps *ProxyState, reqCtx *RequestContext, modExt ModuleE util.WriteStatusCodeError(reqCtx.rw, http.StatusInternalServerError) return } - errorHandlerElapsed := time.Since(errorHandlerStart) - rs.AddMiscDuration("errorHandler", errorHandlerElapsed) - ps.logger.Trace(). - Str("duration", errorHandlerElapsed.String()). - Msg("[STATS] error handler module") + ps.metrics.MeasureModuleDuration( + "error_handler", errorHandlerStart, reqCtx, + ) } else { ps.logger.Err(err).Msg("Error handling request") util.WriteStatusCodeError(reqCtx.rw, http.StatusInternalServerError) return } } else { - requestHandlerElapsed := time.Since(requestHandlerStart) - rs.AddMiscDuration("requestHandler", requestHandlerElapsed) - ps.logger.Trace(). - Str("duration", requestHandlerElapsed.String()). - Msg("[STATS] request handler module") if !reqCtx.rw.HeadersSent() { if reqCtx.rw.BytesWritten() > 0 { reqCtx.rw.WriteHeader(http.StatusOK) diff --git a/internal/proxy/proxy_handler_test.go b/internal/proxy/proxy_handler_test.go index 2df5ce3..04d4f32 100644 --- a/internal/proxy/proxy_handler_test.go +++ b/internal/proxy/proxy_handler_test.go @@ -27,28 +27,29 @@ func TestProxyHandler_ReverseProxy(t *testing.T) { } for _, conf := range configs { ps := proxy.NewProxyState(conf) + + rt, ok := ps.ResourceManager().GetRoute("test", "test") + if !ok { + t.Fatal("namespace not found") + } rpBuilder := proxytest.CreateMockReverseProxyBuilder() - rpBuilder.On("FlushInterval", mock.Anything).Return(rpBuilder).Once() - // rpBuilder.On("CustomRewrite", mock.Anything).Return(rpBuilder).Times(0) + // rpBuilder.On("FlushInterval", mock.Anything).Return(rpBuilder).Once() rpBuilder.On("ModifyResponse", mock.Anything).Return(rpBuilder).Once() rpBuilder.On("ErrorHandler", mock.Anything).Return(rpBuilder).Once() + rpBuilder.On("Clone").Return(rpBuilder).Times(2) rpBuilder.On("Transport", mock.Anything).Return(rpBuilder).Once() - rpBuilder.On("ProxyRewrite", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(rpBuilder).Once() - rpBuilder.On("Clone").Return(rpBuilder).Once() + rpBuilder.On("ProxyRewrite", + rt.StripPath, + rt.PreserveHost, + rt.Service.DisableQueryParams, + conf.ProxyConfig.DisableXForwardedHeaders, + ).Return(rpBuilder).Once() rpe := proxytest.CreateMockReverseProxyExecutor() rpe.On("ServeHTTP", mock.Anything, mock.Anything).Return().Once() rpBuilder.On("Build", mock.Anything, mock.Anything).Return(rpe, nil).Once() - defer rpBuilder.AssertExpectations(t) - defer rpe.AssertExpectations(t) ps.ReverseProxyBuilder = rpBuilder - rm := ps.ResourceManager() - rt, ok := rm.GetRoute("test", "test") - if !ok { - t.Fatal("namespace not found") - } - - reqCtxProvider := proxy.NewRequestContextProvider(rt) + reqCtxProvider := proxy.NewRequestContextProvider(rt, ps) req, wr := proxytest.NewMockRequestAndResponseWriter("GET", "http://localhost:8080/test", []byte{}) // wr.On("WriteHeader", 200).Return().Once() @@ -64,12 +65,13 @@ func TestProxyHandler_ReverseProxy(t *testing.T) { modBuf.On("Borrow").Return(modExt, true).Once() modBuf.On("Return", modExt).Return().Once() reqCtxProvider.SetModuleBuffer(modBuf) - ps.ProxyHandlerFunc(ps, reqCtx) wr.AssertExpectations(t) modBuf.AssertExpectations(t) modExt.AssertExpectations(t) + rpBuilder.AssertExpectations(t) + // rpe.AssertExpectations(t) } } @@ -107,12 +109,12 @@ func TestProxyHandler_ProxyHandler(t *testing.T) { req, wr := proxytest.NewMockRequestAndResponseWriter("GET", "http://localhost:8080/test", []byte("123")) wr.On("WriteHeader", resp.StatusCode).Return().Maybe() wr.On("Header").Return(http.Header{}).Maybe() - wr.On("Write", mock.Anything).Return(0, nil).Once().Run(func(args mock.Arguments) { + wr.On("Write", mock.Anything).Return(3, nil).Once().Run(func(args mock.Arguments) { b := args.Get(0).([]byte) assert.Equal(t, b, []byte("abc")) }) - reqCtxProvider := proxy.NewRequestContextProvider(rt) + reqCtxProvider := proxy.NewRequestContextProvider(rt, ps) modExt := NewMockModuleExtractor() modExt.ConfigureDefaultMock(req, wr, ps, rt) modBuf := NewMockModuleBuffer() @@ -171,7 +173,7 @@ func TestProxyHandler_ProxyHandlerError(t *testing.T) { modBuf.On("Borrow").Return(modExt, true).Once() modBuf.On("Return", modExt).Return().Once() - reqCtxProvider := proxy.NewRequestContextProvider(rt) + reqCtxProvider := proxy.NewRequestContextProvider(rt, ps) reqCtxProvider.SetModuleBuffer(modBuf) reqCtx := reqCtxProvider.CreateRequestContext( context.Background(), wr, req, "/") diff --git a/internal/proxy/proxy_metrics.go b/internal/proxy/proxy_metrics.go new file mode 100644 index 0000000..b39be88 --- /dev/null +++ b/internal/proxy/proxy_metrics.go @@ -0,0 +1,187 @@ +package proxy + +import ( + "context" + "time" + + "github.com/dgate-io/dgate/internal/config" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + api "go.opentelemetry.io/otel/metric" +) + +type ProxyMetrics struct { + resolveNamespaceDurInstrument api.Float64Histogram + resolveCertDurInstrument api.Float64Histogram + proxyDurInstrument api.Float64Histogram + proxyCountInstrument api.Float64Counter + moduleDurInstrument api.Float64Histogram + moduleRunCountInstrument api.Float64Counter + upstreamDurInstrument api.Float64Histogram + resourceDurInstrument api.Float64Histogram +} + +func NewProxyMetrics() *ProxyMetrics { + return &ProxyMetrics{} +} + +func (pm *ProxyMetrics) Setup(config *config.DGateConfig) { + meter := otel.Meter("dgate-proxy-metrics", api.WithInstrumentationAttributes( + attribute.KeyValue{ + Key: "tag", Value: attribute.StringSliceValue(config.Tags), + }, + attribute.KeyValue{ + Key: "storage", Value: attribute.StringValue(string(config.Storage.StorageType)), + }, + )) + + pm.resolveNamespaceDurInstrument, _ = meter.Float64Histogram( + "resolve_namespace_duration", api.WithUnit("us")) + pm.resolveCertDurInstrument, _ = meter.Float64Histogram( + "resolve_cert_duration", api.WithUnit("ms")) + pm.proxyDurInstrument, _ = meter.Float64Histogram( + "request_duration", api.WithUnit("ms")) + pm.moduleDurInstrument, _ = meter.Float64Histogram( + "module_duration", api.WithUnit("ms")) + pm.upstreamDurInstrument, _ = meter.Float64Histogram( + "upstream_duration", api.WithUnit("ms")) + pm.resourceDurInstrument, _ = meter.Float64Histogram( + "resource_duration", api.WithUnit("ms")) + pm.proxyCountInstrument, _ = meter.Float64Counter( + "request_count") + pm.moduleRunCountInstrument, _ = meter.Float64Counter( + "module_executions") +} + +func (pm *ProxyMetrics) MeasureProxyRequest( + start time.Time, reqCtx *RequestContext, +) { + if pm.proxyDurInstrument == nil || pm.proxyCountInstrument == nil { + return + } + serviceAttr := attribute.NewSet() + if reqCtx.route.Service != nil { + serviceAttr = attribute.NewSet( + attribute.String("service", reqCtx.route.Service.Name), + attribute.StringSlice("service_tag", reqCtx.route.Service.Tags), + ) + } + elasped := time.Since(start) + attrSet := attribute.NewSet( + attribute.String("route", reqCtx.route.Name), + attribute.String("namespace", reqCtx.route.Namespace.Name), + attribute.String("method", reqCtx.req.Method), + attribute.String("path", reqCtx.req.URL.Path), + attribute.String("pattern", reqCtx.pattern), + attribute.String("host", reqCtx.req.Host), + attribute.StringSlice("route_tag", reqCtx.route.Tags), + ) + + pm.proxyDurInstrument.Record(reqCtx.ctx, + float64(elasped)/float64(time.Millisecond), + api.WithAttributeSet(attrSet), api.WithAttributeSet(serviceAttr)) + + pm.proxyCountInstrument.Add(reqCtx.ctx, 1, + api.WithAttributeSet(attrSet), api.WithAttributeSet(serviceAttr)) +} + +func (pm *ProxyMetrics) MeasureModuleDuration(moduleFunc string, start time.Time, reqCtx *RequestContext) { + if pm.moduleDurInstrument == nil || pm.moduleRunCountInstrument == nil { + return + } + elasped := time.Since(start) + attrSet := attribute.NewSet( + attribute.String("route", reqCtx.route.Name), + attribute.String("namespace", reqCtx.route.Namespace.Name), + attribute.String("moduleFunc", moduleFunc), + attribute.String("method", reqCtx.req.Method), + attribute.String("path", reqCtx.req.URL.Path), + attribute.String("pattern", reqCtx.pattern), + attribute.String("host", reqCtx.req.Host), + attribute.StringSlice("route_tag", reqCtx.route.Tags), + ) + + pm.moduleDurInstrument.Record(reqCtx.ctx, + float64(elasped)/float64(time.Millisecond), + api.WithAttributeSet(attrSet)) + + pm.moduleRunCountInstrument.Add(reqCtx.ctx, 1, + api.WithAttributeSet(attrSet)) +} + +func (pm *ProxyMetrics) MeasureUpstreamDuration( + start time.Time, upstreamHost string, + reqCtx *RequestContext, +) { + if pm.upstreamDurInstrument == nil { + return + } + elasped := time.Since(start) + attrSet := attribute.NewSet( + attribute.String("route", reqCtx.route.Name), + attribute.String("namespace", reqCtx.route.Namespace.Name), + attribute.String("method", reqCtx.req.Method), + attribute.String("path", reqCtx.req.URL.Path), + attribute.String("pattern", reqCtx.pattern), + attribute.String("host", reqCtx.req.Host), + attribute.String("service", reqCtx.route.Service.Name), + attribute.String("upstream_host", upstreamHost), + attribute.StringSlice("service_tag", reqCtx.route.Service.Tags), + attribute.StringSlice("route_tag", reqCtx.route.Tags), + ) + + pm.upstreamDurInstrument.Record(reqCtx.ctx, + float64(elasped)/float64(time.Millisecond), + api.WithAttributeSet(attrSet)) +} + +// func (pm *ProxyMetrics) MeasureResourceDuration( +// start time.Time, resource, namespace string, +// ) { +// if pm.resourceDurInstrument == nil { +// return +// } +// elasped := time.Since(start) +// attrSet := attribute.NewSet( +// attribute.String("resource", resource), +// attribute.String("namespace", namespace), +// ) + +// pm.resourceDurInstrument.Record(context.TODO(), +// float64(elasped)/float64(time.Millisecond), +// api.WithAttributeSet(attrSet)) +// } + +func (pm *ProxyMetrics) MeasureNamespaceResolutionDuration( + start time.Time, host, namespace string, +) { + if pm.resolveNamespaceDurInstrument == nil { + return + } + elasped := time.Since(start) + attrSet := attribute.NewSet( + attribute.String("host", host), + attribute.String("namespace", namespace), + ) + + pm.resolveNamespaceDurInstrument.Record(context.TODO(), + float64(elasped)/float64(time.Microsecond), + api.WithAttributeSet(attrSet)) +} + +func (pm *ProxyMetrics) MeasureCertResolutionDuration( + start time.Time, host string, cache bool, +) { + if pm.resolveCertDurInstrument == nil { + return + } + elasped := time.Since(start) + attrSet := attribute.NewSet( + attribute.String("host", host), + attribute.Bool("cache", cache), + ) + + pm.resolveCertDurInstrument.Record(context.TODO(), + float64(elasped)/float64(time.Millisecond), + api.WithAttributeSet(attrSet)) +} diff --git a/internal/proxy/proxy_state.go b/internal/proxy/proxy_state.go index 9abd3b2..67475d3 100644 --- a/internal/proxy/proxy_state.go +++ b/internal/proxy/proxy_state.go @@ -1,24 +1,23 @@ package proxy import ( - "bytes" "crypto/tls" "encoding/base64" - "encoding/gob" "encoding/json" "errors" "fmt" - "io" + "log" "net" "net/http" "os" "sync" + "sync/atomic" "time" "github.com/dgate-io/dgate/internal/config" "github.com/dgate-io/dgate/internal/pattern" + "github.com/dgate-io/dgate/internal/proxy/proxy_store" "github.com/dgate-io/dgate/internal/proxy/proxy_transport" - "github.com/dgate-io/dgate/internal/proxy/proxystore" "github.com/dgate-io/dgate/internal/proxy/reverse_proxy" "github.com/dgate-io/dgate/internal/router" "github.com/dgate-io/dgate/pkg/cache" @@ -36,16 +35,17 @@ import ( ) type ProxyState struct { + version string debugMode bool + startTime time.Time config *config.DGateConfig - status ProxyStatus logger zerolog.Logger printer console.Printer - stats *ProxyStats - store *proxystore.ProxyStore + store *proxy_store.ProxyStore proxyLock *sync.RWMutex changeHash uint32 + metrics *ProxyMetrics sharedCache cache.TCache changeChan chan *spec.ChangeLog @@ -55,6 +55,7 @@ type ProxyState struct { providers avl.Tree[string, *RequestContextProvider] modPrograms avl.Tree[string, *goja.Program] + raftReady atomic.Bool replicationSettings *ProxyReplication replicationEnabled bool @@ -65,22 +66,6 @@ type ProxyState struct { ProxyHandlerFunc ProxyHandlerFunc } -type ( - ProxyStatus byte -) - -const ( - ProxyStatusStarting ProxyStatus = iota - ProxyStatusModifying - ProxyStatusRunning - ProxyStatusStopping - ProxyStatusClosed -) - -type ProxySnapshot struct { - ResourceManager *resources.ResourceManager `json:"resource_manager"` -} - func NewProxyState(conf *config.DGateConfig) *ProxyState { var logger zerolog.Logger level, err := zerolog.ParseLevel(conf.LogLevel) @@ -107,10 +92,14 @@ func NewProxyState(conf *config.DGateConfig) *ProxyState { var dataStore storage.Storage switch conf.Storage.StorageType { + case config.StorageTypeDebug: + dataStore = storage.NewDebugStore(&storage.DebugStoreConfig{ + Logger: logger, + }) case config.StorageTypeMemory: - memConfig := storage.MemoryStoreConfig{} - memConfig.Logger = logger - dataStore = storage.NewMemoryStore(&memConfig) + dataStore = storage.NewMemoryStore(&storage.MemoryStoreConfig{ + Logger: logger, + }) case config.StorageTypeFile: fileConfig, err := config.StoreConfig[storage.FileStoreConfig](conf.Storage.Config) if err != nil { @@ -121,7 +110,6 @@ func NewProxyState(conf *config.DGateConfig) *ProxyState { default: panic(fmt.Errorf("invalid storage type: %s", conf.Storage.StorageType)) } - // kl := keylock.NewKeyLock() var opt resources.Options if conf.DisableDefaultNamespace { logger.Debug().Msg("default namespace disabled") @@ -132,23 +120,35 @@ func NewProxyState(conf *config.DGateConfig) *ProxyState { if conf.ProxyConfig.EnableConsoleLogger { printer = NewProxyPrinter(logger) } + rpLogger := Logger(&logger, + WithComponentLogger("test-server-http"), + WithDefaultLevel(zerolog.InfoLevel), + ) + storeLogger := Logger(&logger, + WithComponentLogger("proxy_store"), + WithDefaultLevel(zerolog.InfoLevel), + ) state := &ProxyState{ - logger: logger, - debugMode: conf.Debug, - config: conf, - status: ProxyStatusModifying, - stats: NewProxyStats(20), - printer: printer, - routers: avl.NewTree[string, *router.DynamicRouter](), - changeChan: make(chan *spec.ChangeLog, 1), - rm: resources.NewManager(opt), - providers: avl.NewTree[string, *RequestContextProvider](), - modPrograms: avl.NewTree[string, *goja.Program](), - proxyLock: new(sync.RWMutex), - sharedCache: cache.New(), - + version: "unknown", + startTime: time.Now(), + raftReady: atomic.Bool{}, + logger: logger, + debugMode: conf.Debug, + config: conf, + metrics: NewProxyMetrics(), + printer: printer, + routers: avl.NewTree[string, *router.DynamicRouter](), + changeChan: make(chan *spec.ChangeLog, 1), + rm: resources.NewManager(opt), + providers: avl.NewTree[string, *RequestContextProvider](), + modPrograms: avl.NewTree[string, *goja.Program](), + proxyLock: new(sync.RWMutex), + sharedCache: cache.New(), + store: proxy_store.New(dataStore, storeLogger), + replicationEnabled: conf.AdminConfig.Replication != nil, ReverseProxyBuilder: reverse_proxy.NewBuilder(). - FlushInterval(time.Millisecond * 10). + FlushInterval(-1). + ErrorLogger(log.New(rpLogger, "", 0)). CustomRewrite(func(in *http.Request, out *http.Request) { if in.URL.Scheme == "ws" { out.URL.Scheme = "http" @@ -166,63 +166,37 @@ func NewProxyState(conf *config.DGateConfig) *ProxyState { ProxyHandlerFunc: proxyHandler, } - state.store = proxystore.New(dataStore, state.Logger(WithComponentLogger("proxystore"))) - - if err = state.initConfigResources(conf.ProxyConfig.InitResources); err != nil { - panic("error initializing resources: " + err.Error()) + if conf.Debug { + if err = state.initConfigResources(conf.ProxyConfig.InitResources); err != nil { + panic("error initializing resources: " + err.Error()) + } } return state } -func (ps *ProxyState) RestoreState(r io.Reader) error { - // register empty change to refresh state - defer ps.applyChange(nil) - dec := gob.NewDecoder(r) - var snapshot *ProxySnapshot - err := dec.Decode(snapshot) - if err != nil { - return err - } - // TODO: ps.rm.RestoreState(snapshot) - return nil +func (ps *ProxyState) Version() string { + return ps.version } -// func (ps *ProxyState) CaptureState() *ProxySnapshot { -// unlock := ps.keyLock.LockAll() -// defer unlock() -// return &ProxySnapshot{ -// Namespaces: ps.namespaces, -// NamespaceModuleMap: ps.namespaceModuleMap, -// NamespaceRouteMap: ps.namespaceRouteMap, -// NamespaceServiceMap: ps.namespaceServiceMap, -// } -// } - -func (ps *ProxySnapshot) PersistState(w io.Writer) error { - var buf bytes.Buffer - enc := gob.NewEncoder(&buf) - return enc.Encode(ps) -} - -func (ps *ProxyState) Store() *proxystore.ProxyStore { +func (ps *ProxyState) Store() *proxy_store.ProxyStore { return ps.store } -func (ps *ProxyState) Stats() *ProxyStats { - return ps.stats +func (ps *ProxyState) Logger(opts ...LoggerOptions) *zerolog.Logger { + return Logger(&ps.logger, opts...) } type LoggerOptions func(zerolog.Context) zerolog.Context -func (ps *ProxyState) Logger(opts ...LoggerOptions) *zerolog.Logger { - logCtx := ps.logger.With() +func Logger(logger *zerolog.Logger, opts ...LoggerOptions) *zerolog.Logger { + logCtx := logger.With() for _, opt := range opts { logCtx = opt(logCtx) } - logger := logCtx.Logger() - return &logger + lgr := logCtx.Logger() + return &lgr } func WithComponentLogger(component string) LoggerOptions { @@ -241,6 +215,22 @@ func (ps *ProxyState) ChangeHash() uint32 { return ps.changeHash } +func (ps *ProxyState) SetReady() { + if ps.replicationEnabled && !ps.raftReady.Load() { + ps.logger.Info(). + Msgf("Replication status is now ready after %s", time.Since(ps.startTime)) + ps.raftReady.Store(true) + return + } +} + +func (ps *ProxyState) Ready() bool { + if ps.replicationEnabled { + return ps.raftReady.Load() + } + return true +} + func (ps *ProxyState) Raft() *raft.Raft { if ps.replicationEnabled { return ps.replicationSettings.raft @@ -248,12 +238,10 @@ func (ps *ProxyState) Raft() *raft.Raft { return nil } -func (ps *ProxyState) EnableRaft(r *raft.Raft, rc *raft.Config) { +func (ps *ProxyState) SetupRaft(r *raft.Raft, rc *raft.Config) { ps.proxyLock.Lock() defer ps.proxyLock.Unlock() - ps.replicationEnabled = true ps.replicationSettings = NewProxyReplication(r, rc) - } func (ps *ProxyState) WaitForChanges() { @@ -263,6 +251,11 @@ func (ps *ProxyState) WaitForChanges() { func (ps *ProxyState) ApplyChangeLog(log *spec.ChangeLog) error { if ps.replicationEnabled { + if log.Cmd.IsNoop() { + ps.processChangeLog( + log, true, false, + ) + } r := ps.replicationSettings.raft if r.State() != raft.Leader { return raft.ErrNotLeader @@ -296,8 +289,22 @@ func (ps *ProxyState) SharedCache() cache.TCache { return ps.sharedCache } -func (ps *ProxyState) ReloadState() error { - return <-ps.applyChange(nil) +// ReloadState - reload state checks the change logs to see if a reload is required, +// specifying check as false skips this step and automatically reloads +func (ps *ProxyState) ReloadState(check bool, logs ...*spec.ChangeLog) error { + reload := !check + if check { + for _, log := range logs { + if log.Cmd.Resource().IsRelatedTo(spec.Routes) { + reload = true + continue + } + } + } + if reload { + <-ps.applyChange(nil) + } + return nil } func (ps *ProxyState) ProcessChangeLog(log *spec.ChangeLog, reload bool) error { @@ -345,6 +352,7 @@ func loadCertFromFile(certFile, keyFile string) (*tls.Certificate, error) { } func (ps *ProxyState) getDomainCertificate(domain string) (*tls.Certificate, error) { + start := time.Now() allowedDomains := ps.config.ProxyConfig.AllowedDomains domainAllowed := len(allowedDomains) == 0 if !domainAllowed { @@ -357,7 +365,8 @@ func (ps *ProxyState) getDomainCertificate(domain string) (*tls.Certificate, err } if domainAllowed { for _, d := range ps.rm.GetDomainsByPriority() { - if _, match, err := pattern.MatchAnyPattern(domain, d.Patterns); err != nil { + _, match, err := pattern.MatchAnyPattern(domain, d.Patterns) + if err != nil { ps.logger.Error().Msgf("Error checking domain match list: %s", err.Error()) return nil, err } else if match && d.Cert != "" && d.Key != "" { @@ -365,6 +374,8 @@ func (ps *ProxyState) getDomainCertificate(domain string) (*tls.Certificate, err key := fmt.Sprintf("cert:%s:%s:%d", d.Namespace.Name, d.Name, d.CreatedAt.UnixMilli()) if cert, ok := certBucket.Get(key); ok { + ps.metrics.MeasureCertResolutionDuration( + start, domain, true) return cert.(*tls.Certificate), nil } serverCert, err := tls.X509KeyPair([]byte(d.Cert), []byte(d.Key)) @@ -374,6 +385,8 @@ func (ps *ProxyState) getDomainCertificate(domain string) (*tls.Certificate, err return nil, err } certBucket.Set(key, &serverCert) + ps.metrics.MeasureCertResolutionDuration( + start, domain, false) return &serverCert, nil } } @@ -480,6 +493,7 @@ func (ps *ProxyState) FindNamespaceByRequest(r *http.Request) *spec.DGateNamespa if ps.rm.DomainCountEquals(0) && ps.rm.NamespaceCountEquals(1) { return ps.rm.GetFirstNamespace() } + // search through domains for a match var defaultNsHasDomain bool if domains := ps.rm.GetDomainsByPriority(); len(domains) > 0 { @@ -508,6 +522,7 @@ func (ps *ProxyState) FindNamespaceByRequest(r *http.Request) *spec.DGateNamespa } func (ps *ProxyState) ServeHTTP(w http.ResponseWriter, r *http.Request) { + start := time.Now() if ns := ps.FindNamespaceByRequest(r); ns != nil { allowedDomains := ps.config.ProxyConfig.AllowedDomains // if allowed domains is empty, allow all domains @@ -532,14 +547,13 @@ func (ps *ProxyState) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } } - if r.TLS == nil && len(ps.config.ProxyConfig.RedirectHttpsDomains) > 0 { - _, match, err := pattern.MatchAnyPattern(host, ps.config.ProxyConfig.RedirectHttpsDomains) - if err != nil { + redirectDomains := ps.config.ProxyConfig.RedirectHttpsDomains + if r.TLS == nil && len(redirectDomains) > 0 { + if _, match, err := pattern.MatchAnyPattern(host, redirectDomains); err != nil { ps.logger.Error().Msgf("Error checking domain match list: %s", err.Error()) util.WriteStatusCodeError(w, http.StatusInternalServerError) return - } - if match { + } else if match { url := *r.URL url.Scheme = "https" ps.logger.Info().Msgf("Redirecting to https: %s", url.String()) @@ -550,6 +564,9 @@ func (ps *ProxyState) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } if router, ok := ps.routers.Find(ns.Name); ok { + ps.metrics.MeasureNamespaceResolutionDuration( + start, host, ns.Name, + ) router.ServeHTTP(w, r) } else { ps.logger.Debug().Msgf("No router found for namespace: %s", ns.Name) @@ -563,9 +580,3 @@ func (ps *ProxyState) ServeHTTP(w http.ResponseWriter, r *http.Request) { util.WriteStatusCodeError(w, http.StatusNotFound) } } - -func (ps *ProxyState) Snapshot() *ProxySnapshot { - return &ProxySnapshot{ - ResourceManager: ps.rm, - } -} diff --git a/internal/proxy/proxy_stats.go b/internal/proxy/proxy_stats.go deleted file mode 100644 index 050181d..0000000 --- a/internal/proxy/proxy_stats.go +++ /dev/null @@ -1,123 +0,0 @@ -package proxy - -import ( - "fmt" - "sync" - "time" - - "github.com/dgate-io/dgate/pkg/spec" - "github.com/montanaflynn/stats" -) - -type ProxyStats struct { - serviceRequestCount map[string]int64 - RouteRequestCount map[string]int64 - LastNRequestsDurs []float64 - LastNModExtractDurs []float64 - lock *sync.RWMutex - count int64 - windowSize int - amean, stdev, variance, sum float64 -} - -type RequestStats struct { - Service *spec.DGateService - Route *spec.DGateRoute - UpstreamRequestDur time.Duration - MiscDurs map[string]time.Duration -} - -func NewProxyStats(windowSize int) *ProxyStats { - return &ProxyStats{ - lock: &sync.RWMutex{}, - windowSize: windowSize, - serviceRequestCount: make(map[string]int64), - RouteRequestCount: make(map[string]int64), - LastNRequestsDurs: make([]float64, 0, windowSize), - LastNModExtractDurs: make([]float64, 0, windowSize), - } -} - -func NewRequestStats(route *spec.DGateRoute) *RequestStats { - return &RequestStats{ - Service: route.Service, - Route: route, - MiscDurs: make(map[string]time.Duration), - } -} - -func (rs *RequestStats) AddMiscDuration(name string, dur time.Duration) { - rs.MiscDurs[name] = dur -} - -func (rs *RequestStats) AddUpstreamRequestDuration(dur time.Duration) { - rs.UpstreamRequestDur = dur -} - -func (rs *RequestStats) String() string { - return fmt.Sprintf( - "service=%s route=%s upstream_request_dur=%s module_durs=%s", - rs.Service.Name, - rs.Route.Name, - rs.UpstreamRequestDur, - rs.MiscDurs, - ) -} - -func (ps *ProxyStats) AddRequestStats(rs *RequestStats) { - reqDur := float64(rs.UpstreamRequestDur.Milliseconds()) - ps.lock.Lock() - defer ps.lock.Unlock() - ps.count++ - - if ps.count > 1 { - ps.amean, _ = stats.Mean(ps.LastNRequestsDurs) - ps.variance, _ = stats.Variance(ps.LastNRequestsDurs) - ps.sum, _ = stats.Sum(ps.LastNRequestsDurs) - if ps.count < 3 { - goto SKIP - } - ps.stdev, _ = stats.StandardDeviationSample(ps.LastNRequestsDurs) - } -SKIP: - - if rs.Service != nil { - if src, ok := ps.serviceRequestCount[rs.Service.Name]; ok { - ps.serviceRequestCount[rs.Service.Name] = src + 1 - } else { - ps.serviceRequestCount[rs.Service.Name] = 1 - } - } - - if rs.Route != nil { - if src, ok := ps.RouteRequestCount[rs.Route.Name]; ok { - ps.RouteRequestCount[rs.Route.Name] = src + 1 - } else { - ps.RouteRequestCount[rs.Route.Name] = 1 - } - } - reqDurLen := len(ps.LastNRequestsDurs) - if reqDurLen >= ps.windowSize { - ps.LastNRequestsDurs = ps.LastNRequestsDurs[1:] - ps.LastNModExtractDurs = ps.LastNModExtractDurs[1:] - } - ps.LastNRequestsDurs = append(ps.LastNRequestsDurs, reqDur) - ps.LastNModExtractDurs = append(ps.LastNModExtractDurs, float64(rs.MiscDurs["moduleExtract"].Nanoseconds())) -} - -func (ps *ProxyStats) Snapshot() map[string]any { - data := make(map[string]any) - ps.lock.RLock() - defer ps.lock.RUnlock() - data["req_count"] = ps.count - data["service_request_count"] = ps.serviceRequestCount - data["route_request_count"] = ps.RouteRequestCount - // data["request_durs"] = ps.LastNRequestsDurs - data["mod_extracts_ns"] = ps.LastNModExtractDurs - data["req_dur_mean_ms"] = ps.amean - data["req_dur_stddev_ms"] = ps.stdev - data["req_dur_variance_ms"] = ps.variance - data["req_dur_sum_ms"] = ps.sum - data["req_dur_window_size"] = ps.windowSize - return data -} diff --git a/internal/proxy/proxystore/proxystore.go b/internal/proxy/proxy_store/proxystore.go similarity index 98% rename from internal/proxy/proxystore/proxystore.go rename to internal/proxy/proxy_store/proxystore.go index 148b8ea..908ace5 100644 --- a/internal/proxy/proxystore/proxystore.go +++ b/internal/proxy/proxy_store/proxystore.go @@ -1,4 +1,4 @@ -package proxystore +package proxy_store import ( "encoding/json" @@ -58,7 +58,6 @@ func (store *ProxyStore) FetchChangeLogs() ([]*spec.ChangeLog, error) { return logs, nil } -// TODO: add retry for failed store operations func (store *ProxyStore) StoreChangeLog(cl *spec.ChangeLog) error { clBytes, err := json.Marshal(*cl) if err != nil { diff --git a/internal/proxy/proxy_transport.go b/internal/proxy/proxy_transport.go index a58eba2..397c010 100644 --- a/internal/proxy/proxy_transport.go +++ b/internal/proxy/proxy_transport.go @@ -10,7 +10,7 @@ import ( "golang.org/x/net/http2" ) -func setupTranportFromConfig( +func setupTranportsFromConfig( c config.DGateHttpTransportConfig, modifyTransport func(*net.Dialer, *http.Transport), ) http.RoundTripper { diff --git a/internal/proxy/proxy_transport/proxy_transport.go b/internal/proxy/proxy_transport/proxy_transport.go index 10f65a9..547414d 100644 --- a/internal/proxy/proxy_transport/proxy_transport.go +++ b/internal/proxy/proxy_transport/proxy_transport.go @@ -81,6 +81,9 @@ func create( if requestTimeout < 0 { return nil, errors.New("requestTimeout must be greater than or equal to 0") } + if requestTimeout == 0 && retries == 0 { + return transport, nil + } return &retryRoundTripper{ transport: transport, retries: retries, diff --git a/internal/proxy/request_context.go b/internal/proxy/request_context.go index 9fb3639..15168d9 100644 --- a/internal/proxy/request_context.go +++ b/internal/proxy/request_context.go @@ -2,8 +2,11 @@ package proxy import ( "context" + "crypto/tls" + "net" "net/http" + "github.com/dgate-io/dgate/internal/proxy/reverse_proxy" "github.com/dgate-io/dgate/pkg/spec" ) @@ -12,33 +15,65 @@ type S string type RequestContextProvider struct { ctx context.Context route *spec.DGateRoute + rpb reverse_proxy.Builder modBuf ModuleBuffer } type RequestContext struct { pattern string - context context.Context + ctx context.Context route *spec.DGateRoute rw spec.ResponseWriterTracker req *http.Request provider *RequestContextProvider } -func NewRequestContextProvider(route *spec.DGateRoute) *RequestContextProvider { +func NewRequestContextProvider( + route *spec.DGateRoute, + ps *ProxyState, +) *RequestContextProvider { ctx := context.Background() - // set context values ctx = context.WithValue(ctx, spec.Name("route"), route.Name) ctx = context.WithValue(ctx, spec.Name("namespace"), route.Namespace.Name) - serviceName := "" + + var rpb reverse_proxy.Builder if route.Service != nil { - serviceName = route.Service.Name + ctx = context.WithValue(ctx, spec.Name("service"), route.Service.Name) + transport := setupTranportsFromConfig( + ps.config.ProxyConfig.Transport, + func(dialer *net.Dialer, t *http.Transport) { + t.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: route.Service.TLSSkipVerify, + } + dialer.Timeout = route.Service.ConnectTimeout + t.ForceAttemptHTTP2 = route.Service.HTTP2Only + }, + ) + proxy, err := ps.ProxyTransportBuilder.Clone(). + Transport(transport). + Retries(route.Service.Retries). + RetryTimeout(route.Service.RetryTimeout). + RequestTimeout(route.Service.RequestTimeout). + Build() + if err != nil { + panic(err) + } + rpb = ps.ReverseProxyBuilder.Clone(). + Transport(proxy). + ProxyRewrite( + route.StripPath, + route.PreserveHost, + route.Service.DisableQueryParams, + ps.config.ProxyConfig.DisableXForwardedHeaders, + ) + } - ctx = context.WithValue(ctx, spec.Name("service"), serviceName) return &RequestContextProvider{ ctx: ctx, route: route, + rpb: rpb, } } @@ -58,6 +93,22 @@ func (reqCtxProvider *RequestContextProvider) CreateRequestContext( route: reqCtxProvider.route, provider: reqCtxProvider, pattern: pattern, - context: ctx, + ctx: ctx, } } + +func (reqCtx *RequestContext) Context() context.Context { + return reqCtx.ctx +} + +func (reqCtx *RequestContext) Route() *spec.DGateRoute { + return reqCtx.route +} + +func (reqCtx *RequestContext) Pattern() string { + return reqCtx.pattern +} + +func (reqCtx *RequestContext) Request() *http.Request { + return reqCtx.req +} diff --git a/internal/proxy/reverse_proxy/reverse_proxy.go b/internal/proxy/reverse_proxy/reverse_proxy.go index 52f402f..2964bc8 100644 --- a/internal/proxy/reverse_proxy/reverse_proxy.go +++ b/internal/proxy/reverse_proxy/reverse_proxy.go @@ -58,15 +58,19 @@ type Builder interface { var _ Builder = (*reverseProxyBuilder)(nil) type reverseProxyBuilder struct { - rewrite RewriteFunc - errorLogger *log.Logger - customRewrite RewriteFunc - upstreamUrl *url.URL - proxyPattern string - transport http.RoundTripper - flushInterval time.Duration - modifyResponse ModifyResponseFunc - errorHandler ErrorHandlerFunc + errorLogger *log.Logger + proxyRewrite RewriteFunc + customRewrite RewriteFunc + upstreamUrl *url.URL + proxyPattern string + transport http.RoundTripper + flushInterval time.Duration + modifyResponse ModifyResponseFunc + errorHandler ErrorHandlerFunc + stripPath bool + preserveHost bool + disableQueryParams bool + xForwardedHeaders bool } func NewBuilder() Builder { @@ -75,7 +79,7 @@ func NewBuilder() Builder { func (b *reverseProxyBuilder) Clone() Builder { return &reverseProxyBuilder{ - rewrite: b.rewrite, + proxyRewrite: b.proxyRewrite, errorLogger: b.errorLogger, customRewrite: b.customRewrite, upstreamUrl: b.upstreamUrl, @@ -123,19 +127,14 @@ func (b *reverseProxyBuilder) ProxyRewrite( disableQueryParams bool, xForwardedHeaders bool, ) Builder { - b.rewrite = func(in, out *http.Request) { - in.URL.Scheme = b.upstreamUrl.Scheme - in.URL.Host = b.upstreamUrl.Host - - b.stripPath(stripPath)(in, out) - b.preserveHost(preserveHost)(in, out) - b.disableQueryParams(disableQueryParams)(in, out) - b.xForwardedHeaders(xForwardedHeaders)(in, out) - } + b.stripPath = stripPath + b.preserveHost = preserveHost + b.disableQueryParams = disableQueryParams + b.xForwardedHeaders = xForwardedHeaders return b } -func (b *reverseProxyBuilder) stripPath(strip bool) RewriteFunc { +func (b *reverseProxyBuilder) rewriteStripPath(strip bool) RewriteFunc { return func(in, out *http.Request) { reqCall := in.URL.Path proxyPatternPath := b.proxyPattern @@ -160,7 +159,7 @@ func (b *reverseProxyBuilder) stripPath(strip bool) RewriteFunc { } } -func (b *reverseProxyBuilder) preserveHost(preserve bool) RewriteFunc { +func (b *reverseProxyBuilder) rewritePreserveHost(preserve bool) RewriteFunc { return func(in, out *http.Request) { scheme := "http" out.URL.Host = b.upstreamUrl.Host @@ -182,7 +181,7 @@ func (b *reverseProxyBuilder) preserveHost(preserve bool) RewriteFunc { } } -func (b *reverseProxyBuilder) disableQueryParams(disableQueryParams bool) RewriteFunc { +func (b *reverseProxyBuilder) rewriteDisableQueryParams(disableQueryParams bool) RewriteFunc { return func(in, out *http.Request) { if !disableQueryParams { targetQuery := b.upstreamUrl.RawQuery @@ -197,7 +196,7 @@ func (b *reverseProxyBuilder) disableQueryParams(disableQueryParams bool) Rewrit } } -func (b *reverseProxyBuilder) xForwardedHeaders(xForwardedHeaders bool) RewriteFunc { +func (b *reverseProxyBuilder) rewriteXForwardedHeaders(xForwardedHeaders bool) RewriteFunc { return func(in, out *http.Request) { if xForwardedHeaders { clientIP, _, err := net.SplitHostPort(in.RemoteAddr) @@ -228,10 +227,7 @@ var ( ErrEmptyProxyPattern = errors.New("proxy pattern cannot be empty") ) -func (b *reverseProxyBuilder) Build( - upstreamUrl *url.URL, - proxyPattern string, -) (http.Handler, error) { +func (b *reverseProxyBuilder) Build(upstreamUrl *url.URL, proxyPattern string) (http.Handler, error) { if upstreamUrl == nil { return nil, ErrNilUpstreamUrl } @@ -262,12 +258,13 @@ func (b *reverseProxyBuilder) Build( proxy.Transport = b.transport proxy.ErrorLog = b.errorLogger proxy.Rewrite = func(pr *httputil.ProxyRequest) { + b.rewriteStripPath(b.stripPath)(pr.In, pr.Out) + b.rewritePreserveHost(b.preserveHost)(pr.In, pr.Out) + b.rewriteDisableQueryParams(b.disableQueryParams)(pr.In, pr.Out) + b.rewriteXForwardedHeaders(b.xForwardedHeaders)(pr.In, pr.Out) if b.customRewrite != nil { b.customRewrite(pr.In, pr.Out) } - if b.rewrite != nil { - b.rewrite(pr.In, pr.Out) - } if pr.Out.URL.Path == "/" { pr.Out.URL.Path = "" } diff --git a/internal/proxy/runtime_context.go b/internal/proxy/runtime_context.go index 3f95f11..e523b5c 100644 --- a/internal/proxy/runtime_context.go +++ b/internal/proxy/runtime_context.go @@ -5,6 +5,7 @@ import ( "errors" "sort" "strings" + "time" "github.com/dgate-io/dgate/pkg/eventloop" "github.com/dgate-io/dgate/pkg/modules" @@ -54,11 +55,19 @@ func NewRuntimeContext( if mod.Type == spec.ModuleTypeJavascript { return []byte(mod.Payload), nil } - // TODO: add transpilation cache somewhere + var err error + var key string + transpileBucket := proxyState.sharedCache.Bucket("ts-transpile") + if key, err = HashString(0, mod.Payload); err == nil { + if code, ok := transpileBucket.Get(key); ok { + return code.([]byte), nil + } + } payload, err := typescript.Transpile(mod.Payload) if err != nil { return nil, err } + transpileBucket.SetWithTTL(key, []byte(payload), time.Minute*30) return []byte(payload), nil } }) @@ -73,7 +82,7 @@ func (rtCtx *runtimeContext) SetRequestContext( reqCtx *RequestContext, pathParams map[string]string, ) { if reqCtx != nil { - if err := reqCtx.context.Err(); err != nil { + if err := reqCtx.ctx.Err(); err != nil { panic("context is already closed: " + err.Error()) } } @@ -85,10 +94,7 @@ func (rtCtx *runtimeContext) Clean() { } func (rtCtx *runtimeContext) Context() context.Context { - if rtCtx.reqCtx == nil { - panic("request context is not set") - } - return rtCtx.reqCtx.context + return rtCtx.reqCtx.ctx } func (rtCtx *runtimeContext) EventLoop() *eventloop.EventLoop { diff --git a/internal/proxy/util.go b/internal/proxy/util.go index 8e17e0c..af8856e 100644 --- a/internal/proxy/util.go +++ b/internal/proxy/util.go @@ -3,13 +3,17 @@ package proxy import ( "bytes" "cmp" + "encoding/hex" "encoding/json" "errors" + "hash" "hash/crc32" + "net/http" + "slices" "sort" ) -func HashAny[T any](salt uint32, objs ...any) (uint32, error) { +func saltHash[T any](salt uint32, objs ...T) (hash.Hash32, error) { hash := crc32.NewIEEE() if salt != 0 { // uint32 to byte array @@ -22,17 +26,33 @@ func HashAny[T any](salt uint32, objs ...any) (uint32, error) { } if len(objs) == 0 { - return 0, errors.New("no objects provided") + return nil, errors.New("no objects provided") } for _, r := range objs { b := bytes.Buffer{} err := json.NewEncoder(&b).Encode(r) if err != nil { - return 0, err + return nil, err } hash.Write(b.Bytes()) } - return hash.Sum32(), nil + return hash, nil +} + +func HashAny[T any](salt uint32, objs ...T) (uint32, error) { + h, err := saltHash(salt, objs...) + if err != nil { + return 0, err + } + return h.Sum32(), nil +} + +func HashString[T any](salt uint32, objs ...T) (string, error) { + h, err := saltHash(salt, objs...) + if err != nil { + return "", err + } + return hex.EncodeToString(h.Sum(nil)), nil } func findInSortedWith[T any, K cmp.Ordered](arr []T, k K, f func(T) K) (T, bool) { @@ -45,3 +65,34 @@ func findInSortedWith[T any, K cmp.Ordered](arr []T, k K, f func(T) K) (T, bool) } return t, false } + +var validMethods = []string{ + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + http.MethodOptions, + http.MethodHead, + http.MethodConnect, + http.MethodTrace, +} + +func ValidateMethods(methods []string) error { + methodCount := 0 + for _, m := range methods { + if m == "" { + continue + } else if slices.ContainsFunc(validMethods, func(v string) bool { + return v == m + }) { + methodCount++ + } else { + return errors.New("unsupported method: " + m) + } + } + if methodCount == 0 { + return errors.New("no valid methods provided") + } + return nil +} diff --git a/performance-tests/long-perf-test.js b/performance-tests/long-perf-test.js index a0b8776..7b362e2 100644 --- a/performance-tests/long-perf-test.js +++ b/performance-tests/long-perf-test.js @@ -1,20 +1,56 @@ import http from "k6/http"; import { check, sleep } from 'k6'; +// export let options = { +// stages: [ +// { duration: '5s', target: 200}, +// { duration: '1h', target: 300}, +// { duration: '1h', target: 100}, +// ], +// }; + +const n = 15; export let options = { - stages: [ - { duration: '5s', target: 200}, - { duration: '1h', target: 300}, - { duration: '1h', target: 100}, - ], + scenarios: { + modtest: { + executor: 'constant-vus', + vus: n, + duration: '20m', + exec: 'dgatePath', + env: { DGATE_PATH: '/modtest' }, + // startTime: '25s', + gracefulStop: '5s', + }, + svctest: { + executor: 'constant-vus', + vus: n, + duration: '20m', + exec: 'dgatePath', + env: { DGATE_PATH: "/svctest" }, + // startTime: '10m', + gracefulStop: '5s', + }, + svctest_500ms: { + executor: 'constant-vus', + vus: n, + duration: '20m', + exec: 'dgatePath', + env: { DGATE_PATH: "/svctest?wait=150ms" }, + // startTime: '20m', + gracefulStop: '5s', + }, + }, + discardResponseBodies: true, }; -let url = "http://localhost:80"; -let i = 0; - -export default async function() { - let res = http.get(url + "/modtest", { +export function dgatePath() { + const dgatePath = __ENV.PROXY_URL || 'http://localhost'; + const path = __ENV.DGATE_PATH; + let res = http.get(dgatePath + path, { headers: { Host: 'dgate.dev' }, }); - check(res, { 'status: 204': (r) => r.status == 204 }); + let results = {}; + results[path + ': status is ' + res.status] = + (r) => r.status >= 200 && r.status < 400; + check(res, results); }; diff --git a/performance-tests/perf-test.js b/performance-tests/perf-test.js index 3146807..3b997bd 100644 --- a/performance-tests/perf-test.js +++ b/performance-tests/perf-test.js @@ -1,28 +1,28 @@ import http from "k6/http"; import { check } from 'k6'; -const n = 10; +const n = 15; export let options = { scenarios: { - modtest: { - executor: 'constant-vus', - vus: n, - duration: '20s', - // same function as the scenario above, but with different env vars - exec: 'dgatePath', - env: { DGATE_PATH: '/modtest' }, - // startTime: '25s', - gracefulStop: '5s', - }, - // svctest: { + // modtest: { // executor: 'constant-vus', // vus: n, // duration: '20s', - // exec: 'dgatePath', // same function as the scenario above, but with different env vars - // env: { DGATE_PATH: "/svctest" }, + // // same function as the scenario above, but with different env vars + // exec: 'dgatePath', + // env: { DGATE_PATH: '/modtest' }, // // startTime: '25s', // gracefulStop: '5s', // }, + svctest: { + executor: 'constant-vus', + vus: n, + duration: '20s', + exec: 'dgatePath', // same function as the scenario above, but with different env vars + env: { DGATE_PATH: "/svctest" }, + // startTime: '25s', + gracefulStop: '5s', + }, // blank: { // executor: 'constant-vus', // vus: n, diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index 0a0e87a..3c7cd59 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -103,22 +103,23 @@ func (cache *cacheImpl) newBucket( b.mutex.Lock() defer b.mutex.Unlock() for { - t, v, ok := b.ttlQueue.Peak() - if !ok { + if t, v, ok := b.ttlQueue.Peak(); !ok { break - } - - if t != v.exp.UnixMilli() { - // TODO: test set TTL high, then set low + } else { + // if the expiration time is not the same as the value's expiration time, + // it means the value has been updated, so we pop it from the queue + if t != v.exp.UnixMilli() { + b.ttlQueue.Pop() + continue + } + // if the expiration time is in the future, we break + if v.exp.After(time.Now()) { + break + } + // if the expiration time is in the past, we pop the value from the queue b.ttlQueue.Pop() - continue - } - - if v.exp.After(time.Now()) { - break + delete(b.items, v.key) } - b.ttlQueue.Pop() - delete(b.items, v.key) } }, }) diff --git a/pkg/dgclient/collection.go b/pkg/dgclient/collection_client.go similarity index 84% rename from pkg/dgclient/collection.go rename to pkg/dgclient/collection_client.go index 73066ce..2e4c6f1 100644 --- a/pkg/dgclient/collection.go +++ b/pkg/dgclient/collection_client.go @@ -33,7 +33,10 @@ func (d *DGateClient) DeleteCollection(name, namespace string) error { return commonDelete(d.client, uri, name, namespace) } -func (d *DGateClient) ListCollection() ([]*spec.Collection, error) { +func (d *DGateClient) ListCollection(namespace string) ([]*spec.Collection, error) { + query := d.baseUrl.Query() + query.Set("namespace", namespace) + d.baseUrl.RawQuery = query.Encode() uri, err := url.JoinPath(d.baseUrl.String(), "/api/v1/collection") if err != nil { return nil, err diff --git a/pkg/dgclient/common.go b/pkg/dgclient/common.go index 16973b7..8b22b09 100644 --- a/pkg/dgclient/common.go +++ b/pkg/dgclient/common.go @@ -13,6 +13,23 @@ type clientDoer interface { Do(req *http.Request) (*http.Response, error) } +type NamePayload struct { + Name string `json:"name"` +} + +type NamespacePayload struct { + Namespace string `json:"namespace"` +} + +type DocumentPayload struct { + Document string `json:"document"` + Limit int `json:"limit"` + Offset int `json:"offset"` + Count int `json:"count"` + Collection string `json:"collection"` + Namespace string `json:"namespace"` +} + type ListResponseWrapper[T any] struct { StatusCode int Count int @@ -88,17 +105,8 @@ func commonPut[T any](client clientDoer, uri string, item T) error { return nil } -type M map[string]any - -func commonDelete(client clientDoer, uri, name, namespace string) error { - payload, err := json.Marshal(M{ - "name": name, - "namespace": namespace, - }) - if err != nil { - return err - } - req, err := http.NewRequest("DELETE", uri, bytes.NewReader(payload)) +func basicDelete(client clientDoer, uri string, rdr io.Reader) error { + req, err := http.NewRequest("DELETE", uri, rdr) if err != nil { return err } @@ -113,11 +121,24 @@ func commonDelete(client clientDoer, uri, name, namespace string) error { return nil } +type M map[string]any + +func commonDelete(client clientDoer, uri, name, namespace string) error { + payload, err := json.Marshal(M{ + "name": name, + "namespace": namespace, + }) + if err != nil { + return err + } + return basicDelete(client, uri, bytes.NewReader(payload)) +} + func validateStatusCode(code int) error { if code < 300 { return nil } else if code < 400 { - return errors.New("unexpected Redirect") + return errors.New("redirect from server; retry with the --follow flag") } return fmt.Errorf("error code %d: %s", code, http.StatusText(code)) } diff --git a/pkg/dgclient/dgclient.go b/pkg/dgclient/dgclient.go index a14d5f7..c8aaa48 100644 --- a/pkg/dgclient/dgclient.go +++ b/pkg/dgclient/dgclient.go @@ -55,10 +55,11 @@ func WithHttpClient(client *http.Client) Options { } type customTransport struct { - UserAgent string - Username string - Password string - Transport http.RoundTripper + UserAgent string + Username string + Password string + VerboseLog bool + Transport http.RoundTripper } func (ct *customTransport) RoundTrip(req *http.Request) (*http.Response, error) { @@ -73,8 +74,12 @@ func (ct *customTransport) RoundTrip(req *http.Request) (*http.Response, error) if err != nil { return nil, err } - fmt.Printf("%s %s - %s %v\n", req.Method, - req.URL.String(), resp.Status, time.Since(start)) + if ct.VerboseLog { + fmt.Printf("%s %s %s - %s %v\n", + resp.Proto, req.Method, req.URL, + resp.Status, time.Since(start), + ) + } return resp, err } @@ -99,6 +104,17 @@ func WithBasicAuth(username, password string) Options { } } +func WithFollowRedirect(follow bool) Options { + return func(d *DGateClient) { + if follow { + return + } + d.client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + } +} + func WithUserAgent(ua string) Options { if ua == "" { return func(dc *DGateClient) {} @@ -118,3 +134,19 @@ func WithUserAgent(ua string) Options { } } } + +func WithVerboseLogging(on bool) Options { + return func(d *DGateClient) { + if d.client.Transport == nil { + d.client.Transport = http.DefaultTransport + } + if ct, ok := d.client.Transport.(*customTransport); ok { + ct.VerboseLog = on + } else { + d.client.Transport = &customTransport{ + VerboseLog: on, + Transport: d.client.Transport, + } + } + } +} diff --git a/pkg/dgclient/document.go b/pkg/dgclient/document.go deleted file mode 100644 index 2578227..0000000 --- a/pkg/dgclient/document.go +++ /dev/null @@ -1,42 +0,0 @@ -package dgclient - -import ( - "net/url" - - "github.com/dgate-io/dgate/pkg/spec" -) - -func (d *DGateClient) GetDocument(id, namespace string) (*spec.Document, error) { - query := d.baseUrl.Query() - query.Set("namespace", namespace) - d.baseUrl.RawQuery = query.Encode() - uri, err := url.JoinPath(d.baseUrl.String(), "/api/v1/document", id) - if err != nil { - return nil, err - } - return commonGet[spec.Document](d.client, uri) -} - -func (d *DGateClient) CreateDocument(doc *spec.Document) error { - uri, err := url.JoinPath(d.baseUrl.String(), "/api/v1/document") - if err != nil { - return err - } - return commonPut(d.client, uri, doc) -} - -func (d *DGateClient) DeleteDocument(id, namespace string) error { - uri, err := url.JoinPath(d.baseUrl.String(), "/api/v1/document") - if err != nil { - return err - } - return commonDelete(d.client, uri, id, namespace) -} - -func (d *DGateClient) ListDocument() ([]*spec.Document, error) { - uri, err := url.JoinPath(d.baseUrl.String(), "/api/v1/document") - if err != nil { - return nil, err - } - return commonGetList[*spec.Document](d.client, uri) -} diff --git a/pkg/dgclient/document_client.go b/pkg/dgclient/document_client.go new file mode 100644 index 0000000..0be79be --- /dev/null +++ b/pkg/dgclient/document_client.go @@ -0,0 +1,61 @@ +package dgclient + +import ( + "net/url" + + "github.com/dgate-io/dgate/pkg/spec" +) + +func (d *DGateClient) GetDocument(id, namespace, collection string) (*spec.Document, error) { + query := d.baseUrl.Query() + query.Set("namespace", namespace) + query.Set("collection", collection) + d.baseUrl.RawQuery = query.Encode() + uri, err := url.JoinPath(d.baseUrl.String(), "/api/v1/document", id) + if err != nil { + return nil, err + } + return commonGet[spec.Document](d.client, uri) +} + +func (d *DGateClient) CreateDocument(doc *spec.Document) error { + uri, err := url.JoinPath(d.baseUrl.String(), "/api/v1/document") + if err != nil { + return err + } + return commonPut(d.client, uri, doc) +} + +func (d *DGateClient) DeleteDocument(id, namespace, collection string) error { + query := d.baseUrl.Query() + query.Set("namespace", namespace) + query.Set("collection", collection) + uri, err := url.JoinPath(d.baseUrl.String(), "/api/v1/document", id) + if err != nil { + return err + } + return basicDelete(d.client, uri, nil) +} + +func (d *DGateClient) DeleteAllDocument(namespace, collection string) error { + query := d.baseUrl.Query() + query.Set("namespace", namespace) + query.Set("collection", collection) + uri, err := url.JoinPath(d.baseUrl.String(), "/api/v1/document") + if err != nil { + return err + } + return basicDelete(d.client, uri, nil) +} + +func (d *DGateClient) ListDocument(namespace, collection string) ([]*spec.Document, error) { + query := d.baseUrl.Query() + query.Set("namespace", namespace) + query.Set("collection", collection) + d.baseUrl.RawQuery = query.Encode() + uri, err := url.JoinPath(d.baseUrl.String(), "/api/v1/document") + if err != nil { + return nil, err + } + return commonGetList[*spec.Document](d.client, uri) +} diff --git a/pkg/dgclient/domain.go b/pkg/dgclient/domain_client.go similarity index 84% rename from pkg/dgclient/domain.go rename to pkg/dgclient/domain_client.go index 735dccf..145aa11 100644 --- a/pkg/dgclient/domain.go +++ b/pkg/dgclient/domain_client.go @@ -33,7 +33,10 @@ func (d *DGateClient) DeleteDomain(name, namespace string) error { return commonDelete(d.client, uri, name, namespace) } -func (d *DGateClient) ListDomain() ([]*spec.Domain, error) { +func (d *DGateClient) ListDomain(namespace string) ([]*spec.Domain, error) { + query := d.baseUrl.Query() + query.Set("namespace", namespace) + d.baseUrl.RawQuery = query.Encode() uri, err := url.JoinPath(d.baseUrl.String(), "/api/v1/domain") if err != nil { return nil, err diff --git a/pkg/dgclient/module.go b/pkg/dgclient/module_client.go similarity index 84% rename from pkg/dgclient/module.go rename to pkg/dgclient/module_client.go index a114b26..aa93b6a 100644 --- a/pkg/dgclient/module.go +++ b/pkg/dgclient/module_client.go @@ -33,7 +33,10 @@ func (d *DGateClient) DeleteModule(name, namespace string) error { return commonDelete(d.client, uri, name, namespace) } -func (d *DGateClient) ListModule() ([]*spec.Module, error) { +func (d *DGateClient) ListModule(namespace string) ([]*spec.Module, error) { + query := d.baseUrl.Query() + query.Set("namespace", namespace) + d.baseUrl.RawQuery = query.Encode() uri, err := url.JoinPath(d.baseUrl.String(), "/api/v1/module") if err != nil { return nil, err diff --git a/pkg/dgclient/namespace.go b/pkg/dgclient/namespace_client.go similarity index 100% rename from pkg/dgclient/namespace.go rename to pkg/dgclient/namespace_client.go diff --git a/pkg/dgclient/route.go b/pkg/dgclient/route_client.go similarity index 84% rename from pkg/dgclient/route.go rename to pkg/dgclient/route_client.go index 4193e27..d0c38da 100644 --- a/pkg/dgclient/route.go +++ b/pkg/dgclient/route_client.go @@ -33,7 +33,10 @@ func (d *DGateClient) DeleteRoute(name, namespace string) error { return commonDelete(d.client, uri, name, namespace) } -func (d *DGateClient) ListRoute() ([]*spec.Route, error) { +func (d *DGateClient) ListRoute(namespace string) ([]*spec.Route, error) { + query := d.baseUrl.Query() + query.Set("namespace", namespace) + d.baseUrl.RawQuery = query.Encode() uri, err := url.JoinPath(d.baseUrl.String(), "/api/v1/route") if err != nil { return nil, err diff --git a/pkg/dgclient/secret_client.go b/pkg/dgclient/secret_client.go new file mode 100644 index 0000000..b3d323f --- /dev/null +++ b/pkg/dgclient/secret_client.go @@ -0,0 +1,45 @@ +package dgclient + +import ( + "net/url" + + "github.com/dgate-io/dgate/pkg/spec" +) + +func (d *DGateClient) GetSecret(name, namespace string) (*spec.Secret, error) { + query := d.baseUrl.Query() + query.Set("namespace", namespace) + d.baseUrl.RawQuery = query.Encode() + uri, err := url.JoinPath(d.baseUrl.String(), "/api/v1/secret", name) + if err != nil { + return nil, err + } + return commonGet[spec.Secret](d.client, uri) +} + +func (d *DGateClient) CreateSecret(sec *spec.Secret) error { + uri, err := url.JoinPath(d.baseUrl.String(), "/api/v1/secret") + if err != nil { + return err + } + return commonPut(d.client, uri, sec) +} + +func (d *DGateClient) DeleteSecret(name, namespace string) error { + uri, err := url.JoinPath(d.baseUrl.String(), "/api/v1/secret") + if err != nil { + return err + } + return commonDelete(d.client, uri, name, namespace) +} + +func (d *DGateClient) ListSecret(namespace string) ([]*spec.Secret, error) { + query := d.baseUrl.Query() + query.Set("namespace", namespace) + d.baseUrl.RawQuery = query.Encode() + uri, err := url.JoinPath(d.baseUrl.String(), "/api/v1/secret") + if err != nil { + return nil, err + } + return commonGetList[*spec.Secret](d.client, uri) +} diff --git a/pkg/dgclient/service.go b/pkg/dgclient/service_client.go similarity index 84% rename from pkg/dgclient/service.go rename to pkg/dgclient/service_client.go index efa79d0..7187cf6 100644 --- a/pkg/dgclient/service.go +++ b/pkg/dgclient/service_client.go @@ -33,7 +33,10 @@ func (d *DGateClient) DeleteService(name, namespace string) error { return commonDelete(d.client, uri, name, namespace) } -func (d *DGateClient) ListService() ([]*spec.Service, error) { +func (d *DGateClient) ListService(namespace string) ([]*spec.Service, error) { + query := d.baseUrl.Query() + query.Set("namespace", namespace) + d.baseUrl.RawQuery = query.Encode() uri, err := url.JoinPath(d.baseUrl.String(), "/api/v1/service") if err != nil { return nil, err diff --git a/pkg/modules/dgate/crypto/crypto_mod.go b/pkg/modules/dgate/crypto/crypto_mod.go index e774101..29303af 100644 --- a/pkg/modules/dgate/crypto/crypto_mod.go +++ b/pkg/modules/dgate/crypto/crypto_mod.go @@ -13,6 +13,7 @@ import ( "fmt" "hash" "math/big" + "strings" "github.com/dgate-io/dgate/pkg/modules" "github.com/dgate-io/dgate/pkg/util" @@ -31,30 +32,40 @@ func New(modCtx modules.RuntimeContext) modules.GoModule { } func (c *CryptoModule) Exports() *modules.Exports { - return &modules.Exports{ - Named: map[string]any{ - "createHash": c.createHash, - "createHmac": c.createHmac, - "createSign": nil, // TODO: not implemented - "createVerify": nil, // TODO: not implemented - "hmac": c.hmac, - "md5": c.md5, - "randomBytes": c.randomBytes, - "randomInt": c.randomInt, // - "sha1": c.sha1, - "sha256": c.sha256, - "sha384": c.sha384, - "sha512": c.sha512, - "sha512_224": c.sha512_224, - "sha512_256": c.sha512_256, - "getHashes": func() []string { - return []string{ - // TODO: add more hashes - "md5", "sha1", "sha256", "sha384", "sha512", "sha512-224", "sha512-256", - } - }, - "hexEncode": c.hexEncode, + // Hash functions + hashAlgos := map[string]HashFunc{ + "md5": c.md5, + "sha1": c.sha1, + "sha256": c.sha256, + "sha384": c.sha384, + "sha512": c.sha512, + "sha512_224": c.sha512_224, + "sha512_256": c.sha512_256, + } + // Named exports + namedExports := map[string]any{ + "createHash": c.createHash, + "createHmac": c.createHmac, + "createSign": nil, // TODO: not implemented + "createVerify": nil, // TODO: not implemented + "hmac": c.hmac, + "randomBytes": c.randomBytes, + "randomInt": c.randomInt, + "getHashes": func() []string { + keys := make([]string, 0, len(hashAlgos)) + for k := range hashAlgos { + keys = append(keys, strings.Replace(k, "_", "-", -1)) + } + return keys }, + "hexEncode": c.hexEncode, + } + + for k, v := range hashAlgos { + namedExports[k] = v + } + return &modules.Exports{ + Named: namedExports, } } @@ -99,6 +110,8 @@ func (c *CryptoModule) randomUUID() string { return uuid.New().String() } +type HashFunc func(data any, encoding string) (any, error) + func (c *CryptoModule) md5(data any, encoding string) (any, error) { return c.update("md5", data, encoding) } diff --git a/pkg/modules/dgate/http/http_mod.go b/pkg/modules/dgate/http/http_mod.go index 2bdf74b..ac9114b 100644 --- a/pkg/modules/dgate/http/http_mod.go +++ b/pkg/modules/dgate/http/http_mod.go @@ -170,15 +170,13 @@ func asyncDo(client http.Client, req *http.Request) chan AsyncResults[*http.Resp ch := make(chan AsyncResults[*http.Response], 1) go func() { start := time.Now() - resp, err := client.Do(req) - if err != nil { + if resp, err := client.Do(req); err != nil { ch <- AsyncResults[*http.Response]{Error: err} - return - } - elapsed := time.Since(start) - ch <- AsyncResults[*http.Response]{ - Data: resp, - Time: elapsed, + } else { + ch <- AsyncResults[*http.Response]{ + Data: resp, + Time: time.Since(start), + } } }() return ch diff --git a/pkg/modules/extractors/runtime_test.go b/pkg/modules/extractors/runtime_test.go index 9ab1e29..190ebaa 100644 --- a/pkg/modules/extractors/runtime_test.go +++ b/pkg/modules/extractors/runtime_test.go @@ -3,6 +3,7 @@ package extractors_test import ( "testing" + "github.com/dgate-io/dgate/internal/config/configtest" "github.com/dgate-io/dgate/internal/proxy" "github.com/dgate-io/dgate/pkg/modules/extractors" "github.com/dgate-io/dgate/pkg/modules/testutil" @@ -101,7 +102,9 @@ func TestPrinter(t *testing.T) { program := testutil.CreateJSProgram(t, JS_PAYLOAD_CUSTOMFUNC) cp := &consolePrinter{make(map[string]int)} rt := &spec.DGateRoute{Namespace: &spec.DGateNamespace{}} - rtCtx := proxy.NewRuntimeContext(nil, rt) + conf := configtest.NewTestDGateConfig() + ps := proxy.NewProxyState(conf) + rtCtx := proxy.NewRuntimeContext(ps, rt) loop, err := extractors.NewModuleEventLoop( cp, rtCtx, program, ) @@ -118,13 +121,15 @@ func TestPrinter(t *testing.T) { func BenchmarkNewModuleRuntime(b *testing.B) { program := testutil.CreateTSProgram(b, TS_PAYLOAD_CUSTOMFUNC) + conf := configtest.NewTestDGateConfig() + ps := proxy.NewProxyState(conf) b.ResetTimer() b.Run("CreateModuleRuntime", func(b *testing.B) { for i := 0; i < b.N; i++ { b.StartTimer() rt := &spec.DGateRoute{Namespace: &spec.DGateNamespace{}} - rtCtx := proxy.NewRuntimeContext(nil, rt) + rtCtx := proxy.NewRuntimeContext(ps, rt) _, err := extractors.NewModuleEventLoop(nil, rtCtx, program) b.StopTimer() if err != nil { diff --git a/pkg/resources/resource_manager.go b/pkg/resources/resource_manager.go index 2f86c37..f20fd0a 100644 --- a/pkg/resources/resource_manager.go +++ b/pkg/resources/resource_manager.go @@ -1,7 +1,6 @@ package resources import ( - "encoding/json" "errors" "sort" "sync" @@ -278,8 +277,6 @@ func (rm *ResourceManager) transformRoute(route *spec.Route) (*spec.DGateRoute, // RemoveRoute removes a route from the resource manager func (rm *ResourceManager) RemoveRoute(name, namespace string) error { - // TODO: this function can be improved by checking if - // the links are valid before unlinking them rm.mutex.Lock() defer rm.mutex.Unlock() if nsLk, ok := rm.namespaces.Find(namespace); !ok { @@ -489,7 +486,10 @@ func (rm *ResourceManager) GetDomainsByPriority() []*spec.DGateDomain { sort.Slice(domains, func(i, j int) bool { d1, d2 := domains[j], domains[i] - return d1.Name < d2.Name || d1.Priority < d2.Priority + if d1.Priority == d2.Priority { + return d1.Name < d2.Name + } + return d1.Priority < d2.Priority }) return domains @@ -860,61 +860,6 @@ func (rm *ResourceManager) RemoveSecret(name, namespace string) error { } } -// MarshalJSON marshals the resource manager to json -func (rm *ResourceManager) MarshalJSON() ([]byte, error) { - rm.mutex.RLock() - defer rm.mutex.RUnlock() - return json.Marshal(map[string]interface{}{ - "namespaces": rm.namespaces, - "services": rm.services, - "domains": rm.domains, - "modules": rm.modules, - "routes": rm.routes, - "collections": rm.collections, - }) -} - -// UnmarshalJSON unmarshals the resource manager from json -func (rm *ResourceManager) UnmarshalJSON(data []byte) error { - rm.mutex.RLock() - defer rm.mutex.RUnlock() - var obj map[string]json.RawMessage - if err := json.Unmarshal(data, &obj); err != nil { - return err - } - if collections, ok := obj["collections"]; ok { - if err := json.Unmarshal(collections, &rm.collections); err != nil { - return err - } - } - if routes, ok := obj["routes"]; ok { - if err := json.Unmarshal(routes, &rm.routes); err != nil { - return err - } - } - if modules, ok := obj["modules"]; ok { - if err := json.Unmarshal(modules, &rm.modules); err != nil { - return err - } - } - if domains, ok := obj["domains"]; ok { - if err := json.Unmarshal(domains, &rm.domains); err != nil { - return err - } - } - if services, ok := obj["services"]; ok { - if err := json.Unmarshal(services, &rm.services); err != nil { - return err - } - } - if namespaces, ok := obj["namespaces"]; ok { - if err := json.Unmarshal(namespaces, &rm.namespaces); err != nil { - return err - } - } - return nil -} - func (rm *ResourceManager) Empty() bool { rm.mutex.RLock() defer rm.mutex.RUnlock() diff --git a/pkg/spec/change_log.go b/pkg/spec/change_log.go index 432a02a..29b2ee4 100644 --- a/pkg/spec/change_log.go +++ b/pkg/spec/change_log.go @@ -82,7 +82,9 @@ var ( DeleteCollectionCommand Command = newCommand(Delete, Collections) DeleteDocumentCommand Command = newCommand(Delete, Documents) DeleteSecretCommand Command = newCommand(Delete, Secrets) - NoopCommand Command = Command("noop") + + NoopCommand Command = Command("noop") + StopCommand Command = Command("stop") ) func newCommand(action Action, resource Resource) Command { diff --git a/pkg/spec/external_resources.go b/pkg/spec/external_resources.go index 32fce70..2230f31 100644 --- a/pkg/spec/external_resources.go +++ b/pkg/spec/external_resources.go @@ -1,7 +1,6 @@ package spec import ( - "encoding/json" "time" ) @@ -115,41 +114,16 @@ type Document struct { } type Secret struct { - Name string `json:"name"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - NamespaceName string `json:"namespace"` - Data string `json:"data"` - Tags []string `json:"tags,omitempty"` + Name string `json:"name"` + NamespaceName string `json:"namespace"` + Data string `json:"data,omitempty"` + Tags []string `json:"tags,omitempty"` } func (n *Secret) GetName() string { return n.Name } -type RFC3339Time time.Time - -func (t RFC3339Time) MarshalJSON() ([]byte, error) { - return json.Marshal(time.Time(t).Format(time.RFC3339)) -} - -func (t *RFC3339Time) UnmarshalJSON(data []byte) error { - var str string - if err := json.Unmarshal(data, &str); err != nil { - return err - } - if str == "" { - return nil - } - parsed, err := time.Parse(time.RFC3339, str) - if err != nil { - return err - } - - *t = RFC3339Time(parsed) - return nil -} - func (n *Document) GetName() string { return n.ID } diff --git a/pkg/spec/response_writer_tracker.go b/pkg/spec/response_writer_tracker.go index 2cb622a..25b6740 100644 --- a/pkg/spec/response_writer_tracker.go +++ b/pkg/spec/response_writer_tracker.go @@ -12,8 +12,8 @@ type ResponseWriterTracker interface { } type rwTracker struct { - rw http.ResponseWriter - status int + rw http.ResponseWriter + status int bytesWritten int64 } @@ -29,13 +29,13 @@ func NewResponseWriterTracker(rw http.ResponseWriter) ResponseWriterTracker { } func (t *rwTracker) Header() http.Header { - // if t.HeadersSent() { - // panic("headers already sent") - // } return t.rw.Header() } func (t *rwTracker) Write(b []byte) (int, error) { + if !t.HeadersSent() { + t.WriteHeader(http.StatusOK) + } n, err := t.rw.Write(b) t.bytesWritten += int64(n) return n, err diff --git a/pkg/spec/transformers.go b/pkg/spec/transformers.go index 63e349a..3427f41 100644 --- a/pkg/spec/transformers.go +++ b/pkg/spec/transformers.go @@ -170,27 +170,20 @@ func TransformDGateDocument(document *DGateDocument) *Document { } } -func TransformDGateSecrets(redact bool, secrets ...*DGateSecret) []*Secret { +func TransformDGateSecrets(secrets ...*DGateSecret) []*Secret { newSecrets := make([]*Secret, len(secrets)) for i, secret := range secrets { - newSecrets[i] = TransformDGateSecret(secret, redact) + newSecrets[i] = TransformDGateSecret(secret) } return newSecrets } -func TransformDGateSecret(col *DGateSecret, redact bool) *Secret { - data := "--redacted--" - if !redact { - data = col.Data - if data != "" { - data = base64.StdEncoding.EncodeToString([]byte(data)) - } - } +func TransformDGateSecret(sec *DGateSecret) *Secret { return &Secret{ - Name: col.Name, - NamespaceName: col.Namespace.Name, - Data: data, - Tags: col.Tags, + Name: sec.Name, + NamespaceName: sec.Namespace.Name, + Data: "**redacted**", + Tags: sec.Tags, } } diff --git a/pkg/storage/debug_storage.go b/pkg/storage/debug_storage.go new file mode 100644 index 0000000..ce45ddb --- /dev/null +++ b/pkg/storage/debug_storage.go @@ -0,0 +1,93 @@ +package storage + +import ( + "errors" + "strings" + + "github.com/dgate-io/dgate/pkg/util/tree/avl" + "github.com/rs/zerolog" +) + +type DebugStoreConfig struct { + // Path to the directory where the files will be stored. + // If the directory does not exist, it will be created. + // If the directory exists, it will be used. + Logger zerolog.Logger +} + +type DebugStore struct { + tree avl.Tree[string, []byte] +} + +var _ Storage = &DebugStore{} + +func NewDebugStore(cfg *DebugStoreConfig) *DebugStore { + return &DebugStore{ + tree: avl.NewTree[string, []byte](), + } +} + +func (m *DebugStore) Connect() error { + return nil +} + +func (m *DebugStore) Get(key string) ([]byte, error) { + if b, ok := m.tree.Find(key); ok { + return b, nil + } + return nil, errors.New("key not found") +} + +func (m *DebugStore) Set(key string, value []byte) error { + m.tree.Insert(key, value) + return nil +} + +func (m *DebugStore) IterateValuesPrefix(prefix string, fn func(string, []byte) error) error { + check := true + m.tree.Each(func(k string, v []byte) bool { + if strings.HasPrefix(k, prefix) { + check = true + if err := fn(k, v); err != nil { + return false + } + return true + } + return check + }) + return nil +} + +func (m *DebugStore) IterateTxnPrefix(prefix string, fn func(StorageTxn, string) error) error { + panic("implement me") +} + +func (m *DebugStore) GetPrefix(prefix string, offset, limit int) ([]*KeyValue, error) { + kvs := make([]*KeyValue, 0, limit) + m.IterateValuesPrefix(prefix, func(key string, value []byte) error { + if offset <= 0 { + kvs = append(kvs, &KeyValue{ + Key: key, + Value: value, + }) + if len(kvs) >= limit { + return errors.New("limit reached") + } + } else { + offset-- + } + return nil + }) + return kvs, nil +} + +func (m *DebugStore) Delete(key string) error { + if ok := m.tree.Delete(key); !ok { + return errors.New("key not found") + } + return nil +} + +func (m *DebugStore) Close() error { + return nil +} diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index b93380f..2ebc9cc 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -2,7 +2,6 @@ package storage type Storage interface { StorageTxn - Txn(bool, func(StorageTxn) error) error Connect() error Close() error } diff --git a/pkg/util/http_response.go b/pkg/util/http_response.go index a96532b..1f6896f 100644 --- a/pkg/util/http_response.go +++ b/pkg/util/http_response.go @@ -23,7 +23,7 @@ func JsonResponse(w http.ResponseWriter, statusCode int, data any) { } responseData := map[string]any{ "status_code": statusCode, - "data": data, + "data": data, } if isSlice(data) { responseData["count"] = sliceLen(data) @@ -40,7 +40,7 @@ func JsonError[T any](w http.ResponseWriter, statusCode int, message T) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) json.NewEncoder(w).Encode(map[string]any{ - "error": message, + "error": message, "status": statusCode, }) } diff --git a/pkg/util/linker/linker.go b/pkg/util/linker/linker.go index 6596889..4ff3b69 100644 --- a/pkg/util/linker/linker.go +++ b/pkg/util/linker/linker.go @@ -10,8 +10,8 @@ import ( ) type kv[T, U any] struct { - Key T `json:"key"` - Val U `json:"val"` + key T + val U } type Linker[K cmp.Ordered] interface { @@ -51,7 +51,7 @@ func NewNamedVertexWithValue[K cmp.Ordered, V any](item *V, names ...K) *Link[K, edges := make([]*kv[K, avl.Tree[K, Linker[K]]], len(names)) for i, name := range names { edges[i] = &kv[K, avl.Tree[K, Linker[K]]]{ - Key: name, Val: avl.NewTree[K, Linker[K]](), + key: name, val: avl.NewTree[K, Linker[K]](), } } @@ -66,7 +66,7 @@ func (nl *Link[K, V]) Vertex() Linker[K] { } func (nl *Link[K, V]) Item() *V { - return nl.item.Load() + return nl.item.Read() } func (nl *Link[K, V]) SetItem(item *V) { @@ -75,14 +75,14 @@ func (nl *Link[K, V]) SetItem(item *V) { func (nl *Link[K, V]) Get(name K) Linker[K] { for _, edge := range nl.edges { - if edge.Key == name { - if !edge.Val.Empty() { + if edge.key == name { + if !edge.val.Empty() { count := 0 - edge.Val.Each(func(key K, val Linker[K]) bool { + edge.val.Each(func(key K, val Linker[K]) bool { count++ return count <= 2 }) - if _, lk, ok := edge.Val.RootKeyValue(); ok && count == 1 { + if _, lk, ok := edge.val.RootKeyValue(); ok && count == 1 { return lk } panic("this function should not be called on a vertex with more than one edge per name") @@ -95,8 +95,8 @@ func (nl *Link[K, V]) Get(name K) Linker[K] { func (nl *Link[K, V]) Len(name K) int { for _, edge := range nl.edges { - if edge.Key == name { - return edge.Val.Length() + if edge.key == name { + return edge.val.Length() } } return 0 @@ -104,8 +104,8 @@ func (nl *Link[K, V]) Len(name K) int { func (nl *Link[K, V]) Find(name K, key K) (Linker[K], bool) { for _, edge := range nl.edges { - if edge.Key == name { - return edge.Val.Find(key) + if edge.key == name { + return edge.val.Find(key) } } return nil, false @@ -114,8 +114,8 @@ func (nl *Link[K, V]) Find(name K, key K) (Linker[K], bool) { // LinkOneMany adds an edge from this vertex to specified vertex func (nl *Link[K, V]) LinkOneMany(name K, key K, vtx Linker[K]) { for _, edge := range nl.edges { - if edge.Key == name { - edge.Val.Insert(key, vtx) + if edge.key == name { + edge.val.Insert(key, vtx) return } } @@ -125,8 +125,8 @@ func (nl *Link[K, V]) LinkOneMany(name K, key K, vtx Linker[K]) { // UnlinkOneMany removes links to a vertex and returns the vertex func (nl *Link[K, V]) UnlinkOneMany(name K, key K) (Linker[K], bool) { for _, edge := range nl.edges { - if edge.Key == name { - return edge.Val.Pop(key) + if edge.key == name { + return edge.val.Pop(key) } } panic("name not found for this vertex: " + fmt.Sprint(name)) @@ -135,13 +135,13 @@ func (nl *Link[K, V]) UnlinkOneMany(name K, key K) (Linker[K], bool) { // UnlinkAllOneMany removes all edges from the vertex and returns them func (nl *Link[K, V]) UnlinkAllOneMany(name K) []Linker[K] { for _, edge := range nl.edges { - if edge.Key == name { + if edge.key == name { var removed []Linker[K] - edge.Val.Each(func(key K, val Linker[K]) bool { + edge.val.Each(func(key K, val Linker[K]) bool { removed = append(removed, val) return true }) - edge.Val.Clear() + edge.val.Clear() return removed } } @@ -151,9 +151,9 @@ func (nl *Link[K, V]) UnlinkAllOneMany(name K) []Linker[K] { // LinkOneOne links a vertex to the vertex func (nl *Link[K, V]) LinkOneOne(name K, key K, vertex Linker[K]) { for _, edge := range nl.edges { - if edge.Key == name { - edge.Val.Insert(key, vertex) - if edge.Val.Length() > 1 { + if edge.key == name { + edge.val.Insert(key, vertex) + if edge.val.Length() > 1 { panic("this function should not be called on a vertex with more than one edge per name") } return @@ -165,8 +165,8 @@ func (nl *Link[K, V]) LinkOneOne(name K, key K, vertex Linker[K]) { // UnlinkOneOne unlinks a vertex from the vertex and returns the vertex func (nl *Link[K, V]) UnlinkOneOneByKey(name K, key K) (Linker[K], bool) { for _, edge := range nl.edges { - if edge.Key == name { - return edge.Val.Pop(key) + if edge.key == name { + return edge.val.Pop(key) } } panic("name not found for this vertex: " + fmt.Sprint(name)) @@ -175,10 +175,10 @@ func (nl *Link[K, V]) UnlinkOneOneByKey(name K, key K) (Linker[K], bool) { // UnlinkOneOne unlinks a vertex from the vertex and returns the vertex func (nl *Link[K, V]) UnlinkOneOne(name K) (Linker[K], bool) { for _, edge := range nl.edges { - if edge.Key == name { - _, link, ok := edge.Val.RootKeyValue() + if edge.key == name { + _, link, ok := edge.val.RootKeyValue() if ok { - edge.Val.Clear() + edge.val.Clear() } return link, ok } @@ -191,7 +191,7 @@ func (nl *Link[K, V]) Clone() Linker[K] { edges := make([]*kv[K, avl.Tree[K, Linker[K]]], len(nl.edges)) for i, edge := range nl.edges { edges[i] = &kv[K, avl.Tree[K, Linker[K]]]{ - Key: edge.Key, Val: edge.Val.Clone(), + key: edge.key, val: edge.val.Clone(), } } copiedItem := *nl.item @@ -204,8 +204,8 @@ func (nl *Link[K, V]) Clone() Linker[K] { // Each iterates over all edges func (nl *Link[K, V]) Each(name K, fn func(K, Linker[K])) { for _, edge := range nl.edges { - if edge.Key == name { - edge.Val.Each(func(key K, vertex Linker[K]) bool { + if edge.key == name { + edge.val.Each(func(key K, vertex Linker[K]) bool { fn(key, vertex) return true })