mirror of
				https://github.com/siderolabs/talos.git
				synced 2025-10-31 00:11:36 +01:00 
			
		
		
		
	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:
		
							parent
							
								
									d6342cda53
								
							
						
					
					
						commit
						0a30ef7845
					
				| @ -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() | ||||
|  | ||||
| @ -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 | ||||
| } | ||||
|  | ||||
| @ -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) { | ||||
|  | ||||
| @ -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. | ||||
|  | ||||
| @ -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, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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")) | ||||
|  | ||||
							
								
								
									
										15
									
								
								internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/testdata/grub_write_no_reset_test.cfg
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/testdata/grub_write_no_reset_test.cfg
									
									
									
									
										vendored
									
									
										Normal 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 | ||||
| } | ||||
| @ -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 -}} | ||||
|  | ||||
| @ -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 | ||||
| 	} | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
							
								
								
									
										35
									
								
								pkg/imager/quirks/quirks.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								pkg/imager/quirks/quirks.go
									
									
									
									
									
										Normal 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) | ||||
| } | ||||
							
								
								
									
										37
									
								
								pkg/imager/quirks/quirks_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								pkg/imager/quirks/quirks_test.go
									
									
									
									
									
										Normal 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()) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user