Skip to content

feat: auto-discover META partition from full disk via GPT#5

Merged
Andrei Kvapil (kvaps) merged 1 commit into
cozystack:mainfrom
mcanevet:feat/gpt-auto-discover
Jun 4, 2026
Merged

feat: auto-discover META partition from full disk via GPT#5
Andrei Kvapil (kvaps) merged 1 commit into
cozystack:mainfrom
mcanevet:feat/gpt-auto-discover

Conversation

@mcanevet

@mcanevet Mickaël Canévet (mcanevet) commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Add findMetaPartition which reads the GPT table and locates the partition labelled "META", so -device now accepts a whole disk path (e.g. /dev/sda) instead of requiring the caller to know the partition number. Real block devices use ioctl (BLKGETSIZE64) for correct size via go-blockdevice; regular files fall back to stat, keeping tests root-free. Add //go:build linux since the tool targets Talos Linux.

writeConfig is refactored to accept interface{ io.ReaderAt; io.WriterAt } instead of a device path, which lets it operate on the offset-scoped partition view returned by findMetaPartition.

Add a full-disk test that builds a 20 MiB GPT image with all six Talos partition labels (EFI/BIOS/BOOT/META/STATE/EPHEMERAL) using go-blockdevice/v2, exercises writeConfig via findMetaPartition, and reads back through the partition table to verify the data landed in the right place. Also cover the error paths: zeroed disk (no GPT) and valid GPT without a META partition.

Summary by CodeRabbit

  • Documentation

    • Updated build instructions to build from repository root.
    • Clarified tool behavior and updated usage examples.
  • New Features

    • Tool now auto-discovers the META partition via GPT instead of requiring manual partition specification.
    • Updated command-line interface: -device flag now accepts the full disk path.

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@mcanevet, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 53 minutes and 41 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f19df56d-9a1f-499c-9175-b24581ad1cbd

📥 Commits

Reviewing files that changed from the base of the PR and between f1188cf and c9cfcd6.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (4)
  • README.md
  • go.mod
  • main.go
  • main_test.go
📝 Walkthrough

Walkthrough

The PR refactors talos-meta-tool to discover and write configuration into the GPT META partition instead of a specified device path. It changes writeConfig to accept an io.ReaderAt/io.WriterAt partition handle, adds GPT discovery helpers, updates main to open disks and locate META partitions, and expands test coverage for the new interface and partition discovery scenarios.

Changes

GPT META Partition Discovery and Configuration Writing

Layer / File(s) Summary
ADV Configuration Writing via Partition Handle
main.go
The writeConfig function signature changes to accept an io.ReaderAt/io.WriterAt handle instead of a device path string. It constructs talos.NewADV from the handle, validates the ADV header, writes configuration bytes into the ADV at FixedTag, serializes the result, and writes it to offset 0. Flag help text and argument validation are updated accordingly.
GPT Device and META Partition Discovery
main.go
Introduces partAt to map partition-relative offsets to absolute file offsets, openBlockDevice to open and read GPT metadata, and findMetaPartition to locate the META partition by name. The main function now opens the disk device, defers close, and resolves the META partition before calling writeConfig with that partition handle.
Test Helpers and Build Configuration
main_test.go
Adds Linux build constraint and expands imports for file seeking and error injection. Introduces newTestDisk helper that creates a Talos-like GPT layout with EFI and Linux-type partitions, and errDevice stub type for testing error paths.
Configuration Writing and Partition Discovery Tests
main_test.go
Updated configuration writing tests use file handles instead of device paths and seek back to start using io.SeekStart. Tests for read-only and bad devices verify error handling with the new interface. New tests verify findMetaPartition failure modes when GPT is missing or META partition is absent. Full-disk write/verify test confirms configuration is correctly persisted in the META partition and readable via ADV.
Documentation and Dependency Management
README.md, go.mod
README is updated to clarify the tool writes network metadata into the Talos META partition, documents build command as go build from repository root, and updates usage to show talos-meta-tool -device /dev/sda -config config.yaml with auto-discovery of META partition via GPT. go.mod is reorganized into explicit require blocks for direct and indirect dependencies.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • cozystack/talos-meta-tool#4: Both PRs refactor writeConfig to use talos.NewADV and write validated config into the ADV FixedTag, with corresponding test updates for ADV persistence.

