mirror of https://github.com/0xERR0R/blocky.git
180 lines
4.5 KiB
Go
180 lines
4.5 KiB
Go
// Package migration helps with migrating deprecated config options.
|
|
//
|
|
// `panic` is only used for programmer errors, meaning they will only trigger during development.
|
|
package migration
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/creasty/defaults"
|
|
"github.com/sirupsen/logrus"
|
|
"golang.org/x/exp/maps"
|
|
)
|
|
|
|
// Migrate checks each field of `deprecated` to see if a migration can and should be run.
|
|
//
|
|
// Each field must be a pointer: this allows knowing if the user has set a value in the config.
|
|
func Migrate(logger *logrus.Entry, optPrefix string, deprecated any, newOptions map[string]Migrator) bool {
|
|
deprecatedVal := reflect.ValueOf(deprecated)
|
|
deprecatedTyp := deprecatedVal.Type()
|
|
|
|
usesDepredOpts := false
|
|
|
|
for i := 0; i < deprecatedTyp.NumField(); i++ {
|
|
field := deprecatedTyp.Field(i)
|
|
fieldTag := field.Tag.Get("yaml")
|
|
oldName := fullname(optPrefix, fieldTag)
|
|
|
|
migrator, ok := newOptions[fieldTag]
|
|
if !ok {
|
|
panic(fmt.Errorf("deprecated option %s has no matching %T", oldName, migrator))
|
|
}
|
|
|
|
delete(newOptions, fieldTag) // so we know it's been checked
|
|
|
|
migrator.dest.prefix = optPrefix
|
|
|
|
val := deprecatedVal.Field(i)
|
|
if val.Type().Kind() != reflect.Pointer {
|
|
panic(fmt.Errorf("deprecated option %s must be a pointer", oldName))
|
|
}
|
|
|
|
if field.Tag.Get("default") != "" {
|
|
panic(fmt.Errorf("deprecated option %s must not have a default", oldName))
|
|
}
|
|
|
|
if val.IsNil() {
|
|
// Deprecated option is not defined in the user's config
|
|
continue
|
|
}
|
|
|
|
usesDepredOpts = true
|
|
val = val.Elem() // deref the pointer
|
|
|
|
if !migrator.dest.IsDefault() {
|
|
logger.
|
|
WithFields(logrus.Fields{
|
|
migrator.dest.Name(): migrator.dest.Value.Interface(),
|
|
oldName: val.Interface(),
|
|
}).
|
|
Errorf(
|
|
"config options %q (new) and %q (deprecated) are both set, ignoring the deprecated one",
|
|
migrator.dest, oldName,
|
|
)
|
|
|
|
continue
|
|
}
|
|
|
|
logger.Warnf("config option %q is deprecated, please use %q instead", oldName, migrator.dest)
|
|
|
|
migrator.apply(oldName, val)
|
|
}
|
|
|
|
if len(newOptions) != 0 {
|
|
panic(fmt.Errorf("%q has unused migrations: %v", optPrefix, maps.Keys(newOptions)))
|
|
}
|
|
|
|
return usesDepredOpts
|
|
}
|
|
|
|
type applyFunc func(oldName string, oldValue reflect.Value)
|
|
|
|
type Migrator struct {
|
|
dest *Dest
|
|
apply applyFunc
|
|
}
|
|
|
|
func newMigrator(dest *Dest, apply applyFunc) Migrator {
|
|
return Migrator{dest, apply}
|
|
}
|
|
|
|
// Move copies the deprecated option's value to `dest`.
|
|
func Move(dest *Dest) Migrator {
|
|
return newMigrator(dest, func(oldName string, oldValue reflect.Value) {
|
|
dest.Value.Set(oldValue)
|
|
})
|
|
}
|
|
|
|
// Apply calls `apply` with the deprecated value casted to `T`.
|
|
func Apply[T any](dest *Dest, apply func(oldValue T)) Migrator {
|
|
return newMigrator(dest, func(oldName string, oldValue reflect.Value) {
|
|
valItf := oldValue.Interface()
|
|
valTyped, ok := valItf.(T)
|
|
|
|
if !ok {
|
|
panic(fmt.Errorf("%q migration types don't match: cannot convert %v to %T", oldName, valItf, valTyped))
|
|
}
|
|
|
|
apply(valTyped)
|
|
})
|
|
}
|
|
|
|
type Dest struct {
|
|
prefix string
|
|
name string
|
|
|
|
Value reflect.Value
|
|
Default any
|
|
}
|
|
|
|
// To creates a new `Dest` from an option name (relative to the `Migrate` prefix) and the struct containing that option.
|
|
func To[T any](newName string, newContainerStruct *T) *Dest {
|
|
stVal := reflect.ValueOf(newContainerStruct).Elem()
|
|
|
|
if stVal.Type().Kind() == reflect.Pointer {
|
|
panic(fmt.Errorf("newContainerStruct for %s is a double pointer: %T", newName, newContainerStruct))
|
|
}
|
|
|
|
// Find the field matching `newName` in `newContainerStruct`
|
|
fieldIdx, newVal := func() (int, reflect.Value) {
|
|
parts := strings.Split(newName, ".")
|
|
tag := parts[len(parts)-1]
|
|
|
|
for i := 0; i < stVal.NumField(); i++ {
|
|
field := stVal.Type().Field(i)
|
|
if field.Tag.Get("yaml") == tag {
|
|
return i, stVal.Field(i)
|
|
}
|
|
}
|
|
|
|
panic(fmt.Errorf("migrated option %q not found in %T", newName, *newContainerStruct))
|
|
}()
|
|
|
|
// Get the default value of the new option
|
|
newDefaultVal := func() reflect.Value {
|
|
defaultVals := new(T)
|
|
defaults.MustSet(defaultVals)
|
|
|
|
return reflect.ValueOf(defaultVals).Elem().Field(fieldIdx)
|
|
}()
|
|
|
|
return &Dest{
|
|
prefix: "", // set by Run
|
|
name: newName,
|
|
Value: newVal,
|
|
Default: newDefaultVal.Interface(),
|
|
}
|
|
}
|
|
|
|
func (d *Dest) Name() string {
|
|
return fullname(d.prefix, d.name)
|
|
}
|
|
|
|
func (d *Dest) IsDefault() bool {
|
|
return reflect.DeepEqual(d.Value.Interface(), d.Default)
|
|
}
|
|
|
|
func (d *Dest) String() string {
|
|
return d.Name()
|
|
}
|
|
|
|
func fullname(prefix, name string) string {
|
|
if len(prefix) == 0 {
|
|
return name
|
|
}
|
|
|
|
return fmt.Sprintf("%s.%s", prefix, name)
|
|
}
|