feat: imager overlay

Support overlays for imager.
The `Install` interface is not wired yet, it will be done as a different
PR.

This should be a no-op for existing imager.

Part of: #8350

Signed-off-by: Noel Georgi <git@frezbo.dev>
This commit is contained in:
Noel Georgi 2024-02-28 06:22:58 +05:30
parent 0b9b4da12a
commit 8125e754b8
No known key found for this signature in database
GPG Key ID: 21A9F444075C9E36
9 changed files with 361 additions and 39 deletions

View File

@ -37,6 +37,8 @@ var cmdFlags struct {
OutputPath string
OutputKind string
TarToStdout bool
OverlayName string
OverlayImage string
}
// rootCmd represents the base command when called without any subcommands.
@ -74,6 +76,15 @@ var rootCmd = &cobra.Command{
},
}
if cmdFlags.OverlayName != "" || cmdFlags.OverlayImage != "" {
prof.Overlay = &profile.OverlayOptions{
Name: cmdFlags.OverlayName,
Image: profile.ContainerAsset{
ImageRef: cmdFlags.OverlayImage,
},
}
}
prof.Input.SystemExtensions = xslices.Map(
cmdFlags.SystemExtensionImages,
func(imageRef string) profile.ContainerAsset {
@ -163,4 +174,8 @@ func init() {
rootCmd.PersistentFlags().StringVar(&cmdFlags.OutputPath, "output", "/out", "The output directory path")
rootCmd.PersistentFlags().StringVar(&cmdFlags.OutputKind, "output-kind", "", "Override output kind")
rootCmd.PersistentFlags().BoolVar(&cmdFlags.TarToStdout, "tar-to-stdout", false, "Tar output and send to stdout")
rootCmd.PersistentFlags().StringVar(&cmdFlags.OverlayName, "overlay-name", "", "The name of the overlay to use")
rootCmd.PersistentFlags().StringVar(&cmdFlags.OverlayImage, "overlay-image", "", "The image reference to the overlay")
rootCmd.MarkFlagsMutuallyExclusive("board", "overlay-name")
rootCmd.MarkFlagsMutuallyExclusive("board", "overlay-image")
}

View File

@ -10,21 +10,26 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"github.com/siderolabs/go-procfs/procfs"
"gopkg.in/yaml.v3"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
talosruntime "github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/board"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform"
"github.com/siderolabs/talos/internal/pkg/secureboot/uki"
"github.com/siderolabs/talos/pkg/imager/extensions"
"github.com/siderolabs/talos/pkg/imager/internal/overlay/executor"
"github.com/siderolabs/talos/pkg/imager/profile"
"github.com/siderolabs/talos/pkg/imager/quirks"
"github.com/siderolabs/talos/pkg/imager/utils"
"github.com/siderolabs/talos/pkg/machinery/config/merge"
"github.com/siderolabs/talos/pkg/machinery/constants"
"github.com/siderolabs/talos/pkg/machinery/kernel"
"github.com/siderolabs/talos/pkg/machinery/overlay"
"github.com/siderolabs/talos/pkg/reporter"
"github.com/siderolabs/talos/pkg/version"
)
@ -33,6 +38,8 @@ import (
type Imager struct {
prof profile.Profile
overlayInstaller overlay.Installer
tempDir string
// boot assets
@ -45,34 +52,6 @@ type Imager struct {
// New creates a new Imager.
func New(prof profile.Profile) (*Imager, error) {
// resolve the profile if it contains a base name
if prof.BaseProfileName != "" {
baseProfile, ok := profile.Default[prof.BaseProfileName]
if !ok {
return nil, fmt.Errorf("unknown base profile: %s", prof.BaseProfileName)
}
baseProfile = baseProfile.DeepCopy()
// merge the profiles
if err := merge.Merge(&baseProfile, &prof); err != nil {
return nil, err
}
prof = baseProfile
prof.BaseProfileName = ""
}
if prof.Version == "" {
prof.Version = version.Tag
}
if err := prof.Validate(); err != nil {
return nil, fmt.Errorf("profile is invalid: %w", err)
}
prof.Input.FillDefaults(prof.Arch, prof.Version, prof.SecureBootEnabled())
return &Imager{
prof: prof,
}, nil
@ -89,22 +68,31 @@ func (i *Imager) Execute(ctx context.Context, outputPath string, report *reporte
defer os.RemoveAll(i.tempDir) //nolint:errcheck
// 0. Handle overlays first
if err = i.handleOverlay(ctx, report); err != nil {
return "", err
}
if err = i.handleProf(); err != nil {
return "", err
}
report.Report(reporter.Update{
Message: "profile ready:",
Status: reporter.StatusSucceeded,
})
// 0. Dump the profile.
// 1. Dump the profile.
if err = i.prof.Dump(os.Stderr); err != nil {
return "", err
}
// 1. Transform `initramfs.xz` with system extensions
// 2. Transform `initramfs.xz` with system extensions
if err = i.buildInitramfs(ctx, report); err != nil {
return "", err
}
// 2. Prepare kernel arguments.
// 3. Prepare kernel arguments.
if err = i.buildCmdline(); err != nil {
return "", err
}
@ -114,14 +102,14 @@ func (i *Imager) Execute(ctx context.Context, outputPath string, report *reporte
Status: reporter.StatusSucceeded,
})
// 3. Build UKI if Secure Boot is enabled.
// 4. Build UKI if Secure Boot is enabled.
if i.prof.SecureBootEnabled() {
if err = i.buildUKI(ctx, report); err != nil {
return "", err
}
}
// 4. Build the output.
// 5. Build the output.
outputAssetPath = filepath.Join(outputPath, i.prof.OutputPath())
switch i.prof.Output.Kind {
@ -154,7 +142,7 @@ func (i *Imager) Execute(ctx context.Context, outputPath string, report *reporte
Status: reporter.StatusSucceeded,
})
// 5. Post-process the output.
// 6. Post-process the output.
switch i.prof.Output.OutFormat {
case profile.OutFormatRaw:
// do nothing
@ -172,6 +160,92 @@ func (i *Imager) Execute(ctx context.Context, outputPath string, report *reporte
}
}
func (i *Imager) handleOverlay(ctx context.Context, report *reporter.Reporter) error {
if i.prof.Overlay == nil {
report.Report(reporter.Update{
Message: "skipped pulling overlay (no overlay)",
Status: reporter.StatusSkip,
})
return nil
}
tempOverlayPath := filepath.Join(i.tempDir, "overlay")
if err := os.MkdirAll(tempOverlayPath, 0o755); err != nil {
return fmt.Errorf("failed to create overlay directory: %w", err)
}
if err := i.prof.Overlay.Image.Extract(ctx, tempOverlayPath, runtime.GOARCH, progressPrintf(report, reporter.Update{Message: "pulling overlay...", Status: reporter.StatusRunning})); err != nil {
return err
}
// find all *.yaml files in the tempOverlayPath/profiles/ directory
profileYAMLs, err := filepath.Glob(filepath.Join(tempOverlayPath, "profiles", "*.yaml"))
if err != nil {
return fmt.Errorf("failed to find profiles: %w", err)
}
installerName := i.prof.Overlay.Name
if installerName == "" {
installerName = "default"
}
i.overlayInstaller = executor.New(filepath.Join(tempOverlayPath, "installers", installerName))
for _, profilePath := range profileYAMLs {
profileName := strings.TrimSuffix(filepath.Base(profilePath), ".yaml")
var overlayProfile profile.Profile
profileDataBytes, err := os.ReadFile(profilePath)
if err != nil {
return fmt.Errorf("failed to read profile: %w", err)
}
if err := yaml.Unmarshal(profileDataBytes, &overlayProfile); err != nil {
return fmt.Errorf("failed to unmarshal profile: %w", err)
}
profile.Default[profileName] = overlayProfile
}
return nil
}
func (i *Imager) handleProf() error {
// resolve the profile if it contains a base name
if i.prof.BaseProfileName != "" {
baseProfile, ok := profile.Default[i.prof.BaseProfileName]
if !ok {
return fmt.Errorf("unknown base profile: %s", i.prof.BaseProfileName)
}
baseProfile = baseProfile.DeepCopy()
// merge the profiles
if err := merge.Merge(&baseProfile, &i.prof); err != nil {
return err
}
i.prof = baseProfile
i.prof.BaseProfileName = ""
}
if i.prof.Version == "" {
i.prof.Version = version.Tag
}
if err := i.prof.Validate(); err != nil {
return fmt.Errorf("profile is invalid: %w", err)
}
i.prof.Input.FillDefaults(i.prof.Arch, i.prof.Version, i.prof.SecureBootEnabled())
return nil
}
// buildInitramfs transforms `initramfs.xz` with system extensions.
func (i *Imager) buildInitramfs(ctx context.Context, report *reporter.Reporter) error {
if len(i.prof.Input.SystemExtensions) == 0 {
@ -238,6 +312,8 @@ func (i *Imager) buildInitramfs(ctx context.Context, report *reporter.Reporter)
}
// buildCmdline builds the kernel command line.
//
//nolint:gocyclo
func (i *Imager) buildCmdline() error {
p, err := platform.NewPlatform(i.prof.Platform)
if err != nil {
@ -251,8 +327,9 @@ func (i *Imager) buildCmdline() error {
cmdline.SetAll(p.KernelArgs().Strings())
// board kernel args
// TODO: check if supports overlay quirk
if i.prof.Board != "" {
var b runtime.Board
var b talosruntime.Board
b, err = board.NewBoard(i.prof.Board)
if err != nil {
@ -263,6 +340,16 @@ func (i *Imager) buildCmdline() error {
cmdline.SetAll(b.KernelArgs().Strings())
}
// overlay kernel args
if i.overlayInstaller != nil {
options, optsErr := i.overlayInstaller.GetOptions(i.prof.Overlay.Options)
if optsErr != nil {
return optsErr
}
cmdline.SetAll(options.KernelArgs)
}
// first defaults, then extra kernel args to allow extra kernel args to override defaults
if err = cmdline.AppendAll(kernel.DefaultArgs); err != nil {
return err

View File

@ -0,0 +1,83 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package executor implements overlay.Installer
package executor
import (
"bytes"
"fmt"
"io"
"os/exec"
"gopkg.in/yaml.v2"
"github.com/siderolabs/talos/pkg/machinery/overlay"
)
var _ overlay.Installer = (*Options)(nil)
// Options executor options.
type Options struct {
commandPath string
}
// New returns a new overlay installer executor.
func New(commandPath string) *Options {
return &Options{
commandPath: commandPath,
}
}
// GetOptions returns the options for the overlay installer.
func (o *Options) GetOptions(extra overlay.InstallExtraOptions) (overlay.Options, error) {
// parse extra as yaml
extraYAML, err := yaml.Marshal(extra)
if err != nil {
return overlay.Options{}, fmt.Errorf("failed to marshal extra: %w", err)
}
out, err := o.execute(bytes.NewReader(extraYAML), "get-options")
if err != nil {
return overlay.Options{}, fmt.Errorf("failed to run overlay installer: %w", err)
}
var options overlay.Options
if err := yaml.Unmarshal(out, &options); err != nil {
return overlay.Options{}, fmt.Errorf("failed to unmarshal overlay options: %w", err)
}
return options, nil
}
// Install installs the overlay.
func (o *Options) Install(options overlay.InstallOptions) error {
optionsBytes, err := yaml.Marshal(&options)
if err != nil {
return fmt.Errorf("failed to marshal options: %w", err)
}
if _, err := o.execute(bytes.NewReader(optionsBytes), "install"); err != nil {
return fmt.Errorf("failed to run overlay installer: %w", err)
}
return nil
}
func (o *Options) execute(stdin io.Reader, args ...string) ([]byte, error) {
cmd := exec.Command(o.commandPath, args...)
cmd.Stdin = stdin
var stdOut, stdErr bytes.Buffer
cmd.Stdout = &stdOut
cmd.Stderr = &stdErr
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("failed to run overlay installer: %w, stdErr: %s", err, stdErr.Bytes())
}
return stdOut.Bytes(), nil
}

View File

@ -33,6 +33,16 @@ func (o Profile) DeepCopy() Profile {
cp.Input.SystemExtensions = make([]ContainerAsset, len(o.Input.SystemExtensions))
copy(cp.Input.SystemExtensions, o.Input.SystemExtensions)
}
if o.Overlay != nil {
cp.Overlay = new(OverlayOptions)
*cp.Overlay = *o.Overlay
if o.Overlay.Options != nil {
cp.Overlay.Options = make(map[string]any, len(o.Overlay.Options))
for k4, v4 := range o.Overlay.Options {
cp.Overlay.Options[k4] = v4
}
}
}
if o.Output.ImageOptions != nil {
cp.Output.ImageOptions = new(ImageOptions)
*cp.Output.ImageOptions = *o.Output.ImageOptions

View File

@ -26,6 +26,7 @@ import (
"github.com/siderolabs/talos/pkg/imager/profile/internal/signer/aws"
"github.com/siderolabs/talos/pkg/imager/profile/internal/signer/azure"
"github.com/siderolabs/talos/pkg/imager/profile/internal/signer/file"
"github.com/siderolabs/talos/pkg/imager/quirks"
"github.com/siderolabs/talos/pkg/images"
"github.com/siderolabs/talos/pkg/machinery/constants"
)
@ -166,7 +167,7 @@ const defaultSecureBootPrefix = "/secureboot"
// FillDefaults fills default values for the input.
//
//nolint:gocyclo
//nolint:gocyclo,cyclop
func (i *Input) FillDefaults(arch, version string, secureboot bool) {
var (
zeroFileAsset FileAsset
@ -181,7 +182,7 @@ func (i *Input) FillDefaults(arch, version string, secureboot bool) {
i.Initramfs.Path = fmt.Sprintf(constants.InitramfsAssetPath, arch)
}
if arch == arm64 {
if arch == arm64 && !quirks.New(version).SupportsOverlay() {
if i.DTB == zeroFileAsset {
i.DTB.Path = fmt.Sprintf(constants.DTBAssetPath, arch)
}

View File

@ -37,10 +37,22 @@ type Profile struct {
// Input describes inputs for image generation.
Input Input `yaml:"input"`
// Overlay describes overlay options for image generation.
Overlay *OverlayOptions `yaml:"overlay,omitempty"`
// Output describes image generation result.
Output Output `yaml:"output"`
}
// OverlayOptions describes overlay options for image generation.
type OverlayOptions struct {
// Name of the overlay installer, defaults to `default` if not set.
Name string `yaml:"name"`
// Image to use for the overlay.
Image ContainerAsset `yaml:"image"`
// Options for the overlay.
Options map[string]any `yaml:"options,omitempty"`
}
// CustomizationProfile describes customizations that can be applied to the image.
type CustomizationProfile struct {
// ExtraKernelArgs is a list of extra kernel arguments.
@ -67,7 +79,11 @@ func (p *Profile) Validate() error {
}
if p.Board != "" {
if !(p.Arch == arm64 && p.Platform == "metal") {
if p.Overlay != nil {
return errors.New("overlay is not supported with board options")
}
if p.Arch != arm64 || p.Platform != "metal" {
return errors.New("board is only supported for metal arm64")
}
}

View File

@ -45,3 +45,15 @@ func (q Quirks) SupportsCompressedEncodedMETA() bool {
return q.v.GTE(minVersionCompressedMETA)
}
var minVersionOverlay = semver.MustParse("1.7.0")
// SupportsOverlay returns true if the Talos imager version supports overlay.
func (q Quirks) SupportsOverlay() bool {
// if the version doesn't parse, we assume it's latest Talos
if q.v == nil {
return true
}
return q.v.GTE(minVersionOverlay)
}

View File

@ -0,0 +1,66 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package adapter provides an adapter for the overlay installer.
package adapter
import (
"fmt"
"os"
"gopkg.in/yaml.v3"
"github.com/siderolabs/talos/pkg/machinery/overlay"
)
// Execute executes the overlay installer.
func Execute(installer overlay.Installer) {
if len(os.Args) < 2 {
fmt.Fprint(os.Stderr, "missing command")
os.Exit(1)
}
switch os.Args[1] {
case "install":
install(installer)
case "get-options":
getOptions(installer)
default:
fmt.Fprintf(os.Stderr, "unknown command: %s", os.Args[1])
os.Exit(1)
}
}
func getOptions(installer overlay.Installer) {
var opts overlay.InstallExtraOptions
withErrorHandler(yaml.NewDecoder(os.Stdin).Decode(&opts))
opt, err := installer.GetOptions(opts)
if err != nil {
fmt.Fprint(os.Stderr, err.Error())
os.Exit(1)
}
withErrorHandler(yaml.NewEncoder(os.Stdout).Encode(opt))
}
func install(installer overlay.Installer) {
var opts overlay.InstallOptions
withErrorHandler(yaml.NewDecoder(os.Stdin).Decode(&opts))
withErrorHandler(installer.Install(opts))
}
func withErrorHandler(err error) {
if err != nil {
fmt.Fprint(os.Stderr, err.Error())
os.Exit(1)
}
}

View File

@ -0,0 +1,32 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package overlay provides an interface for overlay installers.
package overlay
// Installer is an interface for overlay installers.
type Installer interface {
GetOptions(extra InstallExtraOptions) (Options, error)
Install(options InstallOptions) error
}
// Options for the overlay installer.
type Options struct {
Name string `yaml:"name"`
KernelArgs []string `yaml:"kernelArgs,omitempty"`
PartitionOptions struct {
Offset uint64
} `yaml:"partitionOptions,omitempty"`
}
// InstallOptions for the overlay installer.
type InstallOptions struct {
InstallDisk string `yaml:"installDisk"`
MountPrefix string `yaml:"mountPrefix"`
ArtifactsPath string `yaml:"artifactsPath"`
ExtraOptions InstallExtraOptions `yaml:"extraOptions,omitempty"`
}
// InstallExtraOptions for the overlay installer.
type InstallExtraOptions map[string]any