UKI improvements (#186)

This commit is contained in:
Itxaka
2023-12-18 16:09:55 +01:00
committed by GitHub
parent 3254b8a36e
commit b8232ae985
7 changed files with 99 additions and 11 deletions

2
go.mod
View File

@@ -13,8 +13,8 @@ require (
github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-multierror v1.1.1
github.com/jaypipes/ghw v0.12.0 github.com/jaypipes/ghw v0.12.0
github.com/joho/godotenv v1.5.1 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/kcrypt v0.8.0
github.com/kairos-io/kairos-sdk v0.0.20
github.com/labstack/echo/v4 v4.11.1 github.com/labstack/echo/v4 v4.11.1
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
github.com/mudler/go-nodepair v0.0.0-20221223092639-ba399a66fdfb github.com/mudler/go-nodepair v0.0.0-20221223092639-ba399a66fdfb

2
go.sum
View File

@@ -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/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 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.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 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.7.1-0.20231206231913-12a8d5d33cf0/go.mod h1:sP+kdJ6WyPPWlzZuDNfkV2wmnCDPWCGpC5nF7KhHX3Q=
github.com/kairos-io/kcrypt v0.8.0 h1:uA5GVF74hzqNOgVvvuue585vAWKXbjMQ93mBJuhKuTE= github.com/kairos-io/kcrypt v0.8.0 h1:uA5GVF74hzqNOgVvvuue585vAWKXbjMQ93mBJuhKuTE=

View File

@@ -70,6 +70,7 @@ func NewConfig(opts ...GenericOptions) *Config {
ImageExtractor: v1.OCIImageExtractor{}, ImageExtractor: v1.OCIImageExtractor{},
SquashFsNoCompression: true, SquashFsNoCompression: true,
Install: &Install{}, Install: &Install{},
UkiMaxEntries: constants.UkiMaxEntries,
} }
for _, o := range opts { for _, o := range opts {
o(c) o(c)
@@ -134,6 +135,7 @@ type Config struct {
Arch string `yaml:"arch,omitempty" mapstructure:"arch"` Arch string `yaml:"arch,omitempty" mapstructure:"arch"`
SquashFsCompressionConfig []string `yaml:"squash-compression,omitempty" mapstructure:"squash-compression"` SquashFsCompressionConfig []string `yaml:"squash-compression,omitempty" mapstructure:"squash-compression"`
SquashFsNoCompression bool `yaml:"squash-no-compression,omitempty" mapstructure:"squash-no-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 // WriteInstallState writes the state.yaml file to the given state and recovery paths

View File

@@ -17,6 +17,7 @@ package config_test
import ( import (
"fmt" "fmt"
"github.com/kairos-io/kairos-sdk/collector"
"path/filepath" "path/filepath"
"reflect" "reflect"
"strings" "strings"
@@ -169,6 +170,11 @@ var _ = Describe("Schema", func() {
AfterEach(func() { AfterEach(func() {
cleanup() 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() { It("Writes and loads an installation data", func() {
err = config.WriteInstallState(installState, statePath, recoveryPath) err = config.WriteInstallState(installState, statePath, recoveryPath)
Expect(err).ShouldNot(HaveOccurred()) Expect(err).ShouldNot(HaveOccurred())

View File

@@ -18,6 +18,8 @@ package config
import ( import (
"fmt" "fmt"
"github.com/google/go-containerregistry/pkg/crane"
"golang.org/x/sys/unix"
"io/fs" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
@@ -538,6 +540,18 @@ func ReadUkiInstallSpecFromConfig(c *Config) (*v1.InstallUkiSpec, error) {
func NewUkiUpgradeSpec(cfg *Config) (*v1.UpgradeUkiSpec, error) { func NewUkiUpgradeSpec(cfg *Config) (*v1.UpgradeUkiSpec, error) {
spec := &v1.UpgradeUkiSpec{} spec := &v1.UpgradeUkiSpec{}
err := unmarshallFullSpec(cfg, "upgrade", spec) 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 // Get the actual source size to calculate the image size and partitions size
size, err := GetSourceSize(cfg, spec.Active.Source) size, err := GetSourceSize(cfg, spec.Active.Source)
if err != nil { if err != nil {
@@ -548,12 +562,27 @@ func NewUkiUpgradeSpec(cfg *Config) (*v1.UpgradeUkiSpec, error) {
spec.Active.Size = uint(size) spec.Active.Size = uint(size)
} }
spec.EfiPartition = &v1.Partition{ // Get EFI partition
FilesystemLabel: constants.EfiLabel, parts, err := partitions.GetAllPartitions()
FS: constants.EfiFs, if err != nil {
Path: constants.UkiEfiDiskByLabel, return spec, fmt.Errorf("could not read host partitions")
MountPoint: constants.UkiEfiDir,
} }
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 return spec, err
} }

View File

@@ -112,6 +112,7 @@ const (
UkiCdromSource = "/run/install/cdrom" UkiCdromSource = "/run/install/cdrom"
UkiEfiDir = "/efi" UkiEfiDir = "/efi"
UkiEfiDiskByLabel = `/dev/disk/by-label/` + EfiLabel UkiEfiDiskByLabel = `/dev/disk/by-label/` + EfiLabel
UkiMaxEntries = 3
) )
func GetCloudInitPaths() []string { func GetCloudInitPaths() []string {

View File

@@ -1,6 +1,7 @@
package uki package uki
import ( import (
"github.com/Masterminds/semver/v3"
hook "github.com/kairos-io/kairos-agent/v2/internal/agent/hooks" 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/config"
"github.com/kairos-io/kairos-agent/v2/pkg/constants" "github.com/kairos-io/kairos-agent/v2/pkg/constants"
@@ -9,6 +10,11 @@ import (
elementalUtils "github.com/kairos-io/kairos-agent/v2/pkg/utils" elementalUtils "github.com/kairos-io/kairos-agent/v2/pkg/utils"
events "github.com/kairos-io/kairos-sdk/bus" events "github.com/kairos-io/kairos-sdk/bus"
"github.com/kairos-io/kairos-sdk/utils" "github.com/kairos-io/kairos-sdk/utils"
"github.com/sanity-io/litter"
"os"
"path/filepath"
"sort"
"strings"
) )
type UpgradeAction struct { type UpgradeAction struct {
@@ -34,19 +40,61 @@ func (i *UpgradeAction) Run() (err error) {
return err return err
} }
cleanup.Push(umount) 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: Check number of existing UKI files
// TODO: Load them, order them via semver efiFiles, err := i.getEfiFiles()
// TODO: Remove the latest one if its over the max number of entries 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 // Dump artifact to efi dir
_, err = e.DumpSource(constants.UkiEfiDir, i.spec.Active.Source) _, err = e.DumpSource(constants.UkiEfiDir, i.spec.Active.Source)
if err != nil { if err != nil {
return err return err
} }
_ = elementalUtils.RunStage(i.cfg, "kairos-uki-upgrade.after") _ = elementalUtils.RunStage(i.cfg, "kairos-uki-upgrade.after")
_ = events.RunHookScript("/usr/bin/kairos-agent.uki.upgrade.after.hook") //nolint:errcheck _ = events.RunHookScript("/usr/bin/kairos-agent.uki.upgrade.after.hook") //nolint:errcheck
return hook.Run(*i.cfg, i.spec, hook.AfterUkiUpgrade...) 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
}