Skip to content

Commit

Permalink
[uki] Lyfecycle (#148)
Browse files Browse the repository at this point in the history
  • Loading branch information
Itxaka authored Oct 3, 2023
1 parent 300cc92 commit 1b967cc
Show file tree
Hide file tree
Showing 9 changed files with 478 additions and 8 deletions.
9 changes: 9 additions & 0 deletions internal/agent/hooks/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ var FirstBoot = []Interface{
&GrubPostInstallOptions{},
}

// AfterUkiInstall sets which Hooks to run after uki runs the install action
var AfterUkiInstall = []Interface{}

// AfterUkiReset sets which Hooks to run after uki runs the install action
var AfterUkiReset = []Interface{}

// AfterUkiUpgrade sets which Hooks to run after uki runs the install action
var AfterUkiUpgrade = []Interface{}

func Run(c config.Config, spec v1.Spec, hooks ...Interface) error {
for _, h := range hooks {
if err := h.Run(c, spec); err != nil {
Expand Down
107 changes: 107 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/kairos-io/kairos-agent/v2/internal/webui"
agentConfig "github.com/kairos-io/kairos-agent/v2/pkg/config"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
"github.com/kairos-io/kairos-agent/v2/pkg/uki"
"github.com/kairos-io/kairos-sdk/bundles"
"github.com/kairos-io/kairos-sdk/collector"
"github.com/kairos-io/kairos-sdk/machine"
Expand Down Expand Up @@ -651,6 +652,112 @@ The validate command expects a configuration file as its only argument. Local fi
return nil
},
},
{
Name: "uki",
Usage: "UKI subcommands",
Description: "UKI subcommands",
// we could set the flag --source at this level so we could have the flag for all subcommands but that translates into an ugly command
// in which you need to put the source flag before the subcommand, which is a mess. Just bad UX.
// command level: kairos-agent uki --source oci:whatever install
// subcommand level: kairos-agent uki install --source oci:whatever
Subcommands: []*cli.Command{
{
Name: "install",
Usage: "Install to disk",
UsageText: "install [--device DEVICE]",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "source",
Usage: "Source for install. Composed of `type:address`. Accepts `file:`,`dir:` or `oci:` for the type of source.\nFor example `file:/var/share/myimage.tar`, `dir:/tmp/extracted` or `oci:repo/image:tag`",
Action: func(c *cli.Context, s string) error {
return validateSource(s)
},
},
&cli.StringFlag{
Name: "device",
},
},
Action: func(c *cli.Context) error {
config, err := agentConfig.Scan(collector.Directories(configScanDir...), collector.NoLogs)
if err != nil {
return err
}
// Load the spec from the config
installSpec, err := agentConfig.ReadUkiInstallSpecFromConfig(config)
if err != nil {
return err
}

if c.String("device") != "" {
installSpec.Target = c.String("device")
}

installAction := uki.NewInstallAction(config, installSpec)
return installAction.Run()
},
},
{
Name: "upgrade",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "source",
Usage: "Source for upgrade. Composed of `type:address`. Accepts `file:`,`dir:` or `oci:` for the type of source.\nFor example `file:/var/share/myimage.tar`, `dir:/tmp/extracted` or `oci:repo/image:tag`",
Action: func(c *cli.Context, s string) error {
return validateSource(s)
},
},
},
Before: func(c *cli.Context) error {
return fmt.Errorf("not implemented")
},
Action: func(c *cli.Context) error {
config, err := agentConfig.Scan(collector.Directories(configScanDir...), collector.NoLogs, collector.StrictValidation(c.Bool("strict-validation")))
if err != nil {
return err
}

// Load the spec from the config
upgradeSpec, err := agentConfig.ReadUkiUpgradeFromConfig(config)
if err != nil {
return err
}

upgradeAction := uki.NewUpgradeAction(config, upgradeSpec)
return upgradeAction.Run()
},
},
{
Name: "reset",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "source",
Usage: "Source for upgrade. Composed of `type:address`. Accepts `file:`,`dir:` or `oci:` for the type of source.\nFor example `file:/var/share/myimage.tar`, `dir:/tmp/extracted` or `oci:repo/image:tag`",
Action: func(c *cli.Context, s string) error {
return validateSource(s)
},
},
},
Before: func(c *cli.Context) error {
return fmt.Errorf("not implemented")
},
Action: func(c *cli.Context) error {
config, err := agentConfig.Scan(collector.Directories(configScanDir...), collector.NoLogs, collector.StrictValidation(c.Bool("strict-validation")))
if err != nil {
return err
}

// Load the spec from the config
resetSpec, err := agentConfig.ReadUkiResetSpecFromConfig(config)
if err != nil {
return err
}

resetAction := uki.NewResetAction(config, resetSpec)
return resetAction.Run()
},
},
},
},
}

