fix: imager should support different Talos versions

Add some quirks to make images generated with newer Talos compatible
with images generated by older Talos.

Specifically, reset options were adding in Talos 1.4, so we shouldn't
add them for older versions.

Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
This commit is contained in:
Andrey Smirnov 2023-12-19 21:23:37 +04:00
parent d6342cda53
commit 0a30ef7845
No known key found for this signature in database
GPG Key ID: FE042E3D4085A811
12 changed files with 155 additions and 19 deletions

View File

@ -157,7 +157,7 @@ func NewInstaller(ctx context.Context, cmdline *procfs.Cmdline, mode Mode, opts
if !bootLoaderPresent { if !bootLoaderPresent {
if mode.IsImage() { if mode.IsImage() {
// on image creation, use the bootloader based on options // on image creation, use the bootloader based on options
i.bootloader = bootloader.New(opts.ImageSecureboot) i.bootloader = bootloader.New(opts.ImageSecureboot, opts.Version)
} else { } else {
// on install/upgrade perform automatic detection // on install/upgrade perform automatic detection
i.bootloader = bootloader.NewAuto() i.bootloader = bootloader.NewAuto()

View File

@ -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/grub"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/options" "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/internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot"
"github.com/siderolabs/talos/pkg/imager/quirks"
) )
// Bootloader describes a bootloader. // Bootloader describes a bootloader.
@ -62,10 +63,13 @@ func NewAuto() Bootloader {
} }
// New returns a new bootloader based on the secureboot flag. // New returns a new bootloader based on the secureboot flag.
func New(secureboot bool) Bootloader { func New(secureboot bool, talosVersion string) Bootloader {
if secureboot { if secureboot {
return sdboot.New() return sdboot.New()
} }
return grub.NewConfig() g := grub.NewConfig()
g.AddResetOption = quirks.New(talosVersion).SupportsResetGRUBOption()
return g
} }

View File

@ -69,44 +69,48 @@ func Decode(c []byte) (*Config, error) {
return nil, err return nil, err
} }
entries, err := parseEntries(c) entries, hasResetOption, err := parseEntries(c)
if err != nil { if err != nil {
return nil, err return nil, err
} }
conf := Config{ conf := Config{
Default: defaultEntry, Default: defaultEntry,
Fallback: fallbackEntry, Fallback: fallbackEntry,
Entries: entries, Entries: entries,
AddResetOption: hasResetOption,
} }
return &conf, nil 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) entries := make(map[BootLabel]MenuEntry)
hasResetOption := false
matches := menuEntryRegex.FindAllSubmatch(conf, -1) matches := menuEntryRegex.FindAllSubmatch(conf, -1)
for _, m := range matches { for _, m := range matches {
if len(m) != 3 { 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] confBlock := m[2]
linux, cmdline, initrd, err := parseConfBlock(confBlock) linux, cmdline, initrd, err := parseConfBlock(confBlock)
if err != nil { if err != nil {
return nil, err return nil, false, err
} }
name := string(m[1]) name := string(m[1])
bootEntry, err := ParseBootLabel(name) bootEntry, err := ParseBootLabel(name)
if err != nil { if err != nil {
return nil, err return nil, false, err
} }
if bootEntry == BootReset { if bootEntry == BootReset {
hasResetOption = true
continue 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) { func parseConfBlock(block []byte) (linux, cmdline, initrd string, err error) {

View File

@ -32,6 +32,7 @@ menuentry "{{ $entry.Name }}" {
} }
{{ end -}} {{ end -}}
{{ if .AddResetOption -}}
{{ $defaultEntry := index .Entries .Default -}} {{ $defaultEntry := index .Entries .Default -}}
menuentry "Reset Talos installation and return to maintenance mode" { menuentry "Reset Talos installation and return to maintenance mode" {
set gfxmode=auto 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 linux {{ $defaultEntry.Linux }} {{ quote $defaultEntry.Cmdline }} talos.experimental.wipe=system:EPHEMERAL,STATE
initrd {{ $defaultEntry.Initrd }} initrd {{ $defaultEntry.Initrd }}
} }
{{ end -}}
` `
// Write the grub configuration to the given file. // Write the grub configuration to the given file.

View File

@ -15,9 +15,10 @@ import (
// Config represents a grub configuration file (grub.cfg). // Config represents a grub configuration file (grub.cfg).
type Config struct { type Config struct {
Default BootLabel Default BootLabel
Fallback BootLabel Fallback BootLabel
Entries map[BootLabel]MenuEntry Entries map[BootLabel]MenuEntry
AddResetOption bool
} }
// MenuEntry represents a grub menu entry in the grub config file. // 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). // NewConfig creates a new grub configuration (nothing is written to disk).
func NewConfig() *Config { func NewConfig() *Config {
return &Config{ return &Config{
Default: BootA, Default: BootA,
Entries: map[BootLabel]MenuEntry{}, Entries: map[BootLabel]MenuEntry{},
AddResetOption: true,
} }
} }

View File

@ -28,6 +28,9 @@ var (
//go:embed testdata/grub_write_test.cfg //go:embed testdata/grub_write_test.cfg
newConfig string newConfig string
//go:embed testdata/grub_write_no_reset_test.cfg
newNoResetConfig string
) )
func TestDecode(t *testing.T) { func TestDecode(t *testing.T) {
@ -50,6 +53,8 @@ func TestDecode(t *testing.T) {
assert.Equal(t, "cmdline B", b.Cmdline) assert.Equal(t, "cmdline B", b.Cmdline)
assert.True(t, strings.HasPrefix(b.Linux, "/B/")) assert.True(t, strings.HasPrefix(b.Linux, "/B/"))
assert.True(t, strings.HasPrefix(b.Initrd, "/B/")) assert.True(t, strings.HasPrefix(b.Initrd, "/B/"))
assert.True(t, conf.AddResetOption)
} }
func TestEncodeDecode(t *testing.T) { func TestEncodeDecode(t *testing.T) {
@ -110,6 +115,31 @@ func TestWrite(t *testing.T) {
assert.Equal(t, newConfig, string(written)) 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) { func TestPut(t *testing.T) {
config := grub.NewConfig() config := grub.NewConfig()
require.NoError(t, config.Put(grub.BootA, "cmdline A", "v1.2.3")) require.NoError(t, config.Put(grub.BootA, "cmdline A", "v1.2.3"))

View File

@ -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
}

View File

@ -13,9 +13,11 @@ menuentry "Talos ISO" {
initrd /boot/initramfs.xz initrd /boot/initramfs.xz
} }
{{ if .AddResetOption -}}
menuentry "Reset Talos installation" { menuentry "Reset Talos installation" {
set gfxmode=auto set gfxmode=auto
set gfxpayload=text set gfxpayload=text
linux /boot/vmlinuz {{ quote .Cmdline }} talos.experimental.wipe=system linux /boot/vmlinuz {{ quote .Cmdline }} talos.experimental.wipe=system
initrd /boot/initramfs.xz initrd /boot/initramfs.xz
} }
{{ end -}}

View File

@ -16,6 +16,7 @@ import (
"github.com/siderolabs/go-cmd/pkg/cmd" "github.com/siderolabs/go-cmd/pkg/cmd"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub" "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" "github.com/siderolabs/talos/pkg/imager/utils"
) )
@ -24,6 +25,7 @@ type GRUBOptions struct {
KernelPath string KernelPath string
InitramfsPath string InitramfsPath string
Cmdline string Cmdline string
Version string
ScratchDir string ScratchDir string
@ -59,9 +61,11 @@ func CreateGRUB(printf func(string, ...any), options GRUBOptions) error {
} }
if err = tmpl.Execute(&grubCfg, struct { 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 { }); err != nil {
return err return err
} }

View File

@ -153,6 +153,7 @@ func (i *Imager) outISO(ctx context.Context, path string, report *reporter.Repor
KernelPath: i.prof.Input.Kernel.Path, KernelPath: i.prof.Input.Kernel.Path,
InitramfsPath: i.initramfsPath, InitramfsPath: i.initramfsPath,
Cmdline: i.cmdline, Cmdline: i.cmdline,
Version: i.prof.Version,
ScratchDir: scratchSpace, ScratchDir: scratchSpace,
OutPath: path, OutPath: path,

View File

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

View File

@ -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())
})
}
}