Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion pkg/admin/prerun/prerun.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic below this point would be easier to unit test if it was pulled out into its own function so the test suite could pass the execVer and dataVer values for different scenarios.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intended that initially but was discouraged by no protection from passing params in wrong order and doing checks wrong. but the ut argument is valid so i'll change it

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
Expand Down
122 changes: 122 additions & 0 deletions pkg/admin/prerun/version.go
Original file line number Diff line number Diff line change
@@ -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
}
53 changes: 53 additions & 0 deletions pkg/admin/prerun/version_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
4 changes: 4 additions & 0 deletions pkg/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down