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
- Variables
- func EffectiveState(sess store.MigrationAgentSession, live bool, now time.Time) string
- func EmptyCount(cs []Comparison) int
- func HashSecret(secret string) string
- func MarkUnlisted(inv *InventoryDoc, refs []ImportedRef)
- func NewToken() (id, token, hash string, err error)
- func OverrideCount(cs []Comparison) int
- func ParseToken(token string) (id, secret string, ok bool)
- func Reopen(c *migrationplan.Checklist) error
- func SecretMatches(storedHash, secret string) bool
- func SourceTarget(app, volume string) string
- func ZeroDiffs(cs []Comparison) bool
- type AdvanceOptions
- type AdvanceResult
- type AppVolume
- type AuditSaver
- type Auditor
- type Comparison
- type Config
- type ContainerDoc
- type Created
- type Event
- type ImportedRef
- type InventoryDoc
- type MountDoc
- type ScanDoc
- type Service
- func (s *Service) Auditor() *Auditor
- func (s *Service) Config() Config
- func (s *Service) Create(ctx context.Context, label, actor string) (Created, error)
- func (s *Service) Discover(ctx context.Context, id string) (InventoryDoc, error)
- func (s *Service) Enroll(ctx context.Context, req *migrationpb.EnrollRequest) (*migrationpb.EnrollResponse, error)
- func (s *Service) Get(ctx context.Context, id string) (SessionView, InventoryDoc, ScanDoc, error)
- func (s *Service) List(ctx context.Context) ([]SessionView, error)
- func (s *Service) Revoke(ctx context.Context, id, actor string) error
- func (s *Service) Scan(ctx context.Context, id string, targets []string) (ScanDoc, error)
- func (s *Service) Session(stream migrationpb.MigrationService_SessionServer) error
- type SessionView
- type Signer
- type StatDoc
- type Store
- type TargetStat
- type VolumeDoc
Constants ¶
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.
const ( StatusMatch = "match" StatusDiffers = "differs" StatusChanged = "changed_during_scan" StatusSourceMissing = "source_missing" StatusTargetMissing = "target_missing" StatusScanError = "scan_error" )
Comparison statuses.
const ( TargetMeasured = "measured" TargetOperator = "operator-supplied" )
Where a target measurement came from. Only a measurement this control plane took itself can advance the checklist.
const Actor = "migration-agent"
Actor is the checklist actor recorded when the agent advances a step.
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 ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
SecretMatches compares a presented secret with a stored hash in constant time.
func SourceTarget ¶
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 ¶
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.
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 ¶
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 ¶
ScanDoc is the latest volume 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 ¶
Auditor exposes the audit writer so API handlers record their events the same way.
func (*Service) Enroll ¶
func (s *Service) Enroll(ctx context.Context, req *migrationpb.EnrollRequest) (*migrationpb.EnrollResponse, error)
Enroll redeems a single-use token for a short-lived client certificate.
func (*Service) Get ¶
func (s *Service) Get(ctx context.Context, id string) (SessionView, InventoryDoc, ScanDoc, error)
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 ¶
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 ¶
Scan asks the agent to count files and bytes for targets and stores the result.
func (*Service) Session ¶
func (s *Service) Session(stream migrationpb.MigrationService_SessionServer) error
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.