From b2bb97e510eb2835fc0890620148f94b56cdf1cb Mon Sep 17 00:00:00 2001 From: David Cassany Viladomat Date: Wed, 25 Sep 2024 09:47:12 +0200 Subject: [PATCH] Add cloud-init paths of the new root in 'after-*' hooks (#2192) * Add cloud-init paths of the new root in 'after-*' hooks This commit enables to run the non chrooted 'after-*' hooks included in the newly deployed image root. This specially applies to the install, reset, upgrade and build-disk commands. Moreover, 'after-disk' command now includes static reference paths to the new root and working directory, so that those can be used within the hooks regardless of the choosen output directory. * Include arm-firwmare feature This commit introduces an arm-firmware feature adding the required after-* hooks to ensure the RPi firmware is copied to the EFI partition. It could be, eventually, extended to support other boards and it does not harm systems which are not including RPi firmware. * Allow features to be passed as arguments Signed-off-by: David Cassany (cherry picked from commit 30a64d70be3ac8013830318334ad42b031eba7af) --- cmd/init.go | 21 +++++++++--- examples/green-rpi/01_rpi-firmware.yaml | 7 ---- examples/green-rpi/Dockerfile | 17 ++++++---- pkg/action/build-disk.go | 34 ++++++++++++++++++- pkg/action/build_test.go | 2 ++ pkg/action/install.go | 9 ++++- pkg/action/reset.go | 12 +++++-- pkg/action/upgrade-recovery.go | 5 ++- pkg/action/upgrade-recovery_test.go | 3 +- pkg/action/upgrade.go | 18 ++++++---- pkg/action/upgrade_test.go | 3 +- pkg/constants/constants.go | 28 ++++++++------- .../system/oem/00_armfirmware.yaml | 20 +++++++++++ pkg/features/features.go | 4 +++ pkg/utils/common.go | 9 +++++ 15 files changed, 142 insertions(+), 50 deletions(-) delete mode 100644 examples/green-rpi/01_rpi-firmware.yaml create mode 100644 pkg/features/embedded/arm-firmware/system/oem/00_armfirmware.yaml diff --git a/cmd/init.go b/cmd/init.go index d77788cb88b..adef813d48f 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -34,10 +34,18 @@ func InitCmd(root *cobra.Command) *cobra.Command { Use: "init FEATURES", Short: "Initialize container image for booting", Long: "Init a container image with elemental configuration\n\n" + - "FEATURES - should be provided as a comma-separated list of features to install.\n" + - " Available features: " + strings.Join(features.All, ",") + "\n" + - " Defaults to " + strings.Join(features.Default, ","), - Args: cobra.MaximumNArgs(1), + "FEATURES - provided as an argument list of features to install.\n" + + " Available features:\n\t" + strings.Join(features.All, "\n\t") + "\n\n" + + " Defaults to:\n\t" + strings.Join(features.Default, "\n\t"), + ValidArgs: features.All, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 1 { + // This is logic is just to keep backward compatibility with + // comma separated values + return cobra.OnlyValidArgs(cmd, strings.Split(args[0], ",")) + } + return cobra.OnlyValidArgs(cmd, args) + }, RunE: func(cmd *cobra.Command, args []string) error { cfg, err := config.ReadConfigRun(viper.GetString("config-dir"), cmd.Flags(), types.NewDummyMounter()) if err != nil { @@ -54,8 +62,11 @@ func InitCmd(root *cobra.Command) *cobra.Command { if len(args) == 0 { spec.Features = features.Default - } else { + } else if len(args) == 1 { + // The old behavior is kept to keep backward compatibiliy spec.Features = strings.Split(args[0], ",") + } else { + spec.Features = args } cfg.Logger.Infof("Initializing system...") diff --git a/examples/green-rpi/01_rpi-firmware.yaml b/examples/green-rpi/01_rpi-firmware.yaml deleted file mode 100644 index 7c05dae0f0b..00000000000 --- a/examples/green-rpi/01_rpi-firmware.yaml +++ /dev/null @@ -1,7 +0,0 @@ -name: "Raspberry Pi post disk hook" -stages: - after-disk: - - ©firmware - name: "Copy firmware to EFI partition" - commands: - - cp -r /build/build/recovery.img.root/boot/vc/* /build/build/efi/ diff --git a/examples/green-rpi/Dockerfile b/examples/green-rpi/Dockerfile index 0aa4d16fcf7..4960d9ee1f7 100644 --- a/examples/green-rpi/Dockerfile +++ b/examples/green-rpi/Dockerfile @@ -55,12 +55,17 @@ COPY --from=TOOLKIT /usr/bin/elemental /usr/bin/elemental RUN systemctl enable NetworkManager.service # Generate initrd with required elemental services -RUN elemental init -f && \ - kernel=$(ls /boot/Image-* | head -n1) && \ - if [ -e "$kernel" ]; then ln -sf "${kernel#/boot/}" /boot/vmlinuz; fi && \ - rm -rf /var/log/update* && \ - >/var/log/lastlog && \ - rm -rf /boot/vmlinux* +RUN elemental --debug init --force \ + elemental-rootfs \ + elemental-sysroot \ + grub-config \ + grub-default-bootargs \ + elemental-setup \ + dracut-config \ + cloud-config-defaults \ + cloud-config-essentials \ + boot-assessment \ + arm-firmware # Update os-release file with some metadata RUN echo IMAGE_REPO=\"${REPO}\" >> /etc/os-release && \ diff --git a/pkg/action/build-disk.go b/pkg/action/build-disk.go index f50da912b70..5e39019b352 100644 --- a/pkg/action/build-disk.go +++ b/pkg/action/build-disk.go @@ -101,10 +101,37 @@ func WithDiskBootloader(bootloader types.Bootloader) BuildDiskActionOption { } } +func (b *BuildDiskAction) createHookSymlinks(root string) error { + err := b.cfg.Fs.Symlink(root, constants.RunElementalBuildLink) + if err != nil { + return err + } + return b.cfg.Fs.Symlink(filepath.Base(b.spec.RecoverySystem.File)+rootSuffix, constants.WorkingImgBuildLink) +} + func (b *BuildDiskAction) buildDiskHook(hook string) error { return Hook(&b.cfg.Config, hook, b.cfg.Strict, b.cfg.CloudInitPaths...) } +// buildAfterDiskHook runs the 'after-disk' hook adding the to the cloud-init path +// the configured init paths rooted to the just deployed root. Moreover it also +// creates a symlink to the build-disk working directory to ensure deployed root +// can be found in an static path, so it can be referenced in after-disk hooks +func (b *BuildDiskAction) buildAfterDiskHook(root string) error { + cIPaths := b.cfg.CloudInitPaths + cIPaths = append(cIPaths, utils.PreAppendRoot(constants.WorkingImgBuildLink, b.cfg.CloudInitPaths...)...) + err := b.createHookSymlinks(root) + if err != nil { + return err + } + defer func() { + _ = b.cfg.Fs.Remove(constants.WorkingImgBuildLink) + _ = b.cfg.Fs.Remove(constants.RunElementalBuildLink) + }() + + return Hook(&b.cfg.Config, constants.AfterDiskHook, b.cfg.Strict, cIPaths...) +} + func (b *BuildDiskAction) buildDiskChrootHook(hook string, root string) error { return ChrootHook(&b.cfg.Config, hook, b.cfg.Strict, root, nil, b.cfg.CloudInitPaths...) } @@ -148,6 +175,11 @@ func (b *BuildDiskAction) BuildDiskRun() (err error) { //nolint:gocyclo } rawImg = filepath.Join(b.cfg.OutDir, rawImg) + err = utils.MkdirAll(b.cfg.Fs, workdir, constants.DirPerm) + if err != nil { + return err + } + // Before disk hook happens before doing anything err = b.buildDiskHook(constants.BeforeDiskHook) if err != nil { @@ -226,7 +258,7 @@ func (b *BuildDiskAction) BuildDiskRun() (err error) { //nolint:gocyclo return elementalError.NewFromError(err, elementalError.HookAfterDiskChroot) } } - err = b.buildDiskHook(constants.AfterDiskHook) + err = b.buildAfterDiskHook(workdir) if err != nil { return elementalError.NewFromError(err, elementalError.HookAfterDisk) } diff --git a/pkg/action/build_test.go b/pkg/action/build_test.go index 16362c0bcf7..4c894dd72a9 100644 --- a/pkg/action/build_test.go +++ b/pkg/action/build_test.go @@ -76,6 +76,8 @@ var _ = Describe("Build Actions", func() { config.WithImageExtractor(extractor), config.WithPlatform("linux/amd64"), ) + // build-disk will create `/run/elemental-build` and assumes /run to exist + Expect(utils.MkdirAll(fs, "/run", constants.DirPerm)).To(Succeed()) }) AfterEach(func() { diff --git a/pkg/action/install.go b/pkg/action/install.go index ee6395fe11b..dc5e85e41fe 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -81,8 +81,15 @@ func NewInstallAction(cfg *types.RunConfig, spec *types.InstallSpec, opts ...Ins return i, err } +// installHook runs the given hook without chroot. Moreover if the hook is 'after-install' +// it appends defiled cloud init paths rooted to the deployed root. This way any +// 'after-install' hook provided by the deployed system image is also taken into account. func (i *InstallAction) installHook(hook string) error { - return Hook(&i.cfg.Config, hook, i.cfg.Strict, i.cfg.CloudInitPaths...) + cIPaths := i.cfg.CloudInitPaths + if hook == cnst.AfterInstallHook { + cIPaths = append(cIPaths, utils.PreAppendRoot(cnst.WorkingImgDir, i.cfg.CloudInitPaths...)...) + } + return Hook(&i.cfg.Config, hook, i.cfg.Strict, cIPaths...) } func (i *InstallAction) installChrootHook(hook string, root string) error { diff --git a/pkg/action/reset.go b/pkg/action/reset.go index db5840f5e7f..9c58f20c50b 100644 --- a/pkg/action/reset.go +++ b/pkg/action/reset.go @@ -24,7 +24,6 @@ import ( "github.com/rancher/elemental-toolkit/v2/pkg/bootloader" "github.com/rancher/elemental-toolkit/v2/pkg/constants" - cnst "github.com/rancher/elemental-toolkit/v2/pkg/constants" "github.com/rancher/elemental-toolkit/v2/pkg/elemental" elementalError "github.com/rancher/elemental-toolkit/v2/pkg/error" "github.com/rancher/elemental-toolkit/v2/pkg/snapshotter" @@ -32,8 +31,15 @@ import ( "github.com/rancher/elemental-toolkit/v2/pkg/utils" ) +// resetHook runs the given hook without chroot. Moreover if the hook is 'after-reset' +// it appends defined cloud init paths rooted to the deployed root. This way any +// 'after-reset' hook provided by the deployed system image is also taken into account. func (r *ResetAction) resetHook(hook string) error { - return Hook(&r.cfg.Config, hook, r.cfg.Strict, r.cfg.CloudInitPaths...) + cIPaths := r.cfg.CloudInitPaths + if hook == constants.AfterResetHook { + cIPaths = append(cIPaths, utils.PreAppendRoot(constants.WorkingImgDir, r.cfg.CloudInitPaths...)...) + } + return Hook(&r.cfg.Config, hook, r.cfg.Strict, cIPaths...) } func (r *ResetAction) resetChrootHook(hook string, root string) error { @@ -144,7 +150,7 @@ func (r *ResetAction) updateInstallState(cleanup *utils.CleanStack) error { Active: true, Labels: r.spec.SnapshotLabels, Date: date, - FromAction: cnst.ActionReset, + FromAction: constants.ActionReset, }, }, }, diff --git a/pkg/action/upgrade-recovery.go b/pkg/action/upgrade-recovery.go index a71ee04e7d9..07137486544 100644 --- a/pkg/action/upgrade-recovery.go +++ b/pkg/action/upgrade-recovery.go @@ -23,7 +23,6 @@ import ( "time" "github.com/rancher/elemental-toolkit/v2/pkg/constants" - cnst "github.com/rancher/elemental-toolkit/v2/pkg/constants" "github.com/rancher/elemental-toolkit/v2/pkg/elemental" elementalError "github.com/rancher/elemental-toolkit/v2/pkg/error" "github.com/rancher/elemental-toolkit/v2/pkg/types" @@ -125,14 +124,14 @@ func (u *UpgradeRecoveryAction) upgradeInstallStateYaml() error { Digest: u.spec.RecoverySystem.Source.GetDigest(), Labels: u.spec.SnapshotLabels, Date: u.spec.State.Date, - FromAction: cnst.ActionUpgradeRecovery, + FromAction: constants.ActionUpgradeRecovery, }, } u.spec.State.Partitions[constants.RecoveryPartName] = recoveryPart } else if recoveryPart.RecoveryImage != nil { recoveryPart.RecoveryImage.Date = u.spec.State.Date recoveryPart.RecoveryImage.Labels = u.spec.SnapshotLabels - recoveryPart.RecoveryImage.FromAction = cnst.ActionUpgradeRecovery + recoveryPart.RecoveryImage.FromAction = constants.ActionUpgradeRecovery } // State partition is mounted in three different locations. diff --git a/pkg/action/upgrade-recovery_test.go b/pkg/action/upgrade-recovery_test.go index f631f8f705d..13325fa6e1d 100644 --- a/pkg/action/upgrade-recovery_test.go +++ b/pkg/action/upgrade-recovery_test.go @@ -31,7 +31,6 @@ import ( "github.com/rancher/elemental-toolkit/v2/pkg/action" conf "github.com/rancher/elemental-toolkit/v2/pkg/config" "github.com/rancher/elemental-toolkit/v2/pkg/constants" - cnst "github.com/rancher/elemental-toolkit/v2/pkg/constants" "github.com/rancher/elemental-toolkit/v2/pkg/mocks" "github.com/rancher/elemental-toolkit/v2/pkg/types" "github.com/rancher/elemental-toolkit/v2/pkg/utils" @@ -212,7 +211,7 @@ var _ = Describe("Upgrade Recovery Actions", func() { // Just a small test to ensure we touched the state file Expect(spec.State.Date).ToNot(BeEmpty(), "post-upgrade state should contain a date") Expect(spec.State.Date).To(Equal(spec.State.Partitions["recovery"].RecoveryImage.Date)) - Expect(spec.State.Partitions["recovery"].RecoveryImage.FromAction).To(Equal(cnst.ActionUpgradeRecovery)) + Expect(spec.State.Partitions["recovery"].RecoveryImage.FromAction).To(Equal(constants.ActionUpgradeRecovery)) Expect(spec.State.Partitions["recovery"].RecoveryImage.Labels["foo"]).To(Equal("bar")) }) It("Successfully skips updateInstallState", Label("docker"), func() { diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 97451232377..cb75bde8c13 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -24,7 +24,6 @@ import ( "github.com/rancher/elemental-toolkit/v2/pkg/bootloader" "github.com/rancher/elemental-toolkit/v2/pkg/constants" - cnst "github.com/rancher/elemental-toolkit/v2/pkg/constants" "github.com/rancher/elemental-toolkit/v2/pkg/elemental" elementalError "github.com/rancher/elemental-toolkit/v2/pkg/error" "github.com/rancher/elemental-toolkit/v2/pkg/snapshotter" @@ -101,13 +100,18 @@ func (u UpgradeAction) Error(s string, args ...interface{}) { u.cfg.Logger.Errorf(s, args...) } +// upgradeHook runs the given hook without chroot. Moreover if the hook is 'after-upgrade' +// it appends defined cloud init paths rooted to the deployed root. This way any +// 'after-upgrade' hook provided by the deployed system image is also taken into account. func (u UpgradeAction) upgradeHook(hook string) error { - u.Info("Applying '%s' hook", hook) - return Hook(&u.cfg.Config, hook, u.cfg.Strict, u.cfg.CloudInitPaths...) + cIPaths := u.cfg.CloudInitPaths + if hook == constants.AfterUpgradeHook { + cIPaths = append(cIPaths, utils.PreAppendRoot(constants.WorkingImgDir, u.cfg.CloudInitPaths...)...) + } + return Hook(&u.cfg.Config, hook, u.cfg.Strict, cIPaths...) } func (u UpgradeAction) upgradeChrootHook(hook string, root string) error { - u.Info("Applying '%s' hook", hook) mountPoints := map[string]string{} oemDevice := u.spec.Partitions.OEM @@ -178,7 +182,7 @@ func (u *UpgradeAction) upgradeInstallStateYaml() error { Active: true, Labels: u.spec.SnapshotLabels, Date: u.spec.State.Date, - FromAction: cnst.ActionUpgrade, + FromAction: constants.ActionUpgrade, } if statePart.Snapshots[oldActiveID] != nil { @@ -203,14 +207,14 @@ func (u *UpgradeAction) upgradeInstallStateYaml() error { Digest: u.spec.RecoverySystem.Source.GetDigest(), Labels: u.spec.SnapshotLabels, Date: u.spec.State.Date, - FromAction: cnst.ActionUpgrade, + FromAction: constants.ActionUpgrade, }, } u.spec.State.Partitions[constants.RecoveryPartName] = recoveryPart } else if recoveryPart.RecoveryImage != nil { recoveryPart.RecoveryImage.Date = u.spec.State.Date recoveryPart.RecoveryImage.Labels = u.spec.SnapshotLabels - recoveryPart.RecoveryImage.FromAction = cnst.ActionUpgrade + recoveryPart.RecoveryImage.FromAction = constants.ActionUpgrade } } diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index 767a4fdb701..191d271251a 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -31,7 +31,6 @@ import ( "github.com/rancher/elemental-toolkit/v2/pkg/action" conf "github.com/rancher/elemental-toolkit/v2/pkg/config" "github.com/rancher/elemental-toolkit/v2/pkg/constants" - cnst "github.com/rancher/elemental-toolkit/v2/pkg/constants" "github.com/rancher/elemental-toolkit/v2/pkg/mocks" "github.com/rancher/elemental-toolkit/v2/pkg/types" "github.com/rancher/elemental-toolkit/v2/pkg/utils" @@ -246,7 +245,7 @@ var _ = Describe("Runtime Actions", func() { Expect(state.Partitions[constants.StatePartName].Snapshots[3].Active). To(BeTrue()) Expect(state.Partitions[constants.StatePartName].Snapshots[3].FromAction). - To(Equal(cnst.ActionUpgrade)) + To(Equal(constants.ActionUpgrade)) Expect(state.Partitions[constants.StatePartName].Snapshots[3].Date). To(Equal(state.Date)) Expect(state.Partitions[constants.StatePartName].Snapshots[3].Labels["foo"]). diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index f43374be245..8d601d1b54b 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -91,19 +91,21 @@ const ( GrubPassiveSnapshots = "passive_snaps" ElementalBootloaderBin = "/usr/lib/elemental/bootloader" - // Mountpoints of images and partitions - RunElementalDir = "/run/elemental" - RecoveryDir = "/run/elemental/recovery" - StateDir = "/run/elemental/state" - OEMDir = "/run/elemental/oem" - PersistentDir = "/run/elemental/persistent" - TransitionDir = "/run/elemental/transition" - BootDir = "/run/elemental/efi" - ImgSrcDir = "/run/elemental/imgsrc" - WorkingImgDir = "/run/elemental/workingtree" - OverlayDir = "/run/elemental/overlay" - PersistentStateDir = ".state" - RunningStateDir = "/run/initramfs/elemental-state" // TODO: converge this constant with StateDir/RecoveryDir when moving to elemental-rootfs as default rootfs feature. + // Mountpoints or links to images and partitions + RunElementalBuildLink = "/run/elemental-build" + RunElementalDir = "/run/elemental" + RecoveryDir = "/run/elemental/recovery" + StateDir = "/run/elemental/state" + OEMDir = "/run/elemental/oem" + PersistentDir = "/run/elemental/persistent" + TransitionDir = "/run/elemental/transition" + BootDir = "/run/elemental/efi" + ImgSrcDir = "/run/elemental/imgsrc" + WorkingImgDir = "/run/elemental/workingtree" + WorkingImgBuildLink = RunElementalBuildLink + "/workingtree" + OverlayDir = "/run/elemental/overlay" + PersistentStateDir = ".state" + RunningStateDir = "/run/initramfs/elemental-state" // TODO: converge this constant with StateDir/RecoveryDir when moving to elemental-rootfs as default rootfs feature. // Running mode sentinel files ActiveMode = "/run/elemental/active_mode" diff --git a/pkg/features/embedded/arm-firmware/system/oem/00_armfirmware.yaml b/pkg/features/embedded/arm-firmware/system/oem/00_armfirmware.yaml new file mode 100644 index 00000000000..419b35a64de --- /dev/null +++ b/pkg/features/embedded/arm-firmware/system/oem/00_armfirmware.yaml @@ -0,0 +1,20 @@ +name: "Set ARM Firmware" +stages: + after-install-chroot: + - &pifirmware + name: Raspberry PI post hook + if: '[ -d "/boot/vc" ]' + commands: + - cp -rf /boot/vc/* /run/elemental/efi/ + + after-upgrade-chroot: + - <<: *pifirmware + + after-reset-chroot: + - <<: *pifirmware + + after-disk: + - name: Raspberry PI post hook + if: '[ -d "/run/elemental-build/workingtree/boot/vc" ]' + commands: + - cp -rf /run/elemental-build/workingtree/boot/vc/* /run/elemental-build/efi/ \ No newline at end of file diff --git a/pkg/features/features.go b/pkg/features/features.go index ffebca0251a..4afc1c44fea 100644 --- a/pkg/features/features.go +++ b/pkg/features/features.go @@ -54,6 +54,7 @@ const ( FeatureCloudConfigEssentials = "cloud-config-essentials" FeatureBootAssessment = "boot-assessment" FeatureAutologin = "autologin" + FeatureArmFirmware = "arm-firmware" ) var ( @@ -67,6 +68,7 @@ var ( FeatureCloudConfigDefaults, FeatureCloudConfigEssentials, FeatureBootAssessment, + FeatureArmFirmware, } Default = []string{ @@ -171,6 +173,8 @@ func Get(names []string) ([]*Feature, error) { features = append(features, New(name, units)) case FeatureAutologin: features = append(features, New(name, nil)) + case FeatureArmFirmware: + features = append(features, New(name, nil)) default: notFound = append(notFound, name) } diff --git a/pkg/utils/common.go b/pkg/utils/common.go index b856757d1f2..b42c58bced9 100644 --- a/pkg/utils/common.go +++ b/pkg/utils/common.go @@ -646,3 +646,12 @@ func CreateRAWFile(fs types.FS, filename string, size uint) error { } return nil } + +// PreAppendRoot simply adds the given root as a prefix to the given paths +func PreAppendRoot(root string, paths ...string) []string { + var newPaths []string + for _, path := range paths { + newPaths = append(newPaths, filepath.Join(root, path)) + } + return newPaths +}