Skip to content

Commit

Permalink
feat: imager overlay
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
frezbo committed Feb 29, 2024
1 parent 0b9b4da commit 8125e75
Show file tree
Hide file tree
Showing 9 changed files with 361 additions and 39 deletions.
15 changes: 15 additions & 0 deletions cmd/installer/cmd/imager/root.go
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
}
159 changes: 123 additions & 36 deletions pkg/imager/imager.go
Expand Up @@ -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"
)
Expand All @@ -33,6 +38,8 @@ import (
type Imager struct {
prof profile.Profile

overlayInstaller overlay.Installer

tempDir string

// boot assets
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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
Expand Down
83 changes: 83 additions & 0 deletions 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
}
10 changes: 10 additions & 0 deletions pkg/imager/profile/deep_copy.generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 8125e75

Please sign in to comment.