From 02bb11191320b9f5dbd0ff5c40fefdda7d149063 Mon Sep 17 00:00:00 2001 From: David Cassany Viladomat Date: Wed, 6 Jul 2022 09:27:10 +0200 Subject: [PATCH] Produce state.yaml files on install, upgrade and reset commands (#278) This commit makes upgrade|reset|install to create and upgrade `state.yaml` file including system wide data (deployed images, partition labels, etc.) It introduces the concept of installation state and stores such a data in `state.yaml` file in two different locations, state partition root and recovery partition root. The purpose of this duplication is to be able to always find the state.yaml file in a known location regardless of the image we are booting. --- Makefile | 6 +- cmd/build-iso_test.go | 2 +- cmd/config/config_test.go | 6 + cmd/pull-image.go | 2 +- pkg/action/build-disk.go | 2 +- pkg/action/build-iso.go | 2 +- pkg/action/build_test.go | 10 +- pkg/action/install.go | 77 ++++- pkg/action/reset.go | 74 ++++- pkg/action/reset_test.go | 5 + pkg/action/upgrade.go | 94 ++++-- pkg/config/config.go | 26 +- pkg/config/config_test.go | 28 +- pkg/constants/constants.go | 1 + pkg/elemental/elemental.go | 71 ++-- pkg/elemental/elemental_test.go | 124 ++++++- pkg/luet/luet.go | 51 ++- pkg/luet/luet_test.go | 25 +- pkg/types/v1/common.go | 33 +- pkg/types/v1/common_test.go | 29 ++ pkg/types/v1/config.go | 141 +++++++- pkg/types/v1/config.go.orig | 573 ++++++++++++++++++++++++++++++++ pkg/types/v1/config_test.go | 107 ++++++ pkg/types/v1/luet.go | 4 +- tests/mocks/luet_mock.go | 16 +- 25 files changed, 1359 insertions(+), 150 deletions(-) create mode 100644 pkg/types/v1/config.go.orig diff --git a/Makefile b/Makefile index c687a4d5a3a..a0333bd3b67 100644 --- a/Makefile +++ b/Makefile @@ -44,20 +44,20 @@ test_deps: go install -mod=mod github.com/onsi/ginkgo/v2/ginkgo test: $(GINKGO) - ginkgo run --label-filter '!root' --fail-fast --slow-spec-threshold 30s --race --covermode=atomic --coverprofile=coverage.txt -p -r ${PKG} + ginkgo run --label-filter '!root' --fail-fast --slow-spec-threshold 30s --race --covermode=atomic --coverprofile=coverage.txt --coverpkg=github.com/rancher/elemental-cli/... -p -r ${PKG} test_root: $(GINKGO) ifneq ($(shell id -u), 0) @echo "This tests require root/sudo to run." @exit 1 else - ginkgo run --label-filter root --fail-fast --slow-spec-threshold 30s --race --covermode=atomic --coverprofile=coverage_root.txt -procs=1 -r ${PKG} + ginkgo run --label-filter root --fail-fast --slow-spec-threshold 30s --race --covermode=atomic --coverprofile=coverage_root.txt --coverpkg=github.com/rancher/elemental-cli/... -procs=1 -r ${PKG} endif # Useful test run for local dev. It does not run tests that require root and it does not run tests that require systemctl checks # which results in a escalation prompt for privileges. This can block a run until a password or the prompt is cancelled test_no_root_no_systemctl: - ginkgo run --label-filter '!root && !systemctl' --fail-fast --slow-spec-threshold 30s --race --covermode=atomic --coverprofile=coverage.txt -p -r ${PKG} + ginkgo run --label-filter '!root && !systemctl' --fail-fast --slow-spec-threshold 30s --race --covermode=atomic --coverprofile=coverage.txt --coverpkg=github.com/rancher/elemental-cli/... -p -r ${PKG} license-check: diff --git a/cmd/build-iso_test.go b/cmd/build-iso_test.go index 0818a36fee9..c0b2f591bef 100644 --- a/cmd/build-iso_test.go +++ b/cmd/build-iso_test.go @@ -50,7 +50,7 @@ var _ = Describe("BuidISO", Label("iso", "cmd"), func() { It("Errors out if rootfs is a non valid argument", Label("flags"), func() { _, _, err := executeCommandC(rootCmd, "build-iso", "/no/image/reference") Expect(err).ToNot(BeNil()) - Expect(err.Error()).To(ContainSubstring("unknown source type for")) + Expect(err.Error()).To(ContainSubstring("invalid image reference")) }) It("Errors out if overlay roofs path does not exist", Label("flags"), func() { _, _, err := executeCommandC( diff --git a/cmd/config/config_test.go b/cmd/config/config_test.go index c0f2d389c2c..ada0148588d 100644 --- a/cmd/config/config_test.go +++ b/cmd/config/config_test.go @@ -314,6 +314,12 @@ var _ = Describe("Config", Label("config"), func() { mainDisk := block.Disk{ Name: "device", Partitions: []*block.Partition{ + { + Name: "device1", + FilesystemLabel: "COS_RECOVERY", + Type: "ext4", + MountPoint: constants.RunningStateDir, + }, { Name: "device2", FilesystemLabel: "COS_STATE", diff --git a/cmd/pull-image.go b/cmd/pull-image.go index 530c73ab7b8..3065628553d 100644 --- a/cmd/pull-image.go +++ b/cmd/pull-image.go @@ -77,7 +77,7 @@ func NewPullImageCmd(root *cobra.Command, addCheckRoot bool) *cobra.Command { l := luet.NewLuet(luet.WithLogger(cfg.Logger), luet.WithAuth(auth), luet.WithPlugins(plugins...)) l.VerifyImageUnpack = verify - err = l.Unpack(destination, image, local) + _, err = l.Unpack(destination, image, local) if err != nil { cfg.Logger.Error(err.Error()) diff --git a/pkg/action/build-disk.go b/pkg/action/build-disk.go index 256a590efe9..a49e4d9c138 100644 --- a/pkg/action/build-disk.go +++ b/pkg/action/build-disk.go @@ -93,7 +93,7 @@ func BuildDiskRun(cfg *v1.BuildConfig, spec *v1.RawDiskArchEntry, imgType string cfg.Logger.Error(err) return err } - err = e.DumpSource( + _, err = e.DumpSource( filepath.Join(baseDir, pkg.Target), imgSource, ) diff --git a/pkg/action/build-iso.go b/pkg/action/build-iso.go index 248a00ce925..cc8f3b7950c 100644 --- a/pkg/action/build-iso.go +++ b/pkg/action/build-iso.go @@ -252,7 +252,7 @@ func (b BuildISOAction) burnISO(root string) error { func (b BuildISOAction) applySources(target string, sources ...*v1.ImageSource) error { for _, src := range sources { - err := b.e.DumpSource(target, src) + _, err := b.e.DumpSource(target, src) if err != nil { return err } diff --git a/pkg/action/build_test.go b/pkg/action/build_test.go index f8fb5dc6baa..65326717e79 100644 --- a/pkg/action/build_test.go +++ b/pkg/action/build_test.go @@ -92,21 +92,21 @@ var _ = Describe("Runtime Actions", func() { imageSrc, _ := v1.NewSrcFromURI("channel:live/bootloader") iso.Image = []*v1.ImageSource{imageSrc} - luet.UnpackSideEffect = func(target string, image string, local bool) error { + luet.UnpackSideEffect = func(target string, image string, local bool) (*v1.DockerImageMeta, error) { bootDir := filepath.Join(target, "boot") err := utils.MkdirAll(fs, bootDir, constants.DirPerm) if err != nil { - return err + return nil, err } _, err = fs.Create(filepath.Join(bootDir, "vmlinuz")) if err != nil { - return err + return nil, err } _, err = fs.Create(filepath.Join(bootDir, "initrd")) if err != nil { - return err + return nil, err } - return nil + return nil, nil } buildISO := action.NewBuildISOAction(cfg, iso) diff --git a/pkg/action/install.go b/pkg/action/install.go index fc2baf7ada3..7f737472370 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -18,6 +18,8 @@ package action import ( "fmt" + "path/filepath" + "time" cnst "github.com/rancher/elemental-cli/pkg/constants" "github.com/rancher/elemental-cli/pkg/elemental" @@ -41,6 +43,69 @@ func (i *InstallAction) installHook(hook string, chroot bool) error { return Hook(&i.cfg.Config, hook, i.cfg.Strict, i.cfg.CloudInitPaths...) } +func (i *InstallAction) createInstallStateYaml(sysMeta, recMeta interface{}) error { + if i.spec.Partitions.State == nil || i.spec.Partitions.Recovery == nil { + return fmt.Errorf("undefined state or recovery partition") + } + + // If recovery image is a copyied file from active reuse the same source and metadata + recSource := i.spec.Recovery.Source + if i.spec.Recovery.Source.IsFile() && i.spec.Active.File == i.spec.Recovery.Source.Value() { + recMeta = sysMeta + recSource = i.spec.Active.Source + } + + installState := &v1.InstallState{ + Date: time.Now().Format(time.RFC3339), + Partitions: map[string]*v1.PartitionState{ + cnst.StatePartName: { + FSLabel: i.spec.Partitions.State.FilesystemLabel, + Images: map[string]*v1.ImageState{ + cnst.ActiveImgName: { + Source: i.spec.Active.Source, + SourceMetadata: sysMeta, + Label: i.spec.Active.Label, + FS: i.spec.Active.FS, + }, + cnst.PassiveImgName: { + Source: i.spec.Active.Source, + SourceMetadata: sysMeta, + Label: i.spec.Passive.Label, + FS: i.spec.Passive.FS, + }, + }, + }, + cnst.RecoveryPartName: { + FSLabel: i.spec.Partitions.Recovery.FilesystemLabel, + Images: map[string]*v1.ImageState{ + cnst.RecoveryImgName: { + Source: recSource, + SourceMetadata: recMeta, + Label: i.spec.Recovery.Label, + FS: i.spec.Recovery.FS, + }, + }, + }, + }, + } + if i.spec.Partitions.OEM != nil { + installState.Partitions[cnst.OEMPartName] = &v1.PartitionState{ + FSLabel: i.spec.Partitions.OEM.FilesystemLabel, + } + } + if i.spec.Partitions.Persistent != nil { + installState.Partitions[cnst.PersistentPartName] = &v1.PartitionState{ + FSLabel: i.spec.Partitions.Persistent.FilesystemLabel, + } + } + + return i.cfg.WriteInstallState( + installState, + filepath.Join(i.spec.Partitions.State.MountPoint, cnst.InstallStateFile), + filepath.Join(i.spec.Partitions.Recovery.MountPoint, cnst.InstallStateFile), + ) +} + type InstallAction struct { cfg *v1.RunConfig spec *v1.InstallSpec @@ -103,7 +168,7 @@ func (i InstallAction) Run() (err error) { }) // Deploy active image - err = e.DeployImage(&i.spec.Active, true) + systemMeta, err := e.DeployImage(&i.spec.Active, true) if err != nil { return err } @@ -164,12 +229,12 @@ func (i InstallAction) Run() (err error) { return err } // Install Recovery - err = e.DeployImage(&i.spec.Recovery, false) + recoveryMeta, err := e.DeployImage(&i.spec.Recovery, false) if err != nil { return err } // Install Passive - err = e.DeployImage(&i.spec.Passive, false) + _, err = e.DeployImage(&i.spec.Passive, false) if err != nil { return err } @@ -179,6 +244,12 @@ func (i InstallAction) Run() (err error) { return err } + // Add state.yaml file on state and recovery partitions + err = i.createInstallStateYaml(systemMeta, recoveryMeta) + if err != nil { + return err + } + // Do not reboot/poweroff on cleanup errors err = cleanup.Cleanup(err) if err != nil { diff --git a/pkg/action/reset.go b/pkg/action/reset.go index c51061e09b9..11d097b7a76 100644 --- a/pkg/action/reset.go +++ b/pkg/action/reset.go @@ -17,6 +17,10 @@ limitations under the License. package action import ( + "fmt" + "path/filepath" + "time" + cnst "github.com/rancher/elemental-cli/pkg/constants" "github.com/rancher/elemental-cli/pkg/elemental" v1 "github.com/rancher/elemental-cli/pkg/types/v1" @@ -48,6 +52,60 @@ func NewResetAction(cfg *v1.RunConfig, spec *v1.ResetSpec) *ResetAction { return &ResetAction{cfg: cfg, spec: spec} } +func (r *ResetAction) updateInstallState(e *elemental.Elemental, cleanup *utils.CleanStack, meta interface{}) error { + if r.spec.Partitions.Recovery == nil || r.spec.Partitions.State == nil { + return fmt.Errorf("undefined state or recovery partition") + } + + installState := &v1.InstallState{ + Date: time.Now().Format(time.RFC3339), + Partitions: map[string]*v1.PartitionState{ + cnst.StatePartName: { + FSLabel: r.spec.Partitions.State.FilesystemLabel, + Images: map[string]*v1.ImageState{ + cnst.ActiveImgName: { + Source: r.spec.Active.Source, + SourceMetadata: meta, + Label: r.spec.Active.Label, + FS: r.spec.Active.FS, + }, + cnst.PassiveImgName: { + Source: r.spec.Active.Source, + SourceMetadata: meta, + Label: r.spec.Passive.Label, + FS: r.spec.Passive.FS, + }, + }, + }, + }, + } + if r.spec.Partitions.OEM != nil { + installState.Partitions[cnst.OEMPartName] = &v1.PartitionState{ + FSLabel: r.spec.Partitions.OEM.FilesystemLabel, + } + } + if r.spec.Partitions.Persistent != nil { + installState.Partitions[cnst.PersistentPartName] = &v1.PartitionState{ + FSLabel: r.spec.Partitions.Persistent.FilesystemLabel, + } + } + if r.spec.State != nil && r.spec.State.Partitions != nil { + installState.Partitions[cnst.RecoveryPartName] = r.spec.State.Partitions[cnst.RecoveryPartName] + } + + umount, err := e.MountRWPartition(r.spec.Partitions.Recovery) + if err != nil { + return err + } + cleanup.Push(umount) + + return r.cfg.WriteInstallState( + installState, + filepath.Join(r.spec.Partitions.State.MountPoint, cnst.InstallStateFile), + filepath.Join(r.spec.Partitions.Recovery.MountPoint, cnst.InstallStateFile), + ) +} + // ResetRun will reset the cos system to by following several steps func (r ResetAction) Run() (err error) { e := elemental.NewElemental(&r.cfg.Config) @@ -60,7 +118,7 @@ func (r ResetAction) Run() (err error) { } // Unmount partitions if any is already mounted before formatting - err = e.UnmountPartitions(r.spec.Partitions.PartitionsByMountPoint(true)) + err = e.UnmountPartitions(r.spec.Partitions.PartitionsByMountPoint(true, r.spec.Partitions.Recovery)) if err != nil { return err } @@ -80,7 +138,6 @@ func (r ResetAction) Run() (err error) { return err } } - } // Reformat OEM @@ -94,16 +151,16 @@ func (r ResetAction) Run() (err error) { } } // Mount configured partitions - err = e.MountPartitions(r.spec.Partitions.PartitionsByMountPoint(false)) + err = e.MountPartitions(r.spec.Partitions.PartitionsByMountPoint(false, r.spec.Partitions.Recovery)) if err != nil { return err } cleanup.Push(func() error { - return e.UnmountPartitions(r.spec.Partitions.PartitionsByMountPoint(true)) + return e.UnmountPartitions(r.spec.Partitions.PartitionsByMountPoint(true, r.spec.Partitions.Recovery)) }) // Deploy active image - err = e.DeployImage(&r.spec.Active, true) + meta, err := e.DeployImage(&r.spec.Active, true) if err != nil { return err } @@ -163,7 +220,7 @@ func (r ResetAction) Run() (err error) { } // Install Passive - err = e.DeployImage(&r.spec.Passive, false) + _, err = e.DeployImage(&r.spec.Passive, false) if err != nil { return err } @@ -173,6 +230,11 @@ func (r ResetAction) Run() (err error) { return err } + err = r.updateInstallState(e, cleanup, meta) + if err != nil { + return err + } + // Do not reboot/poweroff on cleanup errors err = cleanup.Cleanup(err) if err != nil { diff --git a/pkg/action/reset_test.go b/pkg/action/reset_test.go index e46564cd364..e0dc7ad242c 100644 --- a/pkg/action/reset_test.go +++ b/pkg/action/reset_test.go @@ -110,6 +110,11 @@ var _ = Describe("Reset action tests", func() { FilesystemLabel: "COS_OEM", Type: "ext4", }, + { + Name: "device5", + FilesystemLabel: "COS_RECOVERY", + Type: "ext4", + }, }, } ghwTest = v1mock.GhwMock{} diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 25cb8a77a08..8b869612c55 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -17,7 +17,9 @@ limitations under the License. package action import ( + "fmt" "path/filepath" + "time" "github.com/rancher/elemental-cli/pkg/constants" "github.com/rancher/elemental-cli/pkg/elemental" @@ -67,8 +69,53 @@ func (u UpgradeAction) upgradeHook(hook string, chroot bool) error { return Hook(&u.config.Config, hook, u.config.Strict, u.config.CloudInitPaths...) } +func (u *UpgradeAction) upgradeInstallStateYaml(meta interface{}, img v1.Image) error { + if u.spec.Partitions.Recovery == nil || u.spec.Partitions.State == nil { + return fmt.Errorf("undefined state or recovery partition") + } + + if u.spec.State == nil { + u.spec.State = &v1.InstallState{ + Partitions: map[string]*v1.PartitionState{}, + } + } + + u.spec.State.Date = time.Now().Format(time.RFC3339) + imgState := &v1.ImageState{ + Source: img.Source, + SourceMetadata: meta, + Label: img.Label, + FS: img.FS, + } + if u.spec.RecoveryUpgrade { + recoveryPart := u.spec.State.Partitions[constants.RecoveryPartName] + if recoveryPart == nil { + recoveryPart = &v1.PartitionState{ + Images: map[string]*v1.ImageState{}, + } + u.spec.State.Partitions[constants.RecoveryPartName] = recoveryPart + } + recoveryPart.Images[constants.RecoveryImgName] = imgState + } else { + statePart := u.spec.State.Partitions[constants.StatePartName] + if statePart == nil { + statePart = &v1.PartitionState{ + Images: map[string]*v1.ImageState{}, + } + u.spec.State.Partitions[constants.StatePartName] = statePart + } + statePart.Images[constants.PassiveImgName] = statePart.Images[constants.ActiveImgName] + statePart.Images[constants.ActiveImgName] = imgState + } + + return u.config.WriteInstallState( + u.spec.State, + filepath.Join(u.spec.Partitions.State.MountPoint, constants.InstallStateFile), + filepath.Join(u.spec.Partitions.Recovery.MountPoint, constants.InstallStateFile), + ) +} + func (u *UpgradeAction) Run() (err error) { - var mountPart *v1.Partition var upgradeImg v1.Image var finalImageFile string @@ -78,35 +125,27 @@ func (u *UpgradeAction) Run() (err error) { e := elemental.NewElemental(&u.config.Config) if u.spec.RecoveryUpgrade { - mountPart = u.spec.Partitions.Recovery upgradeImg = u.spec.Recovery if upgradeImg.FS == constants.SquashFs { - finalImageFile = filepath.Join(mountPart.MountPoint, "cOS", constants.RecoverySquashFile) + finalImageFile = filepath.Join(u.spec.Partitions.Recovery.MountPoint, "cOS", constants.RecoverySquashFile) } else { - finalImageFile = filepath.Join(mountPart.MountPoint, "cOS", constants.RecoveryImgFile) + finalImageFile = filepath.Join(u.spec.Partitions.Recovery.MountPoint, "cOS", constants.RecoveryImgFile) } } else { - mountPart = u.spec.Partitions.State upgradeImg = u.spec.Active - finalImageFile = filepath.Join(mountPart.MountPoint, "cOS", constants.ActiveImgFile) + finalImageFile = filepath.Join(u.spec.Partitions.State.MountPoint, "cOS", constants.ActiveImgFile) } - u.Info("mounting %s partition as rw", mountPart.Name) - if mnt, _ := utils.IsMounted(&u.config.Config, mountPart); mnt { - err = e.MountPartition(mountPart, "remount", "rw") - if err != nil { - u.Error("failed mounting %s partition: %v", mountPart.Name, err) - return err - } - cleanup.Push(func() error { return e.MountPartition(mountPart, "remount", "ro") }) - } else { - err = e.MountPartition(mountPart, "rw") - if err != nil { - u.Error("failed mounting %s partition: %v", mountPart.Name, err) - return err - } - cleanup.Push(func() error { return e.UnmountPartition(mountPart) }) + umount, err := e.MountRWPartition(u.spec.Partitions.State) + if err != nil { + return err } + cleanup.Push(umount) + umount, err = e.MountRWPartition(u.spec.Partitions.Recovery) + if err != nil { + return err + } + cleanup.Push(umount) // Cleanup transition image file before leaving cleanup.Push(func() error { return u.remove(upgradeImg.File) }) @@ -131,7 +170,7 @@ func (u *UpgradeAction) Run() (err error) { } u.Info("deploying image %s to %s", upgradeImg.Source.Value(), upgradeImg.File) - err = e.DeployImage(&upgradeImg, true) + upgradeMeta, err := e.DeployImage(&upgradeImg, true) if err != nil { u.Error("Failed deploying image to file %s", upgradeImg.File) return err @@ -170,7 +209,7 @@ func (u *UpgradeAction) Run() (err error) { if !u.spec.RecoveryUpgrade { u.Info("rebranding") - err = e.SetDefaultGrubEntry(mountPart.MountPoint, upgradeImg.MountPoint, u.spec.GrubDefEntry) + err = e.SetDefaultGrubEntry(u.spec.Partitions.State.MountPoint, upgradeImg.MountPoint, u.spec.GrubDefEntry) if err != nil { u.Error("failed setting default entry") return err @@ -194,7 +233,7 @@ func (u *UpgradeAction) Run() (err error) { //TODO this step could be part of elemental package // backup current active.img to passive.img before overwriting the active.img u.Info("Backing up current active image") - source := filepath.Join(mountPart.MountPoint, "cOS", constants.ActiveImgFile) + source := filepath.Join(u.spec.Partitions.State.MountPoint, "cOS", constants.ActiveImgFile) u.Info("Moving %s to %s", source, u.spec.Passive.File) _, err := u.config.Runner.Run("mv", "-f", source, u.spec.Passive.File) if err != nil { @@ -222,6 +261,13 @@ func (u *UpgradeAction) Run() (err error) { _, _ = u.config.Runner.Run("sync") + // Update state.yaml file on recovery and state partitions + err = u.upgradeInstallStateYaml(upgradeMeta, upgradeImg) + if err != nil { + u.Error("failed upgrading installation metadata") + return err + } + u.Info("Upgrade completed") // Do not reboot/poweroff on cleanup errors diff --git a/pkg/config/config.go b/pkg/config/config.go index 59b92f69df9..efef393121b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -276,6 +276,11 @@ func NewUpgradeSpec(cfg v1.Config) (*v1.UpgradeSpec, error) { var recLabel, recFs, recMnt string var active, passive, recovery v1.Image + installState, err := cfg.LoadInstallState() + if err != nil { + cfg.Logger.Warnf("failed reading installation state: %s", err.Error()) + } + parts, err := utils.GetAllPartitions() if err != nil { return nil, fmt.Errorf("could not read host partitions") @@ -328,7 +333,7 @@ func NewUpgradeSpec(cfg v1.Config) (*v1.UpgradeSpec, error) { File: filepath.Join(ep.State.MountPoint, "cOS", constants.PassiveImgFile), Label: constants.PassiveLabel, Source: v1.NewFileSrc(active.File), - FS: constants.LinuxImgFs, + FS: active.FS, } } @@ -337,6 +342,7 @@ func NewUpgradeSpec(cfg v1.Config) (*v1.UpgradeSpec, error) { Recovery: recovery, Passive: passive, Partitions: ep, + State: installState, }, nil } @@ -352,16 +358,17 @@ func NewResetSpec(cfg v1.Config) (*v1.ResetSpec, error) { efiExists, _ := utils.Exists(cfg.Fs, constants.EfiDevice) + installState, err := cfg.LoadInstallState() + if err != nil { + cfg.Logger.Warnf("failed reading installation state: %s", err.Error()) + } + parts, err := utils.GetAllPartitions() if err != nil { return nil, fmt.Errorf("could not read host partitions") } ep := v1.NewElementalPartitionsFromList(parts) - // We won't do anything with the recovery partition - // removing it so we can easily loop to mount and unmount - ep.Recovery = nil - if efiExists { if ep.EFI == nil { return nil, fmt.Errorf("EFI partition not found") @@ -379,6 +386,14 @@ func NewResetSpec(cfg v1.Config) (*v1.ResetSpec, error) { ep.State.MountPoint = constants.StateDir } ep.State.Name = constants.StatePartName + + if ep.Recovery == nil { + return nil, fmt.Errorf("recovery partition not found") + } + if ep.Recovery.MountPoint == "" { + ep.Recovery.MountPoint = constants.RecoveryDir + } + target := ep.State.Disk // OEM partition is not a hard requirement @@ -432,6 +447,7 @@ func NewResetSpec(cfg v1.Config) (*v1.ResetSpec, error) { Source: v1.NewFileSrc(activeFile), FS: constants.LinuxImgFs, }, + State: installState, }, nil } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 1c7866a97d2..6690b09ca2b 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -242,7 +242,6 @@ var _ = Describe("Types", Label("types", "config"), func() { Expect(err).ShouldNot(HaveOccurred()) Expect(spec.Active.Source.Value()).To(Equal(constants.IsoBaseTree)) Expect(spec.Partitions.EFI.MountPoint).To(Equal(constants.EfiDir)) - Expect(spec.Partitions.Recovery).To(BeNil()) }) It("sets reset defaults on bios from non-squashed recovery", func() { // Set non-squashfs recovery image detection @@ -255,13 +254,11 @@ var _ = Describe("Types", Label("types", "config"), func() { spec, err := config.NewResetSpec(*c) Expect(err).ShouldNot(HaveOccurred()) Expect(spec.Active.Source.Value()).To(Equal(recoveryImg)) - Expect(spec.Partitions.Recovery).To(BeNil()) }) It("sets reset defaults on bios from unknown recovery", func() { spec, err := config.NewResetSpec(*c) Expect(err).ShouldNot(HaveOccurred()) Expect(spec.Active.Source.IsEmpty()).To(BeTrue()) - Expect(spec.Partitions.Recovery).To(BeNil()) }) }) Describe("Failures", func() { @@ -280,8 +277,14 @@ var _ = Describe("Types", Label("types", "config"), func() { // Set an empty disk for tests, otherwise reads the hosts hardware mainDisk := block.Disk{ - Name: "device", - Partitions: []*block.Partition{}, + Name: "device", + Partitions: []*block.Partition{ + { + Name: "device4", + FilesystemLabel: constants.StateLabel, + Type: "ext4", + }, + }, } ghwTest = v1mock.GhwMock{} ghwTest.AddDisk(mainDisk) @@ -295,7 +298,22 @@ var _ = Describe("Types", Label("types", "config"), func() { Expect(err).Should(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("reset can only be called from the recovery system")) }) + It("fails to set defaults if no recovery partition detected", func() { + bootedFrom = constants.SystemLabel + _, err := config.NewResetSpec(*c) + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("recovery partition not found")) + }) It("fails to set defaults if no state partition detected", func() { + mainDisk := block.Disk{ + Name: "device", + Partitions: []*block.Partition{}, + } + ghwTest = v1mock.GhwMock{} + ghwTest.AddDisk(mainDisk) + ghwTest.CreateDevices() + defer ghwTest.Clean() + bootedFrom = constants.SystemLabel _, err := config.NewResetSpec(*c) Expect(err).Should(HaveOccurred()) diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 160514e3cb8..2d66bc171f2 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -39,6 +39,7 @@ const ( RecoveryPartName = "recovery" StateLabel = "COS_STATE" StatePartName = "state" + InstallStateFile = "state.yaml" PersistentLabel = "COS_PERSISTENT" PersistentPartName = "persistent" OEMLabel = "COS_OEM" diff --git a/pkg/elemental/elemental.go b/pkg/elemental/elemental.go index 8b3b1d624d1..bfb06b5ae81 100644 --- a/pkg/elemental/elemental.go +++ b/pkg/elemental/elemental.go @@ -155,6 +155,26 @@ func (e Elemental) UnmountPartitions(parts v1.PartitionList) error { return nil } +// MountRWPartition mounts, or remounts if needed, a partition with RW permissions +func (e Elemental) MountRWPartition(part *v1.Partition) (umount func() error, err error) { + if mnt, _ := utils.IsMounted(e.config, part); mnt { + err = e.MountPartition(part, "remount", "rw") + if err != nil { + e.config.Logger.Errorf("failed mounting %s partition: %v", part.Name, err) + return nil, err + } + umount = func() error { return e.MountPartition(part, "remount", "ro") } + } else { + err = e.MountPartition(part, "rw") + if err != nil { + e.config.Logger.Error("failed mounting %s partition: %v", part.Name, err) + return nil, err + } + umount = func() error { return e.UnmountPartition(part) } + } + return umount, nil +} + // MountPartition mounts a partition with the given mount options func (e Elemental) MountPartition(part *v1.Partition, opts ...string) error { e.config.Logger.Debugf("Mounting partition %s", part.FilesystemLabel) @@ -262,49 +282,47 @@ func (e Elemental) CreateFileSystemImage(img *v1.Image) error { return nil } -// DeployImage will deploay the given image into the target. This method +// DeployImage will deploy the given image into the target. This method // creates the filesystem image file, mounts it and unmounts it as needed. -func (e *Elemental) DeployImage(img *v1.Image, leaveMounted bool) error { - var err error - +func (e *Elemental) DeployImage(img *v1.Image, leaveMounted bool) (info interface{}, err error) { target := img.MountPoint if !img.Source.IsFile() { if img.FS != cnst.SquashFs { err = e.CreateFileSystemImage(img) if err != nil { - return err + return nil, err } err = e.MountImage(img, "rw") if err != nil { - return err + return nil, err } } else { target = utils.GetTempDir(e.config, "") err := utils.MkdirAll(e.config.Fs, target, cnst.DirPerm) if err != nil { - return err + return nil, err } defer e.config.Fs.RemoveAll(target) // nolint:errcheck } } else { target = img.File } - err = e.DumpSource(target, img.Source) + info, err = e.DumpSource(target, img.Source) if err != nil { _ = e.UnmountImage(img) - return err + return nil, err } if !img.Source.IsFile() { err = utils.CreateDirStructure(e.config.Fs, target) if err != nil { - return err + return nil, err } if img.FS == cnst.SquashFs { squashOptions := append(cnst.GetDefaultSquashfsOptions(), e.config.SquashFsCompressionConfig...) err = utils.CreateSquashFS(e.config.Runner, e.config.Logger, target, img.File, squashOptions) if err != nil { - return err + return nil, err } } } else if img.Label != "" && img.FS != cnst.SquashFs { @@ -312,28 +330,27 @@ func (e *Elemental) DeployImage(img *v1.Image, leaveMounted bool) error { if err != nil { e.config.Logger.Errorf("Failed to apply label %s to $s", img.Label, img.File) _ = e.config.Fs.Remove(img.File) - return err + return nil, err } } if leaveMounted && img.Source.IsFile() { err = e.MountImage(img, "rw") if err != nil { - return err + return nil, err } } if !leaveMounted { err = e.UnmountImage(img) if err != nil { - return err + return nil, err } } - return nil + return info, nil } // DumpSource sets the image data according to the image source type -func (e *Elemental) DumpSource(target string, imgSrc *v1.ImageSource) error { // nolint:gocyclo +func (e *Elemental) DumpSource(target string, imgSrc *v1.ImageSource) (info interface{}, err error) { // nolint:gocyclo e.config.Logger.Infof("Copying %s source...", imgSrc.Value()) - var err error if imgSrc.IsDocker() { if e.config.Cosign { @@ -344,38 +361,38 @@ func (e *Elemental) DumpSource(target string, imgSrc *v1.ImageSource) error { // ) if err != nil { e.config.Logger.Errorf("Cosign verification failed: %s", out) - return err + return nil, err } } - err = e.config.Luet.Unpack(target, imgSrc.Value(), e.config.LocalImage) + info, err = e.config.Luet.Unpack(target, imgSrc.Value(), e.config.LocalImage) if err != nil { - return err + return nil, err } } else if imgSrc.IsDir() { excludes := []string{"/mnt", "/proc", "/sys", "/dev", "/tmp", "/host", "/run"} err = utils.SyncData(e.config.Logger, e.config.Fs, imgSrc.Value(), target, excludes...) if err != nil { - return err + return nil, err } } else if imgSrc.IsChannel() { - err = e.config.Luet.UnpackFromChannel(target, imgSrc.Value(), e.config.Repos...) + info, err = e.config.Luet.UnpackFromChannel(target, imgSrc.Value(), e.config.Repos...) if err != nil { - return err + return nil, err } } else if imgSrc.IsFile() { err := utils.MkdirAll(e.config.Fs, filepath.Dir(target), cnst.DirPerm) if err != nil { - return err + return nil, err } err = utils.CopyFile(e.config.Fs, imgSrc.Value(), target) if err != nil { - return err + return nil, err } } else { - return fmt.Errorf("unknown image source type") + return nil, fmt.Errorf("unknown image source type") } e.config.Logger.Infof("Finished copying %s into %s", imgSrc.Value(), target) - return nil + return info, nil } // CopyCloudConfig will check if there is a cloud init in the config and store it on the target diff --git a/pkg/elemental/elemental_test.go b/pkg/elemental/elemental_test.go index 7b64e6895ae..955f0413894 100644 --- a/pkg/elemental/elemental_test.go +++ b/pkg/elemental/elemental_test.go @@ -74,6 +74,70 @@ var _ = Describe("Elemental", Label("elemental"), func() { ) }) AfterEach(func() { cleanup() }) + Describe("MountRWPartition", Label("mount"), func() { + var el *elemental.Elemental + var parts v1.ElementalPartitions + BeforeEach(func() { + parts = conf.NewInstallElementalParitions() + + err := utils.MkdirAll(fs, "/some", cnst.DirPerm) + Expect(err).ToNot(HaveOccurred()) + _, err = fs.Create("/some/device") + Expect(err).ToNot(HaveOccurred()) + + parts.OEM.Path = "/dev/device1" + + el = elemental.NewElemental(config) + }) + + It("Mounts and umounts a partition with RW", func() { + umount, err := el.MountRWPartition(parts.OEM) + Expect(err).To(BeNil()) + lst, _ := mounter.List() + Expect(len(lst)).To(Equal(1)) + Expect(lst[0].Opts).To(Equal([]string{"rw"})) + + Expect(umount()).ShouldNot(HaveOccurred()) + lst, _ = mounter.List() + Expect(len(lst)).To(Equal(0)) + }) + It("Remounts a partition with RW", func() { + err := el.MountPartition(parts.OEM) + Expect(err).To(BeNil()) + lst, _ := mounter.List() + Expect(len(lst)).To(Equal(1)) + + umount, err := el.MountRWPartition(parts.OEM) + Expect(err).To(BeNil()) + lst, _ = mounter.List() + // fake mounter is not merging remounts it just appends + Expect(len(lst)).To(Equal(2)) + Expect(lst[1].Opts).To(Equal([]string{"remount", "rw"})) + + Expect(umount()).ShouldNot(HaveOccurred()) + lst, _ = mounter.List() + // Increased once more to remount read-onply + Expect(len(lst)).To(Equal(3)) + Expect(lst[2].Opts).To(Equal([]string{"remount", "ro"})) + }) + It("Fails to mount a partition", func() { + mounter.ErrorOnMount = true + _, err := el.MountRWPartition(parts.OEM) + Expect(err).Should(HaveOccurred()) + }) + It("Fails to remount a partition", func() { + err := el.MountPartition(parts.OEM) + Expect(err).To(BeNil()) + lst, _ := mounter.List() + Expect(len(lst)).To(Equal(1)) + + mounter.ErrorOnMount = true + _, err = el.MountRWPartition(parts.OEM) + Expect(err).Should(HaveOccurred()) + lst, _ = mounter.List() + Expect(len(lst)).To(Equal(1)) + }) + }) Describe("MountPartitions", Label("MountPartitions", "disk", "partition", "mount"), func() { var el *elemental.Elemental var parts v1.ElementalPartitions @@ -100,6 +164,15 @@ var _ = Describe("Elemental", Label("elemental"), func() { Expect(len(lst)).To(Equal(4)) }) + It("Mounts disk partitions excluding recovery", func() { + err := el.MountPartitions(parts.PartitionsByMountPoint(false, parts.Recovery)) + Expect(err).To(BeNil()) + lst, _ := mounter.List() + for _, i := range lst { + Expect(i.Path).NotTo(Equal("/dev/device3")) + } + }) + It("Fails if some partition resists to mount ", func() { mounter.ErrorOnMount = true err := el.MountPartitions(parts.PartitionsByMountPoint(false)) @@ -470,7 +543,8 @@ var _ = Describe("Elemental", Label("elemental"), func() { img.Source = v1.NewFileSrc(sourceImg) img.MountPoint = destDir mounter.ErrorOnMount = true - Expect(el.DeployImage(img, true)).NotTo(BeNil()) + _, err = el.DeployImage(img, true) + Expect(err).NotTo(BeNil()) }) It("Deploys a file image and fails to label it", func() { sourceImg := "/source.img" @@ -481,12 +555,14 @@ var _ = Describe("Elemental", Label("elemental"), func() { img.Source = v1.NewFileSrc(sourceImg) img.MountPoint = destDir cmdFail = "tune2fs" - Expect(el.DeployImage(img, true)).NotTo(BeNil()) + _, err = el.DeployImage(img, true) + Expect(err).NotTo(BeNil()) }) It("Fails creating the squashfs filesystem", func() { cmdFail = "mksquashfs" img.FS = constants.SquashFs - Expect(el.DeployImage(img, true)).NotTo(BeNil()) + _, err := el.DeployImage(img, true) + Expect(err).NotTo(BeNil()) Expect(runner.MatchMilestones([][]string{ { "mksquashfs", "/tmp/elemental-tmp", "/tmp/elemental/image.img", @@ -496,19 +572,23 @@ var _ = Describe("Elemental", Label("elemental"), func() { }) It("Fails formatting the image", func() { cmdFail = "mkfs.ext2" - Expect(el.DeployImage(img, true)).NotTo(BeNil()) + _, err := el.DeployImage(img, true) + Expect(err).NotTo(BeNil()) }) It("Fails mounting the image", func() { mounter.ErrorOnMount = true - Expect(el.DeployImage(img, true)).NotTo(BeNil()) + _, err := el.DeployImage(img, true) + Expect(err).NotTo(BeNil()) }) It("Fails copying the image if source does not exist", func() { img.Source = v1.NewDirSrc("/welp") - Expect(el.DeployImage(img, true)).NotTo(BeNil()) + _, err := el.DeployImage(img, true) + Expect(err).NotTo(BeNil()) }) It("Fails unmounting the image after copying", func() { mounter.ErrorOnUnmount = true - Expect(el.DeployImage(img, false)).NotTo(BeNil()) + _, err := el.DeployImage(img, false) + Expect(err).NotTo(BeNil()) }) }) Describe("DumpSource", Label("dump"), func() { @@ -526,30 +606,36 @@ var _ = Describe("Elemental", Label("elemental"), func() { It("Copies files from a directory source", func() { sourceDir, err := utils.TempDir(fs, "", "elemental") Expect(err).ShouldNot(HaveOccurred()) - Expect(e.DumpSource(destDir, v1.NewDirSrc(sourceDir))).To(BeNil()) + _, err = e.DumpSource(destDir, v1.NewDirSrc(sourceDir)) + Expect(err).To(BeNil()) }) It("Fails if source directory does not exist", func() { - Expect(e.DumpSource(destDir, v1.NewDirSrc("/welp"))).ToNot(BeNil()) + _, err := e.DumpSource(destDir, v1.NewDirSrc("/welp")) + Expect(err).ToNot(BeNil()) }) It("Unpacks a docker image to target", Label("docker"), func() { - Expect(e.DumpSource(destDir, v1.NewDockerSrc("docker/image:latest"))).To(BeNil()) + _, err := e.DumpSource(destDir, v1.NewDockerSrc("docker/image:latest")) + Expect(err).To(BeNil()) Expect(luet.UnpackCalled()).To(BeTrue()) }) It("Unpacks a docker image to target with cosign validation", Label("docker", "cosign"), func() { config.Cosign = true - Expect(e.DumpSource(destDir, v1.NewDockerSrc("docker/image:latest"))).To(BeNil()) + _, err := e.DumpSource(destDir, v1.NewDockerSrc("docker/image:latest")) + Expect(err).To(BeNil()) Expect(luet.UnpackCalled()).To(BeTrue()) Expect(runner.CmdsMatch([][]string{{"cosign", "verify", "docker/image:latest"}})) }) It("Fails cosign validation", Label("cosign"), func() { runner.ReturnError = errors.New("cosign error") config.Cosign = true - Expect(e.DumpSource(destDir, v1.NewDockerSrc("docker/image:latest"))).NotTo(BeNil()) + _, err := e.DumpSource(destDir, v1.NewDockerSrc("docker/image:latest")) + Expect(err).NotTo(BeNil()) Expect(runner.CmdsMatch([][]string{{"cosign", "verify", "docker/image:latest"}})) }) It("Fails to unpack a docker image to target", Label("docker"), func() { luet.OnUnpackError = true - Expect(e.DumpSource(destDir, v1.NewDockerSrc("docker/image:latest"))).NotTo(BeNil()) + _, err := e.DumpSource(destDir, v1.NewDockerSrc("docker/image:latest")) + Expect(err).NotTo(BeNil()) Expect(luet.UnpackCalled()).To(BeTrue()) }) It("Copies image file to target", func() { @@ -559,20 +645,24 @@ var _ = Describe("Elemental", Label("elemental"), func() { destFile := filepath.Join(destDir, "active.img") _, err = fs.Stat(destFile) Expect(err).NotTo(BeNil()) - Expect(e.DumpSource(destFile, v1.NewFileSrc(sourceImg))).To(BeNil()) + _, err = e.DumpSource(destFile, v1.NewFileSrc(sourceImg)) + Expect(err).To(BeNil()) _, err = fs.Stat(destFile) Expect(err).To(BeNil()) }) It("Fails to copy, source file is not present", func() { - Expect(e.DumpSource("whatever", v1.NewFileSrc("/source.img"))).NotTo(BeNil()) + _, err := e.DumpSource("whatever", v1.NewFileSrc("/source.img")) + Expect(err).NotTo(BeNil()) }) It("Unpacks from channel to target", func() { - Expect(e.DumpSource(destDir, v1.NewChannelSrc("some/package"))).To(BeNil()) + _, err := e.DumpSource(destDir, v1.NewChannelSrc("some/package")) + Expect(err).To(BeNil()) Expect(luet.UnpackChannelCalled()).To(BeTrue()) }) It("Fails to unpack from channel to target", func() { luet.OnUnpackFromChannelError = true - Expect(e.DumpSource(destDir, v1.NewChannelSrc("some/package"))).NotTo(BeNil()) + _, err := e.DumpSource(destDir, v1.NewChannelSrc("some/package")) + Expect(err).NotTo(BeNil()) Expect(luet.UnpackChannelCalled()).To(BeTrue()) }) }) diff --git a/pkg/luet/luet.go b/pkg/luet/luet.go index 70b2eff9b8b..025d779c8a2 100644 --- a/pkg/luet/luet.go +++ b/pkg/luet/luet.go @@ -157,30 +157,35 @@ func (l *Luet) InitPlugins() { } } -func (l Luet) Unpack(target string, image string, local bool) error { +func (l Luet) Unpack(target string, image string, local bool) (*v1.DockerImageMeta, error) { l.log.Infof("Unpacking a container image: %s", image) l.InitPlugins() + meta := &v1.DockerImageMeta{} if local { l.log.Infof("Using an image from local cache") info, err := docker.ExtractDockerImage(l.context, image, target) if err != nil { if strings.Contains(err.Error(), "reference does not exist") { - return errors.New("Container image does not exist locally") + return nil, errors.New("Container image does not exist locally") } - return err + return nil, err } l.log.Infof("Size: %s", units.BytesSize(float64(info.Target.Size))) + meta.Size = info.Target.Size + meta.Digest = info.Target.Digest.String() } else { l.log.Infof("Pulling an image from remote repository") info, err := docker.DownloadAndExtractDockerImage(l.context, image, target, l.auth, l.VerifyImageUnpack) if err != nil { - return err + return nil, err } l.log.Infof("Pulled: %s %s", info.Target.Digest, info.Name) l.log.Infof("Size: %s", units.BytesSize(float64(info.Target.Size))) + meta.Size = info.Target.Size + meta.Digest = info.Target.Digest.String() } - return nil + return meta, nil } // initLuetRepository returns a Luet repository from a given v1.Repository. It runs heuristics @@ -245,7 +250,7 @@ func (l Luet) initLuetRepository(repo v1.Repository) (luetTypes.LuetRepository, // UnpackFromChannel unpacks/installs a package from the release channel into the target dir by leveraging the // luet install action to install to a local dir -func (l Luet) UnpackFromChannel(target string, pkg string, repositories ...v1.Repository) error { +func (l Luet) UnpackFromChannel(target string, pkg string, repositories ...v1.Repository) (*v1.ChannelImageMeta, error) { var toInstall luetTypes.Packages l.InitPlugins() @@ -263,7 +268,7 @@ func (l Luet) UnpackFromChannel(target string, pkg string, repositories ...v1.Re repo, err := l.initLuetRepository(r) if err != nil { - return err + return nil, err } repos = append(repos, repo) } @@ -287,8 +292,38 @@ func (l Luet) UnpackFromChannel(target string, pkg string, repositories ...v1.Re Database: database.NewInMemoryDatabase(false), Target: target, } + err := inst.Install(toInstall, system) + if err != nil { + return nil, err + } + pkgs, err := system.Database.FindPackageMatch(pkg) + if err != nil { + l.log.Error(err.Error()) + return nil, err + } + + var meta *v1.ChannelImageMeta + if len(pkgs) > 0 { + meta = &v1.ChannelImageMeta{ + Category: pkgs[0].GetCategory(), + Name: pkgs[0].GetName(), + Version: pkgs[0].GetVersion(), + FingerPrint: pkgs[0].GetFingerPrint(), + } + //TODO: ideally we should only include the repository being used + for _, r := range repos { + meta.Repos = append(meta.Repos, v1.Repository{ + Name: r.Name, + Priority: r.Priority, + URI: r.Urls[0], + Type: r.Type, + Arch: r.Arch, + ReferenceID: r.ReferenceID, + }) + } + } - return inst.Install(toInstall, system) + return meta, nil } func (l Luet) parsePackage(p string) *luetTypes.Package { diff --git a/pkg/luet/luet_test.go b/pkg/luet/luet_test.go index ed04479dc8b..8364fb95096 100644 --- a/pkg/luet/luet_test.go +++ b/pkg/luet/luet_test.go @@ -74,12 +74,14 @@ var _ = Describe("Types", Label("luet", "types"), func() { Describe("Luet", func() { It("Fails to unpack without root privileges", Label("unpack"), func() { image := "quay.io/costoolkit/releases-green:cloud-config-system-0.11-1" - Expect(l.Unpack(target, image, false)).NotTo(BeNil()) + _, err := l.Unpack(target, image, false) + Expect(err).NotTo(BeNil()) }) It("Check that luet can unpack the remote image", Label("unpack", "root"), func() { image := "registry.opensuse.org/opensuse/redis" // Check that luet can unpack the remote image - Expect(l.Unpack(target, image, false)).To(BeNil()) + _, err := l.Unpack(target, image, false) + Expect(err).To(BeNil()) }) It("Check that luet can unpack the local image", Label("unpack", "root"), func() { image := "docker.io/library/alpine" @@ -91,43 +93,46 @@ var _ = Describe("Types", Label("luet", "types"), func() { defer reader.Close() _, _ = io.Copy(ioutil.Discard, reader) // Check that luet can unpack the local image - Expect(l.Unpack(target, image, true)).To(BeNil()) + _, err = l.Unpack(target, image, true) + Expect(err).To(BeNil()) }) Describe("UnpackFromChannel", Label("unpack", "channel"), func() { It("Check that luet can unpack from channel", Label("root"), func() { repo := v1.Repository{URI: "quay.io/costoolkit/releases-teal", Arch: constants.Archx86} - Expect(l.UnpackFromChannel(target, "utils/gomplate", repo)).To(BeNil()) + _, err := l.UnpackFromChannel(target, "utils/gomplate", repo) + Expect(err).To(BeNil()) }) It("Fails to unpack with a repository with no URI", func() { repo := v1.Repository{Arch: constants.Archx86} - err := l.UnpackFromChannel(target, "utils/gomplate", repo) + _, err := l.UnpackFromChannel(target, "utils/gomplate", repo) Expect(err.Error()).To(ContainSubstring("no URI is provided")) Expect(err).NotTo(BeNil()) }) It("Fails to unpack with a dir repository that doesnt have anything", func() { dir, _ := utils.TempDir(fs, "", "") repo := v1.Repository{URI: dir} - err := l.UnpackFromChannel(target, "utils/gomplate", repo) + _, err := l.UnpackFromChannel(target, "utils/gomplate", repo) Expect(err).NotTo(BeNil()) }) It("Fails to unpack with a http repository that doesnt exists", func() { repo := v1.Repository{URI: "http://jojo.bizarre.adventure"} - err := l.UnpackFromChannel(target, "utils/gomplate", repo) + _, err := l.UnpackFromChannel(target, "utils/gomplate", repo) Expect(err).NotTo(BeNil()) }) It("Fails to unpack with a strange repository that cant get the type for", func() { repo := v1.Repository{URI: "is:this:real:life", Arch: constants.Archx86} - err := l.UnpackFromChannel(target, "utils/gomplate", repo) + _, err := l.UnpackFromChannel(target, "utils/gomplate", repo) Expect(err.Error()).To(ContainSubstring("Invalid Luet repository URI")) Expect(err).NotTo(BeNil()) }) It("Fails to unpack from channel without root privileges", func() { repo := v1.Repository{URI: "quay.io/costoolkit/releases-teal"} - Expect(l.UnpackFromChannel(target, "utils/gomplate", repo)).ToNot(BeNil()) + _, err := l.UnpackFromChannel(target, "utils/gomplate", repo) + Expect(err).ToNot(BeNil()) }) It("Fails to unpack from channel without matching arch", func() { repo := v1.Repository{URI: "quay.io/costoolkit/releases-teal-arm-64", Arch: constants.ArchArm64} - err := l.UnpackFromChannel(target, "utils/gomplate", repo) + _, err := l.UnpackFromChannel(target, "utils/gomplate", repo) Expect(err.Error()).To(ContainSubstring("package 'utils/gomplate->=0' not found")) Expect(err).NotTo(BeNil()) }) diff --git a/pkg/types/v1/common.go b/pkg/types/v1/common.go index 4ddd17f698d..58a9b70a21a 100644 --- a/pkg/types/v1/common.go +++ b/pkg/types/v1/common.go @@ -21,6 +21,8 @@ import ( "net/url" "path/filepath" + "gopkg.in/yaml.v3" + "github.com/distribution/distribution/reference" ) @@ -68,6 +70,21 @@ func (i ImageSource) IsEmpty() bool { return false } +func (i ImageSource) String() string { + if i.IsEmpty() { + return "" + } + return fmt.Sprintf("%s://%s", i.srcType, i.source) +} + +func (i ImageSource) MarshalYAML() (interface{}, error) { + return i.String(), nil +} + +func (i *ImageSource) UnmarshalYAML(value *yaml.Node) error { + return i.updateFromURI(value.Value) +} + func (i *ImageSource) CustomUnmarshal(data interface{}) (bool, error) { src, ok := data.(string) if !ok { @@ -78,17 +95,6 @@ func (i *ImageSource) CustomUnmarshal(data interface{}) (bool, error) { } func (i *ImageSource) updateFromURI(uri string) error { - eURI := i.parseURI(uri) - if eURI != nil { - eRef := i.parseImageReference(uri) - if eRef != nil { - return fmt.Errorf("%s and %s", eURI.Error(), eRef.Error()) - } - } - return nil -} - -func (i *ImageSource) parseURI(uri string) error { u, err := url.Parse(uri) if err != nil { return err @@ -100,8 +106,7 @@ func (i *ImageSource) parseURI(uri string) error { } switch scheme { case oci, docker: - i.srcType = oci - i.source = value + return i.parseImageReference(value) case channel: i.srcType = channel i.source = value @@ -112,7 +117,7 @@ func (i *ImageSource) parseURI(uri string) error { i.srcType = file i.source = value default: - return fmt.Errorf("unknown source type for %s", uri) + return i.parseImageReference(uri) } return nil } diff --git a/pkg/types/v1/common_test.go b/pkg/types/v1/common_test.go index f8f16a6354a..f982ac711e1 100644 --- a/pkg/types/v1/common_test.go +++ b/pkg/types/v1/common_test.go @@ -41,6 +41,14 @@ var _ = Describe("Types", Label("types", "common"), func() { Expect(o.IsChannel()).To(BeTrue()) o = v1.NewEmptySrc() Expect(o.IsEmpty()).To(BeTrue()) + o, err := v1.NewSrcFromURI("registry.company.org/image") + Expect(o.IsDocker()).To(BeTrue()) + Expect(err).ShouldNot(HaveOccurred()) + Expect(o.Value()).To(Equal("registry.company.org/image:latest")) + o, err = v1.NewSrcFromURI("oci://registry.company.org/image:tag") + Expect(o.IsDocker()).To(BeTrue()) + Expect(err).ShouldNot(HaveOccurred()) + Expect(o.Value()).To(Equal("registry.company.org/image:tag")) }) It("unmarshals each type as expected", func() { o := v1.NewEmptySrc() @@ -76,6 +84,27 @@ var _ = Describe("Types", Label("types", "common"), func() { Expect(o.IsDocker()).To(BeTrue()) Expect(o.Value()).To(Equal("registry.company.org/my/image:tag")) }) + It("convertion to string URI works are expected", func() { + o := v1.NewDirSrc("/some/dir") + Expect(o.IsDir()).To(BeTrue()) + Expect(o.String()).To(Equal("dir:///some/dir")) + o = v1.NewFileSrc("filename") + Expect(o.IsFile()).To(BeTrue()) + Expect(o.String()).To(Equal("file://filename")) + o = v1.NewDockerSrc("container/image") + Expect(o.IsDocker()).To(BeTrue()) + Expect(o.String()).To(Equal("oci://container/image")) + o = v1.NewChannelSrc("luetPackage") + Expect(o.IsChannel()).To(BeTrue()) + Expect(o.String()).To(Equal("channel://luetPackage")) + o = v1.NewEmptySrc() + Expect(o.IsEmpty()).To(BeTrue()) + Expect(o.String()).To(Equal("")) + o, err := v1.NewSrcFromURI("registry.company.org/image") + Expect(o.IsDocker()).To(BeTrue()) + Expect(err).ShouldNot(HaveOccurred()) + Expect(o.String()).To(Equal("oci://registry.company.org/image:latest")) + }) It("fails to unmarshal non string types", func() { o := v1.NewEmptySrc() _, err := o.CustomUnmarshal(map[string]string{}) diff --git a/pkg/types/v1/config.go b/pkg/types/v1/config.go index e19a75a095d..ada02e1089b 100644 --- a/pkg/types/v1/config.go +++ b/pkg/types/v1/config.go @@ -22,6 +22,7 @@ import ( "sort" "github.com/rancher/elemental-cli/pkg/constants" + "gopkg.in/yaml.v3" "k8s.io/mount-utils" ) @@ -56,6 +57,42 @@ type Config struct { SquashFsNoCompression bool `yaml:"squash-no-compression,omitempty" mapstructure:"squash-no-compression"` } +// WriteInstallState writes the state.yaml file to the given state and recovery paths +func (c Config) WriteInstallState(i *InstallState, statePath, recoveryPath string) error { + data, err := yaml.Marshal(i) + if err != nil { + return err + } + + data = append([]byte("# Autogenerated file by elemental client, do not edit\n\n"), data...) + + err = c.Fs.WriteFile(statePath, data, constants.FilePerm) + if err != nil { + return err + } + + err = c.Fs.WriteFile(recoveryPath, data, constants.FilePerm) + if err != nil { + return err + } + + return nil +} + +// LoadInstallState loads the state.yaml file and unmarshals it to an InstallState object +func (c Config) LoadInstallState() (*InstallState, error) { + installState := &InstallState{} + data, err := c.Fs.ReadFile(filepath.Join(constants.RunningStateDir, constants.InstallStateFile)) + if err != nil { + return nil, err + } + err = yaml.Unmarshal(data, installState) + if err != nil { + return nil, err + } + return installState, nil +} + // Sanitize checks the consistency of the struct, returns error // if unsolvable inconsistencies are found func (c *Config) Sanitize() error { @@ -145,6 +182,7 @@ type ResetSpec struct { Target string Efi bool GrubConf string + State *InstallState } // Sanitize checks the consistency of the struct, returns error @@ -166,6 +204,7 @@ type UpgradeSpec struct { GrubDefEntry string `yaml:"grub-entry-name,omitempty" mapstructure:"grub-entry-name"` Passive Image Partitions ElementalPartitions + State *InstallState } // Sanitize checks the consistency of the struct, returns error @@ -297,24 +336,34 @@ func NewElementalPartitionsFromList(pl PartitionList) ElementalPartitions { // PartitionsByInstallOrder sorts partitions according to the default layout // nil partitons are ignored -func (ep ElementalPartitions) PartitionsByInstallOrder() PartitionList { +func (ep ElementalPartitions) PartitionsByInstallOrder(excludes ...*Partition) PartitionList { partitions := PartitionList{} - if ep.BIOS != nil { + + inExcludes := func(part *Partition, list ...*Partition) bool { + for _, p := range list { + if part == p { + return true + } + } + return false + } + + if ep.BIOS != nil && !inExcludes(ep.BIOS, excludes...) { partitions = append(partitions, ep.BIOS) } - if ep.EFI != nil { + if ep.EFI != nil && !inExcludes(ep.EFI, excludes...) { partitions = append(partitions, ep.EFI) } - if ep.OEM != nil { + if ep.OEM != nil && !inExcludes(ep.OEM, excludes...) { partitions = append(partitions, ep.OEM) } - if ep.Recovery != nil { + if ep.Recovery != nil && !inExcludes(ep.Recovery, excludes...) { partitions = append(partitions, ep.Recovery) } - if ep.State != nil { + if ep.State != nil && !inExcludes(ep.State, excludes...) { partitions = append(partitions, ep.State) } - if ep.Persistent != nil { + if ep.Persistent != nil && !inExcludes(ep.Persistent, excludes...) { partitions = append(partitions, ep.Persistent) } return partitions @@ -322,12 +371,12 @@ func (ep ElementalPartitions) PartitionsByInstallOrder() PartitionList { // PartitionsByMountPoint sorts partitions according to its mountpoint, ignores nil // partitions or partitions with an empty mountpoint -func (ep ElementalPartitions) PartitionsByMountPoint(descending bool) PartitionList { +func (ep ElementalPartitions) PartitionsByMountPoint(descending bool, excludes ...*Partition) PartitionList { mountPointKeys := map[string]*Partition{} mountPoints := []string{} partitions := PartitionList{} - for _, p := range ep.PartitionsByInstallOrder() { + for _, p := range ep.PartitionsByInstallOrder(excludes...) { if p.MountPoint != "" { mountPointKeys[p.MountPoint] = p mountPoints = append(mountPoints, p.MountPoint) @@ -439,3 +488,77 @@ type RawDiskPackage struct { Name string `yaml:"name,omitempty"` Target string `yaml:"target,omitempty"` } + +// InstallState tracks the installation data of the whole system +type InstallState struct { + Date string `yaml:"date,omitempty"` + Partitions map[string]*PartitionState `yaml:",omitempty,inline"` +} + +// PartState tracks installation data of a partition +type PartitionState struct { + FSLabel string `yaml:"label,omitempty"` + Images map[string]*ImageState `yaml:",omitempty,inline"` +} + +// ImageState represents data of a deployed image +type ImageState struct { + Source *ImageSource `yaml:"source,omitempty"` + SourceMetadata interface{} `yaml:"source-metadata,omitempty"` + Label string `yaml:"label,omitempty"` + FS string `yaml:"fs,omitempty"` +} + +func (i *ImageState) UnmarshalYAML(value *yaml.Node) error { + type iState ImageState + var srcMeta *yaml.Node + var err error + + err = value.Decode((*iState)(i)) + if err != nil { + return err + } + + if i.SourceMetadata != nil { + for i, n := range value.Content { + if n.Value == "source-metadata" && n.Kind == yaml.ScalarNode { + if len(value.Content) >= i+1 && value.Content[i+1].Kind == yaml.MappingNode { + srcMeta = value.Content[i+1] + } + break + } + } + } + + i.SourceMetadata = nil + if srcMeta != nil { + d := &DockerImageMeta{} + err = srcMeta.Decode(d) + if err == nil && (d.Digest != "" || d.Size != 0) { + i.SourceMetadata = d + return nil + } + c := &ChannelImageMeta{} + err = srcMeta.Decode(c) + if err == nil && c.Name != "" { + i.SourceMetadata = c + } + } + + return err +} + +// DockerImageMeta represents metadata of a docker container image type +type DockerImageMeta struct { + Digest string `yaml:"digest,omitempty"` + Size int64 `yaml:"size,omitempty"` +} + +// ChannelImageMeta represents metadata of a channel image type +type ChannelImageMeta struct { + Category string `yaml:"category,omitempty"` + Name string `yaml:"name,omitempty"` + Version string `yaml:"version,omitempty"` + FingerPrint string `yaml:"finger-print,omitempty"` + Repos []Repository `yaml:"repositories,omitempty"` +} diff --git a/pkg/types/v1/config.go.orig b/pkg/types/v1/config.go.orig new file mode 100644 index 00000000000..27d0d1daa6d --- /dev/null +++ b/pkg/types/v1/config.go.orig @@ -0,0 +1,573 @@ +/* +Copyright © 2022 SUSE LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "fmt" + "path/filepath" + "sort" + + "github.com/rancher/elemental-cli/pkg/constants" + "gopkg.in/yaml.v3" + "k8s.io/mount-utils" +) + +const ( + GPT = "gpt" + BIOS = "bios" + MSDOS = "msdos" + EFI = "efi" + esp = "esp" + bios = "bios_grub" + boot = "boot" +) + +// Config is the struct that includes basic and generic configuration of elemental binary runtime. +// It mostly includes the interfaces used around many methods in elemental code +type Config struct { + Logger Logger + Fs FS + Mounter mount.Interface + Runner Runner + Syscall SyscallInterface + CloudInitRunner CloudInitRunner + Luet LuetInterface + Client HTTPClient + Cosign bool `yaml:"cosign,omitempty" mapstructure:"cosign"` + Verify bool `yaml:"verify,omitempty" mapstructure:"verify"` + CosignPubKey string `yaml:"cosign-key,omitempty" mapstructure:"cosign-key"` + LocalImage bool `yaml:"local,omitempty" mapstructure:"local"` + Repos []Repository `yaml:"repositories,omitempty" mapstructure:"repositories"` + Arch string `yaml:"arch,omitempty" mapstructure:"arch"` + SquashFsCompressionConfig []string `yaml:"squash-compression,omitempty" mapstructure:"squash-compression"` + SquashFsNoCompression bool `yaml:"squash-no-compression,omitempty" mapstructure:"squash-no-compression"` +} + +// WriteInstallState writes the state.yaml file to the given state and recovery paths. On an empty path does nothing +func (c Config) WriteInstallState(i *InstallState, statePath, recoveryPath string) error { + data, err := yaml.Marshal(i) + if err != nil { + return err + } + + data = append([]byte("# Autogenerated file by elemental client, do not edit\n\n"), data...) + + if statePath != "" { + err = c.Fs.WriteFile(statePath, data, constants.FilePerm) + if err != nil { + return err + } + } + if recoveryPath != "" { + err = c.Fs.WriteFile(recoveryPath, data, constants.FilePerm) + if err != nil { + return err + } + } + return nil +} + +// LoadInstallState loads the state.yaml file and unmarshals it to an InstallState object +func (c Config) LoadInstallState() (*InstallState, error) { + installState := &InstallState{} + data, err := c.Fs.ReadFile(filepath.Join(constants.RunningStateDir, constants.InstallStateFile)) + if err != nil { + return nil, err + } + err = yaml.Unmarshal(data, installState) + if err != nil { + return nil, err + } + return installState, nil +} + +// Sanitize checks the consistency of the struct, returns error +// if unsolvable inconsistencies are found +func (c *Config) Sanitize() error { + // Set Luet plugins, we only use the mtree plugin for now + if c.Verify { + c.Luet.SetPlugins(constants.LuetMtreePlugin) + } + // If no squashcompression is set, zero the compression parameters + // By default on NewConfig the SquashFsCompressionConfig is set to the default values, and then override + // on config unmarshall. + if c.SquashFsNoCompression { + c.SquashFsCompressionConfig = []string{} + } + // Ensure luet arch matches Config.Arch + c.Luet.SetArch(c.Arch) + return nil +} + +type RunConfig struct { + Strict bool `yaml:"strict,omitempty" mapstructure:"strict"` + Reboot bool `yaml:"reboot,omitempty" mapstructure:"reboot"` + PowerOff bool `yaml:"poweroff,omitempty" mapstructure:"poweroff"` + CloudInitPaths []string `yaml:"cloud-init-paths,omitempty" mapstructure:"cloud-init-paths"` + EjectCD bool `yaml:"eject-cd,omitempty" mapstructure:"eject-cd"` + + // 'inline' and 'squash' labels ensure config fields + // are embedded from a yaml and map PoV + Config `yaml:",inline" mapstructure:",squash"` +} + +// Sanitize checks the consistency of the struct, returns error +//if unsolvable inconsistencies are found +func (r *RunConfig) Sanitize() error { + return r.Config.Sanitize() +} + +// InstallSpec struct represents all the installation action details +type InstallSpec struct { + Target string `yaml:"target,omitempty" mapstructure:"target"` + Firmware string `yaml:"firmware,omitempty" mapstructure:"firmware"` + PartTable string `yaml:"part-table,omitempty" mapstructure:"part-table"` + Partitions ElementalPartitions `yaml:"partitions,omitempty" mapstructure:"partitions"` + NoFormat bool `yaml:"no-format,omitempty" mapstructure:"no-format"` + Force bool `yaml:"force,omitempty" mapstructure:"force"` + CloudInit string `yaml:"cloud-init,omitempty" mapstructure:"cloud-init"` + Iso string `yaml:"iso,omitempty" mapstructure:"iso"` + GrubDefEntry string `yaml:"grub-entry-name,omitempty" mapstructure:"grub-entry-name"` + Tty string `yaml:"tty,omitempty" mapstructure:"tty"` + Active Image `yaml:"system,omitempty" mapstructure:"system"` + Recovery Image `yaml:"recovery-system,omitempty" mapstructure:"recovery-system"` + Passive Image + GrubConf string +} + +// Sanitize checks the consistency of the struct, returns error +// if unsolvable inconsistencies are found +func (i *InstallSpec) Sanitize() error { + if i.Active.Source.IsEmpty() && i.Iso == "" { + return fmt.Errorf("undefined system source to install") + } + if i.Partitions.State == nil || i.Partitions.State.MountPoint == "" { + return fmt.Errorf("undefined state partition") + } + // Set the image file name depending on the filesystem + recoveryMnt := constants.RecoveryDir + if i.Partitions.Recovery != nil && i.Partitions.Recovery.MountPoint != "" { + recoveryMnt = i.Partitions.Recovery.MountPoint + } + if i.Recovery.FS == constants.SquashFs { + i.Recovery.File = filepath.Join(recoveryMnt, "cOS", constants.RecoverySquashFile) + } else { + i.Recovery.File = filepath.Join(recoveryMnt, "cOS", constants.RecoveryImgFile) + } + return i.Partitions.SetFirmwarePartitions(i.Firmware, i.PartTable) +} + +// ResetSpec struct represents all the reset action details +type ResetSpec struct { + FormatPersistent bool `yaml:"reset-persistent,omitempty" mapstructure:"reset-persistent"` + FormatOEM bool `yaml:"reset-oem,omitempty" mapstructure:"reset-oem"` + + GrubDefEntry string `yaml:"grub-entry-name,omitempty" mapstructure:"grub-entry-name"` + Tty string `yaml:"tty,omitempty" mapstructure:"tty"` + Active Image `yaml:"system,omitempty" mapstructure:"system"` + Passive Image + Partitions ElementalPartitions + Target string + Efi bool + GrubConf string + State *InstallState +} + +// Sanitize checks the consistency of the struct, returns error +// if unsolvable inconsistencies are found +func (r *ResetSpec) Sanitize() error { + if r.Active.Source.IsEmpty() { + return fmt.Errorf("undefined system source to reset to") + } + if r.Partitions.State == nil || r.Partitions.State.MountPoint == "" { + return fmt.Errorf("undefined state partition") + } + return nil +} + +type UpgradeSpec struct { + RecoveryUpgrade bool `yaml:"recovery,omitempty" mapstructure:"recovery"` + 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"` + Passive Image + Partitions ElementalPartitions + State *InstallState +} + +// Sanitize checks the consistency of the struct, returns error +// if unsolvable inconsistencies are found +func (u *UpgradeSpec) Sanitize() error { + if u.RecoveryUpgrade { + if u.Partitions.Recovery == nil || u.Partitions.Recovery.MountPoint == "" { + return fmt.Errorf("undefined recovery partition") + } + if u.Recovery.Source.IsEmpty() { + return fmt.Errorf("undefined upgrade source") + } + } else { + if u.Partitions.State == nil || u.Partitions.State.MountPoint == "" { + return fmt.Errorf("undefined state partition") + } + if u.Active.Source.IsEmpty() { + return fmt.Errorf("undefined upgrade source") + } + } + return nil +} + +// Partition struct represents a partition with its commonly configurable values, size in MiB +type Partition struct { + Name string + FilesystemLabel string `yaml:"label,omitempty" mapstructure:"label"` + Size uint `yaml:"size,omitempty" mapstructure:"size"` + FS string `yaml:"fs,omitempty" mapstrcuture:"fs"` + Flags []string `yaml:"flags,omitempty" mapstrcuture:"flags"` + MountPoint string + Path string + Disk string +} + +type PartitionList []*Partition + +// GetByName gets a partitions by its name from the PartitionList +func (pl PartitionList) GetByName(name string) *Partition { + for _, p := range pl { + if p.Name == name { + return p + } + } + return nil +} + +// GetByLabel gets a partition by its label from the PartitionList +func (pl PartitionList) GetByLabel(label string) *Partition { + for _, p := range pl { + if p.FilesystemLabel == label { + return p + } + } + return nil +} + +type ElementalPartitions struct { + BIOS *Partition + EFI *Partition + OEM *Partition `yaml:"oem,omitempty" mapstructure:"oem"` + Recovery *Partition `yaml:"recovery,omitempty" mapstructure:"recovery"` + State *Partition `yaml:"state,omitempty" mapstructure:"state"` + Persistent *Partition `yaml:"persistent,omitempty" mapstructure:"persistent"` +} + +// SetFirmwarePartitions sets firmware partitions for a given firmware and partition table type +func (ep *ElementalPartitions) SetFirmwarePartitions(firmware string, partTable string) error { + if firmware == EFI && partTable == GPT { + ep.EFI = &Partition{ + FilesystemLabel: constants.EfiLabel, + Size: constants.EfiSize, + Name: constants.EfiPartName, + FS: constants.EfiFs, + MountPoint: constants.EfiDir, + Flags: []string{esp}, + } + ep.BIOS = nil + } else if firmware == BIOS && partTable == GPT { + ep.BIOS = &Partition{ + FilesystemLabel: "", + Size: constants.BiosSize, + Name: constants.BiosPartName, + FS: "", + MountPoint: "", + Flags: []string{bios}, + } + ep.EFI = nil + } else { + if ep.State == nil { + return fmt.Errorf("nil state partition") + } + ep.State.Flags = []string{boot} + ep.EFI = nil + ep.BIOS = nil + } + return nil +} + +// NewElementalPartitionsFromList fills an ElementalPartitions instance from given +// partitions list. First tries to match partitions by partition label, if not, +// it tries to match partitions by default filesystem label +// TODO find a way to map custom labels when partition labels are not available +func NewElementalPartitionsFromList(pl PartitionList) ElementalPartitions { + ep := ElementalPartitions{} + ep.BIOS = pl.GetByName(constants.BiosPartName) + ep.EFI = pl.GetByName(constants.EfiPartName) + if ep.EFI == nil { + ep.EFI = pl.GetByLabel(constants.EfiLabel) + } + ep.OEM = pl.GetByName(constants.OEMPartName) + if ep.OEM == nil { + ep.OEM = pl.GetByLabel(constants.OEMLabel) + } + ep.Recovery = pl.GetByName(constants.RecoveryPartName) + if ep.Recovery == nil { + ep.Recovery = pl.GetByLabel(constants.RecoveryLabel) + } + ep.State = pl.GetByName(constants.StatePartName) + if ep.State == nil { + ep.State = pl.GetByLabel(constants.StateLabel) + } + ep.Persistent = pl.GetByName(constants.PersistentPartName) + if ep.Persistent == nil { + ep.Persistent = pl.GetByLabel(constants.PersistentLabel) + } + return ep +} + +// PartitionsByInstallOrder sorts partitions according to the default layout +// nil partitons are ignored +func (ep ElementalPartitions) PartitionsByInstallOrder(excludes ...*Partition) PartitionList { + partitions := PartitionList{} + + inExcludes := func(part *Partition, list ...*Partition) bool { + for _, p := range list { + if part == p { + return true + } + } + return false + } + + if ep.BIOS != nil && !inExcludes(ep.BIOS, excludes...) { + partitions = append(partitions, ep.BIOS) + } + if ep.EFI != nil && !inExcludes(ep.EFI, excludes...) { + partitions = append(partitions, ep.EFI) + } + if ep.OEM != nil && !inExcludes(ep.OEM, excludes...) { + partitions = append(partitions, ep.OEM) + } + if ep.Recovery != nil && !inExcludes(ep.Recovery, excludes...) { + partitions = append(partitions, ep.Recovery) + } + if ep.State != nil && !inExcludes(ep.State, excludes...) { + partitions = append(partitions, ep.State) + } + if ep.Persistent != nil && !inExcludes(ep.Persistent, excludes...) { + partitions = append(partitions, ep.Persistent) + } + return partitions +} + +// PartitionsByMountPoint sorts partitions according to its mountpoint, ignores nil +// partitions or partitions with an empty mountpoint +func (ep ElementalPartitions) PartitionsByMountPoint(descending bool, excludes ...*Partition) PartitionList { + mountPointKeys := map[string]*Partition{} + mountPoints := []string{} + partitions := PartitionList{} + + for _, p := range ep.PartitionsByInstallOrder(excludes...) { + if p.MountPoint != "" { + mountPointKeys[p.MountPoint] = p + mountPoints = append(mountPoints, p.MountPoint) + } + } + + if descending { + sort.Sort(sort.Reverse(sort.StringSlice(mountPoints))) + } else { + sort.Strings(mountPoints) + } + + for _, mnt := range mountPoints { + partitions = append(partitions, mountPointKeys[mnt]) + } + return partitions +} + +// Image struct represents a file system image with its commonly configurable values, size in MiB +type Image struct { + File string + Label string `yaml:"label,omitempty" mapstructure:"label"` + Size uint `yaml:"size,omitempty" mapstructure:"size"` + FS string `yaml:"fs,omitempty" mapstructure:"fs"` + Source *ImageSource `yaml:"uri,omitempty" mapstructure:"uri"` + MountPoint string + LoopDevice string +} + +// LiveISO represents the configurations needed for a live ISO image +type LiveISO struct { + RootFS []*ImageSource `yaml:"rootfs,omitempty" mapstructure:"rootfs"` + UEFI []*ImageSource `yaml:"uefi,omitempty" mapstructure:"uefi"` + Image []*ImageSource `yaml:"image,omitempty" mapstructure:"image"` + Label string `yaml:"label,omitempty" mapstructure:"label"` + BootCatalog string `yaml:"boot-catalog,omitempty" mapstructure:"boot-catalog"` + BootFile string `yaml:"boot-file,omitempty" mapstructure:"boot-file"` + HybridMBR string `yaml:"hybrid-mbr,omitempty" mapstructure:"hybrid-mbr,omitempty"` +} + +// Sanitize checks the consistency of the struct, returns error +// if unsolvable inconsistencies are found +func (i *LiveISO) Sanitize() error { + for _, src := range i.RootFS { + if src == nil { + return fmt.Errorf("wrong name of source package for rootfs") + } + } + for _, src := range i.UEFI { + if src == nil { + return fmt.Errorf("wrong name of source package for uefi") + } + } + for _, src := range i.Image { + if src == nil { + return fmt.Errorf("wrong name of source package for image") + } + } + + return nil +} + +// Repository represents the basic configuration for a package repository +type Repository struct { + Name string `yaml:"name,omitempty" mapstructure:"name"` + Priority int `yaml:"priority,omitempty" mapstructure:"priority"` + URI string `yaml:"uri,omitempty" mapstructure:"uri"` + Type string `yaml:"type,omitempty" mapstructure:"type"` + Arch string `yaml:"arch,omitempty" mapstructure:"arch"` + ReferenceID string `yaml:"reference,omitempty" mapstructure:"reference"` +} + +// BuildConfig represents the config we need for building isos, raw images, artifacts +type BuildConfig struct { + Date bool `yaml:"date,omitempty" mapstructure:"date"` + Name string `yaml:"name,omitempty" mapstructure:"name"` + OutDir string `yaml:"output,omitempty" mapstructure:"output"` + + // 'inline' and 'squash' labels ensure config fields + // are embedded from a yaml and map PoV + Config `yaml:",inline" mapstructure:",squash"` +} + +// Sanitize checks the consistency of the struct, returns error +//if unsolvable inconsistencies are found +func (b *BuildConfig) Sanitize() error { + return b.Config.Sanitize() +} + +type RawDisk struct { + X86_64 *RawDiskArchEntry `yaml:"x86_64,omitempty" mapstructure:"x86_64"` //nolint:revive + Arm64 *RawDiskArchEntry `yaml:"arm64,omitempty" mapstructure:"arm64"` +} + +// Sanitize checks the consistency of the struct, returns error +// if unsolvable inconsistencies are found +func (d *RawDisk) Sanitize() error { + // No checks for the time being + return nil +} + +// RawDiskArchEntry represents an arch entry in raw_disk +type RawDiskArchEntry struct { + Packages []RawDiskPackage `yaml:"packages,omitempty"` +} + +// RawDiskPackage represents a package entry for raw_disk, with a package name and a target to install to +type RawDiskPackage struct { + Name string `yaml:"name,omitempty"` + Target string `yaml:"target,omitempty"` +} + +// InstallState tracks the installation data of the whole system +type InstallState struct { + Date string `yaml:"date,omitempty"` + Partitions map[string]*PartitionState `yaml:",omitempty,inline"` +} + +// PartState tracks installation data of a partition +type PartitionState struct { + FSLabel string `yaml:"label,omitempty"` + Images map[string]*ImageState `yaml:",omitempty,inline"` +} + +// ImageState represents data of a deployed image +type ImageState struct { + Source *ImageSource `yaml:"source,omitempty"` + SourceMetadata interface{} `yaml:"source-metadata,omitempty"` + Label string `yaml:"label,omitempty"` + FS string `yaml:"fs,omitempty"` + Path string `yaml:"path,omitempty"` +} + +func (i *ImageState) UnmarshalYAML(value *yaml.Node) error { + type iState ImageState + var srcMeta *yaml.Node + var err error + + err = value.Decode((*iState)(i)) + if err != nil { + return err + } + + if i.SourceMetadata != nil { + for i, n := range value.Content { + if n.Value == "source-metadata" && n.Kind == yaml.ScalarNode { + if len(value.Content) >= i+1 && value.Content[i+1].Kind == yaml.MappingNode { + srcMeta = value.Content[i+1] + } + break + } + } + } + + i.SourceMetadata = nil + if srcMeta != nil { + d := &DockerImageMeta{} + err = srcMeta.Decode(d) + if err == nil && (d.Digest != "" || d.Size != 0) { + i.SourceMetadata = d + return nil + } + c := &ChannelImageMeta{} + err = srcMeta.Decode(c) + if err == nil && c.Name != "" { + i.SourceMetadata = c + } +<<<<<<< HEAD +======= + if c.Name != "" { + i.SourceMetadata = c + } +>>>>>>> 1f2dacb (Consider coverage beyond the package the test belongs to) + } + + return err +} + +// DockerImageMeta represents metadata of a docker container image type +type DockerImageMeta struct { + Digest string `yaml:"digest,omitempty"` + Size int64 `yaml:"size,omitempty"` +} + +// ChannelImageMeta represents metadata of a channel image type +type ChannelImageMeta struct { + Category string `yaml:"category,omitempty"` + Name string `yaml:"name,omitempty"` + Version string `yaml:"version,omitempty"` + FingerPrint string `yaml:"finger-print,omitempty"` + Repos []Repository `yaml:"repositories,omitempty"` +} diff --git a/pkg/types/v1/config_test.go b/pkg/types/v1/config_test.go index 5369d75d669..e4fd8aa17ff 100644 --- a/pkg/types/v1/config_test.go +++ b/pkg/types/v1/config_test.go @@ -17,15 +17,122 @@ limitations under the License. package v1_test import ( + "path/filepath" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/rancher/elemental-cli/pkg/config" + conf "github.com/rancher/elemental-cli/pkg/config" "github.com/rancher/elemental-cli/pkg/constants" v1 "github.com/rancher/elemental-cli/pkg/types/v1" + "github.com/rancher/elemental-cli/pkg/utils" v1mocks "github.com/rancher/elemental-cli/tests/mocks" + "github.com/twpayne/go-vfs" + "github.com/twpayne/go-vfs/vfst" ) var _ = Describe("Types", Label("types", "config"), func() { + Describe("Write and load installation state", func() { + var config *v1.RunConfig + var runner *v1mocks.FakeRunner + var fs vfs.FS + var mounter *v1mocks.ErrorMounter + var cleanup func() + var err error + var dockerState, channelState *v1.ImageState + var installState *v1.InstallState + var statePath, recoveryPath string + + BeforeEach(func() { + runner = v1mocks.NewFakeRunner() + mounter = v1mocks.NewErrorMounter() + fs, cleanup, err = vfst.NewTestFS(map[string]interface{}{}) + Expect(err).Should(BeNil()) + + config = conf.NewRunConfig( + conf.WithFs(fs), + conf.WithRunner(runner), + conf.WithMounter(mounter), + ) + dockerState = &v1.ImageState{ + Source: v1.NewDockerSrc("registry.org/my/image:tag"), + Label: "active_label", + FS: "ext2", + SourceMetadata: &v1.DockerImageMeta{ + Digest: "adadgadg", + Size: 23452345, + }, + } + channelState = &v1.ImageState{ + Source: v1.NewChannelSrc("cat/mypkg"), + Label: "active_label", + FS: "ext2", + SourceMetadata: &v1.ChannelImageMeta{ + Category: "cat", + Name: "mypkg", + Version: "0.2.1", + FingerPrint: "mypkg-cat-0.2.1", + Repos: []v1.Repository{{ + Name: "myrepo", + }}, + }, + } + installState = &v1.InstallState{ + Date: "somedate", + Partitions: map[string]*v1.PartitionState{ + "state": { + FSLabel: "state_label", + Images: map[string]*v1.ImageState{ + "active": dockerState, + }, + }, + "recovery": { + FSLabel: "state_label", + Images: map[string]*v1.ImageState{ + "recovery": channelState, + }, + }, + }, + } + + statePath = filepath.Join(constants.RunningStateDir, constants.InstallStateFile) + recoveryPath = "/recoverypart/state.yaml" + err = utils.MkdirAll(fs, filepath.Dir(recoveryPath), constants.DirPerm) + Expect(err).ShouldNot(HaveOccurred()) + err = utils.MkdirAll(fs, filepath.Dir(statePath), constants.DirPerm) + Expect(err).ShouldNot(HaveOccurred()) + }) + AfterEach(func() { + cleanup() + }) + It("Writes and loads an installation data", func() { + err = config.WriteInstallState(installState, statePath, recoveryPath) + Expect(err).ShouldNot(HaveOccurred()) + loadedInstallState, err := config.LoadInstallState() + Expect(err).ShouldNot(HaveOccurred()) + + Expect(*loadedInstallState).To(Equal(*installState)) + }) + It("Fails writing to state partition", func() { + err = fs.RemoveAll(filepath.Dir(statePath)) + Expect(err).ShouldNot(HaveOccurred()) + err = config.WriteInstallState(installState, statePath, recoveryPath) + Expect(err).Should(HaveOccurred()) + }) + It("Fails writing to recovery partition", func() { + err = fs.RemoveAll(filepath.Dir(statePath)) + Expect(err).ShouldNot(HaveOccurred()) + err = config.WriteInstallState(installState, statePath, recoveryPath) + Expect(err).Should(HaveOccurred()) + }) + It("Fails loading state file", func() { + err = config.WriteInstallState(installState, statePath, recoveryPath) + Expect(err).ShouldNot(HaveOccurred()) + err = fs.RemoveAll(filepath.Dir(statePath)) + _, err = config.LoadInstallState() + Expect(err).Should(HaveOccurred()) + }) + }) Describe("ElementalPartitions", func() { var p v1.PartitionList var ep v1.ElementalPartitions diff --git a/pkg/types/v1/luet.go b/pkg/types/v1/luet.go index f49bea47c6f..c9e0e914d50 100644 --- a/pkg/types/v1/luet.go +++ b/pkg/types/v1/luet.go @@ -17,8 +17,8 @@ limitations under the License. package v1 type LuetInterface interface { - Unpack(string, string, bool) error - UnpackFromChannel(string, string, ...Repository) error + Unpack(string, string, bool) (*DockerImageMeta, error) + UnpackFromChannel(string, string, ...Repository) (*ChannelImageMeta, error) SetPlugins(...string) GetPlugins() []string SetArch(string) diff --git a/tests/mocks/luet_mock.go b/tests/mocks/luet_mock.go index 72ab5d8a3d8..3498e5537b3 100644 --- a/tests/mocks/luet_mock.go +++ b/tests/mocks/luet_mock.go @@ -26,8 +26,8 @@ import ( type FakeLuet struct { OnUnpackError bool OnUnpackFromChannelError bool - UnpackSideEffect func(string, string, bool) error - UnpackFromChannelSideEffect func(string, string, ...v1.Repository) error + UnpackSideEffect func(string, string, bool) (*v1.DockerImageMeta, error) + UnpackFromChannelSideEffect func(string, string, ...v1.Repository) (*v1.ChannelImageMeta, error) unpackCalled bool unpackFromChannelCalled bool plugins []string @@ -38,26 +38,26 @@ func NewFakeLuet() *FakeLuet { return &FakeLuet{} } -func (l *FakeLuet) Unpack(target string, image string, local bool) error { +func (l *FakeLuet) Unpack(target string, image string, local bool) (*v1.DockerImageMeta, error) { l.unpackCalled = true if l.OnUnpackError { - return errors.New("Luet install error") + return nil, errors.New("Luet install error") } if l.UnpackSideEffect != nil { return l.UnpackSideEffect(target, image, local) } - return nil + return nil, nil } -func (l *FakeLuet) UnpackFromChannel(target string, pkg string, repos ...v1.Repository) error { +func (l *FakeLuet) UnpackFromChannel(target string, pkg string, repos ...v1.Repository) (*v1.ChannelImageMeta, error) { l.unpackFromChannelCalled = true if l.OnUnpackFromChannelError { - return errors.New("Luet install error") + return nil, errors.New("Luet install error") } if l.UnpackFromChannelSideEffect != nil { return l.UnpackFromChannelSideEffect(target, pkg, repos...) } - return nil + return nil, nil } func (l FakeLuet) UnpackCalled() bool {