diff --git a/go.mod b/go.mod index e982a03a..98ac4a7e 100644 --- a/go.mod +++ b/go.mod @@ -13,8 +13,8 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/jaypipes/ghw v0.12.0 github.com/joho/godotenv v1.5.1 + github.com/kairos-io/kairos-sdk v0.0.21-0.20231218143909-a99f8bb48751 github.com/kairos-io/kcrypt v0.8.0 - github.com/kairos-io/kairos-sdk v0.0.20 github.com/labstack/echo/v4 v4.11.1 github.com/mitchellh/mapstructure v1.5.0 github.com/mudler/go-nodepair v0.0.0-20221223092639-ba399a66fdfb diff --git a/go.sum b/go.sum index 4bef8d0f..c43c0257 100644 --- a/go.sum +++ b/go.sum @@ -367,6 +367,8 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kairos-io/kairos-sdk v0.0.20 h1:iadV3ylhQELgWUFe/fETfs2qFhPtKZwnDN55okZZVgs= github.com/kairos-io/kairos-sdk v0.0.20/go.mod h1:17dpFG2d3Q/TcT86DlLK5nNXEjlSrkYl7bsvO2cpYGE= +github.com/kairos-io/kairos-sdk v0.0.21-0.20231218143909-a99f8bb48751 h1:kyW/RlMT0yujMYR0HATHM1q0Cwb7TNT8j+huykrjzIk= +github.com/kairos-io/kairos-sdk v0.0.21-0.20231218143909-a99f8bb48751/go.mod h1:17dpFG2d3Q/TcT86DlLK5nNXEjlSrkYl7bsvO2cpYGE= github.com/kairos-io/kcrypt v0.7.1-0.20231206231913-12a8d5d33cf0 h1:bInWIHqP+8GNOO0b6mtvZn6HxEQuhMgr5h9QBuarR38= github.com/kairos-io/kcrypt v0.7.1-0.20231206231913-12a8d5d33cf0/go.mod h1:sP+kdJ6WyPPWlzZuDNfkV2wmnCDPWCGpC5nF7KhHX3Q= github.com/kairos-io/kcrypt v0.8.0 h1:uA5GVF74hzqNOgVvvuue585vAWKXbjMQ93mBJuhKuTE= diff --git a/pkg/config/config.go b/pkg/config/config.go index 57f373f8..525e4e08 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -70,6 +70,7 @@ func NewConfig(opts ...GenericOptions) *Config { ImageExtractor: v1.OCIImageExtractor{}, SquashFsNoCompression: true, Install: &Install{}, + UkiMaxEntries: constants.UkiMaxEntries, } for _, o := range opts { o(c) @@ -134,6 +135,7 @@ type Config struct { 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"` + UkiMaxEntries int `yaml:"uki-max-entries,omitempty" mapstructure:"uki-max-entries"` } // WriteInstallState writes the state.yaml file to the given state and recovery paths diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index a5a1a368..0254207d 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -17,6 +17,7 @@ package config_test import ( "fmt" + "github.com/kairos-io/kairos-sdk/collector" "path/filepath" "reflect" "strings" @@ -169,6 +170,11 @@ var _ = Describe("Schema", func() { AfterEach(func() { cleanup() }) + It("Scan can override options", func() { + c, err := Scan(collector.Readers(strings.NewReader(`uki-max-entries: 34`)), collector.NoLogs) + Expect(err).ShouldNot(HaveOccurred()) + Expect(c.UkiMaxEntries).To(Equal(34)) + }) It("Writes and loads an installation data", func() { err = config.WriteInstallState(installState, statePath, recoveryPath) Expect(err).ShouldNot(HaveOccurred()) diff --git a/pkg/config/spec.go b/pkg/config/spec.go index 2a084f9b..df202d1b 100644 --- a/pkg/config/spec.go +++ b/pkg/config/spec.go @@ -18,6 +18,8 @@ package config import ( "fmt" + "github.com/google/go-containerregistry/pkg/crane" + "golang.org/x/sys/unix" "io/fs" "os" "path/filepath" @@ -538,6 +540,18 @@ func ReadUkiInstallSpecFromConfig(c *Config) (*v1.InstallUkiSpec, error) { func NewUkiUpgradeSpec(cfg *Config) (*v1.UpgradeUkiSpec, error) { spec := &v1.UpgradeUkiSpec{} err := unmarshallFullSpec(cfg, "upgrade", spec) + // TODO: Use this everywhere? + cfg.Logger.Infof("Checking if OCI image %s exists", spec.Active.Source.Value()) + if spec.Active.Source.IsDocker() { + _, err := crane.Manifest(spec.Active.Source.Value()) + if err != nil { + if strings.Contains(err.Error(), "MANIFEST_UNKNOWN") { + return nil, fmt.Errorf("oci image %s does not exist", spec.Active.Source.Value()) + } + return nil, err + } + } + // Get the actual source size to calculate the image size and partitions size size, err := GetSourceSize(cfg, spec.Active.Source) if err != nil { @@ -548,12 +562,27 @@ func NewUkiUpgradeSpec(cfg *Config) (*v1.UpgradeUkiSpec, error) { spec.Active.Size = uint(size) } - spec.EfiPartition = &v1.Partition{ - FilesystemLabel: constants.EfiLabel, - FS: constants.EfiFs, - Path: constants.UkiEfiDiskByLabel, - MountPoint: constants.UkiEfiDir, + // Get EFI partition + parts, err := partitions.GetAllPartitions() + if err != nil { + return spec, fmt.Errorf("could not read host partitions") + } + for _, p := range parts { + if p.FilesystemLabel == constants.EfiLabel { + spec.EfiPartition = p + break + } + } + // Get free size of partition + var stat unix.Statfs_t + _ = unix.Statfs(spec.EfiPartition.MountPoint, &stat) + freeSize := stat.Bfree * uint64(stat.Bsize) / 1000 / 1000 + cfg.Logger.Debugf("Partition on mountpoint %s has %dMb free", spec.EfiPartition.MountPoint, freeSize) + // Check if the source is over the free size + if spec.Active.Size > uint(freeSize) { + return spec, fmt.Errorf("source size(%d) is bigger than the free space(%d) on the EFI partition(%s)", spec.Active.Size, freeSize, spec.EfiPartition.MountPoint) } + return spec, err } diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 9701920a..663ba96c 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -112,6 +112,7 @@ const ( UkiCdromSource = "/run/install/cdrom" UkiEfiDir = "/efi" UkiEfiDiskByLabel = `/dev/disk/by-label/` + EfiLabel + UkiMaxEntries = 3 ) func GetCloudInitPaths() []string { diff --git a/pkg/uki/upgrade.go b/pkg/uki/upgrade.go index 28e639b3..e1b60272 100644 --- a/pkg/uki/upgrade.go +++ b/pkg/uki/upgrade.go @@ -1,6 +1,7 @@ package uki import ( + "github.com/Masterminds/semver/v3" hook "github.com/kairos-io/kairos-agent/v2/internal/agent/hooks" "github.com/kairos-io/kairos-agent/v2/pkg/config" "github.com/kairos-io/kairos-agent/v2/pkg/constants" @@ -9,6 +10,11 @@ import ( elementalUtils "github.com/kairos-io/kairos-agent/v2/pkg/utils" events "github.com/kairos-io/kairos-sdk/bus" "github.com/kairos-io/kairos-sdk/utils" + "github.com/sanity-io/litter" + "os" + "path/filepath" + "sort" + "strings" ) type UpgradeAction struct { @@ -34,19 +40,61 @@ func (i *UpgradeAction) Run() (err error) { return err } cleanup.Push(umount) - // TODO: Check size of EFI partition to see if we can upgrade - // TODO: Check size of source to see if we can upgrade // TODO: Check number of existing UKI files - // TODO: Load them, order them via semver - // TODO: Remove the latest one if its over the max number of entries + efiFiles, err := i.getEfiFiles() + if err != nil { + return err + } + i.cfg.Logger.Infof("Found %d UKI files", len(efiFiles)) + if len(efiFiles) > i.cfg.UkiMaxEntries && i.cfg.UkiMaxEntries > 0 { + i.cfg.Logger.Infof("Found %d UKI files, which is over max entries allowed(%d) removing the oldest one", len(efiFiles), i.cfg.UkiMaxEntries) + versionList := semver.Collection{} + for _, f := range efiFiles { + versionList = append(versionList, semver.MustParse(f)) + } + // Sort it so the oldest one is first + sort.Sort(versionList) + i.cfg.Logger.Debugf("All versions found: %s", litter.Sdump(versionList)) + // Remove the oldest one + i.cfg.Logger.Infof("Removing: %s", filepath.Join(i.spec.EfiPartition.MountPoint, "EFI", "kairos", versionList[0].Original())) + err = i.cfg.Fs.Remove(filepath.Join(i.spec.EfiPartition.MountPoint, "EFI", "kairos", versionList[0].Original())) + if err != nil { + return err + } + // Remove the conf file as well + i.cfg.Logger.Infof("Removing: %s", filepath.Join(i.spec.EfiPartition.MountPoint, "loader", "entries", versionList[0].String()+".conf")) + // Don't care about errors here, systemd-boot will ignore any configs if it cant find the efi file mentioned in it + e := i.cfg.Fs.Remove(filepath.Join(i.spec.EfiPartition.MountPoint, "loader", "entries", versionList[0].String()+".conf")) + if e != nil { + i.cfg.Logger.Warnf("Failed to remove conf file: %s", e) + } + } else { + i.cfg.Logger.Infof("Found %d UKI files, which is under max entries allowed(%d) not removing any", len(efiFiles), i.cfg.UkiMaxEntries) + } + // Dump artifact to efi dir _, err = e.DumpSource(constants.UkiEfiDir, i.spec.Active.Source) if err != nil { return err } - + _ = elementalUtils.RunStage(i.cfg, "kairos-uki-upgrade.after") _ = events.RunHookScript("/usr/bin/kairos-agent.uki.upgrade.after.hook") //nolint:errcheck return hook.Run(*i.cfg, i.spec, hook.AfterUkiUpgrade...) } + +func (i *UpgradeAction) getEfiFiles() ([]string, error) { + var efiFiles []string + files, err := os.ReadDir(filepath.Join(i.spec.EfiPartition.MountPoint, "EFI", "kairos")) + if err != nil { + return efiFiles, err + } + + for _, file := range files { + if !file.IsDir() && strings.HasSuffix(file.Name(), ".efi") { + efiFiles = append(efiFiles, file.Name()) + } + } + return efiFiles, nil +}