Skip to content

Commit

Permalink
Add cloud-init paths of the new root in 'after-*' hooks (rancher#2192)
Browse files Browse the repository at this point in the history
* 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 <dcassany@suse.com>
(cherry picked from commit 30a64d7)
  • Loading branch information
davidcassany committed Oct 1, 2024
1 parent 558a01f commit b2bb97e
Show file tree
Hide file tree
Showing 15 changed files with 142 additions and 50 deletions.
21 changes: 16 additions & 5 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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...")
Expand Down
7 changes: 0 additions & 7 deletions examples/green-rpi/01_rpi-firmware.yaml

This file was deleted.

17 changes: 11 additions & 6 deletions examples/green-rpi/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 && \
Expand Down
34 changes: 33 additions & 1 deletion pkg/action/build-disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/action/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
9 changes: 8 additions & 1 deletion pkg/action/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 9 additions & 3 deletions pkg/action/reset.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,22 @@ 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"
"github.com/rancher/elemental-toolkit/v2/pkg/types"
"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 {
Expand Down Expand Up @@ -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,
},
},
},
Expand Down
5 changes: 2 additions & 3 deletions pkg/action/upgrade-recovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 1 addition & 2 deletions pkg/action/upgrade-recovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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() {
Expand Down
18 changes: 11 additions & 7 deletions pkg/action/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
}

Expand Down
3 changes: 1 addition & 2 deletions pkg/action/upgrade_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"]).
Expand Down
28 changes: 15 additions & 13 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
20 changes: 20 additions & 0 deletions pkg/features/embedded/arm-firmware/system/oem/00_armfirmware.yaml
Original file line number Diff line number Diff line change
@@ -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/
4 changes: 4 additions & 0 deletions pkg/features/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const (
FeatureCloudConfigEssentials = "cloud-config-essentials"
FeatureBootAssessment = "boot-assessment"
FeatureAutologin = "autologin"
FeatureArmFirmware = "arm-firmware"
)

var (
Expand All @@ -67,6 +68,7 @@ var (
FeatureCloudConfigDefaults,
FeatureCloudConfigEssentials,
FeatureBootAssessment,
FeatureArmFirmware,
}

Default = []string{
Expand Down Expand Up @@ -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)
}
Expand Down
9 changes: 9 additions & 0 deletions pkg/utils/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit b2bb97e

Please sign in to comment.