Files
Paul Lorenz 187aa11f24 Own the metrics wire format in ziti. Fixes #4036
- adds a common/servermetrics package that owns the metrics MetricsMessage wire
  format and the reporting/usage subsystem (message builder, usage registry,
  interval and usage counters), wrapping the openziti/metrics Registry for
  metric collection
- moves the controllers metrics reporter into the router package and removes it
  from the shared metrics package, breaking a common -> router/env import cycle
- repoints controller and router consumers to common/servermetrics; base metric
  collection stays on openziti/metrics
- keeps the proto field numbers and the metrics content-type identical so the
  encoding is byte-compatible across the move, and uses a distinct proto package
  name so ziti's and the library's messages coexist without a global proto
  registry clash
- adds a round-trip test asserting wire compatibility with the library's
  MetricsMessage
- leaves openziti/metrics unchanged, so sdk-golang and the shared xgress data
  plane are unaffected
2026-06-29 22:37:25 -04:00

382 lines
10 KiB
Go

//go:build apitests
package tests
import (
"io"
"testing"
"time"
"github.com/openziti/channel/v5"
"github.com/openziti/channel/v5/protobufs"
"github.com/openziti/foundation/v2/goroutines"
id "github.com/openziti/identity"
"github.com/openziti/metrics"
"github.com/openziti/sdk-golang/v2/xgress"
"github.com/openziti/transport/v2"
"github.com/openziti/ziti/v2/common/ctrlchan"
"github.com/openziti/ziti/v2/common/pb/ctrl_pb"
"github.com/openziti/ziti/v2/common/servermetrics"
"github.com/openziti/ziti/v2/router/env"
"github.com/openziti/ziti/v2/router/link"
"github.com/openziti/ziti/v2/router/xlink"
"github.com/openziti/ziti/v2/router/xlink_transport"
"github.com/openziti/ziti/v2/tests/testutil"
"github.com/sirupsen/logrus"
"google.golang.org/protobuf/proto"
)
type testXlinkAcceptor struct {
link xlink.Xlink
}
func (self *testXlinkAcceptor) Accept(l xlink.Xlink) error {
logrus.Infof("xlink accepted: %+v", l)
self.link = l
return nil
}
func (self *testXlinkAcceptor) getLink() xlink.Xlink {
return self.link
}
type testBindHandlerFactory struct{}
func (t testBindHandlerFactory) BindChannel(channel.Binding) error {
return nil
}
func (t testBindHandlerFactory) NewBindHandler(l xlink.Xlink, _ bool, _ bool) channel.BindHandler {
_ = l.Init(metrics.NewRegistry("test", nil))
return t
}
type testRegistryEnv struct {
ctrls env.NetworkControllers
closeNotify chan struct{}
metricsRegistry servermetrics.UsageRegistry
}
func (self *testRegistryEnv) GetRouterId() *id.TokenId {
return &id.TokenId{
Token: "test-router",
}
}
func (self *testRegistryEnv) GetNetworkControllers() env.NetworkControllers {
return self.ctrls
}
func (self *testRegistryEnv) GetXlinkDialers() []xlink.Dialer {
panic("implement me")
}
func (self *testRegistryEnv) GetCloseNotify() <-chan struct{} {
return self.closeNotify
}
func (self *testRegistryEnv) GetLinkDialerPool() goroutines.Pool {
panic("implement me")
}
func (self *testRegistryEnv) GetRateLimiterPool() goroutines.Pool {
panic("implement me")
}
func (self *testRegistryEnv) GetMetricsRegistry() servermetrics.UsageRegistry {
return self.metricsRegistry
}
type testDial struct {
Key string
LinkId string
RouterId string
Address string
LinkProtocol string
RouterVersion string
}
func (self *testDial) GetLinkKey() string {
return self.Key
}
func (self *testDial) GetLinkId() string {
return self.LinkId
}
func (self *testDial) GetRouterId() string {
return self.RouterId
}
func (self *testDial) GetAddress() string {
return self.Address
}
func (self *testDial) GetLinkProtocol() string {
return self.LinkProtocol
}
func (self *testDial) GetRouterVersion() string {
return self.RouterVersion
}
func (self *testDial) GetIteration() uint32 {
return 1
}
type dialEnvMock struct{}
func (self *dialEnvMock) GetChannelHeaders() (channel.Headers, error) {
return channel.Headers{}, nil
}
func (self *dialEnvMock) GetConfig() *env.Config {
cfg := &env.Config{}
cfg.Ctrl.DefaultRequestTimeout = time.Second
return cfg
}
func (self *dialEnvMock) GetCtrlChannelBindHandler() channel.BindHandler {
return nil
}
func (self *dialEnvMock) NotifyOfReconnect(ctrlchan.CtrlChannel) {
}
func setupEnv() link.Env {
closeNotify := make(chan struct{})
ctrls := env.NewNetworkControllers(&dialEnvMock{}, env.NewDefaultHeartbeatOptions())
registryConfig := servermetrics.DefaultUsageRegistryConfig("test", closeNotify)
metricsRegistry := servermetrics.NewUsageRegistry(registryConfig)
return &testRegistryEnv{
ctrls: ctrls,
closeNotify: closeNotify,
metricsRegistry: metricsRegistry,
}
}
type testLinkEnv struct {
link.Env
linkRegistry xlink.Registry
}
func (self *testLinkEnv) GetXLinkRegistry() xlink.Registry {
return self.linkRegistry
}
func (self *testLinkEnv) GetLinkPayloadSenderQueueSize() int {
return env.DefaultLinkPayloadSenderQueueSize
}
func (self *testLinkEnv) GetLinkAckSenderQueueSize() int {
return env.DefaultLinkAckSenderQueueSize
}
func newTestLinkEnv() *testLinkEnv {
e := setupEnv()
linkRegistry := link.NewLinkRegistry(e)
return &testLinkEnv{
linkRegistry: linkRegistry,
Env: e,
}
}
func Test_LinkWithValidCertFromUnknownChain(t *testing.T) {
ctx := NewTestContext(t)
defer ctx.Teardown()
ctx.StartServer()
ctx.RequireAdminManagementApiLogin()
ctx.EnrollFabricRouter("001", "router-1", "testdata/pki/ctrl1/certs/001-client.cert")
ctx.startFabricRouter(1)
ctx.Req.NoError(ctx.waitForPort("127.0.0.1:6004", 2*time.Second))
badId, err := id.LoadClientIdentity(
"./testdata/invalid_client_cert/client.cert",
"./testdata/invalid_client_cert/client.key",
"./testdata/pki/root/certs/root.cert")
ctx.Req.NoError(err)
xla := &testXlinkAcceptor{}
tcfg := transport.Configuration{
"split": false,
}
linkEnv := newTestLinkEnv()
factory := xlink_transport.NewFactory(xla, testBindHandlerFactory{}, tcfg, linkEnv)
dialer, err := factory.CreateDialer(badId, tcfg)
ctx.Req.NoError(err)
dialReq := &testDial{
Key: "default->tls:router1->default",
LinkId: "testLinkId",
Address: "tls:127.0.0.1:6004",
RouterId: "002",
LinkProtocol: "tls",
}
_, err = dialer.Dial(dialReq)
ctx.Req.Error(err)
ctx.Req.ErrorIs(err, io.EOF)
}
func Test_UnrequestedLinkFromValidRouter(t *testing.T) {
ctx := NewTestContext(t)
defer ctx.Teardown()
ctx.StartServer()
ctx.RequireAdminManagementApiLogin()
ctx.EnrollFabricRouter("001", "router-1", "testdata/pki/ctrl1/certs/001-client.cert")
ctx.EnrollFabricRouter("002", "router-2", "testdata/pki/ctrl1/certs/002-client.cert")
ctx.startFabricRouter(1)
ctx.Req.NoError(ctx.waitForPort("127.0.0.1:6004", 2*time.Second))
router2Id, err := id.LoadClientIdentity(
"./testdata/pki/ctrl1/certs/002-client.chain.pem",
"./testdata/pki/ctrl1/keys/002.key",
"./testdata/pki/root/certs/root.cert")
ctx.Req.NoError(err)
xla := &testXlinkAcceptor{}
tcfg := transport.Configuration{
"split": false,
}
linkEnv := newTestLinkEnv()
factory := xlink_transport.NewFactory(xla, testBindHandlerFactory{}, tcfg, linkEnv)
dialer, err := factory.CreateDialer(router2Id, tcfg)
ctx.Req.NoError(err)
dialReq := &testDial{
Key: "default->tls:router1->default",
LinkId: "testLinkId",
Address: "tls:127.0.0.1:6004",
RouterId: "002",
LinkProtocol: "tls",
}
_, err = dialer.Dial(dialReq)
if err != nil {
ctx.Req.ErrorIs(err, io.EOF, "unexpected error: %v", err)
} else {
for i := int32(0); i < 100 && err == nil; i++ {
payload := &xgress.Payload{
CircuitId: "hello",
Sequence: i,
Headers: nil,
Data: []byte{1, 2, 3, 4},
}
err = xla.getLink().SendPayload(payload, time.Second, xgress.PayloadTypeXg)
ctx.Req.NoErrorf(err, "iteration %v", i)
}
}
}
func Test_DuplicateLinkWithLinkCloseDialer(t *testing.T) {
ctx := NewTestContext(t)
defer ctx.Teardown()
ctx.StartServer()
ctx.RequireAdminManagementApiLogin()
ctx.EnrollFabricRouter("001", "router-1", "testdata/pki/ctrl1/certs/001-client.cert")
ctx.EnrollFabricRouter("002", "router-2", "testdata/pki/ctrl1/certs/002-client.cert")
ctx.Teardown()
ctrlListener := ctx.NewControlChannelListener()
defer func() { _ = ctrlListener.Close() }()
router1 := ctx.startFabricRouter(1)
linkChecker := testutil.NewLinkChecker(ctx.Req.Assertions)
router1cc := testutil.StartLinkTest(linkChecker, "router-1", ctrlListener, ctx.Req.Assertions)
router1Listeners := &ctrl_pb.Listeners{}
if val, found := router1cc.Underlay().Headers()[int32(ctrl_pb.ControlHeaders_ListenersHeader)]; found {
ctx.Req.NoError(proto.Unmarshal(val, router1Listeners))
}
router2 := ctx.startFabricRouter(2)
router2cc := testutil.StartLinkTest(linkChecker, "router-2", ctrlListener, ctx.Req.Assertions)
router2Listeners := &ctrl_pb.Listeners{}
if val, found := router2cc.Underlay().Headers()[int32(ctrl_pb.ControlHeaders_ListenersHeader)]; found {
ctx.Req.NoError(proto.Unmarshal(val, router2Listeners))
}
peerUpdates1 := &ctrl_pb.PeerStateChanges{
Changes: []*ctrl_pb.PeerStateChange{
{
Id: router1.GetRouterId().Token,
Version: "v0.0.0",
State: ctrl_pb.PeerState_Healthy,
Listeners: router1Listeners.Listeners,
},
},
}
ctx.Req.NoError(protobufs.MarshalTyped(peerUpdates1).WithTimeout(time.Second).SendAndWaitForWire(router2cc))
peerUpdates2 := &ctrl_pb.PeerStateChanges{
Changes: []*ctrl_pb.PeerStateChange{
{
Id: router2.GetRouterId().Token,
Version: "v0.0.0",
State: ctrl_pb.PeerState_Healthy,
Listeners: router2Listeners.Listeners,
},
},
}
ctx.Req.NoError(protobufs.MarshalTyped(peerUpdates2).WithTimeout(time.Second).SendAndWaitForWire(router1cc))
time.Sleep(time.Second)
linkChecker.RequireNoErrors()
link1 := linkChecker.RequireOneActiveLink()
linkChecker.RequireNoErrors()
link2 := linkChecker.RequireOneActiveLink()
ctx.Req.Equal(link1.Id, link2.Id)
// Test closing control ch to router 1. On reconnect the existing link should get reported
ctx.Req.NoError(router1cc.Close())
_ = testutil.StartLinkTest(linkChecker, "router-1", ctrlListener, ctx.Req.Assertions)
time.Sleep(time.Second)
linkChecker.RequireNoErrors()
link1 = linkChecker.RequireOneActiveLink()
ctx.Req.Equal(link1.Id, link2.Id)
// Test closing control ch to router 2. On reconnect the existing link should get reported
ctx.Req.NoError(router2cc.Close())
_ = testutil.StartLinkTest(linkChecker, "router-2", ctrlListener, ctx.Req.Assertions)
time.Sleep(time.Second)
linkChecker.RequireNoErrors()
link2 = linkChecker.RequireOneActiveLink()
ctx.Req.Equal(link1.Id, link2.Id)
// restart router 1
ctx.Req.NoError(router1.Shutdown())
ctx.Req.NoError(ctx.waitForPortClose("localhost:6004", 2*time.Second))
router1 = ctx.startFabricRouter(1)
defer func() {
ctx.Req.NoError(router1.Shutdown())
}()
router1cc = testutil.StartLinkTest(linkChecker, "router-1", ctrlListener, ctx.Req.Assertions)
ctx.Req.NoError(protobufs.MarshalTyped(peerUpdates2).WithTimeout(time.Second).SendAndWaitForWire(router1cc))
linkChecker.RequireNoErrors()
//time.Sleep(time.Minute)
//
//linkCheck1.RequireNoErrors()
//link1 = linkCheck1.RequireOneActiveLink()
//
//linkCheck2.RequireNoErrors()
//link2 = linkCheck1.RequireOneActiveLink()
ctx.Req.Equal(link1.Id, link2.Id)
ctx.Teardown()
_ = router1cc.Close()
_ = router2cc.Close()
}