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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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