func main() {
Expand Down
75 changes: 75 additions & 0 deletions pkg/config/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,73 @@ func ReadInstallSpecFromConfig(c *Config) (*v1.InstallSpec, error) {
return installSpec, nil
}

// ReadUkiResetSpecFromConfig will return a proper v1.ResetUkiSpec based on an agent Config
func ReadUkiResetSpecFromConfig(c *Config) (*v1.ResetUkiSpec, error) {
sp, err := ReadSpecFromCloudConfig(c, "reset-uki")
if err != nil {
return &v1.ResetUkiSpec{}, err
}
resetSpec := sp.(*v1.ResetUkiSpec)
return resetSpec, nil
}

func NewUkiInstallSpec(cfg *Config) (*v1.InstallUkiSpec, error) {
spec := &v1.InstallUkiSpec{
Target: cfg.Install.Device,
}

// Calculate the partitions afterwards so they use the image sizes for the final partition sizes
spec.Partitions.EFI = &v1.Partition{
FilesystemLabel: constants.EfiLabel,
Size: constants.ImgSize, // TODO: Fix this and set proper size based on the source size
Name: constants.EfiPartName,
FS: constants.EfiFs,
MountPoint: constants.EfiDir,
Flags: []string{"esp"},
}
spec.Partitions.OEM = &v1.Partition{
FilesystemLabel: constants.OEMLabel,
Size: constants.OEMSize,
Name: constants.OEMPartName,
FS: constants.LinuxFs,
MountPoint: constants.OEMDir,
Flags: []string{},
}
spec.Partitions.Persistent = &v1.Partition{
FilesystemLabel: constants.PersistentLabel,
Size: constants.PersistentSize,
Name: constants.PersistentPartName,
FS: constants.LinuxFs,
MountPoint: constants.PersistentDir,
Flags: []string{},
}

// TODO: Which key to use? install or install-uki?
err := unmarshallFullSpec(cfg, "install", spec)

return spec, err
}

// ReadUkiInstallSpecFromConfig will return a proper v1.InstallUkiSpec based on an agent Config
func ReadUkiInstallSpecFromConfig(c *Config) (*v1.InstallUkiSpec, error) {
sp, err := ReadSpecFromCloudConfig(c, "install-uki")
if err != nil {
return &v1.InstallUkiSpec{}, err
}
installSpec := sp.(*v1.InstallUkiSpec)
return installSpec, nil
}

// ReadUkiUpgradeFromConfig will return a proper v1.UpgradeUkiSpec based on an agent Config
func ReadUkiUpgradeFromConfig(c *Config) (*v1.UpgradeUkiSpec, error) {
sp, err := ReadSpecFromCloudConfig(c, "upgrade-uki")
if err != nil {
return &v1.UpgradeUkiSpec{}, err
}
upgradeSpec := sp.(*v1.UpgradeUkiSpec)
return upgradeSpec, nil
}

// GetSourceSize will try to gather the actual size of the source
// Useful to create the exact size of images and by side effect the partition size
// This helps adjust the size to be juuuuust right.
Expand Down Expand Up @@ -571,6 +638,14 @@ func ReadSpecFromCloudConfig(r *Config, spec string) (v1.Spec, error) {
sp, err = NewUpgradeSpec(r)
case "reset":
sp, err = NewResetSpec(r)
case "install-uki":
sp, err = NewUkiInstallSpec(r)
case "reset-uki":
// TODO: Fill with proper defaults
sp = &v1.ResetUkiSpec{}
case "upgrade-uki":
// TODO: Fill with proper defaults
sp = &v1.UpgradeUkiSpec{}
default:
return nil, fmt.Errorf("spec not valid: %s", spec)
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ const (
SignedShim = "shim.efi"

Rsync = "rsync"

UkiSource = "/run/install/uki"
UkiCdromSource = "/run/install/cdrom"
)

func GetCloudInitPaths() []string {
Expand Down
12 changes: 6 additions & 6 deletions pkg/elemental/elemental.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,27 +50,27 @@ func (e *Elemental) FormatPartition(part *v1.Partition, opts ...string) error {
// PartitionAndFormatDevice creates a new empty partition table on target disk
// and applies the configured disk layout by creating and formatting all
// required partitions
func (e *Elemental) PartitionAndFormatDevice(i *v1.InstallSpec) error {
func (e *Elemental) PartitionAndFormatDevice(i v1.SharedInstallSpec) error {
disk := partitioner.NewDisk(
i.Target,
i.GetTarget(),
partitioner.WithRunner(e.config.Runner),
partitioner.WithFS(e.config.Fs),
partitioner.WithLogger(e.config.Logger),
)

if !disk.Exists() {
e.config.Logger.Errorf("Disk %s does not exist", i.Target)
return fmt.Errorf("disk %s does not exist", i.Target)
e.config.Logger.Errorf("Disk %s does not exist", i.GetTarget())
return fmt.Errorf("disk %s does not exist", i.GetTarget())
}

e.config.Logger.Infof("Partitioning device...")
out, err := disk.NewPartitionTable(i.PartTable)
out, err := disk.NewPartitionTable(i.GetPartTable())
if err != nil {
e.config.Logger.Errorf("Failed creating new partition table: %s", out)
return err
}

parts := i.Partitions.PartitionsByInstallOrder(i.ExtraPartitions)
parts := i.GetPartitions().PartitionsByInstallOrder(i.GetExtraPartitions())
return e.createPartitions(disk, parts)
}

Expand Down
63 changes: 61 additions & 2 deletions pkg/types/v1/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ type Spec interface {
ShouldShutdown() bool
}

// SharedInstallSpec is the interface that Install specs need to implement
type SharedInstallSpec interface {
GetPartTable() string
GetTarget() string
GetPartitions() ElementalPartitions
GetExtraPartitions() PartitionList
}

// InstallSpec struct represents all the installation action details
type InstallSpec struct {
Target string `yaml:"device,omitempty" mapstructure:"device"`
Expand Down Expand Up @@ -106,8 +114,12 @@ func (i *InstallSpec) Sanitize() error {
return i.Partitions.SetFirmwarePartitions(i.Firmware, i.PartTable)
}

func (i *InstallSpec) ShouldReboot() bool { return i.Reboot }
func (i *InstallSpec) ShouldShutdown() bool { return i.PowerOff }
func (i *InstallSpec) ShouldReboot() bool { return i.Reboot }
func (i *InstallSpec) ShouldShutdown() bool { return i.PowerOff }
func (i *InstallSpec) GetTarget() string { return i.Target }
func (i *InstallSpec) GetPartTable() string { return i.PartTable }
func (i *InstallSpec) GetPartitions() ElementalPartitions { return i.Partitions }
func (i *InstallSpec) GetExtraPartitions() PartitionList { return i.ExtraPartitions }

// ResetSpec struct represents all the reset action details
type ResetSpec struct {
Expand Down Expand Up @@ -476,3 +488,50 @@ type DockerImageMeta struct {
Digest string `yaml:"digest,omitempty"`
Size int64 `yaml:"size,omitempty"`
}

type InstallUkiSpec struct {
Target string `yaml:"device,omitempty" mapstructure:"device"`
Reboot bool `yaml:"reboot,omitempty" mapstructure:"reboot"`
PowerOff bool `yaml:"poweroff,omitempty" mapstructure:"poweroff"`
Partitions ElementalPartitions `yaml:"partitions,omitempty" mapstructure:"partitions"`
ExtraPartitions PartitionList `yaml:"extra-partitions,omitempty" mapstructure:"extra-partitions"`
CloudInit []string `yaml:"cloud-init,omitempty" mapstructure:"cloud-init"`
}

func (i *InstallUkiSpec) Sanitize() error {
var err error
return err
}

func (i *InstallUkiSpec) ShouldReboot() bool { return i.Reboot }
func (i *InstallUkiSpec) ShouldShutdown() bool { return i.PowerOff }
func (i *InstallUkiSpec) GetTarget() string { return i.Target }
func (i *InstallUkiSpec) GetPartTable() string { return "gpt" }
func (i *InstallUkiSpec) GetPartitions() ElementalPartitions { return i.Partitions }
func (i *InstallUkiSpec) GetExtraPartitions() PartitionList { return i.ExtraPartitions }

type UpgradeUkiSpec struct {
Reboot bool `yaml:"reboot,omitempty" mapstructure:"reboot"`
PowerOff bool `yaml:"poweroff,omitempty" mapstructure:"poweroff"`
}

func (i *UpgradeUkiSpec) Sanitize() error {
var err error
return err
}

func (i *UpgradeUkiSpec) ShouldReboot() bool { return i.Reboot }
func (i *UpgradeUkiSpec) ShouldShutdown() bool { return i.PowerOff }

type ResetUkiSpec struct {
Reboot bool `yaml:"reboot,omitempty" mapstructure:"reboot"`
PowerOff bool `yaml:"poweroff,omitempty" mapstructure:"poweroff"`
}

func (i *ResetUkiSpec) Sanitize() error {
var err error
return err
}

func (i *ResetUkiSpec) ShouldReboot() bool { return i.Reboot }
func (i *ResetUkiSpec) ShouldShutdown() bool { return i.PowerOff }
Loading

0 comments on commit 1b967cc

Please sign in to comment.