Skip to content

Commit

Permalink
1633 port sysext command from enki (#101)
Browse files Browse the repository at this point in the history
* Port sysext command from enki

part of: kairos-io/kairos#1633

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* Don't use viper

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* Use global "--debug" option and some minor fixes

- args that weren't used
- command output was not printed

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

---------

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
  • Loading branch information
jimmykarily authored Nov 12, 2024
1 parent e538d26 commit 2a1a58c
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 11 deletions.
2 changes: 1 addition & 1 deletion internal/cmd/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func GetApp(version string) *cli.App {
Version: version,
Authors: []*cli.Author{{Name: "Kairos authors", Email: "members@kairos.io"}},
Usage: "auroraboot",
Commands: []*cli.Command{&BuildISOCmd, &BuildUKICmd},
Commands: []*cli.Command{&BuildISOCmd, &BuildUKICmd, &SysextCmd},
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "set",
Expand Down
19 changes: 9 additions & 10 deletions internal/cmd/build-uki.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,9 @@ var BuildUKICmd = cli.Command{
return errors.New("no image provided")
}

// TODO: Implement log-level flag
logLevel := "debug"
if ctx.String("log-level") != "" {
logLevel = ctx.String("log-level")
logLevel := "warn"
if ctx.Bool("debug") {
logLevel = "debug"
}
logger := sdkTypes.NewKairosLogger("auroraboot", logLevel, false)

Expand Down Expand Up @@ -374,7 +373,7 @@ var BuildUKICmd = cli.Command{
}

//Then remove the output dir files as we dont need them, the container has been loaded
if err := removeUkiFiles(ctx.String("output-dir"), ctx.String("keys"), entries, logger); err != nil {
if err := removeUkiFiles(ctx.String("output-dir"), ctx.String("keys"), entries); err != nil {
return err
}
case string(enkiconstants.DefaultOutput):
Expand Down Expand Up @@ -696,7 +695,7 @@ func createISO(e *elemental.Elemental, sourceDir, outputDir, overlayISO, keysDir
}
defer os.RemoveAll(isoDir)

filesMap, err := imageFiles(sourceDir, keysDir, entries, logger)
filesMap, err := imageFiles(sourceDir, keysDir, entries)
if err != nil {
return err
}
Expand Down Expand Up @@ -759,7 +758,7 @@ func createISO(e *elemental.Elemental, sourceDir, outputDir, overlayISO, keysDir
return nil
}

func imageFiles(sourceDir, keysDir string, entries []enkiutils.BootEntry, logger sdkTypes.KairosLogger) (map[string][]string, error) {
func imageFiles(sourceDir, keysDir string, entries []enkiutils.BootEntry) (map[string][]string, error) {
// the keys are the target dirs
// the values are the source files that should be copied into the target dir
data := map[string][]string{
Expand Down Expand Up @@ -857,7 +856,7 @@ func copyFilesToImg(imgFile string, filesMap map[string][]string) error {
// Create artifact just outputs the files from the sourceDir to the outputDir
// Maintains the same structure as the sourceDir which is the final structure we want
func createArtifact(sourceDir, outputDir, keysDir string, entries []enkiutils.BootEntry, logger sdkTypes.KairosLogger) error {
filesMap, err := imageFiles(sourceDir, keysDir, entries, logger)
filesMap, err := imageFiles(sourceDir, keysDir, entries)
if err != nil {
return err
}
Expand Down Expand Up @@ -934,8 +933,8 @@ func createContainer(sourceDir, outputDir, artifactName, version string, logger

// removeUkiFiles removes all the files and directories inside the output directory that match our filesMap
// so this should only remove the generated intermediate artifacts that we use to build the container
func removeUkiFiles(outputDir, keysDir string, entries []enkiutils.BootEntry, logger sdkTypes.KairosLogger) error {
filesMap, _ := imageFiles(outputDir, keysDir, entries, logger)
func removeUkiFiles(outputDir, keysDir string, entries []enkiutils.BootEntry) error {
filesMap, _ := imageFiles(outputDir, keysDir, entries)
for dir, files := range filesMap {
for _, f := range files {
err := os.Remove(filepath.Join(outputDir, dir, filepath.Base(f)))
Expand Down
160 changes: 160 additions & 0 deletions internal/cmd/sysext.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package cmd

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"

"github.com/gofrs/uuid"
"github.com/kairos-io/kairos-sdk/sysext"
sdkTypes "github.com/kairos-io/kairos-sdk/types"
"github.com/kairos-io/kairos-sdk/utils"
"github.com/urfave/cli/v2"
)

// Use: "build-uki SourceImage",
// Short: "Build a UKI artifact from a container image",
var SysextCmd = cli.Command{
Name: "sysext",
Usage: "Generate a sysextension from the last layer of the given CONTAINER",
ArgsUsage: "<name> <container>",

Flags: []cli.Flag{
&cli.StringFlag{
Name: "private-key",
Value: "",
Usage: "Private key to sign the sysext with",
Required: true,
},
&cli.StringFlag{
Name: "certificate",
Usage: "Certificate to sign the sysext with",
Required: true,
},
&cli.BoolFlag{
Name: "service-load",
Value: false,
Usage: "Make systemctl reload the service when loading the sysext. This is useful for sysext that provide systemd service files.",
},
&cli.StringFlag{
Name: "output",
Usage: "Output dir",
},
&cli.StringFlag{
Name: "arch",
Value: "amd64",
Usage: "Arch to get the image from and build the sysext for. Accepts amd64 and arm64 values.",
},
},
Before: func(ctx *cli.Context) error {
arch := ctx.String("arch")
if arch != "amd64" && arch != "arm64" {
return fmt.Errorf("unsupported architecture: %s", arch)
}
return nil
},
Action: func(ctx *cli.Context) error {
level := "warn"
if ctx.Bool("debug") {
level = "debug"
}
logger := sdkTypes.NewKairosLogger("auroraboot", level, false)
args := ctx.Args()

name := args.Get(0)
if _, err := os.Stat(fmt.Sprintf("%s.sysext.raw", name)); err == nil {
_ = os.Remove(fmt.Sprintf("%s.sysext.raw", name))
}
logger.Info("πŸš€ Start sysext creation")

dir, err := os.MkdirTemp("", "auroraboot-sysext-")
if err != nil {
return fmt.Errorf("creating temp directory: %w", err)
}
defer func(path string) {
err := os.RemoveAll(path)
if err != nil {
logger.Logger.Error().Str("dir", dir).Err(err).Msg("β›” removing dir")
}
}(dir)
logger.Logger.Debug().Str("dir", dir).Msg("creating directory")

// Get the image struct
logger.Info("πŸ’Ώ Getting image info")
platform := fmt.Sprintf("linux/%s", ctx.String("arch"))
image, err := utils.GetImage(args.Get(1), platform, nil, nil)
if err != nil {
logger.Logger.Error().Str("image", args.Get(1)).Err(err).Msg("β›” getting image")
return err
}
// Only for sysext, confext not supported yet
AllowList := regexp.MustCompile(`^usr/*|^/usr/*`)
// extract the files into the temp dir
logger.Info("πŸ“€ Extracting archives from image layer")
err = sysext.ExtractFilesFromLastLayer(image, dir, logger, AllowList)
if err != nil {
logger.Logger.Error().Str("image", args.Get(1)).Err(err).Msg("β›” extracting layer")
}

// Now create the file that tells systemd that this is a sysext!
err = os.MkdirAll(filepath.Join(dir, "/usr/lib/extension-release.d/"), os.ModeDir|os.ModePerm)
if err != nil {
logger.Logger.Error().Str("dir", filepath.Join(dir, "/usr/lib/extension-release.d/")).Err(err).Msg("β›” creating dir")
return err
}

arch := "x86-64"
if ctx.String("arch") == "arm64" {
arch = "arm64"
}

extensionData := fmt.Sprintf("ID=_any\nARCHITECTURE=%s", arch)

// If the extension ships any service files, we want this so systemd is reloaded and the service available immediately
if ctx.Bool("service-reload") {
extensionData = fmt.Sprintf("%s\nEXTENSION_RELOAD_MANAGER=1", extensionData)
}
err = os.WriteFile(filepath.Join(dir, "/usr/lib/extension-release.d/", fmt.Sprintf("extension-release.%s", name)), []byte(extensionData), os.ModePerm)
if err != nil {
logger.Logger.Error().Str("file", fmt.Sprintf("extension-release.%s", name)).Err(err).Msg("β›” creating releasefile")
return err
}

logger.Logger.Info().Msg("πŸ“¦ Packing sysext into raw image")
// Call systemd-repart to create the sysext based off the files
outputFile := fmt.Sprintf("%s.sysext.raw", name)
if outputDir := ctx.String("output"); outputDir != "" {
outputFile = filepath.Join(outputDir, outputFile)
}
// Call systemd-repart to create the sysext based off the files
command := exec.Command(
"systemd-repart",
"--make-ddi=sysext",
"--image-policy=root=verity+signed+absent:usr=verity+signed+absent",
fmt.Sprintf("--architecture=%s", arch),
// Having a fixed predictable seed makes the Image UUID be always the same if the inputs are the same,
// so its a reproducible image. So getting the same files and same cert/key should produce a reproducible image always
// Another layer to verify images, even if its a manual check, we make it easier
fmt.Sprintf("--seed=%s", uuid.NewV5(uuid.NamespaceDNS, "kairos-sysext")),
fmt.Sprintf("--copy-source=%s", dir),
outputFile, // output sysext image
fmt.Sprintf("--private-key=%s", ctx.String("private-key")),
fmt.Sprintf("--certificate=%s", ctx.String("certificate")),
)
out, err := command.CombinedOutput()
logger.Logger.Debug().Str("output", string(out)).Msg("building sysext")
if err != nil {
logger.Logger.Error().Err(err).
Str("command", strings.Join(command.Args, " ")).
Str("output", string(out)).
Msg("β›” building sysext")
return err
}

logger.Logger.Info().Str("output", outputFile).Msg("πŸŽ‰ Done sysext creation")
return nil
},
}

0 comments on commit 2a1a58c

Please sign in to comment.