feat: improve imager APIs

* report the final output path of the asset
* allow 'cmdline' output (just to get the kernel cmdline, e.g. for PXE
  booting)
* support pre-pulled container images for extensions

Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
This commit is contained in:
Andrey Smirnov 2023-08-17 22:12:36 +04:00
parent 2d3ac925ea
commit 44f59a8049
No known key found for this signature in database
GPG Key ID: FE042E3D4085A811
10 changed files with 78 additions and 33 deletions

View File

@ -326,7 +326,7 @@ secureboot-installer: ## Builds UEFI only installer which uses UKI and push it t
@$(MAKE) image-secureboot-installer IMAGER_ARGS="--base-installer-image $(REGISTRY_AND_USERNAME)/installer:$(IMAGE_TAG)" @$(MAKE) image-secureboot-installer IMAGER_ARGS="--base-installer-image $(REGISTRY_AND_USERNAME)/installer:$(IMAGE_TAG)"
@for platform in $(subst $(,),$(space),$(PLATFORM)); do \ @for platform in $(subst $(,),$(space),$(PLATFORM)); do \
arch=$$(basename "$${platform}") && \ arch=$$(basename "$${platform}") && \
crane push $(ARTIFACTS)/metal-$${arch}-secureboot-installer.tar $(REGISTRY_AND_USERNAME)/installer:$(IMAGE_TAG)-$${arch}-secureboot ; \ crane push $(ARTIFACTS)/installer-$${arch}-secureboot.tar $(REGISTRY_AND_USERNAME)/installer:$(IMAGE_TAG)-$${arch}-secureboot ; \
done done
.PHONY: talosctl-cni-bundle .PHONY: talosctl-cni-bundle

View File

