migrateagent

package
v0.8.2 Latest Latest
Warning

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

Go to latest
Published: Oct 22, 2026 License: Apache-2.0 Imports: 31 Imported by: 0

Documentation

Overview

Package migrateagent is the control plane side of the read-only migration agent: single-use tokens, the mTLS session service, discovery storage and the volume comparison that advances the cutover checklist.

Index

Constants

View Source
const (
	ActionTokenIssued      = "migration_agent.token_issued"
	ActionEnrolled         = "migration_agent.enrolled"
	ActionEnrollRefused    = "migration_agent.enroll_refused"
	ActionSessionStarted   = "migration_agent.session_started"
	ActionSessionRejected  = "migration_agent.session_rejected"
	ActionSessionEnded     = "migration_agent.session_ended"
	ActionSessionExpired   = "migration_agent.session_expired"
	ActionDiscovery        = "migration_agent.discovery"
	ActionScan             = "migration_agent.scan"
	ActionAgentDenied      = "migration_agent.agent_denied"
	ActionVerified         = "migration_agent.verified"
	ActionStepAdvanced     = "migration_agent.step_advanced"
	ActionStepReopened     = "migration_agent.step_reopened"
	ActionRevoked          = "migration_agent.revoked"
	ActionProtocolViolated = "migration_agent.protocol_violation"
)

Audit action names, one per session event. The session id is the audit row's actor id.

View Source
const (
	StatusMatch         = "match"
	StatusDiffers       = "differs"
	StatusChanged       = "changed_during_scan"
	StatusSourceMissing = "source_missing"
	StatusTargetMissing = "target_missing"
	StatusScanError     = "scan_error"
)

Comparison statuses.

View Source
const (
	TargetMeasured = "measured"
	TargetOperator = "operator-supplied"
)

Where a target measurement came from. Only a measurement this control plane took itself can advance the checklist.

View Source
const Actor = "migration-agent"

Actor is the checklist actor recorded when the agent advances a step.

View Source
const (

	// CertCommonNamePrefix marks a migration client certificate. A node
	// certificate never carries it, so neither kind opens the other's stream.
	CertCommonNamePrefix = "migration-source:"
)

Variables

View Source
var (
	ErrNotActive = errors.New("migrateagent: session is revoked or expired")
	ErrOffline   = errors.New("migrateagent: the source agent is not connected")
)

Errors returned to API callers.

View Source
var ErrNotAgentStep = errors.New("migrateagent: data_verified was not completed by the agent")

ErrNotAgentStep means Reopen found a data_verified step the agent did not complete.

Functions

func EffectiveState

func EffectiveState(sess store.MigrationAgentSession, live bool, now time.Time) string

EffectiveState folds time and liveness into the stored state: an unused token past its expiry or a session past its cap reads as expired, and a connected session whose stream is gone reads as lost.

func EmptyCount

func EmptyCount(cs []Comparison) int

EmptyCount is how many matched volumes are empty on the source.

func HashSecret

func HashSecret(secret string) string

HashSecret is the stored form of a token secret.

func MarkUnlisted

func MarkUnlisted(inv *InventoryDoc, refs []ImportedRef)

MarkUnlisted flags every container that matches no imported app by name or source id. Matching is a substring test because platforms prefix or suffix container names, so a flag means "worth a look", not proof.

func NewToken

func NewToken() (id, token, hash string, err error)

NewToken returns a session id, the plaintext token to show once, and the hash to store.

func OverrideCount

func OverrideCount(cs []Comparison) int

OverrideCount is how many comparisons used caller-supplied target numbers.

func ParseToken

func ParseToken(token string) (id, secret string, ok bool)

ParseToken splits a token into its session id and secret.

func Reopen

func Reopen(c *migrationplan.Checklist) error

Reopen undoes an agent advance. Later steps are reset with it because they depended on the data being verified.

func SecretMatches

func SecretMatches(storedHash, secret string) bool

SecretMatches compares a presented secret with a stored hash in constant time.

func SourceTarget

func SourceTarget(app, volume string) string

SourceTarget maps an imported app's volume back to the source volume name by dropping the app prefix the importer added. Bind paths pass through.

func ZeroDiffs

func ZeroDiffs(cs []Comparison) bool

ZeroDiffs is true only when there is something to compare and all of it matches.

Types

type AdvanceOptions

type AdvanceOptions struct {
	// AckEmpty accepts volumes that hold no files on either side.
	AckEmpty bool
}

AdvanceOptions carries the operator's explicit acknowledgements.

type AdvanceResult

type AdvanceResult struct {
	Advanced bool     `json:"advanced"`
	Reason   string   `json:"reason,omitempty"`
	Evidence []string `json:"evidence,omitempty"`
}

AdvanceResult says what AutoAdvance did.

func AutoAdvance

func AutoAdvance(c *migrationplan.Checklist, cs []Comparison, databases int, opts AdvanceOptions, now time.Time) AdvanceResult

