Skip to content

Commit

Permalink
Implement to upgrade-recovery separate command
Browse files Browse the repository at this point in the history
Signed-off-by: Andrea Mazzotti <andrea.mazzotti@suse.com>
  • Loading branch information
anmazzotti committed Feb 27, 2024
1 parent 9feacf3 commit 3246717
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 47 deletions.
23 changes: 23 additions & 0 deletions cmd/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,29 @@ func ReadUpgradeSpec(r *v1.RunConfig, flags *pflag.FlagSet) (*v1.UpgradeSpec, er
return upgrade, err
}

func ReadUpgradeRecoverySpec(r *v1.RunConfig, flags *pflag.FlagSet) (*v1.UpgradeSpec, error) {
upgrade, err := config.NewUpgradeSpec(r.Config)
if err != nil {
return nil, fmt.Errorf("failed initializing upgrade recovery spec: %v", err)
}
vp := viper.Sub("upgrade-recovery")
if vp == nil {
vp = viper.New()
}
// Bind upgrade-recovery cmd flags
bindGivenFlags(vp, flags)
// Bind upgrade-recovery env vars
viperReadEnv(vp, "UPGRADE_RECOVERY", constants.GetUpgradeRecoveryKeyEnvMap())

err = vp.Unmarshal(upgrade, setDecoder, decodeHook)
if err != nil {
r.Logger.Warnf("error unmarshalling UpgradeSpec: %s", err)
}
err = upgrade.SanitizeForRecoveryOnly()
r.Logger.Debugf("Loaded upgrade UpgradeSpec: %s", litter.Sdump(upgrade))
return upgrade, err
}