@ -105,7 +105,7 @@ var rootCmd = &cobra.Command{
return err return err
} }
if err = imager.Execute(ctx, cmdFlags.OutputPath, report); err != nil { if _, err = imager.Execute(ctx, cmdFlags.OutputPath, report); err != nil {
report.Report(reporter.Update{ report.Report(reporter.Update{
Message: err.Error(), Message: err.Error(),
Status: reporter.StatusError, Status: reporter.StatusError,

View File

@ -80,12 +80,10 @@ func New(prof profile.Profile) (*Imager, error) {
// Execute image generation. // Execute image generation.
// //
//nolint:gocyclo,cyclop //nolint:gocyclo,cyclop
func (i *Imager) Execute(ctx context.Context, outputPath string, report *reporter.Reporter) error { func (i *Imager) Execute(ctx context.Context, outputPath string, report *reporter.Reporter) (outputAssetPath string, err error) {
var err error
i.tempDir, err = os.MkdirTemp("", "imager") i.tempDir, err = os.MkdirTemp("", "imager")
if err != nil { if err != nil {
return fmt.Errorf("failed to create temporary directory: %w", err) return "", fmt.Errorf("failed to create temporary directory: %w", err)
} }
defer os.RemoveAll(i.tempDir) //nolint:errcheck defer os.RemoveAll(i.tempDir) //nolint:errcheck
@ -97,17 +95,17 @@ func (i *Imager) Execute(ctx context.Context, outputPath string, report *reporte
// 0. Dump the profile. // 0. Dump the profile.
if err = i.prof.Dump(os.Stderr); err != nil { if err = i.prof.Dump(os.Stderr); err != nil {
return err return "", err
} }
// 1. Transform `initramfs.xz` with system extensions // 1. Transform `initramfs.xz` with system extensions
if err = i.buildInitramfs(ctx, report); err != nil { if err = i.buildInitramfs(ctx, report); err != nil {
return err return "", err
} }
// 2. Prepare kernel arguments. // 2. Prepare kernel arguments.
if err = i.buildCmdline(); err != nil { if err = i.buildCmdline(); err != nil {
return err return "", err
} }
report.Report(reporter.Update{ report.Report(reporter.Update{
@ -118,12 +116,12 @@ func (i *Imager) Execute(ctx context.Context, outputPath string, report *reporte
// 3. Build UKI if Secure Boot is enabled. // 3. Build UKI if Secure Boot is enabled.
if i.prof.SecureBootEnabled() { if i.prof.SecureBootEnabled() {
if err = i.buildUKI(report); err != nil { if err = i.buildUKI(report); err != nil {
return err return "", err
} }
} }
// 4. Build the output. // 4. Build the output.
outputAssetPath := filepath.Join(outputPath, i.prof.OutputPath()) outputAssetPath = filepath.Join(outputPath, i.prof.OutputPath())
switch i.prof.Output.Kind { switch i.prof.Output.Kind {
case profile.OutKindISO: case profile.OutKindISO:
@ -134,6 +132,8 @@ func (i *Imager) Execute(ctx context.Context, outputPath string, report *reporte
err = i.outUKI(outputAssetPath, report) err = i.outUKI(outputAssetPath, report)
case profile.OutKindInitramfs: case profile.OutKindInitramfs:
err = i.outInitramfs(outputAssetPath, report) err = i.outInitramfs(outputAssetPath, report)
case profile.OutKindCmdline:
err = i.outCmdline(outputAssetPath)
case profile.OutKindImage: case profile.OutKindImage:
err = i.outImage(ctx, outputAssetPath, report) err = i.outImage(ctx, outputAssetPath, report)
case profile.OutKindInstaller: case profile.OutKindInstaller:
@ -141,11 +141,11 @@ func (i *Imager) Execute(ctx context.Context, outputPath string, report *reporte
case profile.OutKindUnknown: case profile.OutKindUnknown:
fallthrough fallthrough
default: default:
return fmt.Errorf("unknown output kind: %s", i.prof.Output.Kind) return "", fmt.Errorf("unknown output kind: %s", i.prof.Output.Kind)
} }
if err != nil { if err != nil {
return err return "", err
} }
report.Report(reporter.Update{ report.Report(reporter.Update{
@ -157,7 +157,7 @@ func (i *Imager) Execute(ctx context.Context, outputPath string, report *reporte
switch i.prof.Output.OutFormat { switch i.prof.Output.OutFormat {
case profile.OutFormatRaw: case profile.OutFormatRaw:
// do nothing // do nothing
return nil return outputAssetPath, nil
case profile.OutFormatXZ: case profile.OutFormatXZ:
return i.postProcessXz(outputAssetPath, report) return i.postProcessXz(outputAssetPath, report)
case profile.OutFormatGZ: case profile.OutFormatGZ:
@ -167,7 +167,7 @@ func (i *Imager) Execute(ctx context.Context, outputPath string, report *reporte
case profile.OutFormatUnknown: case profile.OutFormatUnknown:
fallthrough fallthrough
default: default:
return fmt.Errorf("unknown output format: %s", i.prof.Output.OutFormat) return "", fmt.Errorf("unknown output format: %s", i.prof.Output.OutFormat)
} }
} }
@ -185,6 +185,11 @@ func (i *Imager) buildInitramfs(ctx context.Context, report *reporter.Reporter)
return nil return nil
} }
if i.prof.Output.Kind == profile.OutKindCmdline || i.prof.Output.Kind == profile.OutKindKernel {
// these outputs don't use initramfs image
return nil
}
printf := progressPrintf(report, reporter.Update{Message: "rebuilding initramfs with system extensions...", Status: reporter.StatusRunning}) printf := progressPrintf(report, reporter.Update{Message: "rebuilding initramfs with system extensions...", Status: reporter.StatusRunning})
// copy the initramfs to a temporary location, as it's going to be modified during the extension build process // copy the initramfs to a temporary location, as it's going to be modified during the extension build process

View File

@ -8,6 +8,7 @@ import (
"context" "context"
"fmt" "fmt"
"log" "log"
"os"
"path/filepath" "path/filepath"
"time" "time"
@ -67,6 +68,10 @@ func (i *Imager) outUKI(path string, report *reporter.Reporter) error {
return nil return nil
} }
func (i *Imager) outCmdline(path string) error {
return os.WriteFile(path, []byte(i.cmdline), 0o644)
}
func (i *Imager) outISO(path string, report *reporter.Reporter) error { func (i *Imager) outISO(path string, report *reporter.Reporter) error {
printf := progressPrintf(report, reporter.Update{Message: "building ISO...", Status: reporter.StatusRunning}) printf := progressPrintf(report, reporter.Update{Message: "building ISO...", Status: reporter.StatusRunning})

View File

@ -14,51 +14,51 @@ import (
"github.com/siderolabs/talos/pkg/reporter" "github.com/siderolabs/talos/pkg/reporter"
) )
func (i *Imager) postProcessTar(filename string, report *reporter.Reporter) error { func (i *Imager) postProcessTar(filename string, report *reporter.Reporter) (string, error) {
report.Report(reporter.Update{Message: "processing .tar.gz", Status: reporter.StatusRunning}) report.Report(reporter.Update{Message: "processing .tar.gz", Status: reporter.StatusRunning})
dir := filepath.Dir(filename) dir := filepath.Dir(filename)
src := "disk.raw" src := "disk.raw"
if err := os.Rename(filename, filepath.Join(dir, src)); err != nil { if err := os.Rename(filename, filepath.Join(dir, src)); err != nil {
return err return "", err
} }
outPath := filename + ".tar.gz" outPath := filename + ".tar.gz"
if _, err := cmd.Run("tar", "-cvf", outPath, "-C", dir, "--sparse", "--use-compress-program=pigz -6", src); err != nil { if _, err := cmd.Run("tar", "-cvf", outPath, "-C", dir, "--sparse", "--use-compress-program=pigz -6", src); err != nil {
return err return "", err
} }
if err := os.Remove(filepath.Join(dir, src)); err != nil { if err := os.Remove(filepath.Join(dir, src)); err != nil {
return err return "", err
} }
report.Report(reporter.Update{Message: fmt.Sprintf("archive is ready: %s", outPath), Status: reporter.StatusSucceeded}) report.Report(reporter.Update{Message: fmt.Sprintf("archive is ready: %s", outPath), Status: reporter.StatusSucceeded})
return nil return outPath, nil
} }
func (i *Imager) postProcessGz(filename string, report *reporter.Reporter) error { func (i *Imager) postProcessGz(filename string, report *reporter.Reporter) (string, error) {
report.Report(reporter.Update{Message: "compressing .gz", Status: reporter.StatusRunning}) report.Report(reporter.Update{Message: "compressing .gz", Status: reporter.StatusRunning})
if _, err := cmd.Run("pigz", "-6", "-f", filename); err != nil { if _, err := cmd.Run("pigz", "-6", "-f", filename); err != nil {
return err return "", err
} }
report.Report(reporter.Update{Message: fmt.Sprintf("compression done: %s.gz", filename), Status: reporter.StatusSucceeded}) report.Report(reporter.Update{Message: fmt.Sprintf("compression done: %s.gz", filename), Status: reporter.StatusSucceeded})
return nil return filename + ".gz", nil
} }
func (i *Imager) postProcessXz(filename string, report *reporter.Reporter) error { func (i *Imager) postProcessXz(filename string, report *reporter.Reporter) (string, error) {
report.Report(reporter.Update{Message: "compressing .xz", Status: reporter.StatusRunning}) report.Report(reporter.Update{Message: "compressing .xz", Status: reporter.StatusRunning})
if _, err := cmd.Run("xz", "-0", "-f", "-T", "0", filename); err != nil { if _, err := cmd.Run("xz", "-0", "-f", "-T", "0", filename); err != nil {
return err return "", err
} }
report.Report(reporter.Update{Message: fmt.Sprintf("compression done: %s.xz", filename), Status: reporter.StatusSucceeded}) report.Report(reporter.Update{Message: fmt.Sprintf("compression done: %s.xz", filename), Status: reporter.StatusSucceeded})
return nil return filename + ".xz", nil
} }

View File

@ -48,6 +48,10 @@ type FileAsset struct {
type ContainerAsset struct { type ContainerAsset struct {
// ImageRef is a reference to the container image. // ImageRef is a reference to the container image.
ImageRef string `yaml:"imageRef"` ImageRef string `yaml:"imageRef"`
// TarballPath is a path to the .tar format container image contents.
//
// If TarballPath is set, ImageRef is ignored.
TarballPath string `yaml:"tarballPath,omitempty"`
} }
// SecureBootAssets describes secureboot assets. // SecureBootAssets describes secureboot assets.
@ -144,6 +148,10 @@ func fileExists(path string) bool {
// Pull the container asset to the path. // Pull the container asset to the path.
func (c *ContainerAsset) Pull(ctx context.Context, arch string, printf func(string, ...any)) (v1.Image, error) { func (c *ContainerAsset) Pull(ctx context.Context, arch string, printf func(string, ...any)) (v1.Image, error) {
if c.TarballPath != "" {
return nil, fmt.Errorf("pulling tarball container image is not supported")
}
printf("pulling %s...", c.ImageRef) printf("pulling %s...", c.ImageRef)
img, err := crane.Pull(c.ImageRef, crane.WithPlatform(&v1.Platform{ img, err := crane.Pull(c.ImageRef, crane.WithPlatform(&v1.Platform{
@ -159,6 +167,19 @@ func (c *ContainerAsset) Pull(ctx context.Context, arch string, printf func(stri
// Extract the container asset to the path. // Extract the container asset to the path.
func (c *ContainerAsset) Extract(ctx context.Context, destination, arch string, printf func(string, ...any)) error { func (c *ContainerAsset) Extract(ctx context.Context, destination, arch string, printf func(string, ...any)) error {
if c.TarballPath != "" {
in, err := os.Open(c.TarballPath)
if err != nil {
return err
}
defer in.Close() //nolint:errcheck
printf("extracting %s...", c.TarballPath)
return archiver.Untar(ctx, in, destination)
}
img, err := c.Pull(ctx, arch, printf) img, err := c.Pull(ctx, arch, printf)
if err != nil { if err != nil {
return err return err

View File

@ -51,6 +51,7 @@ const (
OutKindKernel // kernel OutKindKernel // kernel
OutKindInitramfs // initramfs OutKindInitramfs // initramfs
OutKindUKI // uki OutKindUKI // uki
OutKindCmdline // cmdline
) )
//go:generate enumer -type OutFormat -linecomment -text //go:generate enumer -type OutFormat -linecomment -text

View File

@ -6,9 +6,9 @@ import (
"fmt" "fmt"
) )
const _OutputKindName = "unknownisoimageinstallerkernelinitramfsuki" const _OutputKindName = "unknownisoimageinstallerkernelinitramfsukicmdline"
var _OutputKindIndex = [...]uint8{0, 7, 10, 15, 24, 30, 39, 42} var _OutputKindIndex = [...]uint8{0, 7, 10, 15, 24, 30, 39, 42, 49}
func (i OutputKind) String() string { func (i OutputKind) String() string {
if i < 0 || i >= OutputKind(len(_OutputKindIndex)-1) { if i < 0 || i >= OutputKind(len(_OutputKindIndex)-1) {
@ -17,7 +17,7 @@ func (i OutputKind) String() string {
return _OutputKindName[_OutputKindIndex[i]:_OutputKindIndex[i+1]] return _OutputKindName[_OutputKindIndex[i]:_OutputKindIndex[i+1]]
} }
var _OutputKindValues = []OutputKind{0, 1, 2, 3, 4, 5, 6} var _OutputKindValues = []OutputKind{0, 1, 2, 3, 4, 5, 6, 7}
var _OutputKindNameToValueMap = map[string]OutputKind{ var _OutputKindNameToValueMap = map[string]OutputKind{
_OutputKindName[0:7]: 0, _OutputKindName[0:7]: 0,
@ -27,6 +27,7 @@ var _OutputKindNameToValueMap = map[string]OutputKind{
_OutputKindName[24:30]: 4, _OutputKindName[24:30]: 4,
_OutputKindName[30:39]: 5, _OutputKindName[30:39]: 5,
_OutputKindName[39:42]: 6, _OutputKindName[39:42]: 6,
_OutputKindName[42:49]: 7,
} }
// OutputKindString retrieves an enum value from the enum constants string name. // OutputKindString retrieves an enum value from the enum constants string name.

View File

@ -55,7 +55,7 @@ func (p *Profile) SecureBootEnabled() bool {
// Validate the profile. // Validate the profile.
// //
//nolint:gocyclo //nolint:gocyclo,cyclop
func (p *Profile) Validate() error { func (p *Profile) Validate() error {
if p.Arch != "amd64" && p.Arch != "arm64" { if p.Arch != "amd64" && p.Arch != "arm64" {
return fmt.Errorf("invalid arch %q", p.Arch) return fmt.Errorf("invalid arch %q", p.Arch)
@ -76,6 +76,8 @@ func (p *Profile) Validate() error {
return fmt.Errorf("unknown output kind") return fmt.Errorf("unknown output kind")
case OutKindISO: case OutKindISO:
// ISO supports all kinds of customization // ISO supports all kinds of customization
case OutKindCmdline:
// cmdline supports all kinds of customization
case OutKindImage: case OutKindImage:
// Image supports all kinds of customization // Image supports all kinds of customization
if p.Output.ImageOptions.DiskSize == 0 { if p.Output.ImageOptions.DiskSize == 0 {
@ -111,6 +113,8 @@ func (p *Profile) Validate() error {
} }
// OutputPath generates the output path for the profile. // OutputPath generates the output path for the profile.
//
//nolint:gocyclo
func (p *Profile) OutputPath() string { func (p *Profile) OutputPath() string {
path := p.Platform path := p.Platform
@ -132,13 +136,21 @@ func (p *Profile) OutputPath() string {
case OutKindImage: case OutKindImage:
path += "." + p.Output.ImageOptions.DiskFormat.String() path += "." + p.Output.ImageOptions.DiskFormat.String()
case OutKindInstaller: case OutKindInstaller:
path += "-installer.tar" path = "installer-" + p.Arch
if p.SecureBootEnabled() {
path += "-secureboot"
}
path += ".tar"
case OutKindKernel: case OutKindKernel:
path = "kernel-" + p.Arch path = "kernel-" + p.Arch
case OutKindInitramfs: case OutKindInitramfs:
path = "initramfs-" + path + ".xz" path = "initramfs-" + path + ".xz"
case OutKindUKI: case OutKindUKI:
path += "-uki.efi" path += "-uki.efi"
case OutKindCmdline:
path = "cmdline-" + path
} }
return path return path

View File

@ -139,13 +139,13 @@ skipped initramfs rebuild (no system extensions)
kernel command line: talos.platform=metal console=ttyS0 console=tty0 init_on_alloc=1 slab_nomerge pti=on consoleblank=0 nvme_core.io_timeout=4294967295 printk.devkmsg=on ima_template=ima-ng ima_appraise=fix ima_hash=sha512 lockdown=confidentiality kernel command line: talos.platform=metal console=ttyS0 console=tty0 init_on_alloc=1 slab_nomerge pti=on consoleblank=0 nvme_core.io_timeout=4294967295 printk.devkmsg=on ima_template=ima-ng ima_appraise=fix ima_hash=sha512 lockdown=confidentiality
UKI ready UKI ready
installer container image ready installer container image ready
output asset path: /out/metal-amd64-secureboot-installer.tar output asset path: /out/installer-amd64-secureboot.tar
``` ```
The generated container image should be pushed to some container registry which Talos can access during the installation, e.g.: The generated container image should be pushed to some container registry which Talos can access during the installation, e.g.:
```shell ```shell
crane push _out/metal-amd64-secureboot-installer.tar ghcr.io/<user>/installer-amd64-secureboot:{{< release >}} crane push _out/installer-amd64-secureboot.tar ghcr.io/<user>/installer-amd64-secureboot:{{< release >}}
``` ```
The generated ISO and installer images might be further customized with system extensions, extra kernel command line arguments, etc. The generated ISO and installer images might be further customized with system extensions, extra kernel command line arguments, etc.