mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-01 11:09:15 +02:00
Deleting an account left state behind that DeleteAccount's store associations don't reach. The Agent Network tables outlived the account, keeping its gateway domain claimed and its provider API keys stored. The proxies kept serving its gateway until they next resynced. Cloud-side state, such as managed proxy deployments, had no way to be cleaned up at all. Account deletion now runs registered hooks after the permission check and before any users or data are removed. A failing hook aborts the deletion. Agent Network registers one that tells the proxies to drop the account's gateway mappings. The account's settings, providers, policies, guardrails and budget rules are deleted in the account's transaction. Consumption counters, and the access logs of deleted accounts, are left to the background cleanup; usage records are kept.
177 lines
6.2 KiB
Go
177 lines
6.2 KiB
Go
package agentnetwork
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
goproto "google.golang.org/protobuf/proto"
|
|
|
|
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
|
"github.com/netbirdio/netbird/management/server/types"
|
|
"github.com/netbirdio/netbird/shared/management/proto"
|
|
)
|
|
|
|
// syntheticMapping pairs a synthesised proxy mapping with the address of the
|
|
// proxy that serves it. The cluster is recorded rather than derived from the
|
|
// mapping's domain: ProxyMapping does not carry it, and the previous derivation
|
|
// -- everything after the first DNS label -- is wrong whenever the service's
|
|
// domain is not one label under its proxy's address, which silently addressed
|
|
// updates to a cluster no proxy declares.
|
|
type syntheticMapping struct {
|
|
mapping *proto.ProxyMapping
|
|
cluster string
|
|
}
|
|
|
|
// reconcile recomputes the synthesised reverse-proxy services for an
|
|
// account, diffs them against the previously-synthesised set in the
|
|
// in-memory cache, and emits Create / Update / Delete proxy mappings
|
|
// to the affected clusters. Also triggers a peer-side network-map
|
|
// recompute via accountManager.UpdateAccountPeers so the
|
|
// private-service ACL injection picks up the new state immediately.
|
|
//
|
|
// Reconcile failures are logged and swallowed — the underlying CRUD
|
|
// has already completed, and the next mutation (or proxy reconnect)
|
|
// will re-converge the cluster's view.
|
|
func (m *managerImpl) reconcile(ctx context.Context, accountID string) {
|
|
if accountID == "" {
|
|
return
|
|
}
|
|
|
|
defer func() {
|
|
if m.accountManager != nil {
|
|
m.accountManager.UpdateAccountPeers(ctx, accountID, types.UpdateReason{
|
|
Resource: types.UpdateResourceService,
|
|
Operation: types.UpdateOperationUpdate,
|
|
})
|
|
}
|
|
}()
|
|
|
|
if m.proxyController == nil {
|
|
return
|
|
}
|
|
|
|
services, err := SynthesizeServices(ctx, m.store, accountID)
|
|
if err != nil {
|
|
log.WithContext(ctx).WithError(err).Warnf("agent-network reconcile: synthesise services for account %s", accountID)
|
|
return
|
|
}
|
|
|
|
oidcCfg := m.proxyController.GetOIDCValidationConfig()
|
|
current := make(map[string]syntheticMapping, len(services))
|
|
for _, svc := range services {
|
|
if svc == nil || svc.ID == "" {
|
|
continue
|
|
}
|
|
current[svc.ID] = syntheticMapping{
|
|
mapping: svc.ToProtoMapping(rpservice.Update, "", oidcCfg),
|
|
cluster: svc.ProxyCluster,
|
|
}
|
|
}
|
|
|
|
m.reconcileMu.Lock()
|
|
previous := m.reconcileCache[accountID]
|
|
if previous == nil {
|
|
previous = make(map[string]syntheticMapping)
|
|
}
|
|
|
|
creates, updates, deletes := diffMappings(previous, current)
|
|
if len(current) == 0 {
|
|
delete(m.reconcileCache, accountID)
|
|
} else {
|
|
m.reconcileCache[accountID] = current
|
|
}
|
|
m.reconcileMu.Unlock()
|
|
|
|
m.sendMappings(ctx, accountID, creates, proto.ProxyMappingUpdateType_UPDATE_TYPE_CREATED)
|
|
m.sendMappings(ctx, accountID, updates, proto.ProxyMappingUpdateType_UPDATE_TYPE_MODIFIED)
|
|
m.sendMappings(ctx, accountID, deletes, proto.ProxyMappingUpdateType_UPDATE_TYPE_REMOVED)
|
|
}
|
|
|
|
// sendMappings sends each entry as updateType. It sends a copy: the entries'
|
|
// mappings are shared with reconcileCache, which another reconcile or
|
|
// RemoveAccountGateway may be reading, so they are never written.
|
|
func (m *managerImpl) sendMappings(ctx context.Context, accountID string, entries []syntheticMapping, updateType proto.ProxyMappingUpdateType) {
|
|
for _, entry := range entries {
|
|
update := goproto.Clone(entry.mapping).(*proto.ProxyMapping)
|
|
update.Type = updateType
|
|
m.proxyController.SendServiceUpdateToCluster(ctx, accountID, update, entry.cluster)
|
|
}
|
|
}
|
|
|
|
// RemoveAccountGateway tells the proxies to drop every mapping of the account's
|
|
// gateway, so a deleted account's proxy config, provider API keys included, does
|
|
// not linger in proxy memory until the next resync. It is an account deletion
|
|
// hook: it runs before the account's data is removed, the last point at which
|
|
// the mappings can be synthesised from the store. The cache alone would miss
|
|
// them, since it is per instance and empty after a restart. If the deletion
|
|
// then fails, the gateway stays down until the account's next change reconciles
|
|
// it back.
|
|
func (m *managerImpl) RemoveAccountGateway(ctx context.Context, accountID string) error {
|
|
if m.proxyController == nil {
|
|
return nil
|
|
}
|
|
|
|
services, err := SynthesizeServices(ctx, m.store, accountID)
|
|
if err != nil {
|
|
return fmt.Errorf("synthesise agent network services: %w", err)
|
|
}
|
|
oidcCfg := m.proxyController.GetOIDCValidationConfig()
|
|
removed := make(map[string]syntheticMapping, len(services))
|
|
for _, svc := range services {
|
|
if svc == nil || svc.ID == "" {
|
|
continue
|
|
}
|
|
removed[svc.ID] = syntheticMapping{
|
|
mapping: svc.ToProtoMapping(rpservice.Delete, "", oidcCfg),
|
|
cluster: svc.ProxyCluster,
|
|
}
|
|
}
|
|
|
|
m.reconcileMu.Lock()
|
|
for id, entry := range m.reconcileCache[accountID] {
|
|
if _, ok := removed[id]; !ok {
|
|
removed[id] = entry
|
|
}
|
|
}
|
|
delete(m.reconcileCache, accountID)
|
|
m.reconcileMu.Unlock()
|
|
|
|
entries := make([]syntheticMapping, 0, len(removed))
|
|
for _, entry := range removed {
|
|
entries = append(entries, entry)
|
|
}
|
|
m.sendMappings(ctx, accountID, entries, proto.ProxyMappingUpdateType_UPDATE_TYPE_REMOVED)
|
|
return nil
|
|
}
|
|
|
|
// diffMappings classifies the previous→current transition for a single
|
|
// account into Create / Update / Delete sets.
|
|
//
|
|
// A change of serving proxy for the same service ID is surfaced as a Delete
|
|
// addressed to the old proxy plus a Create addressed to the new one, so the
|
|
// mapping actually moves. Comparing the recorded cluster is what makes that
|
|
// detectable: with a placement-free endpoint the mapping's domain is identical
|
|
// before and after the move, so nothing about the mapping itself reveals it.
|
|
func diffMappings(previous, current map[string]syntheticMapping) (creates, updates, deletes []syntheticMapping) {
|
|
for id, cur := range current {
|
|
prev, existed := previous[id]
|
|
switch {
|
|
case !existed:
|
|
creates = append(creates, cur)
|
|
case prev.mapping.GetDomain() == "" ||
|
|
cur.mapping.GetAccountId() == prev.mapping.GetAccountId() && prev.cluster != cur.cluster:
|
|
deletes = append(deletes, prev)
|
|
creates = append(creates, cur)
|
|
default:
|
|
updates = append(updates, cur)
|
|
}
|
|
}
|
|
for id, prev := range previous {
|
|
if _, stillThere := current[id]; !stillThere {
|
|
deletes = append(deletes, prev)
|
|
}
|
|
}
|
|
return creates, updates, deletes
|
|
}
|