func ReadBuildISO(b *v1.BuildConfig, flags *pflag.FlagSet) (*v1.LiveISO, error) {
iso := config.NewISO()
vp := viper.Sub("iso")
Expand Down
85 changes: 85 additions & 0 deletions cmd/upgrade-recovery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
Copyright © 2022 - 2024 SUSE LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package cmd

import (
"os/exec"

"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/rancher/elemental-toolkit/cmd/config"
"github.com/rancher/elemental-toolkit/pkg/action"
elementalError "github.com/rancher/elemental-toolkit/pkg/error"
v1 "github.com/rancher/elemental-toolkit/pkg/types/v1"
)

// NewUpgradeCmd returns a new instance of the upgrade subcommand and appends it to
// the root command. requireRoot is to initiate it with or without the CheckRoot
// pre-run check. This method is mostly used for testing purposes.
func NewUpgradeRecoveryCmd(root *cobra.Command, addCheckRoot bool) *cobra.Command {
c := &cobra.Command{
Use: "upgrade-recovery",
Short: "Upgrade the Recovery system",
Args: cobra.ExactArgs(0),
PreRunE: func(_ *cobra.Command, _ []string) error {
if addCheckRoot {
return CheckRoot()
}
return nil
},

RunE: func(cmd *cobra.Command, _ []string) error {
path, err := exec.LookPath("mount")
if err != nil {
return err
}
mounter := v1.NewMounter(path)

cfg, err := config.ReadConfigRun(viper.GetString("config-dir"), cmd.Flags(), mounter)
if err != nil {
cfg.Logger.Errorf("Error reading config: %s\n", err)
return elementalError.NewFromError(err, elementalError.ReadingRunConfig)
}

// Set this after parsing of the flags, so it fails on parsing and prints usage properly
cmd.SilenceUsage = true
cmd.SilenceErrors = true // Do not propagate errors down the line, we control them

spec, err := config.ReadUpgradeRecoverySpec(cfg, cmd.Flags())
if err != nil {
cfg.Logger.Errorf("Invalid upgrade-recovery command setup %v", err)
return elementalError.NewFromError(err, elementalError.ReadingSpecConfig)
}

cfg.Logger.Infof("Upgrade Recovery called")
upgrade, err := action.NewUpgradeRecoveryAction(cfg, spec, action.WithUpdateInstallState(true))
if err != nil {
cfg.Logger.Errorf("failed to initialize upgrade-recovery action: %v", err)
return err
}

return upgrade.Run()
},
}
root.AddCommand(c)
addRecoverySystemFlag(c)
return c
}

// register the subcommand into rootCmd
var _ = NewUpgradeRecoveryCmd(rootCmd, true)
126 changes: 104 additions & 22 deletions pkg/action/upgrade-recovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package action
import (
"fmt"
"path/filepath"
"time"

"github.com/rancher/elemental-toolkit/pkg/constants"
"github.com/rancher/elemental-toolkit/pkg/elemental"
Expand All @@ -27,14 +28,41 @@ import (
"github.com/rancher/elemental-toolkit/pkg/utils"
)

// UpgradeRecoveryAction represents the struct that will run the upgrade from start to finish
// UpgradeRecoveryAction represents the struct that will run the recovery upgrade from start to finish
type UpgradeRecoveryAction struct {
cfg *v1.RunConfig
spec *v1.UpgradeSpec
cfg *v1.RunConfig
spec *v1.UpgradeSpec
updateInstallState bool
}

func NewUpgradeRecoveryAction(config *v1.RunConfig, spec *v1.UpgradeSpec) *UpgradeRecoveryAction {
return &UpgradeRecoveryAction{cfg: config, spec: spec}
type UpgradeRecoveryActionOption func(r *UpgradeRecoveryAction) error

func WithUpdateInstallState(updateInstallState bool) func(u *UpgradeRecoveryAction) error {
return func(u *UpgradeRecoveryAction) error {
u.updateInstallState = updateInstallState
return nil
}
}

func NewUpgradeRecoveryAction(config *v1.RunConfig, spec *v1.UpgradeSpec, opts ...UpgradeRecoveryActionOption) (*UpgradeRecoveryAction, error) {
var err error

u := &UpgradeRecoveryAction{cfg: config, spec: spec}

for _, o := range opts {
err = o(u)
if err != nil {
config.Logger.Errorf("error applying config option: %s", err.Error())
return nil, err
}
}

if elemental.IsRecoveryMode(config.Config) {
config.Logger.Errorf("Upgrading recovery image from the recovery system itself is not supported")
return nil, fmt.Errorf("Not supported")
}

return u, nil
}

func (u UpgradeRecoveryAction) Info(s string, args ...interface{}) {
Expand All @@ -54,6 +82,14 @@ func (u *UpgradeRecoveryAction) mountRWPartitions(cleanup *utils.CleanStack) err
return fmt.Errorf("Can not upgrade recovery from recovery partition")
}

if u.updateInstallState {
umount, err := elemental.MountRWPartition(u.cfg.Config, u.spec.Partitions.State)
if err != nil {
return elementalError.NewFromError(err, elementalError.MountStatePartition)
}
cleanup.Push(umount)
}

umount, err := elemental.MountRWPartition(u.cfg.Config, u.spec.Partitions.Recovery)
if err != nil {
return elementalError.NewFromError(err, elementalError.MountRecoveryPartition)
Expand All @@ -63,6 +99,47 @@ func (u *UpgradeRecoveryAction) mountRWPartitions(cleanup *utils.CleanStack) err
return nil
}

func (u *UpgradeRecoveryAction) upgradeInstallStateYaml() error {
if u.spec.Partitions.Recovery == nil || u.spec.Partitions.State == nil {
return fmt.Errorf("undefined state or recovery partition")
}

// A nil State should never be the case.
// However if it happens we need to abort, we we can't recreate
// a correct install state when upgrading recovery only.
if u.spec.State == nil {
return fmt.Errorf("Could not load current install state")
}

u.spec.State.Date = time.Now().Format(time.RFC3339)

recoveryPart := u.spec.State.Partitions[constants.RecoveryPartName]
if recoveryPart == nil {
recoveryPart = &v1.PartitionState{
FSLabel: u.spec.Partitions.Recovery.FilesystemLabel,
RecoveryImage: &v1.SystemState{
FS: u.spec.RecoverySystem.FS,
Label: u.spec.RecoverySystem.Label,
Source: u.spec.RecoverySystem.Source,
Digest: u.spec.RecoverySystem.Source.GetDigest(),
},
}
u.spec.State.Partitions[constants.RecoveryPartName] = recoveryPart
}

// Hack to ensure we are not using / or /.snapshots mountpoints. Btrfs based deployments
// mount state partition into multiple locations
statePath := filepath.Join(u.spec.Partitions.State.MountPoint, constants.InstallStateFile)
if u.spec.Partitions.State.MountPoint == "/" || u.spec.Partitions.State.MountPoint == "/.snapshots" {
statePath = filepath.Join(constants.RunningStateDir, constants.InstallStateFile)
}

return u.cfg.WriteInstallState(
u.spec.State, statePath,
filepath.Join(u.spec.Partitions.Recovery.MountPoint, constants.InstallStateFile),
)
}

func (u *UpgradeRecoveryAction) Run() (err error) {
cleanup := utils.NewCleanStack()
defer func() {
Expand All @@ -76,27 +153,32 @@ func (u *UpgradeRecoveryAction) Run() (err error) {
}

// Upgrade recovery
if u.spec.RecoveryUpgrade {
err = elemental.DeployImage(u.cfg.Config, &u.spec.RecoverySystem)
if err != nil {
u.cfg.Logger.Error("failed deploying recovery image")
return elementalError.NewFromError(err, elementalError.DeployImage)
}
recoveryFile := filepath.Join(u.spec.Partitions.Recovery.MountPoint, constants.RecoveryImgFile)
transitionFile := filepath.Join(u.spec.Partitions.Recovery.MountPoint, constants.TransitionImgFile)
if ok, _ := utils.Exists(u.cfg.Fs, recoveryFile); ok {
err = u.cfg.Fs.Remove(recoveryFile)
if err != nil {
u.Error("failed removing old recovery image")
return err
}
}
err = u.cfg.Fs.Rename(transitionFile, recoveryFile)
err = elemental.DeployImage(u.cfg.Config, &u.spec.RecoverySystem)
if err != nil {
u.cfg.Logger.Error("failed deploying recovery image")
return elementalError.NewFromError(err, elementalError.DeployImage)
}
recoveryFile := filepath.Join(u.spec.Partitions.Recovery.MountPoint, constants.RecoveryImgFile)
transitionFile := filepath.Join(u.spec.Partitions.Recovery.MountPoint, constants.TransitionImgFile)
if ok, _ := utils.Exists(u.cfg.Fs, recoveryFile); ok {
err = u.cfg.Fs.Remove(recoveryFile)
if err != nil {
u.Error("failed renaming transition recovery image")
u.Error("failed removing old recovery image")
return err
}
}
err = u.cfg.Fs.Rename(transitionFile, recoveryFile)
if err != nil {
u.Error("failed renaming transition recovery image")
return err
}

// Update state.yaml file on recovery and state partitions
err = u.upgradeInstallStateYaml()
if err != nil {
u.Error("failed upgrading installation metadata")
return err
}

u.Info("Recovery upgrade completed")

Expand Down
34 changes: 18 additions & 16 deletions pkg/action/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,20 +264,18 @@ func (u *UpgradeAction) Run() (err error) {
cleanup.PushErrorOnly(func() error { return u.snapshotter.CloseTransactionOnError(u.snapshot) })

// Deploy system image
if !u.spec.RecoveryOnlyUpgrade {
err = elemental.DumpSource(u.cfg.Config, u.snapshot.WorkDir, u.spec.System)
if err != nil {
u.cfg.Logger.Errorf("failed deploying source: %s", u.spec.System.String())
return elementalError.NewFromError(err, elementalError.DumpSource)
}
err = elemental.DumpSource(u.cfg.Config, u.snapshot.WorkDir, u.spec.System)
if err != nil {
u.cfg.Logger.Errorf("failed deploying source: %s", u.spec.System.String())
return elementalError.NewFromError(err, elementalError.DumpSource)
}

// Fine tune the dumped tree
u.cfg.Logger.Info("Fine tune the dumped root tree")
err = u.refineDeployment()
if err != nil {
u.cfg.Logger.Error("failed refining system root tree")
return err
}
// Fine tune the dumped tree
u.cfg.Logger.Info("Fine tune the dumped root tree")
err = u.refineDeployment()
if err != nil {
u.cfg.Logger.Error("failed refining system root tree")
return err
}

// Manage legacy recovery. This logic should be removed once toolkit v1.1 gets unsupported.
Expand All @@ -301,7 +299,7 @@ func (u *UpgradeAction) Run() (err error) {
}

// Upgrade recovery
if u.spec.RecoveryUpgrade || u.spec.RecoveryOnlyUpgrade {
if u.spec.RecoveryUpgrade {
recoverySystem := &u.spec.RecoverySystem
u.cfg.Logger.Info("Deploying recovery system")
if recoverySystem.Source.String() == u.spec.System.String() {
Expand All @@ -312,9 +310,13 @@ func (u *UpgradeAction) Run() (err error) {
}
recoverySystem.Source.SetDigest(u.spec.System.GetDigest())
}
upgradeRecoveryAction := NewUpgradeRecoveryAction(u.cfg, u.spec)
upgradeRecoveryAction, err := NewUpgradeRecoveryAction(u.cfg, u.spec, WithUpdateInstallState(false))
if err != nil {
u.Error("Could not initialize Recovery upgrade: %s", err)
return elementalError.NewFromError(err, elementalError.UpgradeRecovery)
}
if err := upgradeRecoveryAction.Run(); err != nil {
u.Error("Upgrading Recovery: %s", err)
u.Error("Could not upgrade Recovery: %s", err)
return elementalError.NewFromError(err, elementalError.UpgradeRecovery)
}
}
Expand Down
13 changes: 13 additions & 0 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,9 +333,22 @@ func GetResetKeyEnvMap() map[string]string {
// GetUpgradeKeyEnvMap returns environment variable bindings to UpgradeSpec data
func GetUpgradeKeyEnvMap() map[string]string {
return map[string]string{
<<<<<<< HEAD

Check failure on line 336 in pkg/constants/constants.go

View workflow job for this annotation

GitHub Actions / build

expected operand, found '<<'
"recovery": "RECOVERY",
"system": "SYSTEM",
"recovery-system.uri": "RECOVERY_SYSTEM",
=======
"recovery": "RECOVERY",
"system": "SYSTEM",
"recovery-system": "RECOVERY_SYSTEM",
>>>>>>> 49451b499 (Implement to upgrade-recovery separate command)
}
}

// GetUpgradeRecoveryKeyEnvMap returns environment variable bindings to UpgradeSpec data
func GetUpgradeRecoveryKeyEnvMap() map[string]string {
return map[string]string{

Check failure on line 350 in pkg/constants/constants.go

View workflow job for this annotation

GitHub Actions / build

missing ',' in composite literal
"recovery-system": "RECOVERY_SYSTEM",
}

Check failure on line 352 in pkg/constants/constants.go

View workflow job for this annotation

GitHub Actions / build

missing ',' before newline in composite literal
}

Expand Down
Loading

0 comments on commit 3246717

Please sign in to comment.