AutoAdvance completes data_verified when every volume matches. It never touches a step after data_verified, and it never runs for any other step. Databases are not measured by the agent, so their presence is recorded in the evidence instead of being claimed as verified.

type AppVolume

type AppVolume struct {
	Name   string `json:"name"`
	Source string `json:"source"`
}

AppVolume pairs an app's volume or bind path on this host with the target it was copied from on the source.

type AuditSaver

type AuditSaver interface {
	SaveAuditEntry(ctx context.Context, e store.AuditEntry) error
}

AuditSaver is the audit log surface; *store.DB satisfies it.

type Auditor

type Auditor struct {
	Saver  AuditSaver
	Logger *slog.Logger
	Now    func() time.Time
}

Auditor writes session events to the existing audit log, so retention follows it.

func (*Auditor) Record

func (a *Auditor) Record(ctx context.Context, ev Event)

Record writes one event. A failed write is logged, never returned, so an audit outage cannot be used to keep a session alive or to break a revoke.

type Comparison

type Comparison struct {
	Name        string `json:"name"`
	Source      string `json:"source"`
	SourceFiles int64  `json:"source_files"`
	SourceBytes int64  `json:"source_bytes"`
	TargetFiles int64  `json:"target_files"`
	TargetBytes int64  `json:"target_bytes"`
	Status      string `json:"status"`
	TargetFrom  string `json:"target_from,omitempty"`
	Detail      string `json:"detail,omitempty"`
}

Comparison is the result for one volume.

func Compare

func Compare(vols []AppVolume, scan []StatDoc, target map[string]TargetStat) []Comparison

Compare lines source scans up against target measurements. Anything other than an exact file count and byte match, a scan taken while files changed, or a missing side is a diff.

type Config

type Config struct {
	TokenTTL       time.Duration
	SessionMax     time.Duration
	RequestTimeout time.Duration
	VolumesDir     string
	// EnrollRate is the enroll attempts one source address may make per EnrollWindow.
	EnrollRate   int
	EnrollWindow time.Duration
	// AgentFieldMax caps the version, os and arch strings an agent reports.
	AgentFieldMax int
}

Config holds the env-tunable limits of the migration agent.

func ConfigFromEnv

func ConfigFromEnv() Config

ConfigFromEnv reads the agent settings from APP_* environment variables.

type ContainerDoc

type ContainerDoc struct {
	ID              string     `json:"id"`
	Name            string     `json:"name"`
	Image           string     `json:"image"`
	State           string     `json:"state"`
	Status          string     `json:"status"`
	EnvKeys         []string   `json:"env_keys,omitempty"`
	Mounts          []MountDoc `json:"mounts,omitempty"`
	Ports           []string   `json:"ports,omitempty"`
	DatabaseEngine  string     `json:"database_engine,omitempty"`
	DatabaseVersion string     `json:"database_version,omitempty"`
	// Unlisted is true when no imported app matches this container, so a
	// platform API read would not have shown it.
	Unlisted bool `json:"unlisted"`
}

ContainerDoc is one container on the source. EnvKeys are names only.

type Created

type Created struct {
	ID        string    `json:"id"`
	Token     string    `json:"token"`
	ExpiresAt time.Time `json:"expires_at"`
}

Created is a new pending session. Token is shown once and never stored.

type Event

type Event struct {
	Action    string
	SessionID string
	Actor     string
	Status    int
	Remote    string
	Detail    map[string]any
}

Event is one audit record. Detail must never hold tokens, keys or env values.

type ImportedRef

type ImportedRef struct {
	App      string
	SourceID string
}

ImportedRef identifies an app that was imported from the source platform.

type InventoryDoc

type InventoryDoc struct {
	DockerVersion string         `json:"docker_version"`
	Containers    []ContainerDoc `json:"containers"`
	Volumes       []VolumeDoc    `json:"volumes"`
	NetworkCount  int            `json:"network_count"`
	CollectedAt   time.Time      `json:"collected_at"`
}

InventoryDoc is the stored and served form of one discovery.

func InventoryFromPB

func InventoryFromPB(in *migrationpb.Inventory) InventoryDoc

InventoryFromPB converts the wire inventory. The agent-reported mountpoint of a volume is dropped: operators only need names.

func ParseInventory

func ParseInventory(raw string) InventoryDoc

ParseInventory decodes a stored inventory, or the zero value when none is stored.

type MountDoc

type MountDoc struct {
	Type        string `json:"type"`
	Name        string `json:"name,omitempty"`
	Source      string `json:"source,omitempty"`
	Destination string `json:"destination"`
	ReadOnly    bool   `json:"read_only"`
}

MountDoc is a container mount.

type ScanDoc

type ScanDoc struct {
	At    time.Time `json:"at"`
	Stats []StatDoc `json:"stats"`
}

ScanDoc is the latest volume scan.