Suggested reviewers

  • kvaps

Poem

🐰 A partition named META, so sleek and so fine,
No more device paths, just discovery divine,
GPT guides us where config should dwell,
ADV writes the truth—what a tale it will tell!
Tests verify all, from errors to reads,
This tool now fulfills all the partition's needs. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: the tool now auto-discovers the META partition from a full disk using GPT instead of requiring a specific partition path.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the Talos metadata writer tool to automatically discover the 'META' partition on a disk device using GPT, removing the need to specify the partition manually. It introduces a partAt struct to handle offset-based reads and writes, and adds comprehensive tests using mock disk images. The review feedback suggests improving safety by using a named field instead of embedding *os.File in partAt to avoid exposing un-offsetted methods, adding defensive nil checks for input parameters, and explicitly calling Sync() on the device to ensure data is flushed to disk.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread main.go
Comment on lines +22 to +33
type partAt struct {
*os.File
offset int64
}

func (p *partAt) ReadAt(buf []byte, off int64) (int, error) {
return p.File.ReadAt(buf, p.offset+off)
}

func (p *partAt) WriteAt(buf []byte, off int64) (int, error) {
return p.File.WriteAt(buf, p.offset+off)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Embedding *os.File in partAt exposes all of *os.File's methods (like Read, Write, Seek, Close) to any consumer of partAt. Since these inherited methods do not respect the partition offset, calling them would read/write from the wrong disk locations. It is safer to use a named field file *os.File to prevent accidental misuse of the underlying file descriptor.

Suggested change
type partAt struct {
*os.File
offset int64
}
func (p *partAt) ReadAt(buf []byte, off int64) (int, error) {
return p.File.ReadAt(buf, p.offset+off)
}
func (p *partAt) WriteAt(buf []byte, off int64) (int, error) {
return p.File.WriteAt(buf, p.offset+off)
}
type partAt struct {
file *os.File
offset int64
}
func (p *partAt) ReadAt(buf []byte, off int64) (int, error) {
return p.file.ReadAt(buf, p.offset+off)
}
func (p *partAt) WriteAt(buf []byte, off int64) (int, error) {
return p.file.WriteAt(buf, p.offset+off)
}

Comment thread main.go
Comment on lines +46 to +47
func findMetaPartition(f *os.File) (interface{ io.ReaderAt; io.WriterAt }, error) {
gptdev, err := openGPTDevice(f)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add a defensive nil check for f to prevent a nil pointer dereference panic when calling openGPTDevice.

Suggested change
func findMetaPartition(f *os.File) (interface{ io.ReaderAt; io.WriterAt }, error) {
gptdev, err := openGPTDevice(f)
func findMetaPartition(f *os.File) (interface{ io.ReaderAt; io.WriterAt }, error) {
if f == nil {
return nil, fmt.Errorf("file is nil")
}
gptdev, err := openGPTDevice(f)

Comment thread main.go Outdated

for _, p := range table.Partitions() {
if p != nil && p.Name == "META" {
return &partAt{File: f, offset: int64(p.FirstLBA) * sectorSize}, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update the initialization of partAt to use the named file field instead of the embedded File field.

Suggested change
return &partAt{File: f, offset: int64(p.FirstLBA) * sectorSize}, nil
return &partAt{file: f, offset: int64(p.FirstLBA) * sectorSize}, nil

Comment thread main.go
Comment on lines +76 to +77
func writeConfig(dev interface{ io.ReaderAt; io.WriterAt }, configData []byte) error {
adv, loadErr := talos.NewADV(io.NewSectionReader(dev, 0, int64(talos.Size)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add a defensive nil check for dev to prevent a nil pointer dereference panic when NewSectionReader or NewADV attempts to read from it.

func writeConfig(dev interface{ io.ReaderAt; io.WriterAt }, configData []byte) error {
	if dev == nil {
		return fmt.Errorf("device is nil")
	}
	adv, loadErr := talos.NewADV(io.NewSectionReader(dev, 0, int64(talos.Size)))

Comment thread main.go
Comment on lines +130 to 132
if err := writeConfig(meta, validatedConfigData); err != nil {
log.Fatalf("Error: %v", err)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When writing to a block device or disk image, it is important to explicitly call Sync() to flush any dirty pages from the OS cache to the physical media before reporting success. This ensures data integrity and prevents silent data loss if the system crashes or the drive is removed immediately after the tool exits.

	if err := writeConfig(meta, validatedConfigData); err != nil {
		log.Fatalf("Error: %v", err)
	}

	if err := device.Sync(); err != nil {
		log.Fatalf("Error syncing device: %v", err)
	}

Add findMetaPartition which reads the GPT table and locates the
partition labelled "META", so -device now accepts a whole disk path
(e.g. /dev/sda) instead of requiring the caller to know the partition
number. Real block devices use ioctl (BLKGETSIZE64) for correct size
via go-blockdevice; regular files fall back to stat, keeping tests
root-free. Add //go:build linux since the tool targets Talos Linux.

writeConfig is refactored to accept interface{ io.ReaderAt; io.WriterAt }
instead of a device path, which lets it operate on the offset-scoped
partition view returned by findMetaPartition.

Add a full-disk test that builds a 20 MiB GPT image with all six Talos
partition labels (EFI/BIOS/BOOT/META/STATE/EPHEMERAL) using
go-blockdevice/v2, exercises writeConfig via findMetaPartition, and
reads back through the partition table to verify the data landed in the
right place. Also cover the error paths: zeroed disk (no GPT) and valid
GPT without a META partition.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Mickaël Canévet <mickael.canevet@proton.ch>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
README.md (1)

9-11: ⚡ Quick win

Add language identifiers to fenced code blocks.

Specifying the language improves syntax highlighting and tooling support.

📝 Proposed fixes
 Compile:
-```
+```bash
 GOOS=linux GOARCH=amd64 go build -o talos-meta-tool .

Usage:
- +bash
talos-meta-tool -device /dev/sda -config config.yaml

Also applies to: 14-16

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 9 - 11, The fenced code blocks in README.md
containing shell commands (e.g., "GOOS=linux GOARCH=amd64 go build -o
talos-meta-tool ." and the usage example "talos-meta-tool -device /dev/sda
-config config.yaml") should include a language identifier for syntax
highlighting; update those fenced blocks to use ```bash instead of ``` so both
the build command block and the Usage block (and the similar block at lines
14-16) are labeled as bash.
main.go (1)

20-33: 💤 Low value

Consider tracking partition size for bounds checking.

The partAt wrapper doesn't store the partition size, which means reads/writes could theoretically extend beyond the partition boundary into adjacent partitions. While talos.Size is small and unlikely to exceed typical META partition sizes, storing and validating against the partition size would provide defense-in-depth.

💡 Optional: Add partition size bounds checking
 type partAt struct {
 	*os.File
 	offset int64
+	size   int64
 }
 
 func (p *partAt) ReadAt(buf []byte, off int64) (int, error) {
+	if off < 0 || off+int64(len(buf)) > p.size {
+		return 0, fmt.Errorf("read at offset %d with length %d exceeds partition size %d", off, len(buf), p.size)
+	}
 	return p.File.ReadAt(buf, p.offset+off)
 }
 
 func (p *partAt) WriteAt(buf []byte, off int64) (int, error) {
+	if off < 0 || off+int64(len(buf)) > p.size {
+		return 0, fmt.Errorf("write at offset %d with length %d exceeds partition size %d", off, len(buf), p.size)
+	}
 	return p.File.WriteAt(buf, p.offset+off)
 }

Then in findMetaPartition:

-			return &partAt{File: f, offset: int64(p.FirstLBA) * sectorSize}, nil
+			size := int64(p.LastLBA-p.FirstLBA+1) * sectorSize
+			return &partAt{File: f, offset: int64(p.FirstLBA) * sectorSize, size: size}, nil
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@main.go` around lines 20 - 33, The partAt wrapper needs a partition size
field and bounds checking: add a size int64 to the partAt struct and in its
ReadAt and WriteAt methods validate that off >= 0 and off+int64(len(buf)) <=
p.size (return io.EOF or io.ErrUnexpectedEOF / appropriate error when
out-of-bounds), and clamp/adjust behavior for partial reads/writes as needed;
ensure wherever partAt is constructed (e.g., in findMetaPartition) you pass the
discovered partition size into the new partAt instance so bounds checks use the
correct partition length.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@main.go`:
- Around line 104-107: The current main() argument check prints usage and
returns with exit code 0 when devicePath or configPath are empty; change this to
print the usage to stderr (use fmt.Fprintln(os.Stderr, ...)) and call os.Exit
with a non-zero code (e.g., os.Exit(1)) instead of returning. Update the block
that checks *devicePath == "" || *configPath == "" in main to use stderr and
os.Exit to signal failure.

---

Nitpick comments:
In `@main.go`:
- Around line 20-33: The partAt wrapper needs a partition size field and bounds
checking: add a size int64 to the partAt struct and in its ReadAt and WriteAt
methods validate that off >= 0 and off+int64(len(buf)) <= p.size (return io.EOF
or io.ErrUnexpectedEOF / appropriate error when out-of-bounds), and clamp/adjust
behavior for partial reads/writes as needed; ensure wherever partAt is
constructed (e.g., in findMetaPartition) you pass the discovered partition size
into the new partAt instance so bounds checks use the correct partition length.

In `@README.md`:
- Around line 9-11: The fenced code blocks in README.md containing shell
commands (e.g., "GOOS=linux GOARCH=amd64 go build -o talos-meta-tool ." and the
usage example "talos-meta-tool -device /dev/sda -config config.yaml") should
include a language identifier for syntax highlighting; update those fenced
blocks to use ```bash instead of ``` so both the build command block and the
Usage block (and the similar block at lines 14-16) are labeled as bash.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cf4cefd7-8255-4dda-8a4b-eb8526a5ca5f

📥 Commits

Reviewing files that changed from the base of the PR and between 06717c5 and f1188cf.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (4)
  • README.md
  • go.mod
  • main.go
  • main_test.go

Comment thread main.go
Comment on lines 104 to 107
if *devicePath == "" || *configPath == "" {
fmt.Println("Usage: go run main.go -device <META-device> -config <path to config file>")
fmt.Println("Usage: talos-meta-tool -device <disk-device> -config <file>")
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Exit with non-zero code on missing arguments.

When required arguments are missing, the tool exits with code 0 (success). CLI tools should exit with a non-zero code to indicate an error condition, allowing callers (scripts, CI) to detect failures.

🔧 Proposed fix
 	if *devicePath == "" || *configPath == "" {
-		fmt.Println("Usage: talos-meta-tool -device <disk-device> -config <file>")
-		return
+		fmt.Fprintln(os.Stderr, "Usage: talos-meta-tool -device <disk-device> -config <file>")
+		os.Exit(2)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@main.go` around lines 104 - 107, The current main() argument check prints
usage and returns with exit code 0 when devicePath or configPath are empty;
change this to print the usage to stderr (use fmt.Fprintln(os.Stderr, ...)) and
call os.Exit with a non-zero code (e.g., os.Exit(1)) instead of returning.
Update the block that checks *devicePath == "" || *configPath == "" in main to
use stderr and os.Exit to signal failure.

@kvaps Andrei Kvapil (kvaps) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — solid implementation, and the test coverage in particular is great. Walking through it:

  • findMetaPartition correctly uses FirstLBA * sectorSize and the GPT partition-name lookup matches the Talos convention.
  • The partAt adapter + interface{ io.ReaderAt; io.WriterAt } decoupling makes writeConfig cleanly testable without touching disks.
  • Adding device.Sync() before exit is the right call — without it block-device writes can sit in the page cache and silently fail later.
  • The 20 MiB GPT integration test (TestWriteConfigFullDisk) building a real Talos-shaped disk via go-blockdevice/v2 and round-tripping through findMetaPartition is impressive — that's the test I would have asked for.

One small nit (non-blocking): openGPTDevice swallows the first error and only surfaces the second. If both paths fail, debugging the actual block-device problem becomes opaque. Could be worth errors.Join(berr, ferr) or just logging the first. Up to you.

@kvaps Andrei Kvapil (kvaps) merged commit 5072cab into cozystack:main Jun 4, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants