From 2a1a58c58e0d0c931ca53a8c3c2a4728a5f3e29a Mon Sep 17 00:00:00 2001 From: Dimitris Karakasilis Date: Tue, 12 Nov 2024 10:14:22 +0200 Subject: [PATCH] 1633 port sysext command from enki (#101) * Port sysext command from enki part of: https://github.com/kairos-io/kairos/issues/1633 Signed-off-by: Dimitris Karakasilis * Don't use viper Signed-off-by: Dimitris Karakasilis * Use global "--debug" option and some minor fixes - args that weren't used - command output was not printed Signed-off-by: Dimitris Karakasilis --------- Signed-off-by: Dimitris Karakasilis --- internal/cmd/app.go | 2 +- internal/cmd/build-uki.go | 19 +++-- internal/cmd/sysext.go | 160 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+), 11 deletions(-) create mode 100644 internal/cmd/sysext.go diff --git a/internal/cmd/app.go b/internal/cmd/app.go index 93753d1..41bf177 100644 --- a/internal/cmd/app.go +++ b/internal/cmd/app.go @@ -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", diff --git a/internal/cmd/build-uki.go b/internal/cmd/build-uki.go index d56595d..7bace8b 100644 --- a/internal/cmd/build-uki.go +++ b/internal/cmd/build-uki.go @@ -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) @@ -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): @@ -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 } @@ -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{ @@ -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 } @@ -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))) diff --git a/internal/cmd/sysext.go b/internal/cmd/sysext.go new file mode 100644 index 0000000..334b757 --- /dev/null +++ b/internal/cmd/sysext.go @@ -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: " ", + + 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 + }, +}