refactor(api): restructured API for type safety, maintainability and docs generation

- These changes makes the API incombatible with previous versions
- Added new types for error handling, success responses, and health checks.
- Updated health check logic to utilize the new types for better clarity and structure.
- Refactored existing handlers to improve response consistency and error handling.
- Updated Makefile to include a new target for generating API types from Swagger.
- Updated "new agent" API to respond an encrypted cert pair
This commit is contained in:
yusing
2025-08-16 13:04:05 +08:00
parent fce9ce21c9
commit 35a3e3fef6
149 changed files with 13173 additions and 2173 deletions

View File

@@ -4,18 +4,20 @@ import (
"context"
"net/http"
"net/url"
"github.com/yusing/go-proxy/internal/types"
)
type RouteContext struct{}
var routeContextKey = RouteContext{}
func WithRouteContext(r *http.Request, route HTTPRoute) *http.Request {
func WithRouteContext(r *http.Request, route types.HTTPRoute) *http.Request {
return r.WithContext(context.WithValue(r.Context(), routeContextKey, route))
}
func TryGetRoute(r *http.Request) HTTPRoute {
if route, ok := r.Context().Value(routeContextKey).(HTTPRoute); ok {
func TryGetRoute(r *http.Request) types.HTTPRoute {
if route, ok := r.Context().Value(routeContextKey).(types.HTTPRoute); ok {
return route
}
return nil

View File

@@ -2,84 +2,80 @@ package routes
import (
"encoding/json"
"fmt"
"math"
"net/url"
"strings"
"time"
"github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/watcher/health"
"github.com/yusing/go-proxy/internal/types"
)
func getHealthInfo(r Route) map[string]string {
mon := r.HealthMonitor()
if mon == nil {
return map[string]string{
"status": "unknown",
"uptime": "n/a",
"latency": "n/a",
"detail": "n/a",
}
}
return map[string]string{
"status": mon.Status().String(),
"uptime": mon.Uptime().Round(time.Second).String(),
"latency": mon.Latency().Round(time.Microsecond).String(),
"detail": mon.Detail(),
}
type HealthInfo struct {
Status types.HealthStatus `json:"status" swaggertype:"string" enums:"healthy,unhealthy,napping,starting,error,unknown"`
Uptime time.Duration `json:"uptime" swaggertype:"number"` // uptime in milliseconds
Latency time.Duration `json:"latency" swaggertype:"number"` // latency in microseconds
Detail string `json:"detail"`
}
type HealthInfoRaw struct {
Status health.Status `json:"status"`
Latency time.Duration `json:"latency"`
}
func (info *HealthInfoRaw) MarshalJSON() ([]byte, error) {
func (info *HealthInfo) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"status": info.Status.String(),
"latency": info.Latency.Milliseconds(),
"latency": info.Latency.Microseconds(),
"uptime": info.Uptime.Milliseconds(),
"detail": info.Detail,
})
}
func (info *HealthInfoRaw) UnmarshalJSON(data []byte) error {
var v map[string]any
func (info *HealthInfo) UnmarshalJSON(data []byte) error {
var v struct {
Status string `json:"status"`
Latency int64 `json:"latency"`
Uptime int64 `json:"uptime"`
Detail string `json:"detail"`
}
if err := json.Unmarshal(data, &v); err != nil {
return err
}
if status, ok := v["status"].(string); ok {
info.Status = health.NewStatus(status)
// overflow check
if math.MaxInt64/time.Microsecond < time.Duration(v.Latency) {
return fmt.Errorf("latency overflow: %d", v.Latency)
}
if latency, ok := v["latency"].(float64); ok {
info.Latency = time.Duration(latency)
if math.MaxInt64/time.Millisecond < time.Duration(v.Uptime) {
return fmt.Errorf("uptime overflow: %d", v.Uptime)
}
info.Status = types.NewHealthStatusFromString(v.Status)
info.Latency = time.Duration(v.Latency) * time.Microsecond
info.Uptime = time.Duration(v.Uptime) * time.Millisecond
info.Detail = v.Detail
return nil
}
func getHealthInfoRaw(r Route) *HealthInfoRaw {
mon := r.HealthMonitor()
if mon == nil {
return &HealthInfoRaw{
Status: health.StatusUnknown,
Latency: time.Duration(0),
}
}
return &HealthInfoRaw{
Status: mon.Status(),
Latency: mon.Latency(),
}
}
func HealthMap() map[string]map[string]string {
healthMap := make(map[string]map[string]string, NumRoutes())
func GetHealthInfo() map[string]HealthInfo {
healthMap := make(map[string]HealthInfo, NumRoutes())
for r := range Iter {
healthMap[r.Name()] = getHealthInfo(r)
}
return healthMap
}
func HealthInfo() map[string]*HealthInfoRaw {
healthMap := make(map[string]*HealthInfoRaw, NumRoutes())
for r := range Iter {
healthMap[r.Name()] = getHealthInfoRaw(r)
func getHealthInfo(r types.Route) HealthInfo {
mon := r.HealthMonitor()
if mon == nil {
return HealthInfo{
Status: types.StatusUnknown,
Detail: "n/a",
}
}
return HealthInfo{
Status: mon.Status(),
Uptime: mon.Uptime(),
Latency: mon.Latency(),
Detail: mon.Detail(),
}
return healthMap
}
func HomepageCategories() []string {
@@ -99,7 +95,13 @@ func HomepageCategories() []string {
return categories
}
func HomepageConfig(categoryFilter, providerFilter string) homepage.Homepage {
func HomepageItems(proto, hostname, categoryFilter, providerFilter string) homepage.Homepage {
switch proto {
case "http", "https":
default:
proto = "http"
}
hp := make(homepage.Homepage)
for _, r := range HTTP.Iter {
@@ -110,13 +112,32 @@ func HomepageConfig(categoryFilter, providerFilter string) homepage.Homepage {
if categoryFilter != "" && item.Category != categoryFilter {
continue
}
// clear url if invalid
_, err := url.Parse(item.URL)
if err != nil {
item.URL = ""
}
// append hostname if provided and only if alias is not FQDN
if hostname != "" && item.URL == "" {
if !strings.Contains(item.Alias, ".") {
item.URL = fmt.Sprintf("%s://%s.%s", proto, item.Alias, hostname)
}
}
// prepend protocol if not exists
if !strings.HasPrefix(item.URL, "http://") && !strings.HasPrefix(item.URL, "https://") {
item.URL = fmt.Sprintf("%s://%s", proto, item.URL)
}
hp.Add(item)
}
return hp
}
func ByProvider() map[string][]Route {
rts := make(map[string][]Route)
func ByProvider() map[string][]types.Route {
rts := make(map[string][]types.Route)
for r := range Iter {
rts[r.ProviderName()] = append(rts[r.ProviderName()], r)
}

View File

@@ -1,69 +0,0 @@
package routes
import (
"net/http"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/internal/docker"
"github.com/yusing/go-proxy/internal/homepage"
idlewatcher "github.com/yusing/go-proxy/internal/idlewatcher/types"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/utils/pool"
"github.com/yusing/go-proxy/internal/watcher/health"
loadbalance "github.com/yusing/go-proxy/internal/net/gphttp/loadbalancer/types"
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
nettypes "github.com/yusing/go-proxy/internal/net/types"
)
type (
//nolint:interfacebloat // this is for avoiding circular imports
Route interface {
task.TaskStarter
task.TaskFinisher
pool.Object
ProviderName() string
GetProvider() Provider
TargetURL() *nettypes.URL
HealthMonitor() health.HealthMonitor
SetHealthMonitor(m health.HealthMonitor)
References() []string
Started() <-chan struct{}
IdlewatcherConfig() *idlewatcher.Config
HealthCheckConfig() *health.HealthCheckConfig
LoadBalanceConfig() *loadbalance.Config
HomepageConfig() *homepage.ItemConfig
HomepageItem() *homepage.Item
ContainerInfo() *docker.Container
GetAgent() *agent.AgentConfig
IsDocker() bool
IsAgent() bool
UseLoadBalance() bool
UseIdleWatcher() bool
UseHealthCheck() bool
UseAccessLog() bool
}
HTTPRoute interface {
Route
http.Handler
}
ReverseProxyRoute interface {
HTTPRoute
ReverseProxy() *reverseproxy.ReverseProxy
}
StreamRoute interface {
Route
nettypes.Stream
Stream() nettypes.Stream
}
Provider interface {
GetRoute(alias string) (r Route, ok bool)
IterRoutes(yield func(alias string, r Route) bool)
FindService(project, service string) (r Route, ok bool)
ShortName() string
}
)

View File

@@ -1,21 +1,22 @@
package routes
import (
"github.com/yusing/go-proxy/internal/types"
"github.com/yusing/go-proxy/internal/utils/pool"
)
var (
HTTP = pool.New[HTTPRoute]("http_routes")
Stream = pool.New[StreamRoute]("stream_routes")
HTTP = pool.New[types.HTTPRoute]("http_routes")
Stream = pool.New[types.StreamRoute]("stream_routes")
// All is a pool of all routes, including HTTP, Stream routes and also excluded routes.
All = pool.New[Route]("all_routes")
All = pool.New[types.Route]("all_routes")
)
func init() {
All.DisableLog()
}
func Iter(yield func(r Route) bool) {
func Iter(yield func(r types.Route) bool) {
for _, r := range All.Iter {
if !yield(r) {
break
@@ -23,7 +24,7 @@ func Iter(yield func(r Route) bool) {
}
}
func IterKV(yield func(alias string, r Route) bool) {
func IterKV(yield func(alias string, r types.Route) bool) {
for k, r := range All.Iter {
if !yield(k, r) {
break
@@ -41,7 +42,7 @@ func Clear() {
All.Clear()
}
func GetHTTPRouteOrExact(alias, host string) (HTTPRoute, bool) {
func GetHTTPRouteOrExact(alias, host string) (types.HTTPRoute, bool) {
r, ok := HTTP.Get(alias)
if ok {
return r, true
@@ -50,6 +51,6 @@ func GetHTTPRouteOrExact(alias, host string) (HTTPRoute, bool) {
return HTTP.Get(host)
}
func Get(alias string) (Route, bool) {
func Get(alias string) (types.Route, bool) {
return All.Get(alias)
}