safing-portmaster/base/database/migration/migration.go
Daniel Hååvi 80664d1a27
Restructure modules ()
* Move portbase into monorepo

* Add new simple module mgr

* [WIP] Switch to new simple module mgr

* Add StateMgr and more worker variants

* [WIP] Switch more modules

* [WIP] Switch more modules

* [WIP] swtich more modules

* [WIP] switch all SPN modules

* [WIP] switch all service modules

* [WIP] Convert all workers to the new module system

* [WIP] add new task system to module manager

* [WIP] Add second take for scheduling workers

* [WIP] Add FIXME for bugs in new scheduler

* [WIP] Add minor improvements to scheduler

* [WIP] Add new worker scheduler

* [WIP] Fix more bug related to new module system

* [WIP] Fix start handing of the new module system

* [WIP] Improve startup process

* [WIP] Fix minor issues

* [WIP] Fix missing subsystem in settings

* [WIP] Initialize managers in constructor

* [WIP] Move module event initialization to constrictors

* [WIP] Fix setting for enabling and disabling the SPN module

* [WIP] Move API registeration into module construction

* [WIP] Update states mgr for all modules

* [WIP] Add CmdLine operation support

* Add state helper methods to module group and instance

* Add notification and module status handling to status package

* Fix starting issues

* Remove pilot widget and update security lock to new status data

* Remove debug logs

* Improve http server shutdown

* Add workaround for cleanly shutting down firewall+netquery

* Improve logging

* Add syncing states with notifications for new module system

* Improve starting, stopping, shutdown; resolve FIXMEs/TODOs

* [WIP] Fix most unit tests

* Review new module system and fix minor issues

* Push shutdown and restart events again via API

* Set sleep mode via interface

* Update example/template module

* [WIP] Fix spn/cabin unit test

* Remove deprecated UI elements

* Make log output more similar for the logging transition phase

* Switch spn hub and observer cmds to new module system

* Fix log sources

* Make worker mgr less error prone

* Fix tests and minor issues

* Fix observation hub

* Improve shutdown and restart handling

* Split up big connection.go source file

* Move varint and dsd packages to structures repo

* Improve expansion test

* Fix linter warnings

* Fix interception module on windows

* Fix linter errors

---------

Co-authored-by: Vladimir Stoilov <vladimir@safing.io>
2024-08-09 18:15:48 +03:00

220 lines
6.3 KiB
Go

