talos

Форк
0
/
imager.go 
441 строка · 12.1 Кб
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/.
4

5
// Package imager contains code related to generation of different boot assets for Talos Linux.
6
package imager
7

8
import (
9
	"context"
10
	"fmt"
11
	"os"
12
	"path/filepath"
13
	"runtime"
14
	"strconv"
15
	"strings"
16

17
	"github.com/siderolabs/go-procfs/procfs"
18
	"gopkg.in/yaml.v3"
19

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"
35
)
36

37
// Imager is an interface for image generation.
38
type Imager struct {
39
	prof profile.Profile
40

41
	overlayInstaller overlay.Installer[overlay.ExtraOptions]
42
	extraProfiles    map[string]profile.Profile
43

44
	tempDir string
45

46
	// boot assets
47
	initramfsPath string
48
	cmdline       string
49

50
	sdBootPath string
51
	ukiPath    string
52
}
53

54
// New creates a new Imager.
55
func New(prof profile.Profile) (*Imager, error) {
56
	return &Imager{
57
		prof: prof,
58
	}, nil
59
}
60

61
// Execute image generation.
62
//
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")
66
	if err != nil {
67
		return "", fmt.Errorf("failed to create temporary directory: %w", err)
68
	}
69

70
	defer os.RemoveAll(i.tempDir) //nolint:errcheck
71

72
	// 0. Handle overlays first
73
	if err = i.handleOverlay(ctx, report); err != nil {
74
		return "", err
75
	}
76

77
	if err = i.handleProf(); err != nil {
78
		return "", err
79
	}
80

81
	report.Report(reporter.Update{
82
		Message: "profile ready:",
83
		Status:  reporter.StatusSucceeded,
84
	})
85

86
	// 1. Dump the profile.
87
	if err = i.prof.Dump(os.Stderr); err != nil {
88
		return "", err
89
	}
90

91
	// 2. Transform `initramfs.xz` with system extensions
92
	if err = i.buildInitramfs(ctx, report); err != nil {
93
		return "", err
94
	}
95

96
	// 3. Prepare kernel arguments.
97
	if err = i.buildCmdline(); err != nil {
98
		return "", err
99
	}
100

101
	report.Report(reporter.Update{
102
		Message: fmt.Sprintf("kernel command line: %s", i.cmdline),
103
		Status:  reporter.StatusSucceeded,
104
	})
105

106
	// 4. Build UKI if Secure Boot is enabled.
107
	if i.prof.SecureBootEnabled() {
108
		if err = i.buildUKI(ctx, report); err != nil {
109
			return "", err
110
		}
111
	}
112

113
	// 5. Build the output.
114
	outputAssetPath = filepath.Join(outputPath, i.prof.OutputPath())
115

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:
132
		fallthrough
133
	default:
134
		return "", fmt.Errorf("unknown output kind: %s", i.prof.Output.Kind)
135
	}
136

137
	if err != nil {
138
		return "", err
139
	}
140

141
	report.Report(reporter.Update{
142
		Message: fmt.Sprintf("output asset path: %s", outputAssetPath),
143
		Status:  reporter.StatusSucceeded,
144
	})
145

146
	// 6. Post-process the output.
147
	switch i.prof.Output.OutFormat {
148
	case profile.OutFormatRaw:
149
		// do nothing
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:
160
		fallthrough
161
	default:
162
		return "", fmt.Errorf("unknown output format: %s", i.prof.Output.OutFormat)
163
	}
164
}
165

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,
171
		})
172

173
		return nil
174
	}
175

176
	tempOverlayPath := filepath.Join(i.tempDir, constants.ImagerOverlayBasePath)
177

178
	if err := os.MkdirAll(tempOverlayPath, 0o755); err != nil {
179
		return fmt.Errorf("failed to create overlay directory: %w", err)
180
	}
181

