mirror of
https://github.com/openziti/ziti.git
synced 2026-10-26 13:39:58 +00:00
0ee288ecbf
- adds a `DataState.Router` event variant (id, name, fingerprint, configs) to the RDM protobuf and the matching ConfigType.Target field flows - loads routers into `RouterDataModelSender` at startup and registers entity-change listeners so router create/update/delete and config reassignment emit RDM events - filters `Config` events per-router at `RouterSender`: each router sees the full `Router` set but only its own router-target Configs - receiver-side: parses and stores `Router` entities; GCs router-target Configs locally when they drop off the router's `Configs` list, so no synthetic remove events are needed on the wire - extends the `router-data-model-test` fablab model with router-config distribution scenarios (assignment, reassignment, controller restart, RDM cache miss) - updates `validate-router-data-model` to recognize the new event shape - refreshes the design doc to describe per-router filtering and the change-notification flow
340 lines
10 KiB
Go
340 lines
10 KiB
Go
/*
|
|
Copyright NetFoundry Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package sync_strats
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/openziti/ziti/v2/common"
|
|
"github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb"
|
|
"github.com/openziti/ziti/v2/controller/model"
|
|
"github.com/openziti/ziti/v2/controller/models"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type stubTimelineSource string
|
|
|
|
func (s stubTimelineSource) TimelineId() string { return string(s) }
|
|
|
|
const (
|
|
testRouterTargetTypeId = "router-target.test"
|
|
testServiceTargetTypeId = "service-target.test"
|
|
)
|
|
|
|
func newTestRtx(routerId string, routerConfigs []string, rdmConfigs map[string]*edge_ctrl_pb.DataState_Config) *RouterSender {
|
|
rdm := common.NewRouterDataModelSender(stubTimelineSource("test-timeline"), 16, 1)
|
|
rdm.ConfigTypes.Set(testRouterTargetTypeId, &edge_ctrl_pb.DataState_ConfigType{
|
|
Id: testRouterTargetTypeId,
|
|
Name: testRouterTargetTypeId,
|
|
Target: common.ConfigTypeTargetRouter,
|
|
})
|
|
rdm.ConfigTypes.Set(testServiceTargetTypeId, &edge_ctrl_pb.DataState_ConfigType{
|
|
Id: testServiceTargetTypeId,
|
|
Name: testServiceTargetTypeId,
|
|
Target: "service",
|
|
})
|
|
for id, cfg := range rdmConfigs {
|
|
// default un-typed test configs to the router-target type, so existing
|
|
// tests still exercise the filter path
|
|
if cfg.TypeId == "" {
|
|
cfg.TypeId = testRouterTargetTypeId
|
|
}
|
|
rdm.Configs.Set(id, cfg)
|
|
}
|
|
rdm.Routers.Set(routerId, &edge_ctrl_pb.DataState_Router{
|
|
Id: routerId,
|
|
Configs: routerConfigs,
|
|
})
|
|
return &RouterSender{
|
|
Router: &model.Router{
|
|
BaseEntity: models.BaseEntity{Id: routerId},
|
|
Configs: routerConfigs,
|
|
},
|
|
routerDataModel: rdm,
|
|
}
|
|
}
|
|
|
|
func cfgEvent(id string) *edge_ctrl_pb.DataState_Event {
|
|
return &edge_ctrl_pb.DataState_Event{
|
|
Action: edge_ctrl_pb.DataState_Create,
|
|
Model: &edge_ctrl_pb.DataState_Event_Config{
|
|
Config: &edge_ctrl_pb.DataState_Config{Id: id, Name: id, TypeId: testRouterTargetTypeId},
|
|
},
|
|
}
|
|
}
|
|
|
|
func serviceCfgEvent(id string) *edge_ctrl_pb.DataState_Event {
|
|
return &edge_ctrl_pb.DataState_Event{
|
|
Action: edge_ctrl_pb.DataState_Create,
|
|
Model: &edge_ctrl_pb.DataState_Event_Config{
|
|
Config: &edge_ctrl_pb.DataState_Config{Id: id, Name: id, TypeId: testServiceTargetTypeId},
|
|
},
|
|
}
|
|
}
|
|
|
|
func routerEvent(id string, configs []string, action edge_ctrl_pb.DataState_Action) *edge_ctrl_pb.DataState_Event {
|
|
return &edge_ctrl_pb.DataState_Event{
|
|
Action: action,
|
|
Model: &edge_ctrl_pb.DataState_Event_Router{
|
|
Router: &edge_ctrl_pb.DataState_Router{Id: id, Configs: configs},
|
|
},
|
|
}
|
|
}
|
|
|
|
func serviceEvent(id string) *edge_ctrl_pb.DataState_Event {
|
|
return &edge_ctrl_pb.DataState_Event{
|
|
Action: edge_ctrl_pb.DataState_Create,
|
|
Model: &edge_ctrl_pb.DataState_Event_Service{
|
|
Service: &edge_ctrl_pb.DataState_Service{Id: id},
|
|
},
|
|
}
|
|
}
|
|
|
|
func Test_filterEventsForRouter_dropsUnrelatedConfigs(t *testing.T) {
|
|
req := require.New(t)
|
|
rtx := newTestRtx("r1", []string{"cfg-A"}, map[string]*edge_ctrl_pb.DataState_Config{
|
|
"cfg-A": {Id: "cfg-A"},
|
|
"cfg-B": {Id: "cfg-B"},
|
|
})
|
|
|
|
in := []*edge_ctrl_pb.DataState_Event{
|
|
cfgEvent("cfg-A"),
|
|
cfgEvent("cfg-B"),
|
|
serviceEvent("svc-1"),
|
|
}
|
|
out, changed := rtx.filterEventsForRouter(in, true)
|
|
|
|
req.True(changed)
|
|
req.Len(out, 2)
|
|
req.Equal("cfg-A", out[0].Model.(*edge_ctrl_pb.DataState_Event_Config).Config.Id)
|
|
req.Equal("svc-1", out[1].Model.(*edge_ctrl_pb.DataState_Event_Service).Service.Id)
|
|
}
|
|
|
|
func Test_filterEventsForRouter_passesNonConfigEvents(t *testing.T) {
|
|
req := require.New(t)
|
|
rtx := newTestRtx("r1", nil, nil)
|
|
|
|
in := []*edge_ctrl_pb.DataState_Event{
|
|
serviceEvent("svc-1"),
|
|
{
|
|
Action: edge_ctrl_pb.DataState_Create,
|
|
Model: &edge_ctrl_pb.DataState_Event_Identity{
|
|
Identity: &edge_ctrl_pb.DataState_Identity{Id: "i1"},
|
|
},
|
|
},
|
|
}
|
|
out, changed := rtx.filterEventsForRouter(in, true)
|
|
|
|
req.False(changed, "expected pass-through to leave events untouched")
|
|
req.Same(&in[0], &out[0], "expected the same backing slice when no changes")
|
|
req.Len(out, 2)
|
|
}
|
|
|
|
func Test_filterEventsForRouter_unchangedWhenAllConfigsBelongToRouter(t *testing.T) {
|
|
req := require.New(t)
|
|
rtx := newTestRtx("r1", []string{"cfg-A", "cfg-B"}, map[string]*edge_ctrl_pb.DataState_Config{
|
|
"cfg-A": {Id: "cfg-A"},
|
|
"cfg-B": {Id: "cfg-B"},
|
|
})
|
|
|
|
in := []*edge_ctrl_pb.DataState_Event{
|
|
cfgEvent("cfg-A"),
|
|
cfgEvent("cfg-B"),
|
|
serviceEvent("svc-1"),
|
|
}
|
|
out, changed := rtx.filterEventsForRouter(in, true)
|
|
|
|
req.False(changed)
|
|
req.Same(&in[0], &out[0])
|
|
req.Len(out, 3)
|
|
}
|
|
|
|
func Test_filterEventsForRouter_synthesizesConfigsForSelfRouterUpdate(t *testing.T) {
|
|
req := require.New(t)
|
|
rtx := newTestRtx("r1", []string{"cfg-A", "cfg-B"}, map[string]*edge_ctrl_pb.DataState_Config{
|
|
"cfg-A": {Id: "cfg-A", Name: "A"},
|
|
"cfg-B": {Id: "cfg-B", Name: "B"},
|
|
})
|
|
|
|
out, changed := rtx.filterEventsForRouter([]*edge_ctrl_pb.DataState_Event{
|
|
routerEvent("r1", []string{"cfg-A", "cfg-B"}, edge_ctrl_pb.DataState_Update),
|
|
}, true)
|
|
|
|
req.True(changed)
|
|
// Two synthetic Config Create events, then the original Router event.
|
|
req.Len(out, 3)
|
|
|
|
cfgA, ok := out[0].Model.(*edge_ctrl_pb.DataState_Event_Config)
|
|
req.True(ok)
|
|
req.Equal("cfg-A", cfgA.Config.Id)
|
|
req.True(out[0].IsSynthetic)
|
|
|
|
cfgB, ok := out[1].Model.(*edge_ctrl_pb.DataState_Event_Config)
|
|
req.True(ok)
|
|
req.Equal("cfg-B", cfgB.Config.Id)
|
|
req.True(out[1].IsSynthetic)
|
|
|
|
_, ok = out[2].Model.(*edge_ctrl_pb.DataState_Event_Router)
|
|
req.True(ok)
|
|
}
|
|
|
|
func Test_filterEventsForRouter_otherRouterEventDoesNotSynthesize(t *testing.T) {
|
|
req := require.New(t)
|
|
rtx := newTestRtx("r1", []string{"cfg-A"}, map[string]*edge_ctrl_pb.DataState_Config{
|
|
"cfg-A": {Id: "cfg-A"},
|
|
"cfg-X": {Id: "cfg-X"},
|
|
})
|
|
|
|
out, changed := rtx.filterEventsForRouter([]*edge_ctrl_pb.DataState_Event{
|
|
routerEvent("r2", []string{"cfg-X"}, edge_ctrl_pb.DataState_Update),
|
|
}, true)
|
|
|
|
req.False(changed)
|
|
req.Len(out, 1)
|
|
_, ok := out[0].Model.(*edge_ctrl_pb.DataState_Event_Router)
|
|
req.True(ok)
|
|
}
|
|
|
|
func Test_filterEventsForRouter_fullSyncDoesNotSynthesize(t *testing.T) {
|
|
req := require.New(t)
|
|
rtx := newTestRtx("r1", []string{"cfg-A"}, map[string]*edge_ctrl_pb.DataState_Config{
|
|
"cfg-A": {Id: "cfg-A"},
|
|
})
|
|
|
|
out, changed := rtx.filterEventsForRouter([]*edge_ctrl_pb.DataState_Event{
|
|
routerEvent("r1", []string{"cfg-A"}, edge_ctrl_pb.DataState_Update),
|
|
}, false)
|
|
|
|
req.False(changed)
|
|
req.Len(out, 1)
|
|
_, ok := out[0].Model.(*edge_ctrl_pb.DataState_Event_Router)
|
|
req.True(ok)
|
|
}
|
|
|
|
func Test_filterEventsForRouter_selfRouterDeleteDoesNotSynthesize(t *testing.T) {
|
|
req := require.New(t)
|
|
rtx := newTestRtx("r1", []string{"cfg-A"}, map[string]*edge_ctrl_pb.DataState_Config{
|
|
"cfg-A": {Id: "cfg-A"},
|
|
})
|
|
|
|
out, changed := rtx.filterEventsForRouter([]*edge_ctrl_pb.DataState_Event{
|
|
routerEvent("r1", []string{"cfg-A"}, edge_ctrl_pb.DataState_Delete),
|
|
}, true)
|
|
|
|
req.False(changed)
|
|
req.Len(out, 1)
|
|
_, ok := out[0].Model.(*edge_ctrl_pb.DataState_Event_Router)
|
|
req.True(ok)
|
|
}
|
|
|
|
func Test_filterEventsForRouter_passesServiceTargetConfigs(t *testing.T) {
|
|
req := require.New(t)
|
|
// Router R1 has no router-target configs at all. A service-target config
|
|
// (any target other than "router") must still flow.
|
|
rtx := newTestRtx("r1", nil, nil)
|
|
in := []*edge_ctrl_pb.DataState_Event{
|
|
serviceCfgEvent("svc-cfg-A"),
|
|
serviceCfgEvent("svc-cfg-B"),
|
|
}
|
|
out, changed := rtx.filterEventsForRouter(in, true)
|
|
|
|
req.False(changed, "service-target configs should pass through unchanged")
|
|
req.Same(&in[0], &out[0])
|
|
req.Len(out, 2)
|
|
}
|
|
|
|
func Test_filterEventsForRouter_filtersOnlyRouterTargetConfigs(t *testing.T) {
|
|
req := require.New(t)
|
|
// Router R1 has router-target cfg-A. Service-target cfg-S1 always flows;
|
|
// router-target cfg-B is dropped because it isn't in R1's list.
|
|
rtx := newTestRtx("r1", []string{"cfg-A"}, map[string]*edge_ctrl_pb.DataState_Config{
|
|
"cfg-A": {Id: "cfg-A"},
|
|
"cfg-B": {Id: "cfg-B"},
|
|
})
|
|
|
|
out, changed := rtx.filterEventsForRouter([]*edge_ctrl_pb.DataState_Event{
|
|
cfgEvent("cfg-A"),
|
|
cfgEvent("cfg-B"),
|
|
serviceCfgEvent("svc-cfg-1"),
|
|
}, true)
|
|
|
|
req.True(changed)
|
|
req.Len(out, 2)
|
|
req.Equal("cfg-A", out[0].Model.(*edge_ctrl_pb.DataState_Event_Config).Config.Id)
|
|
req.Equal("svc-cfg-1", out[1].Model.(*edge_ctrl_pb.DataState_Event_Config).Config.Id)
|
|
}
|
|
|
|
func Test_filterEventsForRouter_unknownTypePassesThrough(t *testing.T) {
|
|
req := require.New(t)
|
|
// Defensive: a Config event whose type can't be resolved from the RDM cache
|
|
// must NOT be silently dropped. It flows through (treated as non-router-target).
|
|
rtx := newTestRtx("r1", nil, nil)
|
|
in := []*edge_ctrl_pb.DataState_Event{
|
|
{
|
|
Action: edge_ctrl_pb.DataState_Create,
|
|
Model: &edge_ctrl_pb.DataState_Event_Config{
|
|
Config: &edge_ctrl_pb.DataState_Config{Id: "cfg-X", TypeId: "unknown-type"},
|
|
},
|
|
},
|
|
}
|
|
out, changed := rtx.filterEventsForRouter(in, true)
|
|
|
|
req.False(changed)
|
|
req.Len(out, 1)
|
|
}
|
|
|
|
func Test_filterEventsForRouter_dropsAllConfigsWhenRouterNotInRdmCache(t *testing.T) {
|
|
req := require.New(t)
|
|
// Router not yet in RDM cache (e.g. brand-new connect, BuildRouters hasn't run).
|
|
// All Config events should be filtered out; non-Config events still flow.
|
|
rtx := newTestRtx("r1", nil, map[string]*edge_ctrl_pb.DataState_Config{
|
|
"cfg-A": {Id: "cfg-A"},
|
|
})
|
|
rtx.routerDataModel.Routers.Remove("r1")
|
|
|
|
out, changed := rtx.filterEventsForRouter([]*edge_ctrl_pb.DataState_Event{
|
|
cfgEvent("cfg-A"),
|
|
serviceEvent("svc-1"),
|
|
}, true)
|
|
|
|
req.True(changed)
|
|
req.Len(out, 1)
|
|
_, ok := out[0].Model.(*edge_ctrl_pb.DataState_Event_Service)
|
|
req.True(ok)
|
|
}
|
|
|
|
func Test_filterEventsForRouter_skipsSynthesisForUnknownConfig(t *testing.T) {
|
|
req := require.New(t)
|
|
// router R has [cfg-A, cfg-missing], but cfg-missing isn't in the RDM cache yet.
|
|
rtx := newTestRtx("r1", []string{"cfg-A", "cfg-missing"}, map[string]*edge_ctrl_pb.DataState_Config{
|
|
"cfg-A": {Id: "cfg-A"},
|
|
})
|
|
|
|
out, changed := rtx.filterEventsForRouter([]*edge_ctrl_pb.DataState_Event{
|
|
routerEvent("r1", []string{"cfg-A", "cfg-missing"}, edge_ctrl_pb.DataState_Update),
|
|
}, true)
|
|
|
|
req.True(changed)
|
|
// Only the Config for cfg-A is synthesized; the original Router event still flows.
|
|
req.Len(out, 2)
|
|
cfgA, ok := out[0].Model.(*edge_ctrl_pb.DataState_Event_Config)
|
|
req.True(ok)
|
|
req.Equal("cfg-A", cfgA.Config.Id)
|
|
_, ok = out[1].Model.(*edge_ctrl_pb.DataState_Event_Router)
|
|
req.True(ok)
|
|
}
|