diff --git a/cmd/build-iso.go b/cmd/build-iso.go index a4d632f7339..94166715674 100644 --- a/cmd/build-iso.go +++ b/cmd/build-iso.go @@ -125,7 +125,7 @@ func NewBuildISO(root *cobra.Command, addCheckRoot bool) *cobra.Command { } buildISO := action.NewBuildISOAction(cfg, spec) - return buildISO.ISORun() + return buildISO.Run() }, } diff --git a/pkg/action/build-disk.go b/pkg/action/build-disk.go index fec6f9c40e0..87fc8f57533 100644 --- a/pkg/action/build-disk.go +++ b/pkg/action/build-disk.go @@ -237,16 +237,9 @@ func (b *BuildDiskAction) BuildDiskRun() (err error) { //nolint:gocyclo return elementalError.NewFromError(err, elementalError.HookAfterDisk) } - // Create recovery image - bootDir := filepath.Join(b.roots[constants.RecoveryPartName], "boot") - if err = utils.MkdirAll(b.cfg.Fs, bootDir, constants.DirPerm); err != nil { - b.cfg.Logger.Errorf("failed creating recovery boot dir: %v", err) - return err - } - tmpSrc := b.spec.RecoverySystem.Source b.spec.RecoverySystem.Source = types.NewDirSrc(recRoot) - err = elemental.DeployRecoverySystem(b.cfg.Config, &b.spec.RecoverySystem, bootDir) + err = elemental.DeployRecoverySystem(b.cfg.Config, &b.spec.RecoverySystem) if err != nil { b.cfg.Logger.Errorf("failed deploying recovery system: %v", err) return err diff --git a/pkg/action/build-iso.go b/pkg/action/build-iso.go index d59ce2cceb0..cf800fc689d 100644 --- a/pkg/action/build-iso.go +++ b/pkg/action/build-iso.go @@ -41,7 +41,7 @@ func grubCfgTemplate(arch string) string { menuentry "%s" --class os --unrestricted { echo Loading kernel... - linux ($root)` + constants.ISOKernelPath(arch) + ` cdroot root=live:CDLABEL=%s rd.live.dir=/ rd.live.squashimg=rootfs.squashfs console=tty1 console=ttyS0 elemental.disable elemental.setup=` + constants.ISOCloudInitPath + ` + linux ($root)` + constants.ISOKernelPath(arch) + ` cdroot root=live:CDLABEL=%s rd.live.dir=` + constants.ISOLoaderPath(arch) + ` rd.live.squashimg=rootfs.squashfs console=tty1 console=ttyS0 elemental.disable elemental.setup=` + constants.ISOCloudInitPath + ` echo Loading initrd... initrd ($root)` + constants.ISOInitrdPath(arch) + ` } @@ -78,8 +78,8 @@ func NewBuildISOAction(cfg *types.BuildConfig, spec *types.LiveISO, opts ...Buil return b } -// BuildISORun will install the system from a given configuration -func (b *BuildISOAction) ISORun() error { +// Run will install the system from a given configuration +func (b *BuildISOAction) Run() error { cleanup := utils.NewCleanStack() var err error defer func() { err = cleanup.Cleanup(err) }() @@ -170,11 +170,11 @@ func (b *BuildISOAction) ISORun() error { image := &types.Image{ Source: types.NewDirSrc(rootDir), - File: filepath.Join(isoDir, constants.ISORootFile), + File: filepath.Join(bootDir, constants.ISORootFile), FS: constants.SquashFs, } - err = elemental.DeployRecoverySystem(b.cfg.Config, image, bootDir) + err = elemental.DeployRecoverySystem(b.cfg.Config, image) if err != nil { b.cfg.Logger.Errorf("Failed preparing ISO's root tree: %v", err) return err diff --git a/pkg/action/build_test.go b/pkg/action/build_test.go index 570c5c4dd5b..37c1fdf8a55 100644 --- a/pkg/action/build_test.go +++ b/pkg/action/build_test.go @@ -127,7 +127,7 @@ var _ = Describe("Build Actions", func() { } buildISO := action.NewBuildISOAction(cfg, iso, action.WithLiveBootloader(bootloader)) - err := buildISO.ISORun() + err := buildISO.Run() Expect(err).ShouldNot(HaveOccurred()) }) @@ -138,7 +138,7 @@ var _ = Describe("Build Actions", func() { iso.RootFS = append(iso.RootFS, rootSrc) buildISO := action.NewBuildISOAction(cfg, iso, action.WithLiveBootloader(bootloader)) - err := buildISO.ISORun() + err := buildISO.Run() Expect(err).Should(HaveOccurred()) }) It("Fails on prepare ISO", func() { @@ -148,7 +148,7 @@ var _ = Describe("Build Actions", func() { iso.RootFS = append(iso.RootFS, rootSrc) buildISO := action.NewBuildISOAction(cfg, iso, action.WithLiveBootloader(bootloader)) - err := buildISO.ISORun() + err := buildISO.Run() Expect(err).Should(HaveOccurred()) }) @@ -161,14 +161,14 @@ var _ = Describe("Build Actions", func() { By("fails without kernel") buildISO := action.NewBuildISOAction(cfg, iso, action.WithLiveBootloader(bootloader)) - err = buildISO.ISORun() + err = buildISO.Run() Expect(err).Should(HaveOccurred()) By("fails without initrd") _, err = fs.Create("/local/dir/boot/vmlinuz") Expect(err).ShouldNot(HaveOccurred()) buildISO = action.NewBuildISOAction(cfg, iso, action.WithLiveBootloader(bootloader)) - err = buildISO.ISORun() + err = buildISO.Run() Expect(err).Should(HaveOccurred()) }) It("Fails installing uefi sources", func() { @@ -178,7 +178,7 @@ var _ = Describe("Build Actions", func() { iso.UEFI = []*types.ImageSource{uefiSrc} buildISO := action.NewBuildISOAction(cfg, iso) - err := buildISO.ISORun() + err := buildISO.Run() Expect(err).Should(HaveOccurred()) }) It("Fails on ISO filesystem creation", func() { @@ -193,7 +193,7 @@ var _ = Describe("Build Actions", func() { } buildISO := action.NewBuildISOAction(cfg, iso, action.WithLiveBootloader(bootloader)) - err := buildISO.ISORun() + err := buildISO.Run() Expect(err).Should(HaveOccurred()) }) @@ -228,7 +228,7 @@ var _ = Describe("Build Actions", func() { Expect(buildDisk.BuildDiskRun()).To(Succeed()) Expect(runner.MatchMilestones([][]string{ - {"mksquashfs", "/tmp/test/build/recovery.img.root", "/tmp/test/build/recovery/recovery.img"}, + {"mksquashfs", "/tmp/test/build/recovery.img.root", "/tmp/test/build/recovery/boot/recovery.img"}, {"mkfs.ext4", "-L", "COS_STATE"}, {"losetup", "--show", "-f", "/tmp/test/build/state.part"}, {"mkfs.vfat", "-n", "COS_GRUB"}, @@ -255,7 +255,7 @@ var _ = Describe("Build Actions", func() { Expect(buildDisk.BuildDiskRun()).To(Succeed()) Expect(runner.MatchMilestones([][]string{ - {"mksquashfs", "/tmp/test/build/recovery.img.root", "/tmp/test/build/recovery/recovery.img"}, + {"mksquashfs", "/tmp/test/build/recovery.img.root", "/tmp/test/build/recovery/boot/recovery.img"}, {"mkfs.vfat", "-n", "COS_GRUB"}, {"mkfs.ext4", "-L", "COS_OEM"}, {"mkfs.ext4", "-L", "COS_RECOVERY"}, @@ -274,7 +274,7 @@ var _ = Describe("Build Actions", func() { Expect(buildDisk.BuildDiskRun()).NotTo(Succeed()) Expect(runner.MatchMilestones([][]string{ - {"mksquashfs", "/tmp/test/build/recovery.img.root", "/tmp/test/build/recovery/recovery.img"}, + {"mksquashfs", "/tmp/test/build/recovery.img.root", "/tmp/test/build/recovery/boot/recovery.img"}, })).To(Succeed()) // failed before preparing partitions images diff --git a/pkg/action/install.go b/pkg/action/install.go index 8bd96ae61bd..5d145d46485 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -249,7 +249,7 @@ func (i InstallAction) Run() (err error) { } recoverySystem.Source.SetDigest(i.spec.System.GetDigest()) } - err = elemental.DeployRecoverySystem(i.cfg.Config, &recoverySystem, recoveryBootDir) + err = elemental.DeployRecoverySystem(i.cfg.Config, &recoverySystem) if err != nil { i.cfg.Logger.Errorf("Failed deploying recovery image: %v", err) return elementalError.NewFromError(err, elementalError.DeployImage) diff --git a/pkg/action/upgrade-recovery.go b/pkg/action/upgrade-recovery.go index 5af0eb31149..b2435ab7ddc 100644 --- a/pkg/action/upgrade-recovery.go +++ b/pkg/action/upgrade-recovery.go @@ -84,18 +84,22 @@ func NewUpgradeRecoveryAction(config *types.RunConfig, spec *types.UpgradeSpec, return u, nil } -func (u UpgradeRecoveryAction) Info(s string, args ...interface{}) { +func (u UpgradeRecoveryAction) Infof(s string, args ...interface{}) { u.cfg.Logger.Infof(s, args...) } -func (u UpgradeRecoveryAction) Debug(s string, args ...interface{}) { +func (u UpgradeRecoveryAction) Debugf(s string, args ...interface{}) { u.cfg.Logger.Debugf(s, args...) } -func (u UpgradeRecoveryAction) Error(s string, args ...interface{}) { +func (u UpgradeRecoveryAction) Errorf(s string, args ...interface{}) { u.cfg.Logger.Errorf(s, args...) } +func (u UpgradeRecoveryAction) Warnf(s string, args ...interface{}) { + u.cfg.Logger.Warnf(s, args...) +} + func (u *UpgradeRecoveryAction) mountRWPartitions(cleanup *utils.CleanStack) error { umount, err := elemental.MountRWPartition(u.cfg.Config, u.spec.Partitions.Recovery) if err != nil { @@ -146,48 +150,79 @@ func (u *UpgradeRecoveryAction) Run() (err error) { return err } - // Create recovery /boot dir if not exists - bootDir := filepath.Join(u.spec.Partitions.Recovery.MountPoint, "boot") - if err := utils.MkdirAll(u.cfg.Fs, bootDir, constants.DirPerm); err != nil { - u.cfg.Logger.Errorf("failed creating recovery boot dir: %v", err) - return elementalError.NewFromError(err, elementalError.CreateDir) + // Remove any traces of previously errored upgrades + transitionDir := filepath.Join(u.spec.Partitions.Recovery.MountPoint, constants.BootTransitionDir) + u.Debugf("removing any orphaned recovery system %s", transitionDir) + err = utils.RemoveAll(u.cfg.Fs, transitionDir) + if err != nil { + u.Errorf("failed removing orphaned recovery image: %s", err.Error()) + return err } - // Upgrade recovery - err = elemental.DeployRecoverySystem(u.cfg.Config, &u.spec.RecoverySystem, bootDir) + // Deploy recovery system to transition dir + err = elemental.DeployRecoverySystem(u.cfg.Config, &u.spec.RecoverySystem) if err != nil { - u.cfg.Logger.Errorf("failed deploying recovery image: %v", err) + u.cfg.Logger.Errorf("failed deploying recovery image: %s", err.Error()) 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) + + // Switch places on /boot and transition-dir + bootDir := filepath.Join(u.spec.Partitions.Recovery.MountPoint, constants.BootDir) + oldBootDir := filepath.Join(u.spec.Partitions.Recovery.MountPoint, constants.OldBootDir) + + // If a previous upgrade failed, remove old boot-dir + err = utils.RemoveAll(u.cfg.Fs, oldBootDir) + if err != nil { + u.Errorf("failed removing orphaned recovery image: %s", err.Error()) + return err + } + + // Rename current boot-dir in case we need to use it again + if ok, _ := utils.Exists(u.cfg.Fs, bootDir); ok { + err = u.cfg.Fs.Rename(bootDir, oldBootDir) if err != nil { - u.Error("failed removing old recovery image") + u.Errorf("failed removing old recovery image: %s", err.Error()) return err } } - err = u.cfg.Fs.Rename(transitionFile, recoveryFile) + + // Move new boot-dir to /boot + err = u.cfg.Fs.Rename(transitionDir, bootDir) if err != nil { - u.Error("failed renaming transition recovery image") + u.cfg.Logger.Errorf("failed renaming transition recovery image: %s", err.Error()) + + // Try to salvage old recovery system + if ok, _ := utils.Exists(u.cfg.Fs, oldBootDir); ok { + err = u.cfg.Fs.Rename(oldBootDir, bootDir) + if err != nil { + u.cfg.Logger.Errorf("failed salvaging old recovery system: %s", err.Error()) + } + } + return err } + // Remove old boot-dir when new recovery system is in place + err = utils.RemoveAll(u.cfg.Fs, oldBootDir) + if err != nil { + u.Warnf("failed removing old recovery image: %s", err.Error()) + } + // Update state.yaml file on recovery and state partitions if u.updateInstallState { err = u.upgradeInstallStateYaml() if err != nil { - u.Error("failed upgrading installation metadata") + u.Errorf("failed upgrading installation metadata: %s", err.Error()) return err } } - u.Info("Recovery upgrade completed") + u.Infof("Recovery upgrade completed") // Do not reboot/poweroff on cleanup errors err = cleanup.Cleanup(err) if err != nil { + u.Errorf("failed cleanup: %s", err.Error()) return elementalError.NewFromError(err, elementalError.Cleanup) } diff --git a/pkg/action/upgrade-recovery_test.go b/pkg/action/upgrade-recovery_test.go index db0af1ad14d..c71a8769f4f 100644 --- a/pkg/action/upgrade-recovery_test.go +++ b/pkg/action/upgrade-recovery_test.go @@ -175,7 +175,7 @@ var _ = Describe("Upgrade Recovery Actions", func() { Expect(err).To(HaveOccurred()) }) It("Successfully upgrades recovery from docker image", Label("docker"), func() { - recoveryImgPath := filepath.Join(constants.LiveDir, constants.RecoveryImgFile) + recoveryImgPath := filepath.Join(constants.LiveDir, constants.BootDir, constants.RecoveryImgFile) spec := PrepareTestRecoveryImage(config, constants.LiveDir, fs, runner) // This should be the old image @@ -212,7 +212,7 @@ var _ = Describe("Upgrade Recovery Actions", func() { Expect(spec.State.Date).ToNot(BeEmpty(), "post-upgrade state should contain a date") }) It("Successfully skips updateInstallState", Label("docker"), func() { - recoveryImgPath := filepath.Join(constants.LiveDir, constants.RecoveryImgFile) + recoveryImgPath := filepath.Join(constants.LiveDir, constants.BootDir, constants.RecoveryImgFile) spec := PrepareTestRecoveryImage(config, constants.LiveDir, fs, runner) // This should be the old image @@ -253,7 +253,6 @@ var _ = Describe("Upgrade Recovery Actions", func() { }) func PrepareTestRecoveryImage(config *types.RunConfig, recoveryPath string, fs vfs.FS, runner *mocks.FakeRunner) *types.UpgradeSpec { - GinkgoHelper() // Create installState with squashed recovery statePath := filepath.Join(constants.RunningStateDir, constants.InstallStateFile) installState := &types.InstallState{ @@ -270,28 +269,30 @@ func PrepareTestRecoveryImage(config *types.RunConfig, recoveryPath string, fs v } Expect(config.WriteInstallState(installState, statePath, statePath)).ShouldNot(HaveOccurred()) - recoveryImgPath := filepath.Join(recoveryPath, constants.RecoveryImgFile) - Expect(fs.WriteFile(recoveryImgPath, []byte("recovery"), constants.FilePerm)).ShouldNot(HaveOccurred()) - - transitionDir := filepath.Join(recoveryPath, "transition.imgTree") - Expect(utils.MkdirAll(fs, filepath.Join(transitionDir, "lib/modules/6.6"), constants.DirPerm)).ShouldNot(HaveOccurred()) - bootDir := filepath.Join(transitionDir, "boot") - Expect(utils.MkdirAll(fs, bootDir, constants.DirPerm)).ShouldNot(HaveOccurred()) - Expect(fs.WriteFile(filepath.Join(bootDir, "vmlinuz-6.6"), []byte("kernel"), constants.FilePerm)).ShouldNot(HaveOccurred()) - Expect(fs.WriteFile(filepath.Join(bootDir, "elemental.initrd-6.6"), []byte("initrd"), constants.FilePerm)).ShouldNot(HaveOccurred()) + for _, rootDir := range []string{"/some/dir", recoveryPath} { + bootDir := filepath.Join(rootDir, "boot") + Expect(utils.MkdirAll(fs, bootDir, constants.DirPerm)).ShouldNot(HaveOccurred()) + recoveryImgPath := filepath.Join(bootDir, constants.RecoveryImgFile) + Expect(fs.WriteFile(recoveryImgPath, []byte("recovery"), constants.FilePerm)).ShouldNot(HaveOccurred()) + Expect(utils.MkdirAll(fs, filepath.Join(rootDir, "lib/modules/6.6"), constants.DirPerm)).ShouldNot(HaveOccurred()) + Expect(utils.MkdirAll(fs, bootDir, constants.DirPerm)).ShouldNot(HaveOccurred()) + Expect(fs.WriteFile(filepath.Join(bootDir, "vmlinuz-6.6"), []byte("kernel"), constants.FilePerm)).ShouldNot(HaveOccurred()) + Expect(fs.WriteFile(filepath.Join(bootDir, "elemental.initrd-6.6"), []byte("initrd"), constants.FilePerm)).ShouldNot(HaveOccurred()) + } spec, err := conf.NewUpgradeSpec(config.Config) Expect(err).ShouldNot(HaveOccurred()) spec.System = types.NewDockerSrc("alpine") spec.RecoveryUpgrade = true - spec.RecoverySystem.Source = spec.System + spec.RecoverySystem.Source = types.NewDirSrc("/some/dir") spec.RecoverySystem.Size = 16 runner.SideEffect = func(command string, args ...string) ([]byte, error) { if command == "mksquashfs" && args[1] == spec.RecoverySystem.File { // create the transition img for squash to fake it - _, _ = fs.Create(spec.RecoverySystem.File) + _, err = fs.Create(spec.RecoverySystem.File) + Expect(err).To(Succeed()) } return []byte{}, nil } diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index b7fb2c97a22..9cdb7ab80f3 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -294,14 +294,12 @@ var _ = Describe("Runtime Actions", func() { Expect(runner.IncludesCmds([][]string{{"poweroff", "-f"}})).To(BeNil()) }) It("Successfully upgrades recovery from docker image", Label("docker"), func() { - recoveryImgPath := filepath.Join(constants.LiveDir, constants.RecoveryImgFile) + recoveryImgPath := filepath.Join(constants.LiveDir, constants.BootDir, constants.RecoveryImgFile) spec := PrepareTestRecoveryImage(config, constants.LiveDir, fs, runner) // This should be the old image info, err := fs.Stat(recoveryImgPath) Expect(err).ToNot(HaveOccurred()) - // Image size should be empty - Expect(info.Size()).To(BeNumerically(">", 0)) Expect(info.IsDir()).To(BeFalse()) f, _ := fs.ReadFile(recoveryImgPath) Expect(f).To(ContainSubstring("recovery")) @@ -314,11 +312,9 @@ var _ = Describe("Runtime Actions", func() { // This should be the new image info, err = fs.Stat(recoveryImgPath) Expect(err).ToNot(HaveOccurred()) - // Image size should be empty - Expect(info.Size()).To(BeNumerically("==", 0)) Expect(info.IsDir()).To(BeFalse()) f, _ = fs.ReadFile(recoveryImgPath) - Expect(f).ToNot(ContainSubstring("recovery")) + Expect(f).To(BeEmpty()) // Transition squash should not exist info, err = fs.Stat(spec.RecoverySystem.File) diff --git a/pkg/config/config.go b/pkg/config/config.go index 0ace63914eb..6e26012199c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -194,7 +194,7 @@ func NewInstallSpec(cfg types.Config) *types.InstallSpec { recoverySystem.Source = system recoverySystem.FS = constants.SquashFs - recoverySystem.File = filepath.Join(constants.RecoveryDir, constants.RecoveryImgFile) + recoverySystem.File = filepath.Join(constants.RecoveryDir, constants.BootDir, constants.RecoveryImgFile) recoverySystem.MountPoint = constants.TransitionDir return &types.InstallSpec{ @@ -334,7 +334,7 @@ func NewUpgradeSpec(cfg types.Config) (*types.UpgradeSpec, error) { } recovery = types.Image{ - File: filepath.Join(ep.Recovery.MountPoint, constants.TransitionImgFile), + File: filepath.Join(ep.Recovery.MountPoint, constants.BootTransitionDir, constants.RecoveryImgFile), Size: constants.ImgSize, Label: rState.Label, FS: rState.FS, @@ -440,10 +440,13 @@ func NewResetSpec(cfg types.Config) (*types.ResetSpec, error) { cfg.Logger.Warnf("no Persistent partition found") } - recoveryImg := filepath.Join(constants.RunningStateDir, constants.RecoveryImgFile) + recoveryImg := filepath.Join(constants.RunningStateDir, constants.BootDir, constants.RecoveryImgFile) + oldRecoveryImg := filepath.Join(constants.RunningStateDir, constants.RecoveryImgFile) if exists, _ := utils.Exists(cfg.Fs, recoveryImg); exists { imgSource = types.NewFileSrc(recoveryImg) + } else if exists, _ := utils.Exists(cfg.Fs, oldRecoveryImg); exists { + imgSource = types.NewFileSrc(oldRecoveryImg) } else { imgSource = types.NewEmptySrc() } @@ -514,7 +517,7 @@ func NewDisk(cfg *types.BuildConfig) *types.DiskSpec { workdir = filepath.Join(cfg.OutDir, constants.DiskWorkDir) recoveryImg.Size = constants.ImgSize - recoveryImg.File = filepath.Join(workdir, constants.RecoveryPartName, constants.RecoveryImgFile) + recoveryImg.File = filepath.Join(workdir, constants.RecoveryPartName, constants.BootDir, constants.RecoveryImgFile) recoveryImg.FS = constants.SquashFs recoveryImg.Source = types.NewEmptySrc() recoveryImg.MountPoint = filepath.Join( diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 9b2faa4d54e..0a6cc28894a 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -119,7 +119,9 @@ const ( PassiveImgName = "passive" RecoveryImgName = "recovery" RecoveryImgFile = "recovery.img" - TransitionImgFile = "transition.img" + BootTransitionDir = "boot-transition" + BootDir = "boot" + OldBootDir = "boot-old" // Yip stages evaluated on reset/upgrade/install/build-disk actions AfterInstallChrootHook = "after-install-chroot" @@ -362,7 +364,7 @@ func GetDiskKeyEnvMap() map[string]string { return map[string]string{} } -// GetBootPath returns path use to store the boot files +// ISOLoaderPath returns path use to store the boot files func ISOLoaderPath(arch string) string { return filepath.Join("/boot", arch, "loader") } diff --git a/pkg/elemental/elemental.go b/pkg/elemental/elemental.go index dcf28872a65..1974091a684 100644 --- a/pkg/elemental/elemental.go +++ b/pkg/elemental/elemental.go @@ -471,15 +471,21 @@ func DeployImage(c types.Config, img *types.Image) error { return nil } -// DeployRecoverySystem deploys kernel, initrd and image to the specified -// paths. +// DeployRecoverySystem deploys the rootfs image from the img parameter and +// extracts kernel+initrd to the same directory. // This can be used for both ISO (all artifacts in same output dir) and raw // disks (kernel and initrd in ESP, rootfs squashfs image in recovery // partition. -func DeployRecoverySystem(cfg types.Config, img *types.Image, bootDir string) error { +func DeployRecoverySystem(cfg types.Config, img *types.Image) error { var err error var cleaner func() error + outputDir := filepath.Dir(img.File) + if err = utils.MkdirAll(cfg.Fs, outputDir, cnst.DirPerm); err != nil { + cfg.Logger.Errorf("Error creating output directory '%s': %s", outputDir, err.Error()) + return err + } + cfg.Logger.Infof("Deploying recovery image: %s", img.File) transientTree := strings.TrimSuffix(img.File, filepath.Ext(img.File)) + ".imgTree" if img.Source.IsDir() { @@ -527,15 +533,7 @@ func DeployRecoverySystem(cfg types.Config, img *types.Image, bootDir string) er continue } - target := filepath.Join(bootDir, filepath.Base(file)) - if exist, _ := utils.Exists(cfg.Fs, target); exist { - cfg.Logger.Debugf("Removing old file %s", target) - err = cfg.Fs.Remove(target) - if err != nil { - return err - } - } - + target := filepath.Join(outputDir, filepath.Base(file)) cfg.Logger.Debugf("Copying file %s to root tree", file) err = utils.CopyFile(cfg.Fs, file, target) if err != nil { @@ -555,13 +553,10 @@ func DeployRecoverySystem(cfg types.Config, img *types.Image, bootDir string) er continue } - source := filepath.Join(bootDir, name) + source := filepath.Join(outputDir, name) if exist, _ := utils.Exists(cfg.Fs, source, true); exist { - cfg.Logger.Debugf("Removing old symlink %s", source) - err = cfg.Fs.Remove(source) - if err != nil { - return err - } + cfg.Logger.Debugf("File already exists, skipping: %s", source) + continue } cfg.Logger.Debugf("Creating boot symlink from %s to %s", source, target) diff --git a/pkg/elemental/elemental_test.go b/pkg/elemental/elemental_test.go index 4d42963d13c..68aedaa5e5e 100644 --- a/pkg/elemental/elemental_test.go +++ b/pkg/elemental/elemental_test.go @@ -1031,8 +1031,6 @@ var _ = Describe("Elemental", Label("elemental"), func() { Describe("DeployRecoverySystem", Label("recovery"), func() { BeforeEach(func() { extractor.SideEffect = func(_, destination, platform string, _ bool) (string, error) { - Expect(destination).To(Equal("/recovery/recovery.imgTree")) - bootDir := filepath.Join(destination, "boot") logger.Debugf("Creating %s", bootDir) err := utils.MkdirAll(fs, bootDir, constants.DirPerm) @@ -1059,11 +1057,11 @@ var _ = Describe("Elemental", Label("elemental"), func() { Expect(fs.Mkdir("/recovery/boot", constants.DirPerm)).To(Succeed()) img := &types.Image{ - File: filepath.Join("/recovery", constants.RecoveryImgFile), + File: filepath.Join("/recovery", constants.BootDir, constants.RecoveryImgFile), Source: types.NewDockerSrc("elemental:latest"), FS: constants.SquashFs, } - err := elemental.DeployRecoverySystem(*config, img, "/recovery/boot") + err := elemental.DeployRecoverySystem(*config, img) Expect(err).ShouldNot(HaveOccurred()) info, err := fs.Stat("/recovery/boot/vmlinuz") diff --git a/pkg/features/embedded/grub-config/etc/elemental/grub.cfg b/pkg/features/embedded/grub-config/etc/elemental/grub.cfg index 41cf405a4f4..060a5def4ca 100644 --- a/pkg/features/embedded/grub-config/etc/elemental/grub.cfg +++ b/pkg/features/embedded/grub-config/etc/elemental/grub.cfg @@ -111,7 +111,7 @@ menuentry "${display_name} recovery" --id recovery { search --no-floppy --label --set=root ${recovery_label} # Check the presence of the image and fallback to legacy path if not present - set img=/recovery.img + set img=/boot/recovery.img if [ -f "${img}" ]; then source (${root})/boot/bootargs.cfg linux (${root})${kernel} ${kernelcmd} ${extra_cmdline} ${extra_recovery_cmdline} diff --git a/pkg/utils/fs.go b/pkg/utils/fs.go index fb5c3567dfb..aed4215d883 100644 --- a/pkg/utils/fs.go +++ b/pkg/utils/fs.go @@ -92,6 +92,17 @@ func Exists(fs types.FS, path string, noFollow ...bool) (bool, error) { return false, err } +// RemoveAll removes the specified path. +// It silently drop NotExists errors. +func RemoveAll(fs types.FS, path string) error { + err := fs.RemoveAll(path) + if !os.IsNotExist(err) { + return err + } + + return nil +} + // IsDir check if the path is a dir func IsDir(fs types.FS, path string) (bool, error) { fi, err := fs.Stat(path)