Allocate virtual ip for resolver/router/splitter config entries (#16760)

This commit is contained in:
Kyle Havlovitz 2023-03-27 13:04:24 -07:00 committed by GitHub
parent 032aba3175
commit 42c5b29713
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 209 additions and 2 deletions

View File

@ -518,6 +518,16 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
}, },
})) }))
// Add a service-resolver entry to get a virtual IP for service foo
resolverEntry := &structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "foo",
}
require.NoError(t, fsm.state.EnsureConfigEntry(34, resolverEntry))
vip, err = fsm.state.VirtualIPForService(structs.PeeredServiceName{ServiceName: structs.NewServiceName("foo", nil)})
require.NoError(t, err)
require.Equal(t, vip, "240.0.0.3")
// Snapshot // Snapshot
snap, err := fsm.Snapshot() snap, err := fsm.Snapshot()
require.NoError(t, err) require.NoError(t, err)
@ -621,6 +631,10 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
vip, err = fsm2.state.VirtualIPForService(psn) vip, err = fsm2.state.VirtualIPForService(psn)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, vip, "240.0.0.2") require.Equal(t, vip, "240.0.0.2")
psn = structs.PeeredServiceName{ServiceName: structs.NewServiceName("foo", nil)}
vip, err = fsm2.state.VirtualIPForService(psn)
require.NoError(t, err)
require.Equal(t, vip, "240.0.0.3")
// Verify key is set // Verify key is set
_, d, err := fsm2.state.KVSGet(nil, "/test", nil) _, d, err := fsm2.state.KVSGet(nil, "/test", nil)

View File