182
	if err := i.prof.Overlay.Image.Extract(ctx, tempOverlayPath, runtime.GOARCH, progressPrintf(report, reporter.Update{Message: "pulling overlay...", Status: reporter.StatusRunning})); err != nil {
183
		return err
184
	}
185

186
	// find all *.yaml files in the overlay/profiles/ directory
187
	profileYAMLs, err := filepath.Glob(filepath.Join(i.tempDir, constants.ImagerOverlayProfilesPath, "*.yaml"))
188
	if err != nil {
189
		return fmt.Errorf("failed to find profiles: %w", err)
190
	}
191

192
	if i.prof.Overlay.Name == "" {
193
		i.prof.Overlay.Name = constants.ImagerOverlayInstallerDefault
194
	}
195

196
	i.overlayInstaller = executor.New(filepath.Join(i.tempDir, constants.ImagerOverlayInstallersPath, i.prof.Overlay.Name))
197

198
	if i.extraProfiles == nil {
199
		i.extraProfiles = make(map[string]profile.Profile)
200
	}
201

202
	for _, profilePath := range profileYAMLs {
203
		profileName := strings.TrimSuffix(filepath.Base(profilePath), ".yaml")
204

205
		var overlayProfile profile.Profile
206

207
		profileDataBytes, err := os.ReadFile(profilePath)
208
		if err != nil {
209
			return fmt.Errorf("failed to read profile: %w", err)
210
		}
211

212
		if err := yaml.Unmarshal(profileDataBytes, &overlayProfile); err != nil {
213
			return fmt.Errorf("failed to unmarshal profile: %w", err)
214
		}
215

216
		i.extraProfiles[profileName] = overlayProfile
217
	}
218

219
	return nil
220
}
221

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]
226

227
		if !ok {
228
			baseProfile, ok = profile.Default[i.prof.BaseProfileName]
229
		}
230

231
		if !ok {
232
			return fmt.Errorf("unknown base profile: %s", i.prof.BaseProfileName)
233
		}
234

235
		baseProfile = baseProfile.DeepCopy()
236

237
		// merge the profiles
238
		if err := merge.Merge(&baseProfile, &i.prof); err != nil {
239
			return err
240
		}
241

242
		i.prof = baseProfile
243
		i.prof.BaseProfileName = ""
244
	}
245

246
	if i.prof.Version == "" {
247
		i.prof.Version = version.Tag
248
	}
249

250
	if err := i.prof.Validate(); err != nil {
251
		return fmt.Errorf("profile is invalid: %w", err)
252
	}
253

254
	i.prof.Input.FillDefaults(i.prof.Arch, i.prof.Version, i.prof.SecureBootEnabled())
255

256
	return nil
257
}
258

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,
265
		})
266

267
		// no system extensions, happy path
268
		i.initramfsPath = i.prof.Input.Initramfs.Path
269

270
		return nil
271
	}
272

273
	if i.prof.Output.Kind == profile.OutKindCmdline || i.prof.Output.Kind == profile.OutKindKernel {
274
		// these outputs don't use initramfs image
275
		return nil
276
	}
277

278
	printf := progressPrintf(report, reporter.Update{Message: "rebuilding initramfs with system extensions...", Status: reporter.StatusRunning})
279

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")
282

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)
285
	}
286

287
	i.initramfsPath = tempInitramfsPath
288

289
	extensionsCheckoutDir := filepath.Join(i.tempDir, "extensions")
290

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))
294

295
		if err := os.MkdirAll(extensionDir, 0o755); err != nil {
296
			return fmt.Errorf("failed to create extension directory: %w", err)
297
		}
298

299
		if err := ext.Extract(ctx, extensionDir, i.prof.Arch, printf); err != nil {
300
			return err
301
		}
302
	}
303

304
	// rebuild initramfs
305
	builder := extensions.Builder{
306
		InitramfsPath:     i.initramfsPath,
307
		Arch:              i.prof.Arch,
308
		ExtensionTreePath: extensionsCheckoutDir,
309
		Printf:            printf,
310
		Quirks:            quirks.New(i.prof.Version),
311
	}
