mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-01 19:19:07 +02:00
[management] Clean up after account deletion (#7812)
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.
This commit is contained in:
@@ -135,6 +135,11 @@ func (c *Combined) DeleteGuardrail(ctx context.Context, id string) error {
|
||||
return anDelete(ctx, c, "/api/agent-network/guardrails/"+id)
|
||||
}
|
||||
|
||||
// CreateBudgetRule creates an account-level agent-network budget rule.
|
||||
func (c *Combined) CreateBudgetRule(ctx context.Context, req api.AgentNetworkBudgetRuleRequest) (api.AgentNetworkBudgetRule, error) {
|
||||
return anRequest[api.AgentNetworkBudgetRule](ctx, c, http.MethodPost, "/api/agent-network/budget-rules", req)
|
||||
}
|
||||
|
||||
// CreateSettings bootstraps the account's agent-network settings row,
|
||||
// assigning the immutable endpoint. Exactly one of req.ProxyAddress (labeled
|
||||
// endpoint beneath that cluster) and req.Endpoint (self-addressed dedicated
|
||||
|
||||
@@ -305,6 +305,33 @@ func (c *Combined) SnapshotStoreDB(dstDir string) (string, error) {
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
// Restart stops and starts the combined container, keeping its bind-mounted
|
||||
// data dir, and waits for the API again. The host port can change across a
|
||||
// restart, so BaseURL and the authenticated client are refreshed. Work that
|
||||
// management only does at startup (such as the agent-network cleanup's first
|
||||
// pass, or re-evaluating whether instance setup is required) runs again.
|
||||
func (c *Combined) Restart(ctx context.Context) error {
|
||||
if err := c.container.Stop(ctx, nil); err != nil {
|
||||
return fmt.Errorf("stop combined container: %w", err)
|
||||
}
|
||||
if err := c.container.Start(ctx); err != nil {
|
||||
return fmt.Errorf("start combined container: %w", err)
|
||||
}
|
||||
host, err := c.container.Host(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("container host: %w", err)
|
||||
}
|
||||
mapped, err := c.container.MappedPort(ctx, nat.Port(combinedHTTPPort))
|
||||
if err != nil {
|
||||
return fmt.Errorf("mapped port: %w", err)
|
||||
}
|
||||
c.BaseURL = fmt.Sprintf("http://%s:%s", host, mapped.Port())
|
||||
if c.PAT != "" {
|
||||
c.api = rest.New(c.BaseURL, c.PAT)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Logs returns the combined server container logs, for diagnostics.
|
||||
func (c *Combined) Logs(ctx context.Context) string {
|
||||
return containerLogs(ctx, c.container)
|
||||
|
||||
@@ -3,13 +3,17 @@
|
||||
package harness
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
tcexec "github.com/testcontainers/testcontainers-go/exec"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
)
|
||||
|
||||
@@ -114,6 +118,41 @@ func StartProxy(ctx context.Context, c *Combined, proxyToken string, envOverride
|
||||
return &Proxy{container: ctr, workDir: workDir}, nil
|
||||
}
|
||||
|
||||
// ProxyDebugClient is one per-account embedded client the proxy runs, as the
|
||||
// proxy's debug endpoint reports it.
|
||||
type ProxyDebugClient struct {
|
||||
AccountID string `json:"account_id"`
|
||||
ServiceCount int `json:"service_count"`
|
||||
ServiceKeys []string `json:"service_keys"`
|
||||
}
|
||||
|
||||
// DebugClients lists the per-account clients the proxy is running, through
|
||||
// the proxy's own debug CLI inside the container. The proxy must be started
|
||||
// with NB_PROXY_DEBUG_ENDPOINT=true.
|
||||
func (p *Proxy) DebugClients(ctx context.Context) ([]ProxyDebugClient, error) {
|
||||
code, reader, err := p.container.Exec(ctx,
|
||||
[]string{"/usr/bin/netbird-proxy", "debug", "clients", "--json"}, tcexec.Multiplexed())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("exec debug clients: %w", err)
|
||||
}
|
||||
out, _ := io.ReadAll(reader)
|
||||
if code != 0 {
|
||||
return nil, fmt.Errorf("debug clients exited %d: %s", code, string(out))
|
||||
}
|
||||
// stderr is multiplexed in; the JSON document starts at the first brace.
|
||||
start := bytes.IndexByte(out, '{')
|
||||
if start < 0 {
|
||||
return nil, fmt.Errorf("no JSON in debug clients output: %s", string(out))
|
||||
}
|
||||
var resp struct {
|
||||
Clients []ProxyDebugClient `json:"clients"`
|
||||
}
|
||||
if err := json.NewDecoder(bytes.NewReader(out[start:])).Decode(&resp); err != nil {
|
||||
return nil, fmt.Errorf("decode debug clients output: %w", err)
|
||||
}
|
||||
return resp.Clients, nil
|
||||
}
|
||||
|
||||
// Logs returns the proxy container logs, for diagnostics on failure.
|
||||
func (p *Proxy) Logs(ctx context.Context) string {
|
||||
return containerLogs(ctx, p.container)
|
||||
|
||||
Reference in New Issue
Block a user