mirror of
https://github.com/openziti/ziti.git
synced 2026-09-10 00:35:41 +00:00
48e4d4a224
- adds a link ConfigHandler (router/link FactoryRegistry) that applies router.link.v1 config: Apply rebuilds the listener/dialer set wholesale, and established Xlinks survive because Listener.Close() only closes the accept loop - translates local link: YAML into router.link.v1 JSON and pushes it through the managed-config registry at startup - adds the UpdateLinkListeners ctrl message so the router republishes its listener set to the controller on change; the controller re-fans via the existing PeerStateChange path - re-evaluates dialers on link group and listener changes via RescanForDialOpportunities For #3743.
506 lines
18 KiB
Go
506 lines
18 KiB
Go
//go:build apitests
|
|
|
|
/*
|
|
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 tests
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/openziti/edge-api/rest_management_api_client/config"
|
|
"github.com/openziti/edge-api/rest_management_api_client/edge_router"
|
|
"github.com/openziti/edge-api/rest_model"
|
|
"github.com/openziti/foundation/v2/util"
|
|
"github.com/openziti/ziti/v2/common/eid"
|
|
"github.com/openziti/ziti/v2/controller/event"
|
|
routerEnv "github.com/openziti/ziti/v2/router/env"
|
|
"github.com/openziti/ziti/v2/router/link"
|
|
"github.com/openziti/ziti/v2/router/managedconfig"
|
|
)
|
|
|
|
// pickFreePort opens a TCP listener on an OS-assigned port, captures the
|
|
// port number, then closes the listener. The returned port is "probably"
|
|
// free for the test's brief window. Used to feed a deterministic port
|
|
// into a controller-pushed config (port 0 is not currently usable in the
|
|
// router.link.v1 transport.bind shape).
|
|
func pickFreePort(t *testing.T) int {
|
|
t.Helper()
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("pickFreePort: %v", err)
|
|
}
|
|
port := l.Addr().(*net.TCPAddr).Port
|
|
_ = l.Close()
|
|
return port
|
|
}
|
|
|
|
// createRouterLinkConfig pushes a router.link.v1 Config through the
|
|
// management API and returns the new config's ID.
|
|
func createRouterLinkConfig(t *testing.T, ctx *TestContext, data map[string]interface{}) string {
|
|
t.Helper()
|
|
mgmtClient := ctx.NewEdgeManagementApi(nil)
|
|
_, err := mgmtClient.Authenticate(ctx.NewAdminCredentials(), nil)
|
|
ctx.Req.NoError(err)
|
|
|
|
configTypeName := "router.link.v1"
|
|
resp, err := mgmtClient.API.Config.CreateConfig(&config.CreateConfigParams{
|
|
Config: &rest_model.ConfigCreate{
|
|
Name: util.Ptr(eid.New()),
|
|
ConfigTypeID: &configTypeName,
|
|
Data: data,
|
|
},
|
|
}, nil)
|
|
ctx.Req.NoError(err)
|
|
return resp.Payload.Data.ID
|
|
}
|
|
|
|
// assignConfigsToRouter PATCHes the edge router's Configs field.
|
|
func assignConfigsToRouter(t *testing.T, ctx *TestContext, routerId string, configIds []string) {
|
|
t.Helper()
|
|
mgmtClient := ctx.NewEdgeManagementApi(nil)
|
|
_, err := mgmtClient.Authenticate(ctx.NewAdminCredentials(), nil)
|
|
ctx.Req.NoError(err)
|
|
|
|
_, err = mgmtClient.API.EdgeRouter.PatchEdgeRouter(&edge_router.PatchEdgeRouterParams{
|
|
ID: routerId,
|
|
EdgeRouter: &rest_model.EdgeRouterPatch{
|
|
Configs: configIds,
|
|
},
|
|
}, nil)
|
|
ctx.Req.NoError(err)
|
|
}
|
|
|
|
// portIsOpen returns true when a TCP connect to addr succeeds. Used to
|
|
// verify the router actually bound a listener for a controller-pushed
|
|
// link config.
|
|
func portIsOpen(addr string) bool {
|
|
conn, err := net.DialTimeout("tcp", addr, 250*time.Millisecond)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
_ = conn.Close()
|
|
return true
|
|
}
|
|
|
|
// Test_ManagedConfig_ControllerAppliesListener pushes a router.link.v1 config
|
|
// containing a listener through the management API and verifies (1) the
|
|
// router applied the controller-source config (registry.Applied source =
|
|
// Controller) and (2) the listener's bind port is actually open.
|
|
func Test_ManagedConfig_ControllerAppliesListener(t *testing.T) {
|
|
ctx := NewTestContext(t)
|
|
defer ctx.Teardown()
|
|
ctx.StartServer()
|
|
ctx.RequireAdminManagementApiLogin()
|
|
|
|
port := pickFreePort(t)
|
|
bindAddr := fmt.Sprintf("tls:127.0.0.1:%d", port)
|
|
|
|
r := ctx.CreateEnrollAndStartEdgeRouterWithCfgTweaks(func(cfg *routerEnv.Config) {
|
|
cfg.ManagedConfig.Allow = []string{"router.link"}
|
|
})
|
|
registry := r.Router.GetRouterConfigRegistry()
|
|
|
|
// Push a router.link.v1 config with a listener.
|
|
configId := createRouterLinkConfig(t, ctx, map[string]interface{}{
|
|
"listeners": []interface{}{
|
|
map[string]interface{}{
|
|
"binding": "transport",
|
|
"bind": bindAddr,
|
|
},
|
|
},
|
|
})
|
|
assignConfigsToRouter(t, ctx, ctx.edgeRouterEntity.id, []string{configId})
|
|
|
|
// Wait for the controller config to apply on the router.
|
|
ctx.Req.Eventually(func() bool {
|
|
src, _, found := registry.Applied(link.ConfigTypeV1)
|
|
return found && src == managedconfig.SourceController
|
|
}, 10*time.Second, 50*time.Millisecond, "controller config should be applied (Applied source = Controller)")
|
|
|
|
// Verify the listener is actually bound.
|
|
ctx.Req.Eventually(func() bool {
|
|
return portIsOpen(fmt.Sprintf("127.0.0.1:%d", port))
|
|
}, 5*time.Second, 50*time.Millisecond, "listener port %d should be open", port)
|
|
|
|
// Sanity: the router's linkSubsystem reports the listener.
|
|
listeners := r.Router.GetXlinkListeners()
|
|
ctx.Req.Len(listeners, 1, "router should have one active listener from controller config")
|
|
ctx.Req.Equal(bindAddr, listeners[0].GetAdvertisement())
|
|
}
|
|
|
|
// Test_ManagedConfig_DelayedConfigStartsListener verifies that a router with
|
|
// no managed config applies *no* listeners until the controller pushes one.
|
|
// Tests the "the listener doesn't come up before the config arrives, and
|
|
// does come up after" promise.
|
|
func Test_ManagedConfig_DelayedConfigStartsListener(t *testing.T) {
|
|
ctx := NewTestContext(t)
|
|
defer ctx.Teardown()
|
|
ctx.StartServer()
|
|
ctx.RequireAdminManagementApiLogin()
|
|
|
|
port := pickFreePort(t)
|
|
bindAddr := fmt.Sprintf("tls:127.0.0.1:%d", port)
|
|
|
|
r := ctx.CreateEnrollAndStartEdgeRouterWithCfgTweaks(func(cfg *routerEnv.Config) {
|
|
cfg.ManagedConfig.Allow = []string{"router.link"}
|
|
})
|
|
registry := r.Router.GetRouterConfigRegistry()
|
|
|
|
// Before any controller config arrives: no listeners, no Applied state,
|
|
// port is closed.
|
|
ctx.Req.Empty(r.Router.GetXlinkListeners(), "no listeners before any config arrives")
|
|
_, _, found := registry.Applied(link.ConfigTypeV1)
|
|
ctx.Req.False(found, "no applied state before any config arrives")
|
|
ctx.Req.False(portIsOpen(fmt.Sprintf("127.0.0.1:%d", port)), "port should not be open before config arrives")
|
|
|
|
// Push the config now.
|
|
configId := createRouterLinkConfig(t, ctx, map[string]interface{}{
|
|
"listeners": []interface{}{
|
|
map[string]interface{}{
|
|
"binding": "transport",
|
|
"bind": bindAddr,
|
|
},
|
|
},
|
|
})
|
|
assignConfigsToRouter(t, ctx, ctx.edgeRouterEntity.id, []string{configId})
|
|
|
|
// After the controller config arrives, the listener comes up.
|
|
ctx.Req.Eventually(func() bool {
|
|
src, _, found := registry.Applied(link.ConfigTypeV1)
|
|
return found && src == managedconfig.SourceController
|
|
}, 10*time.Second, 50*time.Millisecond, "controller config should be applied")
|
|
|
|
ctx.Req.Eventually(func() bool {
|
|
return portIsOpen(fmt.Sprintf("127.0.0.1:%d", port))
|
|
}, 5*time.Second, 50*time.Millisecond, "listener port should be open after config arrives")
|
|
}
|
|
|
|
// Test_ManagedConfig_ListenerUpdatePublishesToController verifies that
|
|
// when a router's link listener set changes mid-session (via managed
|
|
// config Apply), the router pushes UpdateLinkListeners to the controller
|
|
// and the controller's view of the router's listeners updates.
|
|
//
|
|
// Without this, peers would keep dialing the old listener address /
|
|
// using stale group memberships for link matching.
|
|
func Test_ManagedConfig_ListenerUpdatePublishesToController(t *testing.T) {
|
|
ctx := NewTestContext(t)
|
|
defer ctx.Teardown()
|
|
ctx.StartServer()
|
|
ctx.RequireAdminManagementApiLogin()
|
|
|
|
port := pickFreePort(t)
|
|
bindAddr := fmt.Sprintf("tls:127.0.0.1:%d", port)
|
|
|
|
r := ctx.CreateEnrollAndStartEdgeRouterWithCfgTweaks(func(cfg *routerEnv.Config) {
|
|
cfg.ManagedConfig.Allow = []string{"router.link"}
|
|
})
|
|
|
|
// Push a config with the listener in group "a".
|
|
configId := createRouterLinkConfig(t, ctx, map[string]interface{}{
|
|
"listeners": []interface{}{
|
|
map[string]interface{}{"binding": "transport", "bind": bindAddr, "groups": []interface{}{"a"}},
|
|
},
|
|
})
|
|
assignConfigsToRouter(t, ctx, ctx.edgeRouterEntity.id, []string{configId})
|
|
|
|
// Wait for the listener to actually bind, then the controller's
|
|
// view should reflect groups: [a].
|
|
ctx.Req.Eventually(func() bool {
|
|
return portIsOpen(fmt.Sprintf("127.0.0.1:%d", port))
|
|
}, 10*time.Second, 50*time.Millisecond, "listener should bind from initial config")
|
|
|
|
ctx.Req.Eventually(func() bool {
|
|
ctrlRouter := ctx.fabricController.GetNetwork().Router.GetConnected(r.Router.GetRouterId().Token)
|
|
if ctrlRouter == nil {
|
|
return false
|
|
}
|
|
listeners := ctrlRouter.GetLinkListeners()
|
|
return len(listeners) == 1 &&
|
|
len(listeners[0].Groups) == 1 &&
|
|
listeners[0].Groups[0] == "a"
|
|
}, 10*time.Second, 50*time.Millisecond, "controller should see groups=[a] from Hello time")
|
|
|
|
// Update the same config to change groups to ["a","b"].
|
|
mgmtClient := ctx.NewEdgeManagementApi(nil)
|
|
_, err := mgmtClient.Authenticate(ctx.NewAdminCredentials(), nil)
|
|
ctx.Req.NoError(err)
|
|
_, err = mgmtClient.API.Config.UpdateConfig(&config.UpdateConfigParams{
|
|
ID: configId,
|
|
Config: &rest_model.ConfigUpdate{
|
|
Name: util.Ptr(eid.New()),
|
|
Data: map[string]interface{}{
|
|
"listeners": []interface{}{
|
|
map[string]interface{}{"binding": "transport", "bind": bindAddr, "groups": []interface{}{"a", "b"}},
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
ctx.Req.NoError(err)
|
|
|
|
// The router applies the new config and pushes UpdateLinkListeners.
|
|
// The controller's link listener set should reflect the new groups.
|
|
ctx.Req.Eventually(func() bool {
|
|
ctrlRouter := ctx.fabricController.GetNetwork().Router.GetConnected(r.Router.GetRouterId().Token)
|
|
if ctrlRouter == nil {
|
|
return false
|
|
}
|
|
listeners := ctrlRouter.GetLinkListeners()
|
|
if len(listeners) != 1 {
|
|
return false
|
|
}
|
|
groups := listeners[0].Groups
|
|
return len(groups) == 2 && groups[0] == "a" && groups[1] == "b"
|
|
}, 10*time.Second, 50*time.Millisecond, "controller should see updated groups=[a,b] after UpdateLinkListeners")
|
|
}
|
|
|
|
// Test_ManagedConfig_RemoveConfigShutsDownListener verifies that removing
|
|
// a controller-managed config from a router (by unassigning it) closes
|
|
// the listener it provisioned. Exercises the linkSubsystem.Remove path
|
|
// when the router-side subscriber sees a ConfigRemoved event because the
|
|
// per-router filter (Phase 2c) GC'd the config out of the router's view.
|
|
func Test_ManagedConfig_RemoveConfigShutsDownListener(t *testing.T) {
|
|
ctx := NewTestContext(t)
|
|
defer ctx.Teardown()
|
|
ctx.StartServer()
|
|
ctx.RequireAdminManagementApiLogin()
|
|
|
|
port := pickFreePort(t)
|
|
bindAddr := fmt.Sprintf("tls:127.0.0.1:%d", port)
|
|
|
|
r := ctx.CreateEnrollAndStartEdgeRouterWithCfgTweaks(func(cfg *routerEnv.Config) {
|
|
cfg.ManagedConfig.Allow = []string{"router.link"}
|
|
})
|
|
registry := r.Router.GetRouterConfigRegistry()
|
|
|
|
configId := createRouterLinkConfig(t, ctx, map[string]interface{}{
|
|
"listeners": []interface{}{
|
|
map[string]interface{}{"binding": "transport", "bind": bindAddr},
|
|
},
|
|
})
|
|
assignConfigsToRouter(t, ctx, ctx.edgeRouterEntity.id, []string{configId})
|
|
|
|
// Wait for the listener to come up.
|
|
ctx.Req.Eventually(func() bool {
|
|
return portIsOpen(fmt.Sprintf("127.0.0.1:%d", port))
|
|
}, 10*time.Second, 50*time.Millisecond, "listener should bind from controller config")
|
|
|
|
// Unassign the config from the router. Phase 2c filtering + the
|
|
// router-side RDM GC should drop the Config from rdm.Configs, which
|
|
// dispatches OnRouterConfigRemoved through the subscriber, which
|
|
// calls registry.RemoveController, which triggers the registry
|
|
// reconcile path that calls handler.Remove().
|
|
assignConfigsToRouter(t, ctx, ctx.edgeRouterEntity.id, []string{})
|
|
|
|
// Applied state clears.
|
|
ctx.Req.Eventually(func() bool {
|
|
_, _, found := registry.Applied(link.ConfigTypeV1)
|
|
return !found
|
|
}, 10*time.Second, 50*time.Millisecond, "Applied should clear after config is removed")
|
|
|
|
// Listener slice empties.
|
|
ctx.Req.Eventually(func() bool {
|
|
return len(r.Router.GetXlinkListeners()) == 0
|
|
}, 5*time.Second, 50*time.Millisecond, "router should report no active listeners after removal")
|
|
|
|
// Port no longer accepts connections.
|
|
ctx.Req.Eventually(func() bool {
|
|
return !portIsOpen(fmt.Sprintf("127.0.0.1:%d", port))
|
|
}, 5*time.Second, 50*time.Millisecond, "listener port should close after config removal")
|
|
}
|
|
|
|
// Test_ManagedConfig_DefaultsBindingToTransport verifies that a listener
|
|
// entry with no `binding` field gets routed to the "transport" factory
|
|
// (the schema's documented default). Exercises link.defaultBinding.
|
|
func Test_ManagedConfig_DefaultsBindingToTransport(t *testing.T) {
|
|
ctx := NewTestContext(t)
|
|
defer ctx.Teardown()
|
|
ctx.StartServer()
|
|
ctx.RequireAdminManagementApiLogin()
|
|
|
|
port := pickFreePort(t)
|
|
bindAddr := fmt.Sprintf("tls:127.0.0.1:%d", port)
|
|
|
|
r := ctx.CreateEnrollAndStartEdgeRouterWithCfgTweaks(func(cfg *routerEnv.Config) {
|
|
cfg.ManagedConfig.Allow = []string{"router.link"}
|
|
})
|
|
|
|
// Note the omitted "binding" field — defaultBinding should fall back
|
|
// to "transport".
|
|
configId := createRouterLinkConfig(t, ctx, map[string]interface{}{
|
|
"listeners": []interface{}{
|
|
map[string]interface{}{"bind": bindAddr},
|
|
},
|
|
})
|
|
assignConfigsToRouter(t, ctx, ctx.edgeRouterEntity.id, []string{configId})
|
|
|
|
ctx.Req.Eventually(func() bool {
|
|
return portIsOpen(fmt.Sprintf("127.0.0.1:%d", port))
|
|
}, 10*time.Second, 50*time.Millisecond, "listener should open via the default transport factory")
|
|
|
|
listeners := r.Router.GetXlinkListeners()
|
|
ctx.Req.Len(listeners, 1)
|
|
ctx.Req.Equal(bindAddr, listeners[0].GetAdvertisement())
|
|
}
|
|
|
|
// Test_ManagedConfig_UpdateRebindsListener verifies that updating a
|
|
// router.link.v1 config to a different bind address closes the original
|
|
// port and opens the new one. Tests the "rebuild on Apply" semantics in
|
|
// link.Subsystem — the old listener slice is captured and closed
|
|
// after the new one is built.
|
|
func Test_ManagedConfig_UpdateRebindsListener(t *testing.T) {
|
|
ctx := NewTestContext(t)
|
|
defer ctx.Teardown()
|
|
ctx.StartServer()
|
|
ctx.RequireAdminManagementApiLogin()
|
|
|
|
portA := pickFreePort(t)
|
|
portB := pickFreePort(t)
|
|
ctx.Req.NotEqual(portA, portB, "free-port picker handed out the same port twice")
|
|
bindA := fmt.Sprintf("tls:127.0.0.1:%d", portA)
|
|
bindB := fmt.Sprintf("tls:127.0.0.1:%d", portB)
|
|
|
|
r := ctx.CreateEnrollAndStartEdgeRouterWithCfgTweaks(func(cfg *routerEnv.Config) {
|
|
cfg.ManagedConfig.Allow = []string{"router.link"}
|
|
})
|
|
|
|
// Push config with port A → port A opens.
|
|
configId := createRouterLinkConfig(t, ctx, map[string]interface{}{
|
|
"listeners": []interface{}{
|
|
map[string]interface{}{"binding": "transport", "bind": bindA},
|
|
},
|
|
})
|
|
assignConfigsToRouter(t, ctx, ctx.edgeRouterEntity.id, []string{configId})
|
|
|
|
ctx.Req.Eventually(func() bool {
|
|
return portIsOpen(fmt.Sprintf("127.0.0.1:%d", portA))
|
|
}, 10*time.Second, 50*time.Millisecond, "port A should open after first config")
|
|
|
|
// Update config to port B.
|
|
mgmtClient := ctx.NewEdgeManagementApi(nil)
|
|
_, err := mgmtClient.Authenticate(ctx.NewAdminCredentials(), nil)
|
|
ctx.Req.NoError(err)
|
|
_, err = mgmtClient.API.Config.UpdateConfig(&config.UpdateConfigParams{
|
|
ID: configId,
|
|
Config: &rest_model.ConfigUpdate{
|
|
Name: util.Ptr(eid.New()),
|
|
Data: map[string]interface{}{
|
|
"listeners": []interface{}{
|
|
map[string]interface{}{"binding": "transport", "bind": bindB},
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
ctx.Req.NoError(err)
|
|
|
|
// Port B opens.
|
|
ctx.Req.Eventually(func() bool {
|
|
return portIsOpen(fmt.Sprintf("127.0.0.1:%d", portB))
|
|
}, 10*time.Second, 50*time.Millisecond, "port B should open after rebind")
|
|
|
|
// Port A closes (the old listener was Close()d during the rebuild).
|
|
ctx.Req.Eventually(func() bool {
|
|
return !portIsOpen(fmt.Sprintf("127.0.0.1:%d", portA))
|
|
}, 5*time.Second, 50*time.Millisecond, "port A should close after rebind")
|
|
|
|
// The router reports exactly the new listener.
|
|
listeners := r.Router.GetXlinkListeners()
|
|
ctx.Req.Len(listeners, 1)
|
|
ctx.Req.Equal(bindB, listeners[0].GetAdvertisement())
|
|
}
|
|
|
|
// Test_ManagedConfig_BadUpdateRollsBack pushes a valid router.link.v1 config,
|
|
// waits for the listener to come up, then pushes a malformed update that
|
|
// fails handler.Apply. The registry must roll back to the previous-good
|
|
// state and emit an alert. The original listener stays bound throughout.
|
|
func Test_ManagedConfig_BadUpdateRollsBack(t *testing.T) {
|
|
ctx := NewTestContext(t)
|
|
defer ctx.Teardown()
|
|
ctx.StartServer()
|
|
ctx.RequireAdminManagementApiLogin()
|
|
|
|
port := pickFreePort(t)
|
|
bindAddr := fmt.Sprintf("tls:127.0.0.1:%d", port)
|
|
|
|
r := ctx.CreateEnrollAndStartEdgeRouterWithCfgTweaks(func(cfg *routerEnv.Config) {
|
|
cfg.ManagedConfig.Allow = []string{"router.link"}
|
|
})
|
|
registry := r.Router.GetRouterConfigRegistry()
|
|
|
|
alertHandler := newRecordingAlertHandler(16)
|
|
dispatcher := ctx.fabricController.GetNetwork().GetEventDispatcher()
|
|
dispatcher.AddAlertEventHandler(alertHandler)
|
|
defer dispatcher.RemoveAlertEventHandler(alertHandler)
|
|
|
|
// Push good config → listener comes up.
|
|
configId := createRouterLinkConfig(t, ctx, map[string]interface{}{
|
|
"listeners": []interface{}{
|
|
map[string]interface{}{
|
|
"binding": "transport",
|
|
"bind": bindAddr,
|
|
},
|
|
},
|
|
})
|
|
assignConfigsToRouter(t, ctx, ctx.edgeRouterEntity.id, []string{configId})
|
|
|
|
ctx.Req.Eventually(func() bool {
|
|
return portIsOpen(fmt.Sprintf("127.0.0.1:%d", port))
|
|
}, 10*time.Second, 50*time.Millisecond, "good config listener should bind")
|
|
|
|
// Update the same Config to use an unknown binding. JSON parses fine
|
|
// and the controller's schema accepts it (binding only requires
|
|
// minLength: 1); handler.Apply fails when it can't find a factory.
|
|
mgmtClient := ctx.NewEdgeManagementApi(nil)
|
|
_, err := mgmtClient.Authenticate(ctx.NewAdminCredentials(), nil)
|
|
ctx.Req.NoError(err)
|
|
_, err = mgmtClient.API.Config.UpdateConfig(&config.UpdateConfigParams{
|
|
ID: configId,
|
|
Config: &rest_model.ConfigUpdate{
|
|
Name: util.Ptr(eid.New()),
|
|
Data: map[string]interface{}{
|
|
"listeners": []interface{}{
|
|
map[string]interface{}{
|
|
"binding": "made-up",
|
|
"bind": bindAddr,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
ctx.Req.NoError(err)
|
|
|
|
// An alert must fire for the failed apply.
|
|
alert := alertHandler.waitFor(t, 10*time.Second, func(e *event.AlertEvent) bool {
|
|
return e.RelatedEntities["configBaseType"] == "router.link"
|
|
})
|
|
ctx.Req.Equal("error", alert.Severity)
|
|
|
|
// The original listener must still be bound after rollback — the
|
|
// previous-good config's listener wasn't disturbed.
|
|
ctx.Req.True(portIsOpen(fmt.Sprintf("127.0.0.1:%d", port)),
|
|
"port should remain open after rollback to previous-good config")
|
|
|
|
// Registry's Applied should reflect the rolled-back state: still
|
|
// Controller v1 active.
|
|
src, ver, found := registry.Applied(link.ConfigTypeV1)
|
|
ctx.Req.True(found)
|
|
ctx.Req.Equal(managedconfig.SourceController, src)
|
|
ctx.Req.Equal(1, ver)
|
|
}
|