2023-04-26 18:16:42 +03:00
|
|
|
package yamlstore
|
2023-04-19 08:28:09 +03:00
|
|
|
|
|
|
|
import (
|
2023-04-22 16:37:07 +03:00
|
|
|
"context"
|
2023-04-19 08:28:09 +03:00
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
|
2023-11-20 04:06:36 +03:00
|
|
|
"golang.org/x/mod/semver"
|
|
|
|
|
|
|
|
"github.com/neilotoole/sq/cli/buildinfo"
|
2023-04-26 18:16:42 +03:00
|
|
|
"github.com/neilotoole/sq/cli/config"
|
2023-11-20 04:06:36 +03:00
|
|
|
"github.com/neilotoole/sq/libsq/core/errz"
|
2023-04-26 18:16:42 +03:00
|
|
|
"github.com/neilotoole/sq/libsq/core/ioz"
|
2023-04-22 16:37:07 +03:00
|
|
|
"github.com/neilotoole/sq/libsq/core/lg"
|
|
|
|
"github.com/neilotoole/sq/libsq/core/lg/lga"
|
2023-04-19 08:28:09 +03:00
|
|
|
)
|
|
|
|
|
2023-04-26 18:16:42 +03:00
|
|
|
// MinConfigVersion is the minimum semver value of Config.Version.
|
2023-04-19 08:28:09 +03:00
|
|
|
// This is basically how far back in time the config upgrade process
|
|
|
|
// can support. If the config dates from prior to this (unlikely),
|
|
|
|
// then the user needs to start with a new config.
|
2023-04-26 18:16:42 +03:00
|
|
|
const MinConfigVersion = "v0.0.0-dev"
|
2023-04-19 08:28:09 +03:00
|
|
|
|
2023-04-26 18:16:42 +03:00
|
|
|
// UpgradeFunc performs a (single) upgrade of the config file. Typically
|
2023-04-19 08:28:09 +03:00
|
|
|
// a func will read the config data from disk, perform some transformation
|
|
|
|
// on it, and write it back out to disk. Note that the func should not
|
|
|
|
// bind the config file YAML to the Config object, as they may differ
|
|
|
|
// significantly. Instead, the func should bind the YAML to a map, and
|
|
|
|
// manipulate that map directly.
|
2023-04-26 18:16:42 +03:00
|
|
|
type UpgradeFunc func(ctx context.Context, before []byte) (after []byte, err error)
|
2023-04-19 08:28:09 +03:00
|
|
|
|
2023-04-26 18:16:42 +03:00
|
|
|
// UpgradeRegistry is a map of config_version to upgrade funcs.
|
|
|
|
type UpgradeRegistry map[string]UpgradeFunc
|
2023-04-19 08:28:09 +03:00
|
|
|
|
2023-04-26 18:16:42 +03:00
|
|
|
// doUpgrade runs all the registered upgrade funcs between cfg.Version
|
2023-04-19 08:28:09 +03:00
|
|
|
// and targetVersion. Typically this is checked by Load, but can be
|
|
|
|
// explicitly invoked for testing etc.
|
2023-05-01 06:59:34 +03:00
|
|
|
func (fs *Store) doUpgrade(ctx context.Context, startVersion, targetVersion string) (*config.Config, error) {
|
2023-05-03 15:36:10 +03:00
|
|
|
log := lg.FromContext(ctx)
|
2023-04-26 18:16:42 +03:00
|
|
|
log.Debug("Starting config upgrade", lga.From, startVersion, lga.To, targetVersion)
|
|
|
|
|
2023-04-19 08:28:09 +03:00
|
|
|
if !semver.IsValid(targetVersion) {
|
|
|
|
return nil, errz.Errorf("invalid semver for config version {%s}", targetVersion)
|
|
|
|
}
|
|
|
|
|
|
|
|
var err error
|
2023-04-26 18:16:42 +03:00
|
|
|
upgradeFns := fs.UpgradeRegistry.getUpgradeFuncs(startVersion, targetVersion)
|
|
|
|
|
|
|
|
data, err := os.ReadFile(fs.Path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-04-19 08:28:09 +03:00
|
|
|
|
|
|
|
for _, fn := range upgradeFns {
|
2023-04-26 18:16:42 +03:00
|
|
|
log.Debug("Attempting config upgrade step")
|
|
|
|
data, err = fn(ctx, data)
|
2023-04-19 08:28:09 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-04-26 18:16:42 +03:00
|
|
|
log.Debug("Config upgrade step successful")
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = fs.Write(data); err != nil {
|
|
|
|
return nil, err
|
2023-04-19 08:28:09 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Do a final update of the version
|
2023-05-01 06:59:34 +03:00
|
|
|
cfg, err := fs.doLoad(ctx)
|
2023-04-19 08:28:09 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-04-26 18:16:42 +03:00
|
|
|
log.Debug("Setting config.version", lga.Val, targetVersion)
|
|
|
|
cfg.Version = targetVersion
|
2023-04-19 08:28:09 +03:00
|
|
|
|
2023-04-22 16:37:07 +03:00
|
|
|
err = fs.Save(ctx, cfg)
|
2023-04-19 08:28:09 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return cfg, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// getUpgradeFuncs returns the funcs required to upgrade from startingVersion
|
|
|
|
// to targetVersion. We iterate over the set of registered funcs; if the
|
|
|
|
// version (the key) is greater than startingVersion, and less than or equal
|
2023-04-26 18:16:42 +03:00
|
|
|
// to targetVersion, that UpgradeFunc will be included in the return value.
|
|
|
|
func (r UpgradeRegistry) getUpgradeFuncs(startingVersion, targetVersion string) []UpgradeFunc {
|
2023-04-19 08:28:09 +03:00
|
|
|
if len(r) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var vers []string
|
|
|
|
for k := range r {
|
|
|
|
if semver.Compare(k, startingVersion) > 0 && semver.Compare(k, targetVersion) <= 0 {
|
|
|
|
vers = append(vers, k)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(vers) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
semver.Sort(vers)
|
|
|
|
|
2023-04-26 18:16:42 +03:00
|
|
|
upgradeFns := make([]UpgradeFunc, len(vers))
|
2023-04-19 08:28:09 +03:00
|
|
|
for i, v := range vers {
|
|
|
|
upgradeFns[i] = r[v]
|
|
|
|
}
|
|
|
|
|
|
|
|
return upgradeFns
|
|
|
|
}
|
|
|
|
|
2023-04-26 18:16:42 +03:00
|
|
|
// LoadVersionFromFile loads the version from the config file.
|
2023-04-19 08:28:09 +03:00
|
|
|
// If the field is not present, minConfigVersion (and no error) is returned.
|
2023-04-26 18:16:42 +03:00
|
|
|
func LoadVersionFromFile(path string) (string, error) {
|
2023-04-19 08:28:09 +03:00
|
|
|
bytes, err := os.ReadFile(path)
|
|
|
|
if err != nil {
|
|
|
|
return "", errz.Wrapf(err, "failed to load file: %s", path)
|
|
|
|
}
|
|
|
|
|
|
|
|
m := map[string]any{}
|
2023-04-26 18:16:42 +03:00
|
|
|
err = ioz.UnmarshallYAML(bytes, &m)
|
2023-04-19 08:28:09 +03:00
|
|
|
if err != nil {
|
|
|
|
return "", errz.Wrap(err, "failed to unmarshal config YAML")
|
|
|
|
}
|
|
|
|
|
2023-04-30 17:18:56 +03:00
|
|
|
// These are all the historical names for the version field
|
|
|
|
// in the config YAML.
|
|
|
|
candidates := []string{"version", "config_version", "config.version"}
|
2023-04-19 08:28:09 +03:00
|
|
|
|
2023-04-30 17:18:56 +03:00
|
|
|
for _, field := range candidates {
|
|
|
|
if v, ok := m[field]; ok {
|
|
|
|
// Legacy "version" field.
|
|
|
|
s, ok := v.(string)
|
|
|
|
if !ok {
|
|
|
|
return "", errz.Errorf("invalid value for {%s} field: %s", field, v)
|
|
|
|
}
|
2023-04-19 08:28:09 +03:00
|
|
|
|
2023-04-30 17:18:56 +03:00
|
|
|
s = strings.TrimSpace(s)
|
|
|
|
if s == "" {
|
|
|
|
continue
|
|
|
|
}
|
2023-04-19 08:28:09 +03:00
|
|
|
|
2023-04-30 17:18:56 +03:00
|
|
|
if !semver.IsValid(s) {
|
|
|
|
return "", errz.Errorf("invalid semver value for {%s} field: %s", field, s)
|
|
|
|
}
|
2023-04-19 08:28:09 +03:00
|
|
|
|
2023-04-30 17:18:56 +03:00
|
|
|
return s, nil
|
2023-04-19 08:28:09 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-30 17:18:56 +03:00
|
|
|
return "", errz.Errorf("config file does not have a version field: %v", path)
|
2023-04-19 08:28:09 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// checkNeedsUpgrade checks on the config version, returning needsUpgrade
|
|
|
|
// if applicable. The returned foundVers is a valid semver.
|
2023-04-30 17:18:56 +03:00
|
|
|
func checkNeedsUpgrade(ctx context.Context, path string) (needsUpgrade bool, foundVers string, err error) {
|
2023-04-26 18:16:42 +03:00
|
|
|
foundVers, err = LoadVersionFromFile(path)
|
2023-04-19 08:28:09 +03:00
|
|
|
if err != nil {
|
|
|
|
return false, "", err
|
|
|
|
}
|
|
|
|
|
2023-05-03 15:36:10 +03:00
|
|
|
lg.FromContext(ctx).Debug("Found config version in file",
|
2023-04-30 17:18:56 +03:00
|
|
|
lga.Version, foundVers, lga.Path, path)
|
|
|
|
|
2023-04-26 18:16:42 +03:00
|
|
|
if semver.Compare(foundVers, MinConfigVersion) < 0 {
|
2023-04-19 08:28:09 +03:00
|
|
|
return false, foundVers, errz.Errorf("version %q is less than minimum value %q",
|
2023-04-26 18:16:42 +03:00
|
|
|
foundVers, MinConfigVersion)
|
2023-04-19 08:28:09 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
buildVers := buildinfo.Version
|
|
|
|
|
|
|
|
switch semver.Compare(foundVers, buildVers) {
|
|
|
|
case 0:
|
|
|
|
// Versions are the same, nothing to do here
|
|
|
|
return false, foundVers, nil
|
|
|
|
case 1:
|
|
|
|
// sq version is less than config version:
|
|
|
|
// - user needs to upgrade sq
|
|
|
|
// - but we make an exception if sq is prerelease
|
|
|
|
if semver.Prerelease(buildVers) == "" {
|
|
|
|
return false, foundVers, errz.Errorf("config: version %q is newer than sq version %q: upgrade sq to a newer version",
|
|
|
|
foundVers, buildVers)
|
|
|
|
}
|
|
|
|
return false, foundVers, nil
|
|
|
|
|
|
|
|
default:
|
|
|
|
// config version is less than sq version; we need to upgrade config.
|
|
|
|
return true, foundVers, nil
|
|
|
|
}
|
|
|
|
}
|