feature: handle delete operation for IPs
This commit is contained in:
1 parent
4f597505cc
commit
f8336740ad
20 files changed
+1083
-5
No files matched your search
@@ -10,6 +10,7 @@ package orchestrator
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@@ -238,6 +239,105 @@ func (o *Orchestrator) ForceCancel(ctx context.Context, ipAddress string) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteIP disassociates the floating IP if attached, then permanently
|
||||
// removes the address and its full history — differs from ForceCancel,
|
||||
// which keeps a cancelled record instead of deleting it. Works from any
|
||||
// state, including actively checking: it does the same resource-freeing
|
||||
// (FIP disassociation, validator release) ForceCancel does, but goes
|
||||
// straight to physical deletion rather than parking in `cancelled`.
|
||||
// Returns db.ErrNotFound if the address is unknown.
|
||||
func (o *Orchestrator) DeleteIP(ctx context.Context, ipAddress string) error {
|
||||
item, err := o.DB.GetIPByAddress(ctx, ipAddress)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return fmt.Errorf("ip %q: %w", ipAddress, db.ErrNotFound)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if item.FIPID != "" {
|
||||
if err := o.OS.DisassociateFloatingIP(ctx, item.FIPID); err != nil {
|
||||
o.Log.Error("disassociate fip on delete", "ip_id", item.ID, "fip_id", item.FIPID, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := o.DB.DeleteIP(ctx, item.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.event(ctx, "control-api", "", nil, "ip_deleted", fmt.Sprintf(`{"ip_address":%q}`, ipAddress))
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteIPs disassociates the floating IP (best-effort) for every address
|
||||
// in the list that has one attached, then deletes the whole list in one
|
||||
// DB.DeleteIPs call. Addresses not currently in the queue are simply
|
||||
// omitted from the disassociation pass and reported back in NotFound by
|
||||
// DB.DeleteIPs — not an error.
|
||||
func (o *Orchestrator) DeleteIPs(ctx context.Context, addresses []string) (db.DeleteIPsResult, error) {
|
||||
for _, addr := range addresses {
|
||||
item, err := o.DB.GetIPByAddress(ctx, addr)
|
||||
if err != nil {
|
||||
continue // unknown address — DB.DeleteIPs will report it in NotFound
|
||||
}
|
||||
if item.FIPID != "" {
|
||||
if err := o.OS.DisassociateFloatingIP(ctx, item.FIPID); err != nil {
|
||||
o.Log.Error("disassociate fip on delete", "ip_id", item.ID, "fip_id", item.FIPID, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result, err := o.DB.DeleteIPs(ctx, addresses)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
o.event(ctx, "control-api", "", nil, "ips_deleted", deletedAddressesPayload(result.Deleted))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ClearQueue deletes every address currently in the queue, regardless of
|
||||
// state — the "delete everything" operation, implemented as DeleteIPs over
|
||||
// the full current address list rather than a separate DB code path.
|
||||
func (o *Orchestrator) ClearQueue(ctx context.Context) (db.DeleteIPsResult, error) {
|
||||
items, err := o.DB.ListIPs(ctx)
|
||||
if err != nil {
|
||||
return db.DeleteIPsResult{}, fmt.Errorf("list ips: %w", err)
|
||||
}
|
||||
addresses := make([]string, len(items))
|
||||
for i, item := range items {
|
||||
addresses[i] = item.IPAddress
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
if item.FIPID != "" {
|
||||
if err := o.OS.DisassociateFloatingIP(ctx, item.FIPID); err != nil {
|
||||
o.Log.Error("disassociate fip on clear queue", "ip_id", item.ID, "fip_id", item.FIPID, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result, err := o.DB.DeleteIPs(ctx, addresses)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
o.event(ctx, "control-api", "", nil, "queue_cleared", deletedAddressesPayload(result.Deleted))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// deletedAddressesPayload builds the event payload for the batch delete
|
||||
// operations — a proper JSON array via encoding/json rather than fmt's %q
|
||||
// slice formatting (which produces space-separated quoted strings, not
|
||||
// valid JSON).
|
||||
func deletedAddressesPayload(addresses []string) string {
|
||||
b, err := json.Marshal(struct {
|
||||
Addresses []string `json:"addresses"`
|
||||
}{addresses})
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// sweepCheckingWindow moves IPs that have either finished reporting from
|
||||
// every source, or hit the checking-window deadline, into aggregation.
|
||||
func (o *Orchestrator) sweepCheckingWindow(ctx context.Context) error {
|
||||
|
||||
@@ -2,6 +2,7 @@ package orchestrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -333,6 +334,81 @@ func TestInboundChecksDisabled(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteIPDisassociatesFIP proves DeleteIP disassociates a currently
|
||||
// attached floating IP (via the mock) before permanently removing the
|
||||
// address — the same resource-freeing ForceCancel does, but going straight
|
||||
// to physical deletion instead of a `cancelled` record.
|
||||
func TestDeleteIPDisassociatesFIP(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
o, d, mock := newTestOrchestrator(t, 180)
|
||||
mock.Seed("fip-1", "1.2.3.4", "svc-project")
|
||||
_ = d.RegisterValidator(ctx, "validator-1", "host-1", "port-1", "v0.1")
|
||||
_ = d.SeedQueue(ctx, []string{"1.2.3.4"})
|
||||
|
||||
o.Tick(ctx) // claim + associate -> awaiting_self_check, fip attached
|
||||
|
||||
ip, err := d.GetIPByAddress(ctx, "1.2.3.4")
|
||||
if err != nil {
|
||||
t.Fatalf("get ip: %v", err)
|
||||
}
|
||||
if ip.FIPID == "" {
|
||||
t.Fatalf("expected fip associated before delete")
|
||||
}
|
||||
|
||||
if err := o.DeleteIP(ctx, "1.2.3.4"); err != nil {
|
||||
t.Fatalf("delete ip: %v", err)
|
||||
}
|
||||
|
||||
if fip, _ := mock.GetFloatingIPByAddress(ctx, "1.2.3.4"); fip.PortID != "" {
|
||||
t.Fatalf("expected fip disassociated on delete, still on port %q", fip.PortID)
|
||||
}
|
||||
if _, err := d.GetIPByAddress(ctx, "1.2.3.4"); err == nil {
|
||||
t.Fatalf("expected ip row gone after delete")
|
||||
}
|
||||
v, err := d.GetValidator(ctx, "validator-1")
|
||||
if err != nil {
|
||||
t.Fatalf("get validator: %v", err)
|
||||
}
|
||||
if v.State != db.ValidatorIdle || v.CurrentIPID != nil {
|
||||
t.Fatalf("expected validator freed, got state=%s current_ip=%v", v.State, v.CurrentIPID)
|
||||
}
|
||||
|
||||
if err := o.DeleteIP(ctx, "1.2.3.4"); !errors.Is(err, db.ErrNotFound) {
|
||||
t.Fatalf("expected ErrNotFound deleting already-gone ip, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClearQueueDisassociatesAllFIPs proves ClearQueue deletes every
|
||||
// address regardless of state and disassociates any attached floating IPs
|
||||
// along the way.
|
||||
func TestClearQueueDisassociatesAllFIPs(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
o, d, mock := newTestOrchestrator(t, 180)
|
||||
mock.Seed("fip-1", "1.2.3.4", "svc-project")
|
||||
_ = d.RegisterValidator(ctx, "validator-1", "host-1", "port-1", "v0.1")
|
||||
_ = d.SeedQueue(ctx, []string{"1.2.3.4", "5.6.7.8"})
|
||||
|
||||
o.Tick(ctx) // claims + associates 1.2.3.4; 5.6.7.8 stays queued
|
||||
|
||||
result, err := o.ClearQueue(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("clear queue: %v", err)
|
||||
}
|
||||
if len(result.Deleted) != 2 {
|
||||
t.Fatalf("expected both addresses deleted, got %+v", result)
|
||||
}
|
||||
if fip, _ := mock.GetFloatingIPByAddress(ctx, "1.2.3.4"); fip.PortID != "" {
|
||||
t.Fatalf("expected fip disassociated on clear, still on port %q", fip.PortID)
|
||||
}
|
||||
ips, err := d.ListIPs(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("list ips: %v", err)
|
||||
}
|
||||
if len(ips) != 0 {
|
||||
t.Fatalf("expected empty queue after clear, got %+v", ips)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInboundChecksPartialSites confirms a partially-configured sites list
|
||||
// (fewer than 3 slots assigned) only waits on the sites actually
|
||||
// configured — the two unassigned slots are never expected.
|
||||
|
||||
Reference in new issue
Block a user