Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1633 port sysext command from enki #101

Merged
merged 3 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: "[email protected]"}},
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
},
}
Loading