@ -2016,6 +2016,33 @@ func freeServiceVirtualIP(
return nil return nil
} }
// Don't deregister the virtual IP if at least one instance of this service still exists.
q := Query{
Value: psn.ServiceName.Name,
EnterpriseMeta: psn.ServiceName.EnterpriseMeta,
PeerName: psn.Peer,
}
if remainingService, err := tx.First(tableServices, indexService, q); err == nil {
if remainingService != nil {
return nil
}
} else {
return fmt.Errorf("failed service lookup for %q: %s", psn.ServiceName.Name, err)
}
// Don't deregister the virtual IP if at least one resolver/router/splitter config entry still
// references this service.
configEntryVIPKinds := []string{structs.ServiceResolver, structs.ServiceRouter, structs.ServiceSplitter}
for _, kind := range configEntryVIPKinds {
_, entry, err := configEntryTxn(tx, nil, kind, psn.ServiceName.Name, &psn.ServiceName.EnterpriseMeta)
if err != nil {
return fmt.Errorf("failed config entry lookup for %s/%s: %s", kind, psn.ServiceName.Name, err)
}
if entry != nil {
return nil
}
}
// Don't deregister the virtual IP if at least one terminating gateway still references this service. // Don't deregister the virtual IP if at least one terminating gateway still references this service.
termGatewaySupported, err := terminatingGatewayVirtualIPsSupported(tx, nil) termGatewaySupported, err := terminatingGatewayVirtualIPsSupported(tx, nil)
if err != nil { if err != nil {

View File

@ -455,6 +455,15 @@ func deleteConfigEntryTxn(tx WriteTxn, idx uint64, kind, name string, entMeta *a
return fmt.Errorf("failed updating index: %s", err) return fmt.Errorf("failed updating index: %s", err)
} }
// If this is a resolver/router/splitter, attempt to delete the virtual IP associated
// with this service.
if kind == structs.ServiceResolver || kind == structs.ServiceRouter || kind == structs.ServiceSplitter {
psn := structs.PeeredServiceName{ServiceName: sn}
if err := freeServiceVirtualIP(tx, idx, psn, nil); err != nil {
return fmt.Errorf("failed to clean up virtual IP for %q: %v", psn.String(), err)
}
}
return nil return nil
} }
@ -465,14 +474,15 @@ func insertConfigEntryWithTxn(tx WriteTxn, idx uint64, conf structs.ConfigEntry)
// If the config entry is for a terminating or ingress gateway we update the memdb table // If the config entry is for a terminating or ingress gateway we update the memdb table
// that associates gateways <-> services. // that associates gateways <-> services.
if conf.GetKind() == structs.TerminatingGateway || conf.GetKind() == structs.IngressGateway { kind := conf.GetKind()
if kind == structs.TerminatingGateway || kind == structs.IngressGateway {
err := updateGatewayServices(tx, idx, conf, conf.GetEnterpriseMeta()) err := updateGatewayServices(tx, idx, conf, conf.GetEnterpriseMeta())
if err != nil { if err != nil {
return fmt.Errorf("failed to associate services to gateway: %v", err) return fmt.Errorf("failed to associate services to gateway: %v", err)
} }
} }
switch conf.GetKind() { switch kind {
case structs.ServiceDefaults: case structs.ServiceDefaults:
if conf.(*structs.ServiceConfigEntry).Destination != nil { if conf.(*structs.ServiceConfigEntry).Destination != nil {
sn := structs.ServiceName{Name: conf.GetName(), EnterpriseMeta: *conf.GetEnterpriseMeta()} sn := structs.ServiceName{Name: conf.GetName(), EnterpriseMeta: *conf.GetEnterpriseMeta()}
@ -499,6 +509,15 @@ func insertConfigEntryWithTxn(tx WriteTxn, idx uint64, conf structs.ConfigEntry)
if err != nil { if err != nil {
return err return err
} }
case structs.ServiceResolver:
fallthrough
case structs.ServiceRouter:
fallthrough
case structs.ServiceSplitter:
psn := structs.PeeredServiceName{ServiceName: structs.NewServiceName(conf.GetName(), conf.GetEnterpriseMeta())}
if _, err := assignServiceVirtualIP(tx, idx, psn); err != nil {
return err
}
} }
// Insert the config entry and update the index // Insert the config entry and update the index

View File

@ -1,6 +1,7 @@
package state package state
import ( import (
"fmt"
"testing" "testing"
"time" "time"
@ -3032,3 +3033,149 @@ func TestStore_ValidateServiceIntentionsErrorOnIncompatibleProtocols(t *testing.
}) })
} }
} }
func TestStateStore_ConfigEntry_VirtualIP(t *testing.T) {
createServiceInstance := func(t *testing.T, s *Store, name string) {
ns1 := &structs.NodeService{
ID: name,
Service: name,
Address: "1.1.1.1",
Port: 1111,
Connect: structs.ServiceConnect{Native: true},
}
require.NoError(t, s.EnsureService(0, "node1", ns1))
}
deleteServiceInstance := func(t *testing.T, s *Store, name string) {
require.NoError(t, s.DeleteService(0, "node1", name, nil, ""))
}
createServiceResolver := func(t *testing.T, s *Store, name string) {
require.NoError(t, s.EnsureConfigEntry(0, &structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: name,
}))
}
createServiceRouter := func(t *testing.T, s *Store, name string) {
require.NoError(t, s.EnsureConfigEntry(0, &structs.ServiceRouterConfigEntry{
Kind: structs.ServiceRouter,
Name: name,
}))
}
createServiceSplitter := func(t *testing.T, s *Store, name string) {
require.NoError(t, s.EnsureConfigEntry(0, &structs.ServiceSplitterConfigEntry{
Kind: structs.ServiceSplitter,
Name: name,
Splits: []structs.ServiceSplit{
{Weight: 100},
},
}))
}
deleteConfigEntry := func(t *testing.T, s *Store, kind, name string) {
require.NoError(t, s.DeleteConfigEntry(0, kind, name, nil))
}
ensureVirtualIP := func(t *testing.T, s *Store, service string, value string) {
vip, err := s.VirtualIPForService(structs.PeeredServiceName{ServiceName: structs.ServiceName{Name: service}})
require.NoError(t, err)
require.Equal(t, value, vip)
}
testVIPStateStore := func(t *testing.T) *Store {
s := testStateStore(t)
setVirtualIPFlags(t, s)
testRegisterNode(t, s, 0, "node1")
s.EnsureConfigEntry(0, &structs.ProxyConfigEntry{
Kind: structs.ProxyDefaults,
Name: structs.ProxyConfigGlobal,
Config: map[string]interface{}{
"protocol": "http",
},
})
return s
}
cases := []struct {
kind string
createFunc func(*testing.T, *Store, string)
}{
{
kind: structs.ServiceResolver,
createFunc: createServiceResolver,
},
{
kind: structs.ServiceRouter,
createFunc: createServiceRouter,
},
{
kind: structs.ServiceSplitter,
createFunc: createServiceSplitter,
},
}
for _, tc := range cases {
t.Run(fmt.Sprintf("create and delete %s with no service instances", tc.kind), func(t *testing.T) {
s := testVIPStateStore(t)
// Create unrelated service instance
createServiceInstance(t, s, "unrelated")
// Create the config entry and make sure a virtual ip is allocated
ensureVirtualIP(t, s, "foo", "")
tc.createFunc(t, s, "foo")
ensureVirtualIP(t, s, "foo", "240.0.0.2")
// Delete the config entry and make sure the virtual ip is freed and reused
ensureVirtualIP(t, s, "bar", "")
deleteConfigEntry(t, s, tc.kind, "foo")
ensureVirtualIP(t, s, "foo", "")
tc.createFunc(t, s, "bar")
ensureVirtualIP(t, s, "bar", "240.0.0.2")
})
t.Run(fmt.Sprintf("create and delete %s with service instances", tc.kind), func(t *testing.T) {
s := testVIPStateStore(t)
// Create a foo service instance and an unrelated service instance
createServiceInstance(t, s, "foo")
// Creating the config entry should not affect the service virtual IP
ensureVirtualIP(t, s, "foo", "240.0.0.1")
tc.createFunc(t, s, "foo")
ensureVirtualIP(t, s, "foo", "240.0.0.1")
// Deleting should also not affect the service virtual IP because there are still existing
// service instances that need the VIP.
deleteConfigEntry(t, s, tc.kind, "foo")
ensureVirtualIP(t, s, "foo", "240.0.0.1")
// Now delete the service instance, which should free up the virtual IP
deleteServiceInstance(t, s, "foo")
ensureVirtualIP(t, s, "foo", "")
// Make sure the free address can be reused
tc.createFunc(t, s, "bar")
ensureVirtualIP(t, s, "bar", "240.0.0.1")
})
t.Run(fmt.Sprintf("create and delete service instance while %s still exists", tc.kind), func(t *testing.T) {
s := testVIPStateStore(t)
// Create the config entry to get the virtual IP
tc.createFunc(t, s, "foo")
ensureVirtualIP(t, s, "foo", "240.0.0.1")
// Creating service instance should not affect virtual IP
createServiceInstance(t, s, "foo")
ensureVirtualIP(t, s, "foo", "240.0.0.1")
// Deleting should also not affect the service virtual IP because the config entry still exists.
deleteServiceInstance(t, s, "foo")
ensureVirtualIP(t, s, "foo", "240.0.0.1")
// Now delete the config entry, which should free up the ip
deleteConfigEntry(t, s, tc.kind, "foo")
ensureVirtualIP(t, s, "foo", "")
// Make sure the free address can be reused
tc.createFunc(t, s, "bar")
ensureVirtualIP(t, s, "bar", "240.0.0.1")
})
}
}