312

313
	if err := builder.Build(); err != nil {
314
		return err
315
	}
316

317
	report.Report(reporter.Update{
318
		Message: "initramfs ready",
319
		Status:  reporter.StatusSucceeded,
320
	})
321

322
	return nil
323
}
324

325
// buildCmdline builds the kernel command line.
326
//
327
//nolint:gocyclo
328
func (i *Imager) buildCmdline() error {
329
	p, err := platform.NewPlatform(i.prof.Platform)
330
	if err != nil {
331
		return err
332
	}
333

334
	cmdline := procfs.NewCmdline("")
335

336
	// platform kernel args
337
	cmdline.Append(constants.KernelParamPlatform, p.Name())
338
	cmdline.SetAll(p.KernelArgs(i.prof.Arch).Strings())
339

340
	// board kernel args
341
	if i.prof.Board != "" && !quirks.New(i.prof.Version).SupportsOverlay() {
342
		var b talosruntime.Board
343

344
		b, err = board.NewBoard(i.prof.Board)
345
		if err != nil {
346
			return err
347
		}
348

349
		cmdline.Append(constants.KernelParamBoard, b.Name())
350
		cmdline.SetAll(b.KernelArgs().Strings())
351
	}
352

353
	// overlay kernel args
354
	if i.overlayInstaller != nil {
355
		options, optsErr := i.overlayInstaller.GetOptions(i.prof.Overlay.ExtraOptions)
356
		if optsErr != nil {
357
			return optsErr
358
		}
359

360
		cmdline.SetAll(options.KernelArgs)
361
	}
362

363
	// first defaults, then extra kernel args to allow extra kernel args to override defaults
364
	if err = cmdline.AppendAll(kernel.DefaultArgs); err != nil {
365
		return err
366
	}
367

368
	if i.prof.SecureBootEnabled() {
369
		if err = cmdline.AppendAll(kernel.SecureBootArgs); err != nil {
370
			return err
371
		}
372
	}
373

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
377
		cmdline.Append(
378
			constants.KernelParamEnvironment,
379
			constants.MetaValuesEnvVar+"="+i.prof.Customization.MetaContents.Encode(quirks.New(i.prof.Version).SupportsCompressedEncodedMETA()),
380
		)
381
	}
382

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(),
389
	); err != nil {
390
		return err
391
	}
392

393
	i.cmdline = cmdline.String()
394

395
	return nil
396
}
397

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})
401

402
	i.sdBootPath = filepath.Join(i.tempDir, "systemd-boot.efi.signed")
403
	i.ukiPath = filepath.Join(i.tempDir, "vmlinuz.efi.signed")
404

405
	pcrSigner, err := i.prof.Input.SecureBoot.PCRSigner.GetSigner(ctx)
406
	if err != nil {
407
		return fmt.Errorf("failed to get PCR signer: %w", err)
408
	}
409

410
	securebootSigner, err := i.prof.Input.SecureBoot.SecureBootSigner.GetSigner(ctx)
411
	if err != nil {
412
		return fmt.Errorf("failed to get SecureBoot signer: %w", err)
413
	}
414

415
	builder := uki.Builder{
416
		Arch:       i.prof.Arch,
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,
422
		Cmdline:    i.cmdline,
423

424
		SecureBootSigner: securebootSigner,
425
		PCRSigner:        pcrSigner,
426

427
		OutSdBootPath: i.sdBootPath,
428
		OutUKIPath:    i.ukiPath,
429
	}
430

431
	if err := builder.Build(printf); err != nil {
432
		return err
433
	}
434

435
	report.Report(reporter.Update{
436
		Message: "UKI ready",
437
		Status:  reporter.StatusSucceeded,
438
	})
439

440
	return nil
441
}
442

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.