diff --git a/pkg/admin/prerun/prerun.go b/pkg/admin/prerun/prerun.go index 8b0ed32b4b..0679a86e90 100644 --- a/pkg/admin/prerun/prerun.go +++ b/pkg/admin/prerun/prerun.go @@ -65,7 +65,23 @@ func (pr *PreRun) Perform() error { klog.InfoS("Previous boot", "health", health.Health, "deploymentID", health.DeploymentID, "bootID", health.BootID) if health.IsHealthy() { - return pr.backup(health) + if err := pr.backup(health); err != nil { + return err + } + + migrationNeeded, err := pr.checkVersions() + if err != nil { + return err + } + + klog.InfoS("Version checks successful", "is-migration-needed?", migrationNeeded) + + if migrationNeeded { + _ = migrationNeeded + // TODO: data migration + } + + return nil } return pr.restore() @@ -127,6 +143,31 @@ func (pr *PreRun) restore() error { return pr.dataManager.Restore(backupsForDeployment[0]) } +// checkVersions compares version of data and executable +// +// It returns true if migration should be performed. +// It returns non-nil error if difference between versions is unsupported. +func (pr *PreRun) checkVersions() (bool, error) { + execVer, err := getVersionOfExecutable() + if err != nil { + return false, err + } + + dataVer, err := getVersionOfData() + if err != nil { + if errors.Is(err, errDataVersionDoesNotExist) { + klog.InfoS("Version file of data does not exist - assuming data version is 4.13") + // TODO: 4.13 + return true, nil + } + return false, err + } + + klog.InfoS("Checking version difference between data and executable", "data", dataVer, "exec", execVer) + + return checkVersionDiff(execVer, dataVer) +} + func getCurrentDeploymentID() (string, error) { cmd := exec.Command("rpm-ostree", "status", "--jsonpath=$.deployments[0].id", "--booted") var stdout, stderr bytes.Buffer diff --git a/pkg/admin/prerun/version.go b/pkg/admin/prerun/version.go new file mode 100644 index 0000000000..c057d8f645 --- /dev/null +++ b/pkg/admin/prerun/version.go @@ -0,0 +1,122 @@ +package prerun + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/openshift/microshift/pkg/config" + "github.com/openshift/microshift/pkg/util" + "github.com/openshift/microshift/pkg/version" + "k8s.io/klog/v2" +) + +var ( + versionFilePath = filepath.Join(config.DataDir, "version") + + errDataVersionDoesNotExist = errors.New("version file for MicroShift data does not exist") +) + +// CreateOrValidateDataVersion creates or compares data version against executable's version +// +// Function is intended to be invoked by main MicroShift run procedure, just before starting, +// to ensure that storage migration (which should update the version file) was performed. +func CreateOrValidateDataVersion() error { + execVer, err := getVersionOfExecutable() + if err != nil { + return err + } + + dataVer, err := getVersionOfData() + if err != nil { + if errors.Is(err, errDataVersionDoesNotExist) { + // First run of MicroShift, create version file shortly after creating DataDir + execVerS := execVer.String() + klog.InfoS("Version file in data directory does not exist - creating", "version", execVerS) + + if err := os.WriteFile(versionFilePath, []byte(execVerS), 0600); err != nil { + return fmt.Errorf("writing '%s' to %s failed: %w", execVerS, versionFilePath, err) + } + return nil + } + return err + } + klog.InfoS("Comparing versions of MicroShift data on disk and executable", "data", dataVer, "exec", execVer) + + if execVer != dataVer { + return fmt.Errorf("data version (%s) does not match binary version (%s) - missing migration?", dataVer, execVer) + } + + return nil +} + +type versionMetadata struct { + Major, Minor int +} + +func (v versionMetadata) String() string { + return fmt.Sprintf("%d.%d", v.Major, v.Minor) +} + +// versionMetadataFromString creates versionMetadata object from "major.minor" string where X and Y are integers +func versionMetadataFromString(majorMinor string) (versionMetadata, error) { + majorMinor = strings.TrimSpace(majorMinor) + majorMinorSplit := strings.Split(majorMinor, ".") + if len(majorMinorSplit) != 2 { + return versionMetadata{}, fmt.Errorf("invalid version string (%s): expected X.Y", majorMinor) + } + + major, err := strconv.Atoi(majorMinorSplit[0]) + if err != nil { + return versionMetadata{}, fmt.Errorf("converting '%s' to an int failed: %w", majorMinorSplit[0], err) + } + + minor, err := strconv.Atoi(majorMinorSplit[1]) + if err != nil { + return versionMetadata{}, fmt.Errorf("converting '%s' to an int failed: %w", majorMinorSplit[1], err) + } + + return versionMetadata{Major: major, Minor: minor}, nil +} + +func getVersionOfExecutable() (versionMetadata, error) { + ver := version.Get() + return versionMetadataFromString(fmt.Sprintf("%s.%s", ver.Major, ver.Minor)) +} + +func getVersionOfData() (versionMetadata, error) { + exists, err := util.PathExists(versionFilePath) + if err != nil { + return versionMetadata{}, fmt.Errorf("checking if path exists failed: %w", err) + } + + if !exists { + return versionMetadata{}, errDataVersionDoesNotExist + } + + versionFileContents, err := os.ReadFile(versionFilePath) + if err != nil { + return versionMetadata{}, fmt.Errorf("reading %s failed: %w", versionFilePath, err) + } + + return versionMetadataFromString(string(versionFileContents)) +} + +func checkVersionDiff(execVer, dataVer versionMetadata) (bool, error) { + if execVer == dataVer { + return false, nil + } + + if execVer.Major != dataVer.Major { + return false, fmt.Errorf("major versions are different: %d and %d", dataVer.Major, execVer.Major) + } + + if execVer.Minor < dataVer.Minor { + return false, fmt.Errorf("executable (%s) is older than existing data (%s): migrating data to older version is not supported", execVer.String(), dataVer.String()) + } + + return false, nil +} diff --git a/pkg/admin/prerun/version_test.go b/pkg/admin/prerun/version_test.go new file mode 100644 index 0000000000..93584a5bcc --- /dev/null +++ b/pkg/admin/prerun/version_test.go @@ -0,0 +1,53 @@ +package prerun + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCheckVersionDiff(t *testing.T) { + testData := []struct { + name string + execVer versionMetadata + dataVer versionMetadata + expectedMigrationRequired bool + errExpected bool + }{ + { + name: "equal versions: no migration, no error", + execVer: versionMetadata{Major: 4, Minor: 14}, + dataVer: versionMetadata{Major: 4, Minor: 14}, + expectedMigrationRequired: false, + errExpected: false, + }, + { + name: "X versions must be the same", + execVer: versionMetadata{Major: 4, Minor: 14}, + dataVer: versionMetadata{Major: 5, Minor: 14}, + expectedMigrationRequired: false, + errExpected: true, + }, + { + name: "binary must not be older than data", + execVer: versionMetadata{Major: 4, Minor: 14}, + dataVer: versionMetadata{Major: 4, Minor: 15}, + expectedMigrationRequired: false, + errExpected: true, + }, + } + + for _, td := range testData { + td := td + t.Run(td.name, func(t *testing.T) { + migrationRequired, err := checkVersionDiff(td.execVer, td.dataVer) + + assert.Equal(t, td.expectedMigrationRequired, migrationRequired) + if td.errExpected { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index e48ee378f2..ccdb9a342a 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -119,6 +119,10 @@ func RunMicroshift(cfg *config.Config) error { return fmt.Errorf("failed to create dir %q: %w", config.DataDir, err) } + if err := prerun.CreateOrValidateDataVersion(); err != nil { + return err + } + // TODO: change to only initialize what is strictly necessary for the selected role(s) certChains, err := initCerts(cfg) if err != nil {