func ParseScan

func ParseScan(raw string) ScanDoc

ParseScan decodes a stored scan.

func ScanFromPB

func ScanFromPB(in *migrationpb.ScanResult, at time.Time) ScanDoc

ScanFromPB converts the wire scan result.

type Service

type Service struct {
	migrationpb.UnimplementedMigrationServiceServer
	// contains filtered or unexported fields
}

Service implements migrationpb.MigrationServiceServer and the live-session hub.

func NewService

func NewService(st Store, ca Signer, cfg Config, logger *slog.Logger, now func() time.Time) *Service

NewService builds the service. now may be nil for the wall clock.

func (*Service) Auditor

func (s *Service) Auditor() *Auditor

Auditor exposes the audit writer so API handlers record their events the same way.

func (*Service) Config

func (s *Service) Config() Config

Config returns the effective limits.

func (*Service) Create

func (s *Service) Create(ctx context.Context, label, actor string) (Created, error)

Create issues a single-use token bound to a new session.

func (*Service) Discover

func (s *Service) Discover(ctx context.Context, id string) (InventoryDoc, error)

Discover asks the agent for a fresh inventory and stores it.

func (*Service) Enroll

Enroll redeems a single-use token for a short-lived client certificate.

func (*Service) Get

Get returns one session with its stored inventory and scan.

func (*Service) List

func (s *Service) List(ctx context.Context) ([]SessionView, error)

List returns every session, newest first.

func (*Service) Revoke

func (s *Service) Revoke(ctx context.Context, id, actor string) error

Revoke ends a session for good: the state is persisted first so the revoke holds even when the source host is unreachable, then any live stream is closed.

func (*Service) Scan

func (s *Service) Scan(ctx context.Context, id string, targets []string) (ScanDoc, error)

Scan asks the agent to count files and bytes for targets and stores the result.

func (*Service) Session

Session serves one source host's stream. It sends only the three migration control messages and answers nothing else.

type SessionView

type SessionView struct {
	ID               string     `json:"id"`
	Label            string     `json:"label"`
	State            string     `json:"state"`
	TokenExpiresAt   time.Time  `json:"token_expires_at"`
	SessionExpiresAt *time.Time `json:"session_expires_at,omitempty"`
	ConnectedAt      *time.Time `json:"connected_at,omitempty"`
	LastSeenAt       *time.Time `json:"last_seen_at,omitempty"`
	AgentVersion     string     `json:"agent_version,omitempty"`
	AgentOS          string     `json:"agent_os,omitempty"`
	AgentArch        string     `json:"agent_arch,omitempty"`
	CreatedBy        string     `json:"created_by,omitempty"`
	CreatedAt        time.Time  `json:"created_at"`
}

SessionView is the API shape of a session. It never carries token material.

type Signer

type Signer interface {
	SignClientCSR(commonName string, csrDER []byte, validFor time.Duration, now time.Time) (agent.IssuedCert, error)
	CertPEM() []byte
}

Signer issues the short-lived client certificate; *agent.CA satisfies it.

type StatDoc

type StatDoc struct {
	Target            string `json:"target"`
	Files             int64  `json:"files"`
	Bytes             int64  `json:"bytes"`
	ChangedDuringScan bool   `json:"changed_during_scan,omitempty"`
	Error             string `json:"error,omitempty"`
}

StatDoc is one scanned target.

type Store

type Store interface {
	SaveMigrationAgentSession(ctx context.Context, s store.MigrationAgentSession) error
	GetMigrationAgentSession(ctx context.Context, id string) (store.MigrationAgentSession, error)
	ListMigrationAgentSessions(ctx context.Context) ([]store.MigrationAgentSession, error)
	RedeemMigrationAgentToken(ctx context.Context, id, fingerprint, serial string, sessionExpires, now time.Time) error
	SetMigrationAgentState(ctx context.Context, id, state string, now time.Time) error
	TouchMigrationAgentSession(ctx context.Context, id, version, goos, arch string, now time.Time) error
	SaveMigrationAgentInventory(ctx context.Context, id, inventoryJSON string, now time.Time) error
	SaveMigrationAgentScan(ctx context.Context, id, scanJSON string, now time.Time) error
	AuditSaver
}

Store is the persistence surface Service needs; *store.DB satisfies it.

type TargetStat

type TargetStat struct {
	Files int64
	Bytes int64
	Err   string
	// Operator marks numbers typed in by the caller instead of measured here.
	Operator bool
}

TargetStat is the measured size of a volume on this host.

func MeasureTarget

func MeasureTarget(ctx context.Context, dir, name string) TargetStat

MeasureTarget walks a volume's data directory under dir, or a bind path as given.

type VolumeDoc

type VolumeDoc struct {
	Name   string `json:"name"`
	Driver string `json:"driver"`
}

VolumeDoc is a named volume on the source.

Jump to

Keyboard shortcuts

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