stage 1
This commit is contained in:
@@ -3,13 +3,12 @@ package server
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/any-hub/any-hub/internal/config"
|
||||
"github.com/any-hub/any-hub/internal/hubmodule"
|
||||
)
|
||||
|
||||
func moduleMetadataForHub(hub config.HubConfig) (hubmodule.ModuleMetadata, error) {
|
||||
if meta, ok := hubmodule.Resolve(hub.Module); ok {
|
||||
func moduleMetadataForKey(key string) (hubmodule.ModuleMetadata, error) {
|
||||
if meta, ok := hubmodule.Resolve(key); ok {
|
||||
return meta, nil
|
||||
}
|
||||
return hubmodule.ModuleMetadata{}, fmt.Errorf("module %s is not registered", hub.Module)
|
||||
return hubmodule.ModuleMetadata{}, fmt.Errorf("module %s is not registered", key)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/any-hub/any-hub/internal/config"
|
||||
"github.com/any-hub/any-hub/internal/hubmodule"
|
||||
"github.com/any-hub/any-hub/internal/hubmodule/legacy"
|
||||
)
|
||||
|
||||
// HubRoute 将 Hub 配置与派生属性(如缓存 TTL、解析后的 Upstream/Proxy URL)
|
||||
@@ -28,6 +29,10 @@ type HubRoute struct {
|
||||
// ModuleKey/Module 记录当前 hub 选用的模块及其元数据,便于日志与观测。
|
||||
ModuleKey string
|
||||
Module hubmodule.ModuleMetadata
|
||||
// CacheStrategy 代表模块默认策略与 hub 覆盖后的最终结果。
|
||||
CacheStrategy hubmodule.CacheStrategyProfile
|
||||
// RolloutFlag 反映当前 hub 的 legacy → modular 迁移状态,供日志/诊断使用。
|
||||
RolloutFlag legacy.RolloutFlag
|
||||
}
|
||||
|
||||
// HubRegistry 提供 Host/Host:port 到 HubRoute 的查询能力,所有 Hub 共享同一个监听端口。
|
||||
@@ -100,7 +105,9 @@ func (r *HubRegistry) List() []HubRoute {
|
||||
}
|
||||
|
||||
func buildHubRoute(cfg *config.Config, hub config.HubConfig) (*HubRoute, error) {
|
||||
meta, err := moduleMetadataForHub(hub)
|
||||
flag := hub.RolloutFlagValue()
|
||||
effectiveKey := config.EffectiveModuleKey(hub.Module, flag)
|
||||
meta, err := moduleMetadataForKey(effectiveKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hub %s: %w", hub.Name, err)
|
||||
}
|
||||
@@ -118,14 +125,20 @@ func buildHubRoute(cfg *config.Config, hub config.HubConfig) (*HubRoute, error)
|
||||
}
|
||||
}
|
||||
|
||||
effectiveTTL := cfg.EffectiveCacheTTL(hub)
|
||||
runtime := config.BuildHubRuntime(hub, meta, effectiveTTL, flag)
|
||||
legacy.RecordAdapterState(hub.Name, runtime.Module.Key, flag)
|
||||
|
||||
return &HubRoute{
|
||||
Config: hub,
|
||||
ListenPort: cfg.Global.ListenPort,
|
||||
CacheTTL: cfg.EffectiveCacheTTL(hub),
|
||||
UpstreamURL: upstreamURL,
|
||||
ProxyURL: proxyURL,
|
||||
ModuleKey: meta.Key,
|
||||
Module: meta,
|
||||
Config: hub,
|
||||
ListenPort: cfg.Global.ListenPort,
|
||||
CacheTTL: effectiveTTL,
|
||||
UpstreamURL: upstreamURL,
|
||||
ProxyURL: proxyURL,
|
||||
ModuleKey: runtime.Module.Key,
|
||||
Module: runtime.Module,
|
||||
CacheStrategy: runtime.CacheStrategy,
|
||||
RolloutFlag: runtime.Rollout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/any-hub/any-hub/internal/config"
|
||||
"github.com/any-hub/any-hub/internal/hubmodule/legacy"
|
||||
)
|
||||
|
||||
func TestHubRegistryLookupByHost(t *testing.T) {
|
||||
@@ -48,6 +49,15 @@ func TestHubRegistryLookupByHost(t *testing.T) {
|
||||
if route.CacheTTL != cfg.EffectiveCacheTTL(route.Config) {
|
||||
t.Errorf("cache ttl mismatch: got %s", route.CacheTTL)
|
||||
}
|
||||
if route.CacheStrategy.TTLHint != route.CacheTTL {
|
||||
t.Errorf("cache strategy ttl mismatch: %s vs %s", route.CacheStrategy.TTLHint, route.CacheTTL)
|
||||
}
|
||||
if route.CacheStrategy.ValidationMode == "" {
|
||||
t.Fatalf("cache strategy validation mode should not be empty")
|
||||
}
|
||||
if route.RolloutFlag != legacy.RolloutLegacyOnly {
|
||||
t.Fatalf("default rollout flag should be legacy-only")
|
||||
}
|
||||
|
||||
if route.UpstreamURL.String() != "https://registry-1.docker.io" {
|
||||
t.Errorf("unexpected upstream URL: %s", route.UpstreamURL)
|
||||
|
||||
@@ -79,6 +79,10 @@ func requestContextMiddleware(opts AppOptions) fiber.Handler {
|
||||
c.Locals(contextKeyRequestID, reqID)
|
||||
c.Set("X-Request-ID", reqID)
|
||||
|
||||
if isDiagnosticsPath(string(c.Request().URI().Path())) {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
rawHost := strings.TrimSpace(getHostHeader(c))
|
||||
route, ok := opts.Registry.Lookup(rawHost)
|
||||
if !ok {
|
||||
@@ -153,13 +157,19 @@ func ensureRouterHubType(route *HubRoute) error {
|
||||
|
||||
func renderTypeUnsupported(c fiber.Ctx, logger *logrus.Logger, route *HubRoute, err error) error {
|
||||
fields := logrus.Fields{
|
||||
"action": "hub_type_check",
|
||||
"hub": route.Config.Name,
|
||||
"hub_type": route.Config.Type,
|
||||
"error": "hub_type_unsupported",
|
||||
"action": "hub_type_check",
|
||||
"hub": route.Config.Name,
|
||||
"hub_type": route.Config.Type,
|
||||
"module_key": route.ModuleKey,
|
||||
"rollout_flag": string(route.RolloutFlag),
|
||||
"error": "hub_type_unsupported",
|
||||
}
|
||||
logger.WithFields(fields).Error(err.Error())
|
||||
return c.Status(fiber.StatusNotImplemented).JSON(fiber.Map{
|
||||
"error": "hub_type_unsupported",
|
||||
})
|
||||
}
|
||||
|
||||
func isDiagnosticsPath(path string) bool {
|
||||
return strings.HasPrefix(path, "/-/")
|
||||
}
|
||||
|
||||
114
internal/server/routes/modules.go
Normal file
114
internal/server/routes/modules.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
|
||||
"github.com/any-hub/any-hub/internal/hubmodule"
|
||||
"github.com/any-hub/any-hub/internal/server"
|
||||
)
|
||||
|
||||
// RegisterModuleRoutes 暴露 /-/modules 诊断接口,供 SRE 查询模块与 Hub 绑定关系。
|
||||
func RegisterModuleRoutes(app *fiber.App, registry *server.HubRegistry) {
|
||||
if app == nil || registry == nil {
|
||||
return
|
||||
}
|
||||
|
||||
app.Get("/-/modules", func(c fiber.Ctx) error {
|
||||
payload := fiber.Map{
|
||||
"modules": encodeModules(hubmodule.List()),
|
||||
"hubs": encodeHubBindings(registry.List()),
|
||||
}
|
||||
return c.JSON(payload)
|
||||
})
|
||||
|
||||
app.Get("/-/modules/:key", func(c fiber.Ctx) error {
|
||||
key := strings.ToLower(strings.TrimSpace(c.Params("key")))
|
||||
if key == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "module_key_required"})
|
||||
}
|
||||
meta, ok := hubmodule.Resolve(key)
|
||||
if !ok {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "module_not_found"})
|
||||
}
|
||||
return c.JSON(encodeModule(meta))
|
||||
})
|
||||
}
|
||||
|
||||
type modulePayload struct {
|
||||
Key string `json:"key"`
|
||||
Description string `json:"description"`
|
||||
MigrationState hubmodule.MigrationState `json:"migration_state"`
|
||||
SupportedProtocols []string `json:"supported_protocols"`
|
||||
CacheStrategy cacheStrategyPayload `json:"cache_strategy"`
|
||||
}
|
||||
|
||||
type cacheStrategyPayload struct {
|
||||
TTLSeconds int64 `json:"ttl_seconds"`
|
||||
ValidationMode string `json:"validation_mode"`
|
||||
DiskLayout string `json:"disk_layout"`
|
||||
RequiresMetadataFile bool `json:"requires_metadata_file"`
|
||||
SupportsStreamingWrite bool `json:"supports_streaming_write"`
|
||||
}
|
||||
|
||||
type hubBindingPayload struct {
|
||||
HubName string `json:"hub_name"`
|
||||
ModuleKey string `json:"module_key"`
|
||||
Domain string `json:"domain"`
|
||||
Port int `json:"port"`
|
||||
Rollout string `json:"rollout_flag"`
|
||||
}
|
||||
|
||||
func encodeModules(mods []hubmodule.ModuleMetadata) []modulePayload {
|
||||
if len(mods) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Slice(mods, func(i, j int) bool {
|
||||
return mods[i].Key < mods[j].Key
|
||||
})
|
||||
result := make([]modulePayload, 0, len(mods))
|
||||
for _, meta := range mods {
|
||||
result = append(result, encodeModule(meta))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func encodeModule(meta hubmodule.ModuleMetadata) modulePayload {
|
||||
strategy := meta.CacheStrategy
|
||||
return modulePayload{
|
||||
Key: meta.Key,
|
||||
Description: meta.Description,
|
||||
MigrationState: meta.MigrationState,
|
||||
SupportedProtocols: append([]string(nil), meta.SupportedProtocols...),
|
||||
CacheStrategy: cacheStrategyPayload{
|
||||
TTLSeconds: int64(strategy.TTLHint / time.Second),
|
||||
ValidationMode: string(strategy.ValidationMode),
|
||||
DiskLayout: strategy.DiskLayout,
|
||||
RequiresMetadataFile: strategy.RequiresMetadataFile,
|
||||
SupportsStreamingWrite: strategy.SupportsStreamingWrite,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func encodeHubBindings(routes []server.HubRoute) []hubBindingPayload {
|
||||
if len(routes) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
return routes[i].Config.Name < routes[j].Config.Name
|
||||
})
|
||||
result := make([]hubBindingPayload, 0, len(routes))
|
||||
for _, route := range routes {
|
||||
result = append(result, hubBindingPayload{
|
||||
HubName: route.Config.Name,
|
||||
ModuleKey: route.ModuleKey,
|
||||
Domain: route.Config.Domain,
|
||||
Port: route.ListenPort,
|
||||
Rollout: string(route.RolloutFlag),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user