From 72e2651f50b2cd70417a91c76809dee452e7550f Mon Sep 17 00:00:00 2001 From: Noel Georgi Date: Wed, 28 Feb 2024 06:22:58 +0530 Subject: [PATCH] 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 --- cmd/installer/cmd/imager/root.go | 15 ++ pkg/imager/imager.go | 159 ++++++++++++++---- .../internal/overlay/executor/executor.go | 83 +++++++++ pkg/imager/profile/deep_copy.generated.go | 10 ++ pkg/imager/profile/input.go | 5 +- pkg/imager/profile/profile.go | 18 +- pkg/imager/quirks/quirks.go | 12 ++ pkg/machinery/overlay/adapter/adapter.go | 66 ++++++++ pkg/machinery/overlay/overlay.go | 32 ++++ 9 files changed, 361 insertions(+), 39 deletions(-) create mode 100644 pkg/imager/internal/overlay/executor/executor.go create mode 100644 pkg/machinery/overlay/adapter/adapter.go create mode 100644 pkg/machinery/overlay/overlay.go diff --git a/cmd/installer/cmd/imager/root.go b/cmd/installer/cmd/imager/root.go index 9bc48f7b0f..a94669f382 100644 --- a/cmd/installer/cmd/imager/root.go +++ b/cmd/installer/cmd/imager/root.go @@ -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") } diff --git a/pkg/imager/imager.go b/pkg/imager/imager.go index 55024151e3..db1fad4283 100644 --- a/pkg/imager/imager.go +++ b/pkg/imager/imager.go @@ -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 diff --git a/pkg/imager/internal/overlay/executor/executor.go b/pkg/imager/internal/overlay/executor/executor.go new file mode 100644 index 0000000000..bad953d463 --- /dev/null +++ b/pkg/imager/internal/overlay/executor/executor.go @@ -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 +} diff --git a/pkg/imager/profile/deep_copy.generated.go b/pkg/imager/profile/deep_copy.generated.go index 1b0665ac36..0fd0bb1d6e 100644 --- a/pkg/imager/profile/deep_copy.generated.go +++ b/pkg/imager/profile/deep_copy.generated.go @@ -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 diff --git a/pkg/imager/profile/input.go b/pkg/imager/profile/input.go index 4c019df091..e8bdbced30 100644 --- a/pkg/imager/profile/input.go +++ b/pkg/imager/profile/input.go @@ -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) } diff --git a/pkg/imager/profile/profile.go b/pkg/imager/profile/profile.go index fdafc8a925..387e42436d 100644 --- a/pkg/imager/profile/profile.go +++ b/pkg/imager/profile/profile.go @@ -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") } } diff --git a/pkg/imager/quirks/quirks.go b/pkg/imager/quirks/quirks.go index 9d1c407384..9a31ceeb99 100644 --- a/pkg/imager/quirks/quirks.go +++ b/pkg/imager/quirks/quirks.go @@ -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) +} diff --git a/pkg/machinery/overlay/adapter/adapter.go b/pkg/machinery/overlay/adapter/adapter.go new file mode 100644 index 0000000000..2880d9c0db --- /dev/null +++ b/pkg/machinery/overlay/adapter/adapter.go @@ -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) + } +} diff --git a/pkg/machinery/overlay/overlay.go b/pkg/machinery/overlay/overlay.go new file mode 100644 index 0000000000..c5828ac872 --- /dev/null +++ b/pkg/machinery/overlay/overlay.go @@ -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