diff --git a/internal/agent/upgrade.go b/internal/agent/upgrade.go index 774692e..c9ef518 100644 --- a/internal/agent/upgrade.go +++ b/internal/agent/upgrade.go @@ -65,18 +65,18 @@ func ListNewerReleases(includePrereleases bool) ([]string, error) { } func Upgrade( - source string, force, strictValidations bool, dirs []string, preReleases, upgradeRecovery bool) error { + source string, force, strictValidations bool, dirs []string, upgradeEntry string, preReleases bool) error { bus.Manager.Initialize() if internalutils.UkiBootMode() == internalutils.UkiHDD { - return upgradeUki(source, dirs, strictValidations, upgradeRecovery) + return upgradeUki(source, dirs, upgradeEntry, strictValidations) } else { - return upgrade(source, force, strictValidations, dirs, preReleases, upgradeRecovery) + return upgrade(source, force, strictValidations, dirs, upgradeEntry, preReleases) } } -func upgrade(source string, force, strictValidations bool, dirs []string, preReleases, upgradeRecovery bool) error { - upgradeSpec, c, err := generateUpgradeSpec(source, force, strictValidations, dirs, preReleases, upgradeRecovery) +func upgrade(source string, force, strictValidations bool, dirs []string, upgradeEntry string, preReleases bool) error { + upgradeSpec, c, err := generateUpgradeSpec(source, force, strictValidations, dirs, upgradeEntry, preReleases) if err != nil { return err } @@ -135,12 +135,10 @@ func newerReleases() (versioneer.TagList, error) { // generateUpgradeConfForCLIArgs creates a kairos configuration for `--source` and `--recovery` // command line arguments. It will be added to the rest of the configurations. -func generateUpgradeConfForCLIArgs(source string, upgradeRecovery bool) (string, error) { +func generateUpgradeConfForCLIArgs(source, upgradeEntry string) (string, error) { upgradeConfig := ExtraConfigUpgrade{} - if upgradeRecovery { - upgradeConfig.Upgrade.Recovery = true - } + upgradeConfig.Upgrade.Entry = upgradeEntry // Set uri both for active and recovery because we don't know what we are // actually upgrading. The "upgradeRecovery" is just the command line argument. @@ -157,8 +155,8 @@ func generateUpgradeConfForCLIArgs(source string, upgradeRecovery bool) (string, return string(d), err } -func generateUpgradeSpec(sourceImageURL string, force, strictValidations bool, dirs []string, preReleases, upgradeRecovery bool) (*v1.UpgradeSpec, *config.Config, error) { - cliConf, err := generateUpgradeConfForCLIArgs(sourceImageURL, upgradeRecovery) +func generateUpgradeSpec(sourceImageURL string, force, strictValidations bool, dirs []string, upgradeEntry string, preReleases bool) (*v1.UpgradeSpec, *config.Config, error) { + cliConf, err := generateUpgradeConfForCLIArgs(sourceImageURL, upgradeEntry) if err != nil { return nil, nil, err } @@ -201,8 +199,8 @@ func getReleasesFromProvider(includePrereleases bool) ([]string, error) { return result, nil } -func upgradeUki(source string, dirs []string, strictValidations, upgradeRecovery bool) error { - cliConf, err := generateUpgradeConfForCLIArgs(source, upgradeRecovery) +func upgradeUki(source string, dirs []string, upgradeEntry string, strictValidations bool) error { + cliConf, err := generateUpgradeConfForCLIArgs(source, upgradeEntry) if err != nil { return err } @@ -240,7 +238,7 @@ func upgradeUki(source string, dirs []string, strictValidations, upgradeRecovery // ExtraConfigUpgrade is the struct that holds the upgrade options that come from flags and events type ExtraConfigUpgrade struct { Upgrade struct { - Recovery bool `json:"recovery,omitempty"` + Entry string `json:"entry,omitempty"` RecoverySystem struct { URI string `json:"uri,omitempty"` } `json:"recovery-system,omitempty"` diff --git a/main.go b/main.go index a25068a..2716b28 100644 --- a/main.go +++ b/main.go @@ -69,6 +69,7 @@ var cmds = []*cli.Command{ Usage: "[DEPRECATED] Specify a full image reference, e.g.: quay.io/some/image:tag", }, &sourceFlag, + &cli.StringFlag{Name: "boot-entry", Usage: "Specify a systemd-boot entry to upgrade (other than active/passive/recovery). The value should match the name of the '.efi' file."}, &cli.BoolFlag{Name: "pre", Usage: "Include pre-releases (rc, beta, alpha)"}, &cli.BoolFlag{Name: "recovery", Usage: "Upgrade recovery"}, }, @@ -185,9 +186,20 @@ See https://kairos.io/docs/upgrade/manual/ for documentation. source = fmt.Sprintf("oci:%s", image) } + if c.Bool("recovery") && c.String("boot-entry") != "" { + return fmt.Errorf("only one of '--recovery' and '--boot-entry' can be set") + } + + upgradeEntry := "" + if c.Bool("recovery") { + upgradeEntry = constants.BootEntryRecovery + } else if c.String("boot-entry") != "" { + upgradeEntry = c.String("boot-entry") + } + return agent.Upgrade(source, c.Bool("force"), c.Bool("strict-validation"), constants.GetConfigScanDirs(), - c.Bool("pre"), c.Bool("recovery"), + upgradeEntry, c.Bool("pre"), ) }, }, diff --git a/pkg/action/bootentries.go b/pkg/action/bootentries.go index 821da59..b336783 100644 --- a/pkg/action/bootentries.go +++ b/pkg/action/bootentries.go @@ -214,7 +214,7 @@ func systemdConfToBootName(conf string) (string, error) { return bootName, nil } - return "", fmt.Errorf("unknown systemd-boot conf: %s", conf) + return strings.ReplaceAll(fileName, "_", " "), nil } func bootNameToSystemdConf(name string) (string, error) { @@ -253,10 +253,9 @@ func bootNameToSystemdConf(name string) (string, error) { differenciator = "_" + strings.TrimPrefix(name, "statereset ") } return "statereset" + differenciator + ".conf", nil - } - return "", fmt.Errorf("unknown boot name: %s", name) + return strings.ReplaceAll(name, " ", "_") + ".conf", nil } // listBootEntriesSystemd lists the boot entries available in the systemd-boot config files @@ -320,6 +319,10 @@ func listBootEntriesSystemd(cfg *config.Config) error { func listSystemdEntries(cfg *config.Config, efiPartition *v1.Partition) ([]string, error) { var entries []string err := fsutils.WalkDirFs(cfg.Fs, filepath.Join(efiPartition.MountPoint, "loader/entries/"), func(path string, info os.DirEntry, err error) error { + if err != nil { + cfg.Logger.Errorf("Walking the dir %s", err.Error()) + } + cfg.Logger.Debugf("Checking file %s", path) if info == nil { return nil diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 694c3e2..73af2e3 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -26,7 +26,7 @@ import ( "github.com/kairos-io/kairos-agent/v2/pkg/elemental" v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1" "github.com/kairos-io/kairos-agent/v2/pkg/utils" - "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs" + fsutils "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs" "github.com/kairos-io/kairos-sdk/state" ) @@ -90,7 +90,7 @@ func (u *UpgradeAction) upgradeInstallStateYaml(meta interface{}, img v1.Image) Label: img.Label, FS: img.FS, } - if u.spec.RecoveryUpgrade { + if u.spec.RecoveryUpgrade() { recoveryPart := u.spec.State.Partitions[constants.RecoveryPartName] if recoveryPart == nil { recoveryPart = &v1.PartitionState{ @@ -137,7 +137,7 @@ func (u *UpgradeAction) Run() (err error) { e := elemental.NewElemental(u.config) - if u.spec.RecoveryUpgrade { + if u.spec.RecoveryUpgrade() { upgradeImg = u.spec.Recovery if upgradeImg.FS == constants.SquashFs { finalImageFile = filepath.Join(u.spec.Partitions.Recovery.MountPoint, "cOS", constants.RecoverySquashFile) @@ -227,7 +227,7 @@ func (u *UpgradeAction) Run() (err error) { } // Only apply rebrand stage for system upgrades - if !u.spec.RecoveryUpgrade { + if !u.spec.RecoveryUpgrade() { u.Info("rebranding") if rebrandingErr := e.SetDefaultGrubEntry(u.spec.Partitions.State.MountPoint, upgradeImg.MountPoint, u.spec.GrubDefEntry); rebrandingErr != nil { u.config.Logger.Warn("failure while rebranding GRUB default entry (ignoring), run with --debug to see more details") @@ -244,7 +244,7 @@ func (u *UpgradeAction) Run() (err error) { // If not upgrading recovery and booting from non passive, backup active into passive // We dont want to overwrite passive if we are booting from passive as it could mean that active is broken and we would // be overriding a working passive with a broken/unknown active - if u.spec.RecoveryUpgrade == false && bootedFrom != state.Passive { + if !u.spec.RecoveryUpgrade() && bootedFrom != state.Passive { // backup current active.img to passive.img before overwriting the active.img u.Info("Backing up current active image") source := filepath.Join(u.spec.Partitions.State.MountPoint, "cOS", constants.ActiveImgFile) @@ -289,7 +289,7 @@ func (u *UpgradeAction) Run() (err error) { } u.Info("Upgrade completed") - if !u.spec.RecoveryUpgrade { + if !u.spec.RecoveryUpgrade() { u.config.Logger.Warn("Remember that recovery is upgraded separately by passing the --recovery flag to the upgrade command!\n" + "See more info about this on https://kairos.io/docs/upgrade/") } diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index a53edd6..3254716 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -19,9 +19,10 @@ package action_test import ( "bytes" "fmt" - sdkTypes "github.com/kairos-io/kairos-sdk/types" "path/filepath" + sdkTypes "github.com/kairos-io/kairos-sdk/types" + agentConfig "github.com/kairos-io/kairos-agent/v2/pkg/config" fsutils "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs" @@ -449,8 +450,7 @@ var _ = Describe("Runtime Actions", func() { spec.Active.Size = 10 spec.Passive.Size = 10 spec.Recovery.Size = 10 - - spec.RecoveryUpgrade = true + spec.Entry = constants.BootEntryRecovery err = fsutils.MkdirAll(config.Fs, "/proc", constants.DirPerm) Expect(err).ShouldNot(HaveOccurred()) @@ -542,8 +542,7 @@ var _ = Describe("Runtime Actions", func() { spec.Active.Size = 10 spec.Passive.Size = 10 spec.Recovery.Size = 10 - - spec.RecoveryUpgrade = true + spec.Entry = constants.BootEntryRecovery err = fsutils.MkdirAll(config.Fs, "/proc", constants.DirPerm) Expect(err).ShouldNot(HaveOccurred()) diff --git a/pkg/config/spec.go b/pkg/config/spec.go index 8b1196c..a59ef8e 100644 --- a/pkg/config/spec.go +++ b/pkg/config/spec.go @@ -24,6 +24,7 @@ import ( "reflect" "strings" + "github.com/kairos-io/kairos-sdk/collector" sdkTypes "github.com/kairos-io/kairos-sdk/types" "github.com/google/go-containerregistry/pkg/crane" @@ -323,7 +324,21 @@ func NewUpgradeSpec(cfg *Config) (*v1.UpgradeSpec, error) { } } + // Deep look to see if upgrade.recovery == true in the config + // if yes, we set the upgrade spec "Entry" to "recovery" + entry := "" + _, ok := cfg.Config["upgrade"] + if ok { + _, ok = cfg.Config["upgrade"].(collector.Config)["recovery"] + if ok { + if cfg.Config["upgrade"].(collector.Config)["recovery"].(bool) { + entry = constants.BootEntryRecovery + } + } + } + spec := &v1.UpgradeSpec{ + Entry: entry, Active: active, Recovery: recovery, Passive: passive, @@ -346,7 +361,7 @@ func setUpgradeSourceSize(cfg *Config, spec *v1.UpgradeSpec) error { var err error var targetSpec *v1.Image - if spec.RecoveryUpgrade { + if spec.RecoveryUpgrade() { targetSpec = &(spec.Recovery) } else { targetSpec = &(spec.Active) diff --git a/pkg/config/spec_test.go b/pkg/config/spec_test.go index e35e202..07c5a82 100644 --- a/pkg/config/spec_test.go +++ b/pkg/config/spec_test.go @@ -18,11 +18,12 @@ package config_test import ( "fmt" - sdkTypes "github.com/kairos-io/kairos-sdk/types" - "github.com/rs/zerolog" "os" "path/filepath" + sdkTypes "github.com/kairos-io/kairos-sdk/types" + "github.com/rs/zerolog" + "github.com/jaypipes/ghw/pkg/block" config "github.com/kairos-io/kairos-agent/v2/pkg/config" "github.com/kairos-io/kairos-agent/v2/pkg/constants" @@ -587,7 +588,7 @@ cloud-init-paths: spec, err := config.ReadSpecFromCloudConfig(cfg, "upgrade") Expect(err).ToNot(HaveOccurred()) upgradeSpec := spec.(*v1.UpgradeSpec) - Expect(upgradeSpec.RecoveryUpgrade).To(BeTrue()) + Expect(upgradeSpec.RecoveryUpgrade()).To(BeTrue()) }) It("Fails when a wrong action is read", func() { cfg, err := config.Scan(collector.Directories([]string{dir}...), collector.NoLogs) diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index fd57ca1..4fe6f01 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -88,6 +88,7 @@ const ( GPT = "gpt" UsrLocalPath = "/usr/local" OEMPath = "/oem" + BootEntryRecovery = "recovery" // SELinux targeted policy paths SELinuxTargetedPath = "/etc/selinux/targeted" diff --git a/pkg/types/v1/config.go b/pkg/types/v1/config.go index c4fbe73..7d4f646 100644 --- a/pkg/types/v1/config.go +++ b/pkg/types/v1/config.go @@ -170,7 +170,7 @@ func (r *ResetSpec) ShouldReboot() bool { return r.Reboot } func (r *ResetSpec) ShouldShutdown() bool { return r.PowerOff } type UpgradeSpec struct { - RecoveryUpgrade bool `yaml:"recovery,omitempty" mapstructure:"recovery"` + Entry string `yaml:"entry,omitempty" mapstructure:"entry"` Active Image `yaml:"system,omitempty" mapstructure:"system"` Recovery Image `yaml:"recovery-system,omitempty" mapstructure:"recovery-system"` GrubDefEntry string `yaml:"grub-entry-name,omitempty" mapstructure:"grub-entry-name"` @@ -182,10 +182,14 @@ type UpgradeSpec struct { State *InstallState } +func (u *UpgradeSpec) RecoveryUpgrade() bool { + return u.Entry == constants.BootEntryRecovery +} + // Sanitize checks the consistency of the struct, returns error // if unsolvable inconsistencies are found func (u *UpgradeSpec) Sanitize() error { - if u.RecoveryUpgrade { + if u.RecoveryUpgrade() { if u.Recovery.Source.IsEmpty() { return fmt.Errorf(constants.UpgradeNoSourceError) } @@ -528,11 +532,15 @@ func (i *InstallUkiSpec) GetPartitions() ElementalPartitions { return i.Partitio func (i *InstallUkiSpec) GetExtraPartitions() PartitionList { return i.ExtraPartitions } type UpgradeUkiSpec struct { - RecoveryUpgrade bool `yaml:"recovery,omitempty" mapstructure:"recovery"` - Active Image `yaml:"system,omitempty" mapstructure:"system"` - Reboot bool `yaml:"reboot,omitempty" mapstructure:"reboot"` - PowerOff bool `yaml:"poweroff,omitempty" mapstructure:"poweroff"` - EfiPartition *Partition `yaml:"efi-partition,omitempty" mapstructure:"efi-partition"` + Entry string `yaml:"entry,omitempty" mapstructure:"entry"` + Active Image `yaml:"system,omitempty" mapstructure:"system"` + Reboot bool `yaml:"reboot,omitempty" mapstructure:"reboot"` + PowerOff bool `yaml:"poweroff,omitempty" mapstructure:"poweroff"` + EfiPartition *Partition `yaml:"efi-partition,omitempty" mapstructure:"efi-partition"` +} + +func (i *UpgradeUkiSpec) RecoveryUpgrade() bool { + return i.Entry == constants.BootEntryRecovery } func (i *UpgradeUkiSpec) Sanitize() error { diff --git a/pkg/types/v1/config_test.go b/pkg/types/v1/config_test.go index 877e7c1..04dd223 100644 --- a/pkg/types/v1/config_test.go +++ b/pkg/types/v1/config_test.go @@ -17,11 +17,12 @@ limitations under the License. package v1_test import ( + "path/filepath" + "github.com/kairos-io/kairos-agent/v2/pkg/constants" v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "path/filepath" ) var _ = Describe("Types", Label("types", "config"), func() { @@ -444,7 +445,7 @@ var _ = Describe("Types", Label("types", "config"), func() { }) Describe("Recovery upgrade", func() { BeforeEach(func() { - spec.RecoveryUpgrade = true + spec.Entry = constants.BootEntryRecovery }) It("fails with empty source", func() { err := spec.Sanitize() diff --git a/pkg/uki/upgrade.go b/pkg/uki/upgrade.go index e5e7f9e..7b369ed 100644 --- a/pkg/uki/upgrade.go +++ b/pkg/uki/upgrade.go @@ -51,13 +51,20 @@ func (i *UpgradeAction) Run() (err error) { // If we decide to first copy and then rotate, we need ~4 times the size of // the artifact set [TBD] - // When upgrading recovery, we don't want to replace loader.conf or any other + // When upgrading recovery or single entries, we don't want to replace loader.conf or any other // files, thus we take a simpler approach and only install the new efi file // and the relevant conf - if i.spec.RecoveryUpgrade { + if i.spec.RecoveryUpgrade() { + i.cfg.Logger.Infof("installing entry: recovery") return i.installRecovery() } + if i.spec.Entry != "" { // single entry upgrade + i.cfg.Logger.Infof("installing entry: %s", i.spec.Entry) + return i.installEntry(i.spec.Entry) + } + + i.cfg.Logger.Infof("installing entry: active") // Dump artifact to efi dir _, err = e.DumpSource(constants.UkiEfiDir, i.spec.Active.Source) if err != nil { @@ -122,9 +129,12 @@ func (i *UpgradeAction) Run() (err error) { return nil } -// installRecovery replaces the "recovery" role efi and conf files with -// the UnassignedArtifactRole efi and loader files from dir -func (i *UpgradeAction) installRecovery() error { +func (i *UpgradeAction) installEntry(entry string) error { + targetEntryFile := filepath.Join(constants.UkiEfiDir, "EFI", "kairos", fmt.Sprintf("%s.efi", entry)) + if _, err := os.Stat(targetEntryFile); err != nil { + return fmt.Errorf("could not stat target efi file for entry %s: %s", entry, err) + } + tmpDir, err := os.MkdirTemp("", "") if err != nil { i.cfg.Logger.Errorf("creating a tmp dir: %s", err.Error()) @@ -140,15 +150,13 @@ func (i *UpgradeAction) installRecovery() error { return err } - err = copyFile( - filepath.Join(tmpDir, "EFI", "kairos", UnassignedArtifactRole+".efi"), - filepath.Join(constants.UkiEfiDir, "EFI", "kairos", "recovery.efi")) + err = copyFile(filepath.Join(tmpDir, "EFI", "kairos", UnassignedArtifactRole+".efi"), targetEntryFile) if err != nil { i.cfg.Logger.Errorf("copying efi files: %s", err.Error()) return err } - targetConfPath := filepath.Join(constants.UkiEfiDir, "loader", "entries", "recovery.conf") + targetConfPath := filepath.Join(constants.UkiEfiDir, "loader", "entries", fmt.Sprintf("%s.conf", entry)) err = copyFile( filepath.Join(tmpDir, "loader", "entries", UnassignedArtifactRole+".conf"), targetConfPath) @@ -156,13 +164,24 @@ func (i *UpgradeAction) installRecovery() error { i.cfg.Logger.Errorf("copying conf files: %s", err.Error()) return err } - err = replaceRoleInKey(targetConfPath, "efi", UnassignedArtifactRole, "recovery", i.cfg.Logger) + err = replaceRoleInKey(targetConfPath, "efi", UnassignedArtifactRole, entry, i.cfg.Logger) if err != nil { i.cfg.Logger.Errorf("replacing role in in key %s: %s", "efi", err.Error()) return err } - err = replaceConfTitle(targetConfPath, "recovery") + return nil +} + +// installRecovery replaces the "recovery" role efi and conf files with +// the UnassignedArtifactRole efi and loader files from dir +func (i *UpgradeAction) installRecovery() error { + if err := i.installEntry("recovery"); err != nil { + return err + } + + targetConfPath := filepath.Join(constants.UkiEfiDir, "loader", "entries", "recovery.conf") + err := replaceConfTitle(targetConfPath, "recovery") if err != nil { i.cfg.Logger.Errorf("replacing conf title: %s", err.Error()) return err