1
// This Source Code Form is subject to the terms of the Mozilla Public
2
// License, v. 2.0. If a copy of the MPL was not distributed with this
3
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
// Package imager contains code related to generation of different boot assets for Talos Linux.
17
"github.com/siderolabs/go-procfs/procfs"
20
talosruntime "github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
21
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/board"
22
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform"
23
"github.com/siderolabs/talos/internal/pkg/secureboot/uki"
24
"github.com/siderolabs/talos/pkg/imager/extensions"
25
"github.com/siderolabs/talos/pkg/imager/overlay/executor"
26
"github.com/siderolabs/talos/pkg/imager/profile"
27
"github.com/siderolabs/talos/pkg/imager/utils"
28
"github.com/siderolabs/talos/pkg/machinery/config/merge"
29
"github.com/siderolabs/talos/pkg/machinery/constants"
30
"github.com/siderolabs/talos/pkg/machinery/imager/quirks"
31
"github.com/siderolabs/talos/pkg/machinery/kernel"
32
"github.com/siderolabs/talos/pkg/machinery/overlay"
33
"github.com/siderolabs/talos/pkg/machinery/version"
34
"github.com/siderolabs/talos/pkg/reporter"
37
// Imager is an interface for image generation.
41
overlayInstaller overlay.Installer[overlay.ExtraOptions]
42
extraProfiles map[string]profile.Profile
54
// New creates a new Imager.
55
func New(prof profile.Profile) (*Imager, error) {
61
// Execute image generation.
63
//nolint:gocyclo,cyclop
64
func (i *Imager) Execute(ctx context.Context, outputPath string, report *reporter.Reporter) (outputAssetPath string, err error) {
65
i.tempDir, err = os.MkdirTemp("", "imager")
67
return "", fmt.Errorf("failed to create temporary directory: %w", err)
70
defer os.RemoveAll(i.tempDir) //nolint:errcheck
72
// 0. Handle overlays first
73
if err = i.handleOverlay(ctx, report); err != nil {
77
if err = i.handleProf(); err != nil {
81
report.Report(reporter.Update{
82
Message: "profile ready:",
83
Status: reporter.StatusSucceeded,
86
// 1. Dump the profile.
87
if err = i.prof.Dump(os.Stderr); err != nil {
91
// 2. Transform `initramfs.xz` with system extensions
92
if err = i.buildInitramfs(ctx, report); err != nil {
96
// 3. Prepare kernel arguments.
97
if err = i.buildCmdline(); err != nil {
101
report.Report(reporter.Update{
102
Message: fmt.Sprintf("kernel command line: %s", i.cmdline),
103
Status: reporter.StatusSucceeded,
106
// 4. Build UKI if Secure Boot is enabled.
107
if i.prof.SecureBootEnabled() {
108
if err = i.buildUKI(ctx, report); err != nil {
113
// 5. Build the output.
114
outputAssetPath = filepath.Join(outputPath, i.prof.OutputPath())
116
switch i.prof.Output.Kind {
117
case profile.OutKindISO:
118
err = i.outISO(ctx, outputAssetPath, report)
119
case profile.OutKindKernel:
120
err = i.outKernel(outputAssetPath, report)
121
case profile.OutKindUKI:
122
err = i.outUKI(outputAssetPath, report)
123
case profile.OutKindInitramfs:
124
err = i.outInitramfs(outputAssetPath, report)
125
case profile.OutKindCmdline:
126
err = i.outCmdline(outputAssetPath)
127
case profile.OutKindImage:
128
err = i.outImage(ctx, outputAssetPath, report)
129
case profile.OutKindInstaller:
130
err = i.outInstaller(ctx, outputAssetPath, report)
131
case profile.OutKindUnknown:
134
return "", fmt.Errorf("unknown output kind: %s", i.prof.Output.Kind)
141
report.Report(reporter.Update{
142
Message: fmt.Sprintf("output asset path: %s", outputAssetPath),
143
Status: reporter.StatusSucceeded,
146
// 6. Post-process the output.
147
switch i.prof.Output.OutFormat {
148
case profile.OutFormatRaw:
150
return outputAssetPath, nil
151
case profile.OutFormatXZ:
152
return i.postProcessXz(outputAssetPath, report)
153
case profile.OutFormatGZ:
154
return i.postProcessGz(outputAssetPath, report)
155
case profile.OutFormatZSTD:
156
return i.postProcessZstd(outputAssetPath, report)
157
case profile.OutFormatTar:
158
return i.postProcessTar(outputAssetPath, report)
159
case profile.OutFormatUnknown:
162
return "", fmt.Errorf("unknown output format: %s", i.prof.Output.OutFormat)
166
func (i *Imager) handleOverlay(ctx context.Context, report *reporter.Reporter) error {
167
if i.prof.Overlay == nil {
168
report.Report(reporter.Update{
169
Message: "skipped pulling overlay (no overlay)",
170
Status: reporter.StatusSkip,
176
tempOverlayPath := filepath.Join(i.tempDir, constants.ImagerOverlayBasePath)
178
if err := os.MkdirAll(tempOverlayPath, 0o755); err != nil {
179
return fmt.Errorf("failed to create overlay directory: %w", err)
182
if err := i.prof.Overlay.Image.Extract(ctx, tempOverlayPath, runtime.GOARCH, progressPrintf(report, reporter.Update{Message: "pulling overlay...", Status: reporter.StatusRunning})); err != nil {
186
// find all *.yaml files in the overlay/profiles/ directory
187
profileYAMLs, err := filepath.Glob(filepath.Join(i.tempDir, constants.ImagerOverlayProfilesPath, "*.yaml"))
189
return fmt.Errorf("failed to find profiles: %w", err)
192
if i.prof.Overlay.Name == "" {
193
i.prof.Overlay.Name = constants.ImagerOverlayInstallerDefault
196
i.overlayInstaller = executor.New(filepath.Join(i.tempDir, constants.ImagerOverlayInstallersPath, i.prof.Overlay.Name))
198
if i.extraProfiles == nil {
199
i.extraProfiles = make(map[string]profile.Profile)
202
for _, profilePath := range profileYAMLs {
203
profileName := strings.TrimSuffix(filepath.Base(profilePath), ".yaml")
205
var overlayProfile profile.Profile
207
profileDataBytes, err := os.ReadFile(profilePath)
209
return fmt.Errorf("failed to read profile: %w", err)
212
if err := yaml.Unmarshal(profileDataBytes, &overlayProfile); err != nil {
213
return fmt.Errorf("failed to unmarshal profile: %w", err)
216
i.extraProfiles[profileName] = overlayProfile
222
func (i *Imager) handleProf() error {
223
// resolve the profile if it contains a base name
224
if i.prof.BaseProfileName != "" {
225
baseProfile, ok := i.extraProfiles[i.prof.BaseProfileName]
228
baseProfile, ok = profile.Default[i.prof.BaseProfileName]
232
return fmt.Errorf("unknown base profile: %s", i.prof.BaseProfileName)
235
baseProfile = baseProfile.DeepCopy()
237
// merge the profiles
238
if err := merge.Merge(&baseProfile, &i.prof); err != nil {
243
i.prof.BaseProfileName = ""
246
if i.prof.Version == "" {
247
i.prof.Version = version.Tag
250
if err := i.prof.Validate(); err != nil {
251
return fmt.Errorf("profile is invalid: %w", err)
254
i.prof.Input.FillDefaults(i.prof.Arch, i.prof.Version, i.prof.SecureBootEnabled())
259
// buildInitramfs transforms `initramfs.xz` with system extensions.
260
func (i *Imager) buildInitramfs(ctx context.Context, report *reporter.Reporter) error {
261
if len(i.prof.Input.SystemExtensions) == 0 {
262
report.Report(reporter.Update{
263
Message: "skipped initramfs rebuild (no system extensions)",
264
Status: reporter.StatusSkip,
267
// no system extensions, happy path
268
i.initramfsPath = i.prof.Input.Initramfs.Path
273
if i.prof.Output.Kind == profile.OutKindCmdline || i.prof.Output.Kind == profile.OutKindKernel {
274
// these outputs don't use initramfs image
278
printf := progressPrintf(report, reporter.Update{Message: "rebuilding initramfs with system extensions...", Status: reporter.StatusRunning})
280
// copy the initramfs to a temporary location, as it's going to be modified during the extension build process
281
tempInitramfsPath := filepath.Join(i.tempDir, "initramfs.xz")
283
if err := utils.CopyFiles(printf, utils.SourceDestination(i.prof.Input.Initramfs.Path, tempInitramfsPath)); err != nil {
284
return fmt.Errorf("failed to copy initramfs: %w", err)
287
i.initramfsPath = tempInitramfsPath
289
extensionsCheckoutDir := filepath.Join(i.tempDir, "extensions")
291
// pull every extension to a temporary location
292
for j, ext := range i.prof.Input.SystemExtensions {
293
extensionDir := filepath.Join(extensionsCheckoutDir, strconv.Itoa(j))
295
if err := os.MkdirAll(extensionDir, 0o755); err != nil {
296
return fmt.Errorf("failed to create extension directory: %w", err)
299
if err := ext.Extract(ctx, extensionDir, i.prof.Arch, printf); err != nil {
305
builder := extensions.Builder{
306
InitramfsPath: i.initramfsPath,
308
ExtensionTreePath: extensionsCheckoutDir,
310
Quirks: quirks.New(i.prof.Version),
313
if err := builder.Build(); err != nil {
317
report.Report(reporter.Update{
318
Message: "initramfs ready",
319
Status: reporter.StatusSucceeded,
325
// buildCmdline builds the kernel command line.
328
func (i *Imager) buildCmdline() error {
329
p, err := platform.NewPlatform(i.prof.Platform)
334
cmdline := procfs.NewCmdline("")
336
// platform kernel args
337
cmdline.Append(constants.KernelParamPlatform, p.Name())
338
cmdline.SetAll(p.KernelArgs(i.prof.Arch).Strings())
341
if i.prof.Board != "" && !quirks.New(i.prof.Version).SupportsOverlay() {
342
var b talosruntime.Board
344
b, err = board.NewBoard(i.prof.Board)
349
cmdline.Append(constants.KernelParamBoard, b.Name())
350
cmdline.SetAll(b.KernelArgs().Strings())
353
// overlay kernel args
354
if i.overlayInstaller != nil {
355
options, optsErr := i.overlayInstaller.GetOptions(i.prof.Overlay.ExtraOptions)
360
cmdline.SetAll(options.KernelArgs)
363
// first defaults, then extra kernel args to allow extra kernel args to override defaults
364
if err = cmdline.AppendAll(kernel.DefaultArgs); err != nil {
368
if i.prof.SecureBootEnabled() {
369
if err = cmdline.AppendAll(kernel.SecureBootArgs); err != nil {
374
// meta values can be written only to the "image" output
375
if len(i.prof.Customization.MetaContents) > 0 && i.prof.Output.Kind != profile.OutKindImage {
376
// pass META values as kernel talos.environment args which will be passed via the environment to the installer
378
constants.KernelParamEnvironment,
379
constants.MetaValuesEnvVar+"="+i.prof.Customization.MetaContents.Encode(quirks.New(i.prof.Version).SupportsCompressedEncodedMETA()),
383
// apply customization
384
if err = cmdline.AppendAll(
385
i.prof.Customization.ExtraKernelArgs,
386
procfs.WithOverwriteArgs("console"),
387
procfs.WithOverwriteArgs(constants.KernelParamPlatform),
388
procfs.WithDeleteNegatedArgs(),
393
i.cmdline = cmdline.String()
398
// buildUKI assembles the UKI and signs it.
399
func (i *Imager) buildUKI(ctx context.Context, report *reporter.Reporter) error {
400
printf := progressPrintf(report, reporter.Update{Message: "building UKI...", Status: reporter.StatusRunning})
402
i.sdBootPath = filepath.Join(i.tempDir, "systemd-boot.efi.signed")
403
i.ukiPath = filepath.Join(i.tempDir, "vmlinuz.efi.signed")
405
pcrSigner, err := i.prof.Input.SecureBoot.PCRSigner.GetSigner(ctx)
407
return fmt.Errorf("failed to get PCR signer: %w", err)
410
securebootSigner, err := i.prof.Input.SecureBoot.SecureBootSigner.GetSigner(ctx)
412
return fmt.Errorf("failed to get SecureBoot signer: %w", err)
415
builder := uki.Builder{
417
Version: i.prof.Version,
418
SdStubPath: i.prof.Input.SDStub.Path,
419
SdBootPath: i.prof.Input.SDBoot.Path,
420
KernelPath: i.prof.Input.Kernel.Path,
421
InitrdPath: i.initramfsPath,
424
SecureBootSigner: securebootSigner,
425
PCRSigner: pcrSigner,
427
OutSdBootPath: i.sdBootPath,
428
OutUKIPath: i.ukiPath,
431
if err := builder.Build(printf); err != nil {
435
report.Report(reporter.Update{
436
Message: "UKI ready",
437
Status: reporter.StatusSucceeded,