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 +}