forwardlegacy

package
v4.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package forwardlegacy is the v4.2 forwarding upgrade (forward-sdk.md section 10, F5c, gate H15): it archives the flux forwarding data, checks that every forward node is clean of the old runtime, and, only when an administrator confirms it on the command line, drops the flux tables.

The drop is the only place the flux tables are dropped, and it is never automatic. Once it is recorded (v4_forward_legacy_drop), Control no longer creates those tables at start (KeepModels, UnlessDropped) and no longer runs the flux workers.

Index

Constants

View Source
const (
	PathAgent   = "agent"
	PathNodeX   = "nodex"
	PathAnsible = "ansible"
)

Check paths.

View Source
const (
	StateUnchecked   = "unchecked"
	StateClean       = "clean"
	StateDirty       = "dirty"
	StateUnreachable = "unreachable"
	StateAbandoned   = "abandoned"
)

Node states.

View Source
const ArchiveDirName = "forward-legacy"

ArchiveDirName is the archive directory under Control's data directory.

View Source
const ArchiveSchema = "anixops.forward.legacy-archive.v1"

ArchiveSchema is the archive file's schema version.

View Source
const BackupMaxAge = 24 * time.Hour

BackupMaxAge is how old a database backup the drop accepts may be.

View Source
const ConfirmPhrase = "DROP v4.1 FORWARDING TABLES"

ConfirmPhrase is what `forward legacy drop --confirm` must be given, exactly.

View Source
const DefaultLockTimeout = 10 * time.Second

DefaultLockTimeout bounds how long the drop waits on PostgreSQL for a table another session still reads.

View Source
const PathNotNeeded = "not_needed"

PathNotNeeded is a path result for a path that had nothing to clean on the node.

View Source
const Redacted = "[redacted]"

Redacted replaces a secret value the archive leaves out.

View Source
const SingletonLease = "control.singleton-workers"

SingletonLease is the lease the running Control's singleton workers hold (cmd/server); the drop refuses while it is held.

Variables

View Source
var DropTables = []string{
	"v2_forward_port_binding",
	"v2_forward_traffic_cursor",
	"v2_forward_agent_bridge_task",
	"v2_forward_runtime_job",
	"v2_forward_user_tunnel",
	"v2_speed_limit",
	"v2_forward",
	"v2_forward_tunnel",
	"v2_forward_rule",
	"v2_forward_route",
	"v2_forward_log",
	"v2_forward_stats",
}

DropTables are the flux forwarding tables the drop removes, dependents first, so a foreign key never points at a table already gone. The F5 audit: the flux forwards, tunnels, user tunnel grants and speed limits; the legacy rules; the runtime jobs with their port claims, traffic cursors and clean agent bridge tasks; and the pre-flux smart routes, connection log and hourly stats, which nothing reads any more.

View Source
var ErrNotUnreachable = errors.New("only a node whose latest check found it unreachable can be abandoned")

ErrNotUnreachable refuses abandoning a node that is not unreachable.

View Source
var KeptTables = []string{"v2_forward_node", "v2_forward_clean_agent"}

KeptTables are archived for reference but not dropped:

  • v2_forward_node is the node inventory and the Agent identity (forward-<id>), which v4.2 keeps (section 10);
  • v2_forward_clean_agent is a table of the node credential split (nodesecrets) and the source of kapi_forward_clean_agent_v1; it goes with the clean agent's code, not with the flux data.

v2_forward_latency_bucket is neither: its probes also cover proxy nodes (LatencyTargetTypeNode), so it is not flux data.

Functions

func AnyDropTablePresent

func AnyDropTablePresent(db *gorm.DB) bool

AnyDropTablePresent reports whether one of the flux tables exists.

func ArchiveFileName

func ArchiveFileName(now time.Time) string

ArchiveFileName is the timestamped name of an archive written at now.

func ControlRunning

func ControlRunning(ctx context.Context, db *gorm.DB, now time.Time) (string, time.Time, bool, error)

ControlRunning answers the holder of the running Control's singleton lease and until when, when it is held now.

func Dropped

func Dropped(db *gorm.DB) bool

Dropped reports whether the flux tables were dropped. An error reading the record answers false: Control then behaves as before the drop.

func EnsureSchema

func EnsureSchema(db *gorm.DB) error

EnsureSchema creates the upgrade's own tables. They are new tables: nothing existing is altered.

func KeepModels

func KeepModels(db *gorm.DB, models []any) []any

KeepModels answers models without the flux tables once they were dropped, so the schema migration does not create them again. It keys on table names, so it keeps working whichever models a later release still lists.

func LatestArchive

func LatestArchive(db *gorm.DB) (*model.ForwardLegacyArchive, error)

LatestArchive answers the newest recorded archive, nil when none.

func LatestDrop

func LatestDrop(db *gorm.DB) (*model.ForwardLegacyDrop, error)

LatestDrop answers the recorded drop, nil when the tables were not dropped.

func MissingTable