package migration
import (
"context"
"errors"
"fmt"
"sort"
"sync"
"time"
"github.com/hashicorp/go-version"
"github.com/safing/portmaster/base/database"
"github.com/safing/portmaster/base/database/record"
"github.com/safing/portmaster/base/log"
"github.com/safing/structures/dsd"
)
// MigrateFunc is called when a migration should be applied to the
// database. It receives the current version (from) and the target
// version (to) of the database and a dedicated interface for
// interacting with data stored in the DB.
// A dedicated log.ContextTracer is added to ctx for each migration
// run.
type MigrateFunc func(ctx context.Context, from, to *version.Version, dbInterface *database.Interface) error
// Migration represents a registered data-migration that should be applied to
// some database. Migrations are stacked on top and executed in order of increasing
// version number (see Version field).
type Migration struct {
// Description provides a short human-readable description of the
// migration.
Description string
// Version should hold the version of the database/subsystem after
// the migration has been applied.
Version string
// MigrateFuc is executed when the migration should be performed.
MigrateFunc MigrateFunc
}
// Registry holds a migration stack.
type Registry struct {
key string
lock sync.Mutex
migrations []Migration
}
// New creates a new migration registry.
// The key should be the name of the database key that is used to store
// the version of the last successfully applied migration.
func New(key string) *Registry {
return &Registry{
key: key,
}
}
// Add adds one or more migrations to reg.
func (reg *Registry) Add(migrations ...Migration) error {
reg.lock.Lock()
defer reg.lock.Unlock()
for _, m := range migrations {
if _, err := version.NewSemver(m.Version); err != nil {
return fmt.Errorf("migration %q: invalid version %s: %w", m.Description, m.Version, err)
}
reg.migrations = append(reg.migrations, m)
}
return nil
}
// Migrate migrates the database by executing all registered
// migration in order of increasing version numbers. The error
// returned, if not nil, is always of type *Diagnostics.
func (reg *Registry) Migrate(ctx context.Context) (err error) {
reg.lock.Lock()
defer reg.lock.Unlock()
start := time.Now()
log.Infof("migration: migration of %s started", reg.key)
defer func() {
if err != nil {
log.Errorf("migration: migration of %s failed after %s: %s", reg.key, time.Since(start), err)
} else {
log.Infof("migration: migration of %s finished after %s", reg.key, time.Since(start))
}
}()
db := database.NewInterface(&database.Options{
Local: true,
Internal: true,
})
startOfMigration, err := reg.getLatestSuccessfulMigration(db)
if err != nil {
return err
}
execPlan, diag, err := reg.getExecutionPlan(startOfMigration)
if err != nil {
return err
}
if len(execPlan) == 0 {
return nil
}
diag.TargetVersion = execPlan[len(execPlan)-1].Version
// finally, apply our migrations
lastAppliedMigration := startOfMigration
for _, m := range execPlan {
target, _ := version.NewSemver(m.Version) // we can safely ignore the error here
migrationCtx, tracer := log.AddTracer(ctx)
if err := m.MigrateFunc(migrationCtx, lastAppliedMigration, target, db); err != nil {
diag.Wrapped = err
diag.FailedMigration = m.Description
tracer.Errorf("migration: migration for %s failed: %s - %s", reg.key, target.String(), m.Description)
tracer.Submit()
return diag
}
lastAppliedMigration = target
diag.LastSuccessfulMigration = lastAppliedMigration.String()
if err := reg.saveLastSuccessfulMigration(db, target); err != nil {
diag.Message = "failed to persist migration status"
diag.Wrapped = err
diag.FailedMigration = m.Description
}
tracer.Infof("migration: applied migration for %s: %s - %s", reg.key, target.String(), m.Description)
tracer.Submit()
}
// all migrations have been applied successfully, we're done here
return nil
}
func (reg *Registry) getLatestSuccessfulMigration(db *database.Interface) (*version.Version, error) {
// find the latest version stored in the database
rec, err := db.Get(reg.key)
if errors.Is(err, database.ErrNotFound) {
return nil, nil
}
if err != nil {
return nil, &Diagnostics{
Message: "failed to query database for migration status",
Wrapped: err,
}
}
// Unwrap the record to get the actual database
r, ok := rec.(*record.Wrapper)
if !ok {
return nil, &Diagnostics{
Wrapped: errors.New("expected wrapped database record"),
}
}
sv, err := version.NewSemver(string(r.Data))
if err != nil {
return nil, &Diagnostics{
Message: "failed to parse version stored in migration status record",
Wrapped: err,
}
}
return sv, nil
}
func (reg *Registry) saveLastSuccessfulMigration(db *database.Interface, ver *version.Version) error {
r := &record.Wrapper{
Data: []byte(ver.String()),
Format: dsd.RAW,
}
r.SetKey(reg.key)
return db.Put(r)
}
func (reg *Registry) getExecutionPlan(startOfMigration *version.Version) ([]Migration, *Diagnostics, error) {
// create a look-up map for migrations indexed by their semver created a
// list of version (sorted by increasing number) that we use as our execution
// plan.
lm := make(map[string]Migration)
versions := make(version.Collection, 0, len(reg.migrations))
for _, m := range reg.migrations {
ver, err := version.NewSemver(m.Version)
if err != nil {
return nil, nil, &Diagnostics{
Message: "failed to parse version of migration",
Wrapped: err,
FailedMigration: m.Description,
}
}
lm[ver.String()] = m // use .String() for a normalized string representation
versions = append(versions, ver)
}
sort.Sort(versions)
diag := new(Diagnostics)
if startOfMigration != nil {
diag.StartOfMigration = startOfMigration.String()
}
// prepare our diagnostics and the execution plan
execPlan := make([]Migration, 0, len(versions))
for _, ver := range versions {
// skip an migration that has already been applied.
if startOfMigration != nil && startOfMigration.GreaterThanOrEqual(ver) {
continue
}
m := lm[ver.String()]
diag.ExecutionPlan = append(diag.ExecutionPlan, DiagnosticStep{
Description: m.Description,
Version: ver.String(),
})
execPlan = append(execPlan, m)
}
return execPlan, diag, nil
}