[management] Add support for disabling resources and routing peers in networks (#3154)

* sync openapi changes

Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>

* add option to disable network resource(s)

Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>

* add network resource enabled state from api

Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>

* fix tests

Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>

* add option to disable network router(s)

Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>

* fix tests

Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>

* Add tests

Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>

* migrate old network resources and routers

Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>

---------

Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
This commit is contained in:
Bethuel Mmbaga 2025-01-08 19:35:57 +03:00 committed by GitHub
parent 9e6e34b42d
commit 409003b4f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 188 additions and 14 deletions

View File

@ -800,15 +800,18 @@ components:
items:
type: string
example: "ch8i4ug6lnn4g9hqv797"
sourceResource:
description: Policy rule source resource that the rule is applied to
$ref: '#/components/schemas/Resource'
destinations:
description: Policy rule destination group IDs
type: array
items:
type: string
example: "ch8i4ug6lnn4g9h7v7m0"
required:
- sources
- destinations
destinationResource:
description: Policy rule destination resource that the rule is applied to
$ref: '#/components/schemas/Resource'
PolicyRuleCreate:
allOf:
@ -1325,9 +1328,14 @@ components:
description: Network resource address (either a direct host like 1.1.1.1 or 1.1.1.1/32, or a subnet like 192.168.178.0/24, or domains like example.com and *.example.com)
type: string
example: "1.1.1.1"
enabled:
description: Network resource status
type: boolean
example: true
required:
- name
- address
- enabled
NetworkResourceRequest:
allOf:
- $ref: '#/components/schemas/NetworkResourceMinimum'
@ -1390,12 +1398,17 @@ components:
description: Indicate if peer should masquerade traffic to this route's prefix
type: boolean
example: true
enabled:
description: Network router status
type: boolean
example: true
required:
# Only one property has to be set
#- peer
#- peer_groups
- metric
- masquerade
- enabled
NetworkRouter:
allOf:
- type: object

View File

@ -560,6 +560,9 @@ type NetworkResource struct {
// Description Network resource description
Description *string `json:"description,omitempty"`
// Enabled Network resource status
Enabled bool `json:"enabled"`
// Groups Groups that the resource belongs to
Groups []GroupMinimum `json:"groups"`
@ -581,6 +584,9 @@ type NetworkResourceMinimum struct {
// Description Network resource description
Description *string `json:"description,omitempty"`
// Enabled Network resource status
Enabled bool `json:"enabled"`
// Name Network resource name
Name string `json:"name"`
}
@ -593,6 +599,9 @@ type NetworkResourceRequest struct {
// Description Network resource description
Description *string `json:"description,omitempty"`
// Enabled Network resource status
Enabled bool `json:"enabled"`
// Groups Group IDs containing the resource
Groups []string `json:"groups"`
@ -605,6 +614,9 @@ type NetworkResourceType string
// NetworkRouter defines model for NetworkRouter.
type NetworkRouter struct {
// Enabled Network router status
Enabled bool `json:"enabled"`
// Id Network Router Id
Id string `json:"id"`
@ -623,6 +635,9 @@ type NetworkRouter struct {
// NetworkRouterRequest defines model for NetworkRouterRequest.
type NetworkRouterRequest struct {
// Enabled Network router status
Enabled bool `json:"enabled"`
// Masquerade Indicate if peer should masquerade traffic to this route's prefix
Masquerade bool `json:"masquerade"`

View File

@ -305,3 +305,53 @@ func hiddenKey(key string, length int) string {
}
return prefix + strings.Repeat("*", length)
}
func MigrateNewField[T any](ctx context.Context, db *gorm.DB, columnName string, defaultValue any) error {
var model T
if !db.Migrator().HasTable(&model) {
log.WithContext(ctx).Debugf("Table for %T does not exist, no migration needed", model)
return nil
}
stmt := &gorm.Statement{DB: db}
err := stmt.Parse(&model)
if err != nil {
return fmt.Errorf("parse model: %w", err)
}
tableName := stmt.Schema.Table
if err := db.Transaction(func(tx *gorm.DB) error {
if !tx.Migrator().HasColumn(&model, columnName) {
log.WithContext(ctx).Infof("Column %s does not exist in table %s, adding it", columnName, tableName)
if err := tx.Migrator().AddColumn(&model, columnName); err != nil {
return fmt.Errorf("add column %s: %w", columnName, err)
}
}
var rows []map[string]any
if err := tx.Table(tableName).
Select("id", columnName).
Where(columnName + " IS NULL OR " + columnName + " = ''").
Find(&rows).Error; err != nil {
return fmt.Errorf("failed to find rows with empty %s: %w", columnName, err)
}
if len(rows) == 0 {
log.WithContext(ctx).Infof("No rows with empty %s found in table %s, no migration needed", columnName, tableName)
return nil
}
for _, row := range rows {
if err := tx.Table(tableName).Where("id = ?", row["id"]).Update(columnName, defaultValue).Error; err != nil {
return fmt.Errorf("failed to update row with id %v: %w", row["id"], err)
}
}
return nil
}); err != nil {
return err
}
log.WithContext(ctx).Infof("Migration of empty %s to default value in table %s completed", columnName, tableName)
return nil
}

View File

@ -101,7 +101,7 @@ func (m *managerImpl) CreateResource(ctx context.Context, userID string, resourc
return nil, status.NewPermissionDeniedError()
}
resource, err = types.NewNetworkResource(resource.AccountID, resource.NetworkID, resource.Name, resource.Description, resource.Address, resource.GroupIDs)
resource, err = types.NewNetworkResource(resource.AccountID, resource.NetworkID, resource.Name, resource.Description, resource.Address, resource.GroupIDs, resource.Enabled)
if err != nil {
return nil, fmt.Errorf("failed to create new network resource: %w", err)
}

View File

@ -40,9 +40,10 @@ type NetworkResource struct {
GroupIDs []string `gorm:"-"`
Domain string
Prefix netip.Prefix `gorm:"serializer:json"`
Enabled bool
}
func NewNetworkResource(accountID, networkID, name, description, address string, groupIDs []string) (*NetworkResource, error) {
func NewNetworkResource(accountID, networkID, name, description, address string, groupIDs []string, enabled bool) (*NetworkResource, error) {
resourceType, domain, prefix, err := GetResourceType(address)
if err != nil {
return nil, fmt.Errorf("invalid address: %w", err)
@ -59,6 +60,7 @@ func NewNetworkResource(accountID, networkID, name, description, address string,
Domain: domain,
Prefix: prefix,
GroupIDs: groupIDs,
Enabled: enabled,
}, nil
}
@ -75,6 +77,7 @@ func (n *NetworkResource) ToAPIResponse(groups []api.GroupMinimum) *api.NetworkR
Type: api.NetworkResourceType(n.Type.String()),
Address: addr,
Groups: groups,
Enabled: n.Enabled,
}
}
@ -86,6 +89,7 @@ func (n *NetworkResource) FromAPIRequest(req *api.NetworkResourceRequest) {
}
n.Address = req.Address
n.GroupIDs = req.Groups
n.Enabled = req.Enabled
}
func (n *NetworkResource) Copy() *NetworkResource {
@ -100,6 +104,7 @@ func (n *NetworkResource) Copy() *NetworkResource {
Domain: n.Domain,
Prefix: n.Prefix,
GroupIDs: n.GroupIDs,
Enabled: n.Enabled,
}
}
@ -115,7 +120,7 @@ func (n *NetworkResource) ToRoute(peer *nbpeer.Peer, router *routerTypes.Network
PeerGroups: nil,
Masquerade: router.Masquerade,
Metric: router.Metric,
Enabled: true,
Enabled: n.Enabled,
Groups: nil,
AccessControlGroups: nil,
}

View File

@ -101,7 +101,7 @@ func Test_GetRouterReturnsPermissionDenied(t *testing.T) {
func Test_CreateRouterSuccessfully(t *testing.T) {
ctx := context.Background()
userID := "allowedUser"
router, err := types.NewNetworkRouter("testAccountId", "testNetworkId", "testPeerId", []string{}, false, 9999)
router, err := types.NewNetworkRouter("testAccountId", "testNetworkId", "testPeerId", []string{}, false, 9999, true)
if err != nil {
require.NoError(t, err)
}
@ -127,7 +127,7 @@ func Test_CreateRouterSuccessfully(t *testing.T) {
func Test_CreateRouterFailsWithPermissionDenied(t *testing.T) {
ctx := context.Background()
userID := "invalidUser"
router, err := types.NewNetworkRouter("testAccountId", "testNetworkId", "testPeerId", []string{}, false, 9999)
router, err := types.NewNetworkRouter("testAccountId", "testNetworkId", "testPeerId", []string{}, false, 9999, true)
if err != nil {
require.NoError(t, err)
}
@ -191,7 +191,7 @@ func Test_DeleteRouterFailsWithPermissionDenied(t *testing.T) {
func Test_UpdateRouterSuccessfully(t *testing.T) {
ctx := context.Background()
userID := "allowedUser"
router, err := types.NewNetworkRouter("testAccountId", "testNetworkId", "testPeerId", []string{}, false, 1)
router, err := types.NewNetworkRouter("testAccountId", "testNetworkId", "testPeerId", []string{}, false, 1, true)
if err != nil {
require.NoError(t, err)
}
@ -213,7 +213,7 @@ func Test_UpdateRouterSuccessfully(t *testing.T) {
func Test_UpdateRouterFailsWithPermissionDenied(t *testing.T) {
ctx := context.Background()
userID := "invalidUser"
router, err := types.NewNetworkRouter("testAccountId", "testNetworkId", "testPeerId", []string{}, false, 1)
router, err := types.NewNetworkRouter("testAccountId", "testNetworkId", "testPeerId", []string{}, false, 1, true)
if err != nil {
require.NoError(t, err)
}

View File

@ -17,9 +17,10 @@ type NetworkRouter struct {
PeerGroups []string `gorm:"serializer:json"`
Masquerade bool
Metric int
Enabled bool
}
func NewNetworkRouter(accountID string, networkID string, peer string, peerGroups []string, masquerade bool, metric int) (*NetworkRouter, error) {
func NewNetworkRouter(accountID string, networkID string, peer string, peerGroups []string, masquerade bool, metric int, enabled bool) (*NetworkRouter, error) {
if peer != "" && len(peerGroups) > 0 {
return nil, errors.New("peer and peerGroups cannot be set at the same time")
}
@ -32,6 +33,7 @@ func NewNetworkRouter(accountID string, networkID string, peer string, peerGroup
PeerGroups: peerGroups,
Masquerade: masquerade,
Metric: metric,
Enabled: enabled,
}, nil
}
@ -42,6 +44,7 @@ func (n *NetworkRouter) ToAPIResponse() *api.NetworkRouter {
PeerGroups: &n.PeerGroups,
Masquerade: n.Masquerade,
Metric: n.Metric,
Enabled: n.Enabled,
}
}
@ -56,6 +59,7 @@ func (n *NetworkRouter) FromAPIRequest(req *api.NetworkRouterRequest) {
n.Masquerade = req.Masquerade
n.Metric = req.Metric
n.Enabled = req.Enabled
}
func (n *NetworkRouter) Copy() *NetworkRouter {
@ -67,6 +71,7 @@ func (n *NetworkRouter) Copy() *NetworkRouter {
PeerGroups: n.PeerGroups,
Masquerade: n.Masquerade,
Metric: n.Metric,
Enabled: n.Enabled,
}
}

View File

@ -11,6 +11,7 @@ func TestNewNetworkRouter(t *testing.T) {
peerGroups []string
masquerade bool
metric int
enabled bool
expectedError bool
}{
// Valid cases
@ -22,6 +23,7 @@ func TestNewNetworkRouter(t *testing.T) {
peerGroups: nil,
masquerade: true,
metric: 100,
enabled: true,
expectedError: false,
},
{
@ -32,6 +34,7 @@ func TestNewNetworkRouter(t *testing.T) {
peerGroups: []string{"group-1", "group-2"},
masquerade: false,
metric: 200,
enabled: false,
expectedError: false,
},
{
@ -42,6 +45,7 @@ func TestNewNetworkRouter(t *testing.T) {
peerGroups: nil,
masquerade: true,
metric: 300,
enabled: true,
expectedError: false,
},
@ -54,13 +58,14 @@ func TestNewNetworkRouter(t *testing.T) {
peerGroups: []string{"group-3"},
masquerade: false,
metric: 400,
enabled: false,
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
router, err := NewNetworkRouter(tt.accountID, tt.networkID, tt.peer, tt.peerGroups, tt.masquerade, tt.metric)
router, err := NewNetworkRouter(tt.accountID, tt.networkID, tt.peer, tt.peerGroups, tt.masquerade, tt.metric, tt.enabled)
if tt.expectedError && err == nil {
t.Fatalf("Expected an error, got nil")
@ -94,6 +99,10 @@ func TestNewNetworkRouter(t *testing.T) {
if router.Metric != tt.metric {
t.Errorf("Expected Metric %d, got %d", tt.metric, router.Metric)
}
if router.Enabled != tt.enabled {
t.Errorf("Expected Enabled %v, got %v", tt.enabled, router.Enabled)
}
}
})
}

View File

@ -2388,6 +2388,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
PeerGroups: nil,
Masquerade: false,
Metric: 9999,
Enabled: true,
},
{
ID: "router2",
@ -2395,12 +2396,14 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
PeerGroups: []string{"router1", "router2"},
Masquerade: false,
Metric: 9999,
Enabled: true,
},
{
ID: "router3",
NetworkID: "network3",
Peer: "peerE",
PeerGroups: []string{},
Enabled: true,
},
{
ID: "router4",
@ -2408,6 +2411,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
PeerGroups: []string{"router1"},
Masquerade: false,
Metric: 9999,
Enabled: true,
},
{
ID: "router5",
@ -2415,6 +2419,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
Peer: "peerL",
Masquerade: false,
Metric: 9999,
Enabled: true,
},
{
ID: "router6",
@ -2422,6 +2427,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
Peer: "peerN",
Masquerade: false,
Metric: 9999,
Enabled: true,
},
},
NetworkResources: []*resourceTypes.NetworkResource{
@ -2431,6 +2437,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
Name: "Resource 1",
Type: "subnet",
Prefix: netip.MustParsePrefix("10.10.10.0/24"),
Enabled: true,
},
{
ID: "resource2",
@ -2438,6 +2445,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
Name: "Resource 2",
Type: "subnet",
Prefix: netip.MustParsePrefix("192.168.0.0/16"),
Enabled: true,
},
{
ID: "resource3",
@ -2445,6 +2453,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
Name: "Resource 3",
Type: "domain",
Domain: "example.com",
Enabled: true,
},
{
ID: "resource4",
@ -2452,6 +2461,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
Name: "Resource 4",
Type: "domain",
Domain: "example.com",
Enabled: true,
},
{
ID: "resource5",
@ -2459,6 +2469,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
Name: "Resource 5",
Type: "host",
Prefix: netip.MustParsePrefix("10.12.12.1/32"),
Enabled: true,
},
{
ID: "resource6",
@ -2466,6 +2477,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
Name: "Resource 6",
Type: "domain",
Domain: "*.google.com",
Enabled: true,
},
},
Policies: []*types.Policy{

View File

@ -2377,7 +2377,7 @@ func TestSqlStore_SaveNetworkRouter(t *testing.T) {
accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
networkID := "ct286bi7qv930dsrrug0"
netRouter, err := routerTypes.NewNetworkRouter(accountID, networkID, "", []string{"net-router-grp"}, true, 0)
netRouter, err := routerTypes.NewNetworkRouter(accountID, networkID, "", []string{"net-router-grp"}, true, 0, true)
require.NoError(t, err)
err = store.SaveNetworkRouter(context.Background(), LockingStrengthUpdate, netRouter)
@ -2494,7 +2494,7 @@ func TestSqlStore_SaveNetworkResource(t *testing.T) {
accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
networkID := "ct286bi7qv930dsrrug0"
netResource, err := resourceTypes.NewNetworkResource(accountID, networkID, "resource-name", "", "example.com", []string{})
netResource, err := resourceTypes.NewNetworkResource(accountID, networkID, "resource-name", "", "example.com", []string{}, true)
require.NoError(t, err)
err = store.SaveNetworkResource(context.Background(), LockingStrengthUpdate, netResource)

View File

@ -288,6 +288,12 @@ func getMigrations(ctx context.Context) []migrationFunc {
func(db *gorm.DB) error {
return migration.MigrateSetupKeyToHashedSetupKey[types.SetupKey](ctx, db)
},
func(db *gorm.DB) error {
return migration.MigrateNewField[resourceTypes.NetworkResource](ctx, db, "enabled", true)
},
func(db *gorm.DB) error {
return migration.MigrateNewField[routerTypes.NetworkRouter](ctx, db, "enabled", true)
},
}
}

View File

@ -1288,6 +1288,10 @@ func (a *Account) getNetworkResourceGroups(resourceID string) []*Group {
func (a *Account) GetResourcePoliciesMap() map[string][]*Policy {
resourcePolicies := make(map[string][]*Policy)
for _, resource := range a.NetworkResources {
if !resource.Enabled {
continue
}
resourceAppliedPolicies := a.GetPoliciesForNetworkResource(resource.ID)
resourcePolicies[resource.ID] = resourceAppliedPolicies
}
@ -1301,6 +1305,10 @@ func (a *Account) GetNetworkResourcesRoutesToSync(ctx context.Context, peerID st
allSourcePeers := make(map[string]struct{}, len(a.Peers))
for _, resource := range a.NetworkResources {
if !resource.Enabled {
continue
}
var addSourcePeers bool
networkRoutingPeers, exists := routers[resource.NetworkID]
@ -1455,6 +1463,10 @@ func (a *Account) GetResourceRoutersMap() map[string]map[string]*routerTypes.Net
routers := make(map[string]map[string]*routerTypes.NetworkRouter)
for _, router := range a.NetworkRouters {
if !router.Enabled {
continue
}
if routers[router.NetworkID] == nil {
routers[router.NetworkID] = make(map[string]*routerTypes.NetworkRouter)
}

View File

@ -139,6 +139,11 @@ func setupTestAccount() *Account {
AccountID: "accountID",
Name: "network2",
},
{
ID: "network3ID",
AccountID: "accountID",
Name: "network3",
},
},
NetworkRouters: []*routerTypes.NetworkRouter{
{
@ -149,6 +154,7 @@ func setupTestAccount() *Account {
PeerGroups: []string{},
Masquerade: false,
Metric: 100,
Enabled: true,
},
{
ID: "router2ID",
@ -158,6 +164,7 @@ func setupTestAccount() *Account {
PeerGroups: []string{},
Masquerade: false,
Metric: 100,
Enabled: true,
},
{
ID: "router3ID",
@ -167,6 +174,7 @@ func setupTestAccount() *Account {
PeerGroups: []string{},
Masquerade: false,
Metric: 100,
Enabled: true,
},
{
ID: "router4ID",
@ -176,6 +184,7 @@ func setupTestAccount() *Account {
PeerGroups: []string{"group1"},
Masquerade: false,
Metric: 100,
Enabled: true,
},
{
ID: "router5ID",
@ -185,6 +194,7 @@ func setupTestAccount() *Account {
PeerGroups: []string{"group2", "group3"},
Masquerade: false,
Metric: 100,
Enabled: true,
},
{
ID: "router6ID",
@ -194,6 +204,17 @@ func setupTestAccount() *Account {
PeerGroups: []string{"group4"},
Masquerade: false,
Metric: 100,
Enabled: true,
},
{
ID: "router6ID",
NetworkID: "network3ID",
AccountID: "accountID",
Peer: "",
PeerGroups: []string{"group6"},
Masquerade: false,
Metric: 100,
Enabled: false,
},
},
NetworkResources: []*resourceTypes.NetworkResource{
@ -201,21 +222,31 @@ func setupTestAccount() *Account {
ID: "resource1ID",
AccountID: "accountID",
NetworkID: "network1ID",
Enabled: true,
},
{
ID: "resource2ID",
AccountID: "accountID",
NetworkID: "network2ID",
Enabled: true,
},
{
ID: "resource3ID",
AccountID: "accountID",
NetworkID: "network1ID",
Enabled: true,
},
{
ID: "resource4ID",
AccountID: "accountID",
NetworkID: "network1ID",
Enabled: true,
},
{
ID: "resource5ID",
AccountID: "accountID",
NetworkID: "network3ID",
Enabled: false,
},
},
Policies: []*Policy{
@ -281,6 +312,17 @@ func setupTestAccount() *Account {
},
},
},
{
ID: "policy6ID",
AccountID: "accountID",
Enabled: true,
Rules: []*PolicyRule{
{
ID: "rule6ID",
Enabled: true,
},
},
},
},
}
}
@ -302,6 +344,8 @@ func Test_GetResourceRoutersMap(t *testing.T) {
require.Equal(t, 2, len(routers["network2ID"]))
require.NotNil(t, routers["network2ID"]["peer2"])
require.NotNil(t, routers["network2ID"]["peer41"])
require.Equal(t, 0, len(routers["network3ID"]))
}
func Test_GetResourcePoliciesMap(t *testing.T) {
@ -312,6 +356,7 @@ func Test_GetResourcePoliciesMap(t *testing.T) {
require.Equal(t, 1, len(policies["resource2ID"]))
require.Equal(t, 2, len(policies["resource3ID"]))
require.Equal(t, 1, len(policies["resource4ID"]))
require.Equal(t, 0, len(policies["resource5ID"]))
}
func Test_AddNetworksRoutingPeersAddsMissingPeers(t *testing.T) {
@ -476,6 +521,7 @@ func getBasicAccountsWithResource() *Account {
PeerGroups: []string{},
Masquerade: false,
Metric: 100,
Enabled: true,
},
},
NetworkResources: []*resourceTypes.NetworkResource{
@ -486,6 +532,7 @@ func getBasicAccountsWithResource() *Account {
Address: "10.10.10.0/24",
Prefix: netip.MustParsePrefix("10.10.10.0/24"),
Type: resourceTypes.NetworkResourceType("subnet"),
Enabled: true,
},
},
Policies: []*Policy{