func MissingTable(err error, table string) bool

MissingTable reports whether err is the database saying that table does not exist: SQLite's "no such table: <table>", or PostgreSQL's undefined table (SQLSTATE 42P01) naming it. It is how a statement on a flux table finds out that Drop removed the table, without a metadata query before every statement: the code that runs on a database with the table keeps its statements and its errors, and only this answer means "dropped".

Do not guard with Migrator().HasTable instead. It answers false when its own query fails (a cancelled context, a lost connection, a statement timeout), so a guard built on it takes any failure for a dropped table and answers "nothing there" where the caller must have the error. Another table missing, and every other error, is false.

func NodeRef

func NodeRef(id uint) string

NodeRef is a forward node's reference (its Agent identity name).

func UnlessDropped

func UnlessDropped(step func(*gorm.DB) error) func(*gorm.DB) error

UnlessDropped wraps a schema step that creates a flux table: once the tables were dropped it does nothing.

Types

type Archive

type Archive struct {
	Schema         string         `json:"schema"`
	CreatedAt      time.Time      `json:"created_at"`
	ControlVersion string         `json:"control_version,omitempty"`
	DatabaseDriver string         `json:"database_driver"`
	Note           string         `json:"note"`
	Tables         []ArchiveTable `json:"tables"`
}

Archive is the archive file's document.

func BuildArchive

func BuildArchive(ctx context.Context, db *gorm.DB, now time.Time, controlVersion string) (*Archive, error)

BuildArchive reads the flux tables and the kept reference tables into an archive with every secret left out.

func VerifyArchive

func VerifyArchive(record *model.ForwardLegacyArchive) (*Archive, error)

VerifyArchive reads a recorded archive's file and checks it: readable, the recorded SHA-256, and the archive schema. It answers the archive.

func (*Archive) RowCounts

func (a *Archive) RowCounts() map[string]int64

RowCounts answers the archive's row count per table, -1 for a table that did not exist.

type ArchiveTable

type ArchiveTable struct {
	Name string `json:"name"`
	// Dropped tells whether the upgrade's drop removes the table.
	Dropped bool `json:"dropped"`
	// Present is false when the table did not exist (a release that
	// already removed it): Rows is then empty.
	Present  bool `json:"present"`
	RowCount int  `json:"row_count"`
	// RedactedColumns are columns whose values were left out.
	RedactedColumns []string         `json:"redacted_columns,omitempty"`
	Rows            []map[string]any `json:"rows"`
}

ArchiveTable is one table's rows.

type Backup

type Backup struct {
	// Source is "backup_record" (Control's own backup service) or
	// "operator" (--backup-taken).
	Source     string    `json:"source"`
	Path       string    `json:"path"`
	SizeBytes  int64     `json:"size_bytes"`
	SHA256     string    `json:"sha256"`
	ModifiedAt time.Time `json:"modified_at"`
	RecordID   uint      `json:"record_id,omitempty"`
}

Backup is the database backup a drop accepted.

type CheckOptions

type CheckOptions struct {
	NodeX   Cleaner
	Ansible Cleaner
	// Nodes limits the check to these node names or references; empty
	// checks every forward node.
	Nodes []string
	Now   time.Time
}

CheckOptions configure a check.

type Cleaner

type Cleaner interface {
	CleanNode(ctx context.Context, node NodeInfo) PathResult
}

