diff --git a/cmd/installer/pkg/install/install.go b/cmd/installer/pkg/install/install.go index 1615ddfa71..aa5dc731a9 100644 --- a/cmd/installer/pkg/install/install.go +++ b/cmd/installer/pkg/install/install.go @@ -157,7 +157,7 @@ func NewInstaller(ctx context.Context, cmdline *procfs.Cmdline, mode Mode, opts if !bootLoaderPresent { if mode.IsImage() { // on image creation, use the bootloader based on options - i.bootloader = bootloader.New(opts.ImageSecureboot) + i.bootloader = bootloader.New(opts.ImageSecureboot, opts.Version) } else { // on install/upgrade perform automatic detection i.bootloader = bootloader.NewAuto() diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/bootloader.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/bootloader.go index 4dc833855f..a4d8a556f0 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/bootloader.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/bootloader.go @@ -12,6 +12,7 @@ import ( "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub" "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/options" "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot" + "github.com/siderolabs/talos/pkg/imager/quirks" ) // Bootloader describes a bootloader. @@ -62,10 +63,13 @@ func NewAuto() Bootloader { } // New returns a new bootloader based on the secureboot flag. -func New(secureboot bool) Bootloader { +func New(secureboot bool, talosVersion string) Bootloader { if secureboot { return sdboot.New() } - return grub.NewConfig() + g := grub.NewConfig() + g.AddResetOption = quirks.New(talosVersion).SupportsResetGRUBOption() + + return g } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/decode.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/decode.go index 740e201896..94bdd7a2d4 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/decode.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/decode.go @@ -69,44 +69,48 @@ func Decode(c []byte) (*Config, error) { return nil, err } - entries, err := parseEntries(c) + entries, hasResetOption, err := parseEntries(c) if err != nil { return nil, err } conf := Config{ - Default: defaultEntry, - Fallback: fallbackEntry, - Entries: entries, + Default: defaultEntry, + Fallback: fallbackEntry, + Entries: entries, + AddResetOption: hasResetOption, } return &conf, nil } -func parseEntries(conf []byte) (map[BootLabel]MenuEntry, error) { +func parseEntries(conf []byte) (map[BootLabel]MenuEntry, bool, error) { entries := make(map[BootLabel]MenuEntry) + hasResetOption := false matches := menuEntryRegex.FindAllSubmatch(conf, -1) for _, m := range matches { if len(m) != 3 { - return nil, fmt.Errorf("conf block: expected 3 matches, got %d", len(m)) + return nil, false, fmt.Errorf("conf block: expected 3 matches, got %d", len(m)) } confBlock := m[2] linux, cmdline, initrd, err := parseConfBlock(confBlock) if err != nil { - return nil, err + return nil, false, err } name := string(m[1]) bootEntry, err := ParseBootLabel(name) if err != nil { - return nil, err + return nil, false, err } if bootEntry == BootReset { + hasResetOption = true + continue } @@ -118,7 +122,7 @@ func parseEntries(conf []byte) (map[BootLabel]MenuEntry, error) { } } - return entries, nil + return entries, hasResetOption, nil } func parseConfBlock(block []byte) (linux, cmdline, initrd string, err error) { diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/encode.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/encode.go index 73af2a4423..cbf5b63715 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/encode.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/encode.go @@ -32,6 +32,7 @@ menuentry "{{ $entry.Name }}" { } {{ end -}} +{{ if .AddResetOption -}} {{ $defaultEntry := index .Entries .Default -}} menuentry "Reset Talos installation and return to maintenance mode" { set gfxmode=auto @@ -39,6 +40,7 @@ menuentry "Reset Talos installation and return to maintenance mode" { linux {{ $defaultEntry.Linux }} {{ quote $defaultEntry.Cmdline }} talos.experimental.wipe=system:EPHEMERAL,STATE initrd {{ $defaultEntry.Initrd }} } +{{ end -}} ` // Write the grub configuration to the given file. diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub.go index 38afb68bd5..7891222994 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub.go @@ -15,9 +15,10 @@ import ( // Config represents a grub configuration file (grub.cfg). type Config struct { - Default BootLabel - Fallback BootLabel - Entries map[BootLabel]MenuEntry + Default BootLabel + Fallback BootLabel + Entries map[BootLabel]MenuEntry + AddResetOption bool } // MenuEntry represents a grub menu entry in the grub config file. @@ -35,8 +36,9 @@ func (e bootloaderNotInstalledError) Error() string { // NewConfig creates a new grub configuration (nothing is written to disk). func NewConfig() *Config { return &Config{ - Default: BootA, - Entries: map[BootLabel]MenuEntry{}, + Default: BootA, + Entries: map[BootLabel]MenuEntry{}, + AddResetOption: true, } } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub_test.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub_test.go index 8c7dd1caaa..a144977471 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub_test.go @@ -28,6 +28,9 @@ var ( //go:embed testdata/grub_write_test.cfg newConfig string + + //go:embed testdata/grub_write_no_reset_test.cfg + newNoResetConfig string ) func TestDecode(t *testing.T) { @@ -50,6 +53,8 @@ func TestDecode(t *testing.T) { assert.Equal(t, "cmdline B", b.Cmdline) assert.True(t, strings.HasPrefix(b.Linux, "/B/")) assert.True(t, strings.HasPrefix(b.Initrd, "/B/")) + + assert.True(t, conf.AddResetOption) } func TestEncodeDecode(t *testing.T) { @@ -110,6 +115,31 @@ func TestWrite(t *testing.T) { assert.Equal(t, newConfig, string(written)) } +//nolint:errcheck +func TestWriteNoReset(t *testing.T) { + oldName := version.Name + + t.Cleanup(func() { + version.Name = oldName + }) + + version.Name = "TestOld" + + tempFile, _ := os.CreateTemp("", "talos-test-grub-*.cfg") + + t.Cleanup(func() { require.NoError(t, os.Remove(tempFile.Name())) }) + + config := grub.NewConfig() + config.AddResetOption = false + require.NoError(t, config.Put(grub.BootA, "cmdline A", "v0.0.1")) + + err := config.Write(tempFile.Name(), t.Logf) + assert.NoError(t, err) + + written, _ := os.ReadFile(tempFile.Name()) + assert.Equal(t, newNoResetConfig, string(written)) +} + func TestPut(t *testing.T) { config := grub.NewConfig() require.NoError(t, config.Put(grub.BootA, "cmdline A", "v1.2.3")) diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/testdata/grub_write_no_reset_test.cfg b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/testdata/grub_write_no_reset_test.cfg new file mode 100644 index 0000000000..44632d3513 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/testdata/grub_write_no_reset_test.cfg @@ -0,0 +1,15 @@ +set default="A - TestOld v0.0.1" + +set timeout=3 + +insmod all_video + +terminal_input console +terminal_output console + +menuentry "A - TestOld v0.0.1" { + set gfxmode=auto + set gfxpayload=text + linux /A/vmlinuz cmdline A + initrd /A/initramfs.xz +} diff --git a/pkg/imager/iso/grub.cfg b/pkg/imager/iso/grub.cfg index 158874bba3..ea61bff903 100644 --- a/pkg/imager/iso/grub.cfg +++ b/pkg/imager/iso/grub.cfg @@ -13,9 +13,11 @@ menuentry "Talos ISO" { initrd /boot/initramfs.xz } +{{ if .AddResetOption -}} menuentry "Reset Talos installation" { set gfxmode=auto set gfxpayload=text linux /boot/vmlinuz {{ quote .Cmdline }} talos.experimental.wipe=system initrd /boot/initramfs.xz } +{{ end -}} diff --git a/pkg/imager/iso/grub.go b/pkg/imager/iso/grub.go index 3818811a15..1feec3a406 100644 --- a/pkg/imager/iso/grub.go +++ b/pkg/imager/iso/grub.go @@ -16,6 +16,7 @@ import ( "github.com/siderolabs/go-cmd/pkg/cmd" "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub" + "github.com/siderolabs/talos/pkg/imager/quirks" "github.com/siderolabs/talos/pkg/imager/utils" ) @@ -24,6 +25,7 @@ type GRUBOptions struct { KernelPath string InitramfsPath string Cmdline string + Version string ScratchDir string @@ -59,9 +61,11 @@ func CreateGRUB(printf func(string, ...any), options GRUBOptions) error { } if err = tmpl.Execute(&grubCfg, struct { - Cmdline string + Cmdline string + AddResetOption bool }{ - Cmdline: options.Cmdline, + Cmdline: options.Cmdline, + AddResetOption: quirks.New(options.Version).SupportsResetGRUBOption(), }); err != nil { return err } diff --git a/pkg/imager/out.go b/pkg/imager/out.go index 90ab0f3374..153f0dcc8a 100644 --- a/pkg/imager/out.go +++ b/pkg/imager/out.go @@ -153,6 +153,7 @@ func (i *Imager) outISO(ctx context.Context, path string, report *reporter.Repor KernelPath: i.prof.Input.Kernel.Path, InitramfsPath: i.initramfsPath, Cmdline: i.cmdline, + Version: i.prof.Version, ScratchDir: scratchSpace, OutPath: path, diff --git a/pkg/imager/quirks/quirks.go b/pkg/imager/quirks/quirks.go new file mode 100644 index 0000000000..63768e0008 --- /dev/null +++ b/pkg/imager/quirks/quirks.go @@ -0,0 +1,35 @@ +// 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 quirks contains the quirks for Talos image generation. +package quirks + +import "github.com/blang/semver/v4" + +// Quirks contains the quirks for Talos image generation. +type Quirks struct { + v *semver.Version +} + +// New returns a new Quirks instance based on Talos version for the image. +func New(talosVersion string) Quirks { + v, err := semver.ParseTolerant(talosVersion) // ignore the error + if err != nil { + return Quirks{} + } + + return Quirks{v: &v} +} + +var minVersionResetOption = semver.MustParse("1.4.0") + +// SupportsResetGRUBOption returns true if the Talos version supports the reset option in GRUB menu (image and ISO). +func (q Quirks) SupportsResetGRUBOption() bool { + // if the version doesn't parse, we assume it's latest Talos + if q.v == nil { + return true + } + + return q.v.GTE(minVersionResetOption) +} diff --git a/pkg/imager/quirks/quirks_test.go b/pkg/imager/quirks/quirks_test.go new file mode 100644 index 0000000000..852325e65c --- /dev/null +++ b/pkg/imager/quirks/quirks_test.go @@ -0,0 +1,37 @@ +// 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 quirks_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/siderolabs/talos/pkg/imager/quirks" +) + +func TestSupportsResetOption(t *testing.T) { + for _, test := range []struct { + version string + + expected bool + }{ + { + version: "1.5.0", + expected: true, + }, + { + expected: true, + }, + { + version: "1.3.7", + expected: false, + }, + } { + t.Run(test.version, func(t *testing.T) { + assert.Equal(t, test.expected, quirks.New(test.version).SupportsResetGRUBOption()) + }) + } +}