Adds a CLI subcommand that downloads a signed Tailscale appliance image (Gokrazy archive format, GAF) from pkgs.tailscale.com, constructs a fresh GPT-partitioned disk from it (mbr.img + a synthesized partition table + boot.img + root.img), formats /perm as ext4 in pure Go via go-diskfs, and ejects the disk so a user running on a regular workstation can flash an SD card or homelab VM disk in one command without installing e2fsprogs. On macOS the target disk is auto-discovered via diskutil, skipping the boot disk and anything bigger than 256 GB out of paranoia. On Linux the user passes --disk=/dev/sdX explicitly. Windows is not supported yet and the command returns an error. The GPT layout matches monogok's full-disk layout via the new public github.com/bradfitz/monogok/disklayout package; a drift- guard test inside monogok asserts the two implementations stay byte-identical so OTA updates against monogok-built images keep working. Behind a ts_omit_flashappliance build tag (on by default). Updates #1866 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: Ic1a8cd185e7039edccb7702ab4104544fcb58d29
55 lines
1.4 KiB
Go
55 lines
1.4 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build !ts_omit_flashappliance
|
|
|
|
package cli
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"testing"
|
|
)
|
|
|
|
func TestCheckPartitionFits(t *testing.T) {
|
|
files := buildZip(t, map[string][]byte{
|
|
"boot.img": bytes.Repeat([]byte{0xAB}, 1<<20),
|
|
"root.img": bytes.Repeat([]byte{0xCD}, 4<<20),
|
|
})
|
|
|
|
if err := checkPartitionFits(files, "boot.img", 2<<20); err != nil {
|
|
t.Errorf("boot.img within limit: %v", err)
|
|
}
|
|
if err := checkPartitionFits(files, "root.img", 1<<20); err == nil {
|
|
t.Errorf("root.img over limit: expected error")
|
|
}
|
|
if err := checkPartitionFits(files, "missing.img", 1<<20); err == nil {
|
|
t.Errorf("missing file: expected error")
|
|
}
|
|
}
|
|
|
|
// buildZip returns the *zip.File entries for an in-memory zip containing
|
|
// the given members.
|
|
func buildZip(t *testing.T, members map[string][]byte) []*zip.File {
|
|
t.Helper()
|
|
var buf bytes.Buffer
|
|
zw := zip.NewWriter(&buf)
|
|
for name, data := range members {
|
|
w, err := zw.Create(name)
|
|
if err != nil {
|
|
t.Fatalf("zip.Create %s: %v", name, err)
|
|
}
|
|
if _, err := w.Write(data); err != nil {
|
|
t.Fatalf("zip.Write %s: %v", name, err)
|
|
}
|
|
}
|
|
if err := zw.Close(); err != nil {
|
|
t.Fatalf("zip.Close: %v", err)
|
|
}
|
|
zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
|
|
if err != nil {
|
|
t.Fatalf("zip.NewReader: %v", err)
|
|
}
|
|
return zr.File
|
|
}
|