Cleaner removes the old forward runtime of a node through one path (NodeX's HTTP API, the Ansible fallback) and verifies it is gone.

type DropOptions

type DropOptions struct {
	// Confirm must be ConfirmPhrase exactly.
	Confirm    string
	BackupPath string
	Actor      string
	// LockTimeout bounds the wait for a table lock on PostgreSQL;
	// DefaultLockTimeout when zero.
	LockTimeout time.Duration
	Now         time.Time
}

DropOptions configure the drop.

type DropResult

type DropResult struct {
	Record         model.ForwardLegacyDrop `json:"record"`
	Dropped        []string                `json:"dropped"`
	AlreadyMissing []string                `json:"already_missing"`
	Backup         *Backup                 `json:"backup"`
}

DropResult is a completed drop.

func Drop

func Drop(ctx context.Context, db *gorm.DB, options DropOptions) (*DropResult, error)

Drop drops the flux tables, all in one transaction, and records it. It refuses unless the phrase is exact, the archive is readable and current, every forward node is clean or abandoned, no Control runs on the database and a database backup was taken. Tables already gone are reported, not an error.

type NodeInfo

type NodeInfo struct {
	ID   uint   `json:"id"`
	Ref  string `json:"node_ref"`
	Name string `json:"name"`
	Host string `json:"host"`
}

NodeInfo is a forward node as a cleaner sees it.

func ResolveNode

func ResolveNode(ctx context.Context, db *gorm.DB, given string) (NodeInfo, error)

ResolveNode finds a forward node by its reference (forward-<id>) or its name, which must then be unique.

type NodeStatus

type NodeStatus struct {
	NodeID uint   `json:"node_id"`
	Ref    string `json:"node_ref"`
	Name   string `json:"name"`
	// State is the node's standing: unchecked, clean, dirty, unreachable
	// or abandoned.
	State string `json:"state"`
	// CheckState is the latest check's own result.
	CheckState    string       `json:"check_state,omitempty"`
	Checks        []PathResult `json:"checks,omitempty"`
	Detail        string       `json:"detail,omitempty"`
	CheckedAt     *time.Time   `json:"checked_at,omitempty"`
	AbandonedAt   *time.Time   `json:"abandoned_at,omitempty"`
	AbandonedBy   string       `json:"abandoned_by,omitempty"`
	AbandonReason string       `json:"abandon_reason,omitempty"`
}

NodeStatus is a forward node's standing in the upgrade.

func Abandon

func Abandon(ctx context.Context, db *gorm.DB, given, reason, actor string, now time.Time) (NodeStatus, error)

Abandon marks an unreachable node as abandoned: the drop no longer waits for it. Its old runtime, if any, stays on the machine.

func Check

func Check(ctx context.Context, db *gorm.DB, options CheckOptions) ([]NodeStatus, error)

Check runs every path on the forward nodes, records each node's result and answers the nodes' standing.

func Status

func Status(ctx context.Context, db *gorm.DB) ([]NodeStatus, error)

Status answers every forward node's standing, unchecked nodes included.

func (NodeStatus) Ready

func (s NodeStatus) Ready() bool

Ready reports whether the node no longer holds up the drop.

type PackageAdoption

type PackageAdoption struct {
	PluginID string   `json:"plugin_id"`
	Target   string   `json:"target"`
	Version  string   `json:"version"`
	Tables   []string `json:"tables"`
}

PackageAdoption is an installed package release that still adopts flux tables. A storage lease fails as a whole when an adopted table is missing (packagestore.ErrGrantTargetMissing), so dropping a table such a release adopts would take the whole package down, the forward package's v4 API included, until a release without the flux routes (F5d) is installed.

type PathResult

type PathResult struct {
	Path string `json:"path"`
	// State is clean, dirty, unreachable or not_needed.
	State  string `json:"state"`
	Detail string `json:"detail,omitempty"`
	// Items counts the legacy resources the path removed or checked.
	Items int `json:"items,omitempty"`
}

PathResult is one path's result on one node.

type PreconditionOptions

type PreconditionOptions struct {
	// BackupPath is the operator's database backup (--backup-taken); empty
	// looks for a backup Control's backup service made.
	BackupPath string
	// SkipBackup leaves the backup check out (the status command, which
	// only reports).
	SkipBackup bool
	Now        time.Time
}

PreconditionOptions configure the checks.

type Preconditions

type Preconditions struct {
	Archive *model.ForwardLegacyArchive `json:"archive"`
	Nodes   []NodeStatus                `json:"nodes"`
	Backup  *Backup                     `json:"backup"`
	// Packages are installed package releases that still adopt a flux
	// table.
	Packages []PackageAdoption `json:"packages"`
	// Present and Missing split DropTables by whether they exist now.
	Present []string `json:"present"`
	Missing []string `json:"missing"`
	// Blockers lists every unmet precondition; the drop runs only when it
	// is empty.
	Blockers []string `json:"blockers"`
}

Preconditions are what the drop needs, and what is missing.

func CheckPreconditions

func CheckPreconditions(ctx context.Context, db *gorm.DB, options PreconditionOptions) (*Preconditions, error)

CheckPreconditions answers every precondition of the drop except the confirmation phrase.

type RefusedError

type RefusedError struct{ Reasons []string }

RefusedError lists every reason the drop refused.

func (*RefusedError) Error

func (e *RefusedError) Error() string

type WriteOptions

type WriteOptions struct {
	// Output is a file that must not exist yet, or an existing directory
	// that receives a timestamped file. Empty means Dir.
	Output string
	// Dir is the archive directory (created 0700 when missing).
	Dir            string
	Trigger        string
	Actor          string
	ControlVersion string
	Now            time.Time
}

WriteOptions say where an archive goes and who wrote it.

type WriteResult

type WriteResult struct {
	Record model.ForwardLegacyArchive `json:"record"`
	// Tables is each archived table's row count, -1 when it did not exist.
	Tables map[string]int64 `json:"tables"`
}

WriteResult is a written and recorded archive.

func StartupArchive

func StartupArchive(ctx context.Context, db *gorm.DB, dir, controlVersion string, now time.Time) (*WriteResult, error)

StartupArchive writes the archive at Control's first v4.2 start: when a flux table exists, the tables were not dropped, and no recorded archive file is still there. It answers nil when there was nothing to do.

func WriteArchive

func WriteArchive(ctx context.Context, db *gorm.DB, options WriteOptions) (*WriteResult, error)

WriteArchive builds the archive, writes it (mode 0600, never over an existing file) and records it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL