mirror of
https://github.com/go-gost/x.git
synced 2026-10-19 16:16:32 +00:00
feat: Global nodes registry for reusing nodes across hops
This commit is contained in:
@@ -774,6 +774,7 @@ type Config struct {
|
||||
Services []*ServiceConfig `json:"services"`
|
||||
Chains []*ChainConfig `yaml:",omitempty" json:"chains,omitempty"`
|
||||
Hops []*HopConfig `yaml:",omitempty" json:"hops,omitempty"`
|
||||
Nodes []*NodeConfig `yaml:",omitempty" json:"nodes,omitempty"`
|
||||
Authers []*AutherConfig `yaml:",omitempty" json:"authers,omitempty"`
|
||||
Admissions []*AdmissionConfig `yaml:",omitempty" json:"admissions,omitempty"`
|
||||
Bypasses []*BypassConfig `yaml:",omitempty" json:"bypasses,omitempty"`
|
||||
|
||||
+24
-2
@@ -6,6 +6,9 @@
|
||||
package loader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-gost/core/admission"
|
||||
"github.com/go-gost/core/auth"
|
||||
"github.com/go-gost/core/bypass"
|
||||
@@ -22,8 +25,8 @@ import (
|
||||
"github.com/go-gost/core/recorder"
|
||||
reg "github.com/go-gost/core/registry"
|
||||
"github.com/go-gost/core/resolver"
|
||||
"github.com/go-gost/core/router"
|
||||
"github.com/go-gost/core/rewriter"
|
||||
"github.com/go-gost/core/router"
|
||||
"github.com/go-gost/core/sd"
|
||||
"github.com/go-gost/x/config"
|
||||
"github.com/go-gost/x/config/parsing"
|
||||
@@ -320,7 +323,26 @@ func register(cfg *config.Config) error {
|
||||
}
|
||||
}
|
||||
|
||||
// --- hops (references bypasses, resolvers, hosts from registries) ---
|
||||
// --- nodes (definitions referenced by hops) ---
|
||||
|
||||
{
|
||||
var entries []named[*config.NodeConfig]
|
||||
for _, c := range cfg.Nodes {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
c.Name = strings.TrimSpace(c.Name)
|
||||
if c.Name == "" {
|
||||
return fmt.Errorf("global node name is required")
|
||||
}
|
||||
entries = append(entries, named[*config.NodeConfig]{c.Name, c})
|
||||
}
|
||||
if err := registerGroup(entries, registry.NodeRegistry()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// --- hops (references nodes, bypasses, resolvers, hosts from registries) ---
|
||||
|
||||
{
|
||||
var entries []named[hop.Hop]
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-gost/core/handler"
|
||||
"github.com/go-gost/core/hop"
|
||||
"github.com/go-gost/core/listener"
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/metadata"
|
||||
@@ -17,6 +18,9 @@ import (
|
||||
"github.com/go-gost/x/config"
|
||||
xlogger "github.com/go-gost/x/logger"
|
||||
"github.com/go-gost/x/registry"
|
||||
|
||||
_ "github.com/go-gost/x/connector/http"
|
||||
_ "github.com/go-gost/x/dialer/tcp"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -465,6 +469,77 @@ func TestRegister_Hops(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_GlobalNodesBeforeHops(t *testing.T) {
|
||||
nodeName := "test-global-node"
|
||||
hopName := "test-global-node-hop"
|
||||
t.Cleanup(func() {
|
||||
registry.HopRegistry().Unregister(hopName)
|
||||
registry.NodeRegistry().Unregister(nodeName)
|
||||
})
|
||||
|
||||
cfg := &config.Config{
|
||||
Nodes: []*config.NodeConfig{{
|
||||
Name: nodeName,
|
||||
Addr: "example.com:8080",
|
||||
}},
|
||||
Hops: []*config.HopConfig{{
|
||||
Name: hopName,
|
||||
Nodes: []*config.NodeConfig{{Name: nodeName}},
|
||||
}},
|
||||
}
|
||||
if err := register(cfg); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
definition := registry.NodeRegistry().Get(nodeName)
|
||||
if definition == nil {
|
||||
t.Fatal("expected global node to be registered")
|
||||
}
|
||||
nodes := registry.HopRegistry().Get(hopName).(hop.NodeList).Nodes()
|
||||
if len(nodes) != 1 || nodes[0].Name != definition.Name || nodes[0].Addr != definition.Addr {
|
||||
t.Fatalf("hop nodes = %v, want global definition %q at %q", nodes, definition.Name, definition.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_GlobalNodeReferenceFromYAML(t *testing.T) {
|
||||
const nodeName = "yaml-global-node"
|
||||
const hopName = "yaml-global-hop"
|
||||
t.Cleanup(func() {
|
||||
registry.HopRegistry().Unregister(hopName)
|
||||
registry.NodeRegistry().Unregister(nodeName)
|
||||
})
|
||||
|
||||
cfg := &config.Config{}
|
||||
err := cfg.Read(strings.NewReader(`
|
||||
nodes:
|
||||
- name: yaml-global-node
|
||||
addr: global.example:8080
|
||||
hops:
|
||||
- name: yaml-global-hop
|
||||
nodes:
|
||||
- name: yaml-global-node
|
||||
- name: inline-node
|
||||
addr: inline.example:8081
|
||||
`), "yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("read YAML: %v", err)
|
||||
}
|
||||
if err := register(cfg); err != nil {
|
||||
t.Fatalf("register: %v", err)
|
||||
}
|
||||
|
||||
nodes := registry.HopRegistry().Get(hopName).(hop.NodeList).Nodes()
|
||||
if len(nodes) != 2 {
|
||||
t.Fatalf("hop nodes = %d, want 2", len(nodes))
|
||||
}
|
||||
if nodes[0].Name != nodeName || nodes[0].Addr != "global.example:8080" {
|
||||
t.Fatalf("referenced node = %q at %q", nodes[0].Name, nodes[0].Addr)
|
||||
}
|
||||
if nodes[1].Name != "inline-node" || nodes[1].Addr != "inline.example:8081" {
|
||||
t.Fatalf("inline node = %q at %q", nodes[1].Name, nodes[1].Addr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_Chains(t *testing.T) {
|
||||
r := registry.ChainRegistry()
|
||||
name := "test-chain"
|
||||
|
||||
@@ -88,6 +88,12 @@ func ParseHop(cfg *config.HopConfig, log logger.Logger) (hop.Hop, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
var err error
|
||||
v, err = node_parser.ResolveConfig(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build a merged metadata map for inheritance without mutating
|
||||
// the original node config.
|
||||
merged := make(map[string]any)
|
||||
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/go-gost/core/chain"
|
||||
"github.com/go-gost/core/hop"
|
||||
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/x/config"
|
||||
"github.com/go-gost/x/config/parsing"
|
||||
xlogger "github.com/go-gost/x/logger"
|
||||
mdutil "github.com/go-gost/x/metadata/util"
|
||||
"github.com/go-gost/x/registry"
|
||||
|
||||
// Register connector and dialer implementations needed for node parsing.
|
||||
_ "github.com/go-gost/x/connector/http"
|
||||
@@ -185,6 +186,81 @@ func TestParseHop_WithNodeNilEntries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHop_GlobalNodeReference(t *testing.T) {
|
||||
global := &config.NodeConfig{Name: "global-node", Addr: "example.com:8080"}
|
||||
registry.NodeRegistry().Register(global.Name, global)
|
||||
t.Cleanup(func() { registry.NodeRegistry().Unregister(global.Name) })
|
||||
|
||||
h, err := ParseHop(&config.HopConfig{
|
||||
Name: "reference-hop",
|
||||
Nodes: []*config.NodeConfig{{Name: global.Name}},
|
||||
}, testLogger())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
nodes := h.(hop.NodeList).Nodes()
|
||||
if len(nodes) != 1 || nodes[0].Name != global.Name || nodes[0].Addr != global.Addr {
|
||||
t.Fatalf("referenced nodes = %v, want node %q at %q", nodes, global.Name, global.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHop_GlobalNodeReferenceNotFound(t *testing.T) {
|
||||
_, err := ParseHop(&config.HopConfig{
|
||||
Name: "reference-hop",
|
||||
Nodes: []*config.NodeConfig{{Name: "missing-global-node"}},
|
||||
}, testLogger())
|
||||
if err == nil {
|
||||
t.Fatal("expected missing global node error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHop_NodeWithAdditionalFieldRemainsInline(t *testing.T) {
|
||||
global := &config.NodeConfig{Name: "shared-name", Addr: "global.example:8080"}
|
||||
registry.NodeRegistry().Register(global.Name, global)
|
||||
t.Cleanup(func() { registry.NodeRegistry().Unregister(global.Name) })
|
||||
|
||||
h, err := ParseHop(&config.HopConfig{
|
||||
Name: "inline-hop",
|
||||
Nodes: []*config.NodeConfig{{
|
||||
Name: global.Name,
|
||||
Addr: "inline.example:8080",
|
||||
}},
|
||||
}, testLogger())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
nodes := h.(hop.NodeList).Nodes()
|
||||
if len(nodes) != 1 {
|
||||
t.Fatalf("expected 1 node, got %d", len(nodes))
|
||||
}
|
||||
if nodes[0].Addr != "inline.example:8080" {
|
||||
t.Fatalf("node addr = %q, want inline node address", nodes[0].Addr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHop_GlobalNodeCreatesIndependentRuntimeNodes(t *testing.T) {
|
||||
global := &config.NodeConfig{Name: "independent-node", Addr: "example.com:8080"}
|
||||
registry.NodeRegistry().Register(global.Name, global)
|
||||
t.Cleanup(func() { registry.NodeRegistry().Unregister(global.Name) })
|
||||
|
||||
parse := func(name string) *chain.Node {
|
||||
h, err := ParseHop(&config.HopConfig{
|
||||
Name: name, Nodes: []*config.NodeConfig{{Name: global.Name}},
|
||||
}, testLogger())
|
||||
if err != nil {
|
||||
t.Fatalf("ParseHop: %v", err)
|
||||
}
|
||||
return h.(hop.NodeList).Nodes()[0]
|
||||
}
|
||||
|
||||
if first, second := parse("hop-1"), parse("hop-2"); first == second {
|
||||
t.Fatal("global node references must create independent runtime nodes")
|
||||
}
|
||||
if global.Connector != nil || global.Dialer != nil {
|
||||
t.Fatal("parsing a reference must not mutate the global definition")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHop_SockOptsMark(t *testing.T) {
|
||||
h, err := ParseHop(&config.HopConfig{
|
||||
Name: "sockopts-hop",
|
||||
|
||||
@@ -3,6 +3,7 @@ package node
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -37,6 +38,64 @@ const DefaultMatcherBodySize = 1 << 20 // 1MB
|
||||
// when a node opts in to body matching.
|
||||
const MaxMatcherBodySize = 10 << 20 // 10MB
|
||||
|
||||
// IsReference reports whether cfg contains only a non-empty name. Such a
|
||||
// config references a definition from the top-level nodes section.
|
||||
func IsReference(cfg *config.NodeConfig) bool {
|
||||
if cfg == nil || strings.TrimSpace(cfg.Name) == "" {
|
||||
return false
|
||||
}
|
||||
return reflect.DeepEqual(cfg, &config.NodeConfig{Name: cfg.Name})
|
||||
}
|
||||
|
||||
// ResolveConfig resolves a name-only config through the global node registry
|
||||
// and returns an independent copy of the definition. Inline configs are
|
||||
// returned unchanged to preserve their existing parsing behavior.
|
||||
func ResolveConfig(cfg *config.NodeConfig) (*config.NodeConfig, error) {
|
||||
if cfg == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if !IsReference(cfg) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
name := cfg.Name
|
||||
cfg = registry.NodeRegistry().Get(name)
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("node %q not found", name)
|
||||
}
|
||||
return cloneConfig(cfg), nil
|
||||
}
|
||||
|
||||
// cloneConfig copies the fields that parsing may default or inherit. Other
|
||||
// nested values are read-only during parsing and can safely remain shared.
|
||||
func cloneConfig(cfg *config.NodeConfig) *config.NodeConfig {
|
||||
c := *cfg
|
||||
c.Bypasses = append([]string(nil), cfg.Bypasses...)
|
||||
if cfg.Metadata != nil {
|
||||
c.Metadata = make(map[string]any, len(cfg.Metadata))
|
||||
for k, v := range cfg.Metadata {
|
||||
c.Metadata[k] = v
|
||||
}
|
||||
}
|
||||
if cfg.Connector != nil {
|
||||
connector := *cfg.Connector
|
||||
if cfg.Connector.TLS != nil {
|
||||
tls := *cfg.Connector.TLS
|
||||
connector.TLS = &tls
|
||||
}
|
||||
c.Connector = &connector
|
||||
}
|
||||
if cfg.Dialer != nil {
|
||||
dialer := *cfg.Dialer
|
||||
if cfg.Dialer.TLS != nil {
|
||||
tls := *cfg.Dialer.TLS
|
||||
dialer.TLS = &tls
|
||||
}
|
||||
c.Dialer = &dialer
|
||||
}
|
||||
return &c
|
||||
}
|
||||
|
||||
// filterToMatcherRule converts a deprecated NodeFilterConfig (host/protocol/
|
||||
// path) into an equivalent matcher DSL rule. Empty fields are omitted; an
|
||||
// empty result means the node is unconditional. Path maps to PathPrefix
|
||||
|
||||
@@ -43,6 +43,7 @@ func TestMergeConfig_AppendsAllSlices(t *testing.T) {
|
||||
Services: []*config.ServiceConfig{{Name: "s1"}},
|
||||
Chains: []*config.ChainConfig{{Name: "c1"}},
|
||||
Hops: []*config.HopConfig{{Name: "h1"}},
|
||||
Nodes: []*config.NodeConfig{{Name: "n1", Addr: "n1.example:80"}},
|
||||
Authers: []*config.AutherConfig{{Name: "a1"}},
|
||||
Admissions: []*config.AdmissionConfig{{Name: "ad1"}},
|
||||
Bypasses: []*config.BypassConfig{{Name: "b1"}},
|
||||
@@ -63,6 +64,7 @@ func TestMergeConfig_AppendsAllSlices(t *testing.T) {
|
||||
Services: []*config.ServiceConfig{{Name: "s2"}},
|
||||
Chains: []*config.ChainConfig{{Name: "c2"}},
|
||||
Hops: []*config.HopConfig{{Name: "h2"}},
|
||||
Nodes: []*config.NodeConfig{{Name: "n2", Addr: "n2.example:80"}},
|
||||
Authers: []*config.AutherConfig{{Name: "a2"}},
|
||||
Admissions: []*config.AdmissionConfig{{Name: "ad2"}},
|
||||
Bypasses: []*config.BypassConfig{{Name: "b2"}},
|
||||
@@ -91,6 +93,9 @@ func TestMergeConfig_AppendsAllSlices(t *testing.T) {
|
||||
if len(got.Hops) != 2 {
|
||||
t.Fatal("hops not properly appended")
|
||||
}
|
||||
if len(got.Nodes) != 2 {
|
||||
t.Fatal("nodes not properly appended")
|
||||
}
|
||||
if len(got.Authers) != 2 {
|
||||
t.Fatal("authers not properly appended")
|
||||
}
|
||||
|
||||
@@ -320,6 +320,7 @@ func mergeConfig(cfg1, cfg2 *config.Config) *config.Config {
|
||||
Services: append(cfg1.Services, cfg2.Services...),
|
||||
Chains: append(cfg1.Chains, cfg2.Chains...),
|
||||
Hops: append(cfg1.Hops, cfg2.Hops...),
|
||||
Nodes: append(cfg1.Nodes, cfg2.Nodes...),
|
||||
Authers: append(cfg1.Authers, cfg2.Authers...),
|
||||
Admissions: append(cfg1.Admissions, cfg2.Admissions...),
|
||||
Bypasses: append(cfg1.Bypasses, cfg2.Bypasses...),
|
||||
|
||||
+8
-1
@@ -484,6 +484,14 @@ func (p *chainHop) parseNode(r io.Reader) ([]*chain.Node, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
name := nc.Name
|
||||
nc, err := node_parser.ResolveConfig(nc)
|
||||
if err != nil {
|
||||
p.logger.Warnf("skip node %s: %v", name, err)
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
node, err := node_parser.ParseNode(p.options.name, nc, logger.Default())
|
||||
if err != nil {
|
||||
p.logger.Warnf("skip node %s: %v", nc.Name, err)
|
||||
@@ -513,4 +521,3 @@ func (p *chainHop) Close() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+23
-3
@@ -17,11 +17,12 @@ import (
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/metadata"
|
||||
"github.com/go-gost/core/routing"
|
||||
xmd "github.com/go-gost/x/metadata"
|
||||
"github.com/go-gost/x/config"
|
||||
xlogger "github.com/go-gost/x/logger"
|
||||
xselector "github.com/go-gost/x/selector"
|
||||
xrouting "github.com/go-gost/x/routing"
|
||||
xmd "github.com/go-gost/x/metadata"
|
||||
"github.com/go-gost/x/registry"
|
||||
xrouting "github.com/go-gost/x/routing"
|
||||
xselector "github.com/go-gost/x/selector"
|
||||
)
|
||||
|
||||
// mustMatcher parses a matcher DSL rule, failing the test on error.
|
||||
@@ -1035,6 +1036,24 @@ func TestParseNode_ValidJSON(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNode_GlobalReference(t *testing.T) {
|
||||
const name = "reloaded-global-node"
|
||||
registry.NodeRegistry().Register(name, &config.NodeConfig{
|
||||
Name: name,
|
||||
Addr: "10.0.0.2:8080",
|
||||
})
|
||||
t.Cleanup(func() { registry.NodeRegistry().Unregister(name) })
|
||||
|
||||
ch := &chainHop{logger: xlogger.Nop(), options: options{name: "test-hop"}}
|
||||
nodes, err := ch.parseNode(strings.NewReader(`[{"name":"reloaded-global-node"}]`))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(nodes) != 1 || nodes[0].Name != name || nodes[0].Addr != "10.0.0.2:8080" {
|
||||
t.Fatalf("unexpected nodes: %+v", nodes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNode_EmptyArray(t *testing.T) {
|
||||
ch := &chainHop{logger: xlogger.Nop()}
|
||||
r := strings.NewReader(`[]`)
|
||||
@@ -1277,6 +1296,7 @@ func TestSelect_BackupPriority_LowerPriorityPrimaryWins(t *testing.T) {
|
||||
t.Errorf("expected 'primary-low' (lower-priority primary beats higher-priority backup), got %q", node.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Interface satisfaction
|
||||
// =============================================================================
|
||||
|
||||
@@ -21,10 +21,11 @@ import (
|
||||
"github.com/go-gost/core/recorder"
|
||||
reg "github.com/go-gost/core/registry"
|
||||
"github.com/go-gost/core/resolver"
|
||||
"github.com/go-gost/core/router"
|
||||
"github.com/go-gost/core/rewriter"
|
||||
"github.com/go-gost/core/router"
|
||||
"github.com/go-gost/core/sd"
|
||||
"github.com/go-gost/core/service"
|
||||
"github.com/go-gost/x/config"
|
||||
"github.com/go-gost/x/limiter/quota"
|
||||
xp2p "github.com/go-gost/x/p2p"
|
||||
)
|
||||
@@ -43,6 +44,7 @@ var (
|
||||
serviceReg reg.Registry[service.Service] = new(serviceRegistry)
|
||||
chainReg reg.Registry[chain.Chainer] = new(chainRegistry)
|
||||
hopReg reg.Registry[hop.Hop] = new(hopRegistry)
|
||||
nodeReg reg.Registry[*config.NodeConfig] = new(registry[*config.NodeConfig])
|
||||
autherReg reg.Registry[auth.Authenticator] = new(autherRegistry)
|
||||
admissionReg reg.Registry[admission.Admission] = new(admissionRegistry)
|
||||
bypassReg reg.Registry[bypass.Bypass] = new(bypassRegistry)
|
||||
@@ -164,6 +166,11 @@ func HopRegistry() reg.Registry[hop.Hop] {
|
||||
return hopReg
|
||||
}
|
||||
|
||||
// NodeRegistry returns the global registry of reusable node configurations.
|
||||
func NodeRegistry() reg.Registry[*config.NodeConfig] {
|
||||
return nodeReg
|
||||
}
|
||||
|
||||
// AutherRegistry returns the global registry of authenticator instances.
|
||||
func AutherRegistry() reg.Registry[auth.Authenticator] {
|
||||
return autherReg
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
"github.com/go-gost/core/sd"
|
||||
"github.com/go-gost/core/selector"
|
||||
"github.com/go-gost/core/service"
|
||||
"github.com/go-gost/x/config"
|
||||
)
|
||||
|
||||
// --- Base registry[T] tests ---
|
||||
@@ -213,6 +214,9 @@ func TestGlobalRegistryAccessors(t *testing.T) {
|
||||
if HopRegistry() == nil {
|
||||
t.Error("HopRegistry() returned nil")
|
||||
}
|
||||
if NodeRegistry() == nil {
|
||||
t.Error("NodeRegistry() returned nil")
|
||||
}
|
||||
if AutherRegistry() == nil {
|
||||
t.Error("AutherRegistry() returned nil")
|
||||
}
|
||||
@@ -280,6 +284,9 @@ func TestGlobalRegistrySingleton(t *testing.T) {
|
||||
if HopRegistry() != HopRegistry() {
|
||||
t.Error("HopRegistry not singleton")
|
||||
}
|
||||
if NodeRegistry() != NodeRegistry() {
|
||||
t.Error("NodeRegistry not singleton")
|
||||
}
|
||||
if AutherRegistry() != AutherRegistry() {
|
||||
t.Error("AutherRegistry not singleton")
|
||||
}
|
||||
@@ -1047,4 +1054,4 @@ var _ reg.Registry[router.Router] = (*routerRegistry)(nil)
|
||||
var _ reg.Registry[sd.SD] = (*sdRegistry)(nil)
|
||||
var _ reg.Registry[observer.Observer] = (*observerRegistry)(nil)
|
||||
var _ reg.Registry[logger.Logger] = (*loggerRegistry)(nil)
|
||||
|
||||
var _ reg.Registry[*config.NodeConfig] = NodeRegistry()
|
||||
|
||||
Reference in New Issue
Block a user