golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/docker2boot/docker2boot.go (about)

     1  // Copyright 2015 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // The docker2boot command converts a Docker image into a bootable GCE
     6  // VM image.
     7  package main
     8  
     9  import (
    10  	"flag"
    11  	"fmt"
    12  	"io"
    13  	"log"
    14  	"net/http"
    15  	"os"
    16  	"os/exec"
    17  	"path/filepath"
    18  	"regexp"
    19  	"runtime"
    20  	"strconv"
    21  	"strings"
    22  	"time"
    23  )
    24  
    25  var (
    26  	numGB   = flag.Int("gb", 2, "size of raw disk, in gigabytes")
    27  	rawFile = flag.String("disk", "disk.raw", "temporary raw disk file to create and delete")
    28  	img     = flag.String("image", "", "Docker image to convert. Required.")
    29  	outFile = flag.String("out", "image.tar.gz", "GCE output .tar.gz image file to create")
    30  
    31  	justRaw = flag.Bool("justraw", false, "If true, stop after preparing the raw file, but before creating the tar.gz")
    32  )
    33  
    34  // This is a Linux kernel and initrd that boots on GCE. It's the
    35  // standard one that comes with the GCE Debian image.
    36  const (
    37  	bootTarURL = "https://storage.googleapis.com/go-builder-data/boot-linux-3.16-0.bpo.3-amd64.tar.gz"
    38  
    39  	// bootUUID is the filesystem UUID in the bootTarURL snapshot.
    40  	// TODO(bradfitz): parse this out of boot/grub/grub.cfg
    41  	// instead, or write that file completely, so this doesn't
    42  	// need to exist and stay in sync with the kernel snapshot.
    43  	bootUUID = "906181f7-4e10-4a4e-8fd8-43b20ec980ff"
    44  )
    45  
    46  func main() {
    47  	flag.Parse()
    48  	defer os.Exit(1) // otherwise we call os.Exit(0) at the bottom
    49  	if runtime.GOOS != "linux" {
    50  		failf("docker2boot only runs on Linux")
    51  	}
    52  	if *img == "" {
    53  		failf("Missing required --image Docker image flag.")
    54  	}
    55  	if *outFile == "" {
    56  		failf("Missing required --out flag")
    57  	}
    58  	if strings.Contains(slurpFile("/proc/mounts"), "nbd0p1") {
    59  		failf("/proc/mounts shows nbd0p1 already mounted. Unmount that first.")
    60  	}
    61  
    62  	checkDeps()
    63  
    64  	mntDir, err := os.MkdirTemp("", "docker2boot")
    65  	if err != nil {
    66  		failf("Failed to create mount temp dir: %v", err)
    67  	}
    68  	defer os.RemoveAll(mntDir)
    69  
    70  	out, err := exec.Command("docker", "run", "-d", *img, "/bin/true").CombinedOutput()
    71  	if err != nil {
    72  		failf("Error creating container to snapshot: %v, %s", err, out)
    73  	}
    74  	container := strings.TrimSpace(string(out))
    75  
    76  	if os.Getenv("USER") != "root" {
    77  		failf("this tool requires root. Re-run with sudo.")
    78  	}
    79  
    80  	// Install the kernel's network block device driver, if it's not already.
    81  	// The qemu-nbd command would probably do this too, but this is a good place
    82  	// to fail early if it's not available.
    83  	run("modprobe", "nbd")
    84  
    85  	if strings.Contains(slurpFile("/proc/partitions"), "nbd0") {
    86  		// TODO(bradfitz): make the nbd device configurable,
    87  		// or auto-select a free one.  Hard-coding the first
    88  		// one is lazy, but works. Who uses NBD anyway?
    89  		failf("Looks like /dev/nbd0 is already in use. Maybe a previous run failed in the middle? Try sudo qemu-nbd -d /dev/nbd0")
    90  	}
    91  	if _, err := os.Stat(*rawFile); !os.IsNotExist(err) {
    92  		failf("File %s already exists. Delete it and try again, or use a different --disk flag value.", *rawFile)
    93  	}
    94  	defer os.Remove(*rawFile)
    95  
    96  	// Make a big empty file full of zeros. Using fallocate to make a sparse
    97  	// file is much quicker (~immediate) than using dd to write from /dev/zero.
    98  	// GCE requires disk images to be sized by the gigabyte.
    99  	run("fallocate", "-l", strconv.Itoa(*numGB)+"G", *rawFile)
   100  
   101  	// Start a NBD server so the kernel's /dev/nbd0 reads/writes
   102  	// from our disk image, currently all zeros.
   103  	run("qemu-nbd", "-c", "/dev/nbd0", "--format=raw", *rawFile)
   104  	defer exec.Command("qemu-nbd", "-d", "/dev/nbd0").Run()
   105  
   106  	// Put a MS-DOS partition table on it (GCE requirement), with
   107  	// the first partition's initial sector far enough in to leave
   108  	// room for the grub boot loader.
   109  	fdisk := exec.Command("/sbin/fdisk", "/dev/nbd0")
   110  	fdisk.Stdin = strings.NewReader("o\nn\np\n1\n2048\n\nw\n")
   111  	out, err = fdisk.CombinedOutput()
   112  	if err != nil {
   113  		failf("fdisk: %v, %s", err, out)
   114  	}
   115  
   116  	// Wait for the kernel to notice the partition. fdisk does an ioctl
   117  	// to make the kernel rescan for partitions.
   118  	deadline := time.Now().Add(5 * time.Second)
   119  	for !strings.Contains(slurpFile("/proc/partitions"), "nbd0p1") {
   120  		if time.Now().After(deadline) {
   121  			failf("timeout waiting for nbd0p1 to appear")
   122  		}
   123  		time.Sleep(50 * time.Millisecond)
   124  	}
   125  
   126  	// Now that the partition is available, make a filesystem on it.
   127  	run("mkfs.ext4", "/dev/nbd0p1")
   128  	run("mount", "/dev/nbd0p1", mntDir)
   129  	defer exec.Command("umount", mntDir).Run()
   130  
   131  	log.Printf("Populating /boot/ partition from %s", bootTarURL)
   132  	pipeInto(httpGet(bootTarURL), "tar", "-zx", "-C", mntDir)
   133  
   134  	log.Printf("Exporting Docker container %s into fs", container)
   135  	exp := exec.Command("docker", "export", container)
   136  	tarPipe, err := exp.StdoutPipe()
   137  	if err != nil {
   138  		failf("Pipe: %v", err)
   139  	}
   140  	if err := exp.Start(); err != nil {
   141  		failf("docker export: %v", err)
   142  	}
   143  	pipeInto(tarPipe, "tar", "-x", "-C", mntDir)
   144  	if err := exp.Wait(); err != nil {
   145  		failf("docker export: %v", err)
   146  	}
   147  
   148  	// Docker normally provides these etc files, so they're not in
   149  	// the export and we have to include them ourselves.
   150  	writeFile(filepath.Join(mntDir, "etc", "hosts"), "127.0.0.1\tlocalhost\n")
   151  	writeFile(filepath.Join(mntDir, "etc", "resolv.conf"), "nameserver 8.8.8.8\n")
   152  
   153  	// Append the source image id & docker version to /etc/issue.
   154  	issue, err := os.ReadFile("/etc/issue")
   155  	if err != nil && !os.IsNotExist(err) {
   156  		failf("Failed to read /etc/issue: %v", err)
   157  	}
   158  	out, err = exec.Command("docker", "inspect", "-f", "{{.Id}}", *img).CombinedOutput()
   159  	if err != nil {
   160  		failf("Error getting image id: %v, %s", err, out)
   161  	}
   162  	id := strings.TrimSpace(string(out))
   163  	out, err = exec.Command("docker", "-v").CombinedOutput()
   164  	if err != nil {
   165  		failf("Error getting docker version: %v, %s", err, out)
   166  	}
   167  	dockerVersion := strings.TrimSpace(string(out))
   168  	d2bissue := fmt.Sprintf("%s\nPrepared by docker2boot\nSource Docker image: %s %s\n%s\n", issue, *img, id, dockerVersion)
   169  	writeFile(filepath.Join(mntDir, "etc", "issue"), d2bissue)
   170  
   171  	// Install grub. Adjust the grub.cfg to have the correct
   172  	// filesystem UUID of the filesystem made above.
   173  	fsUUID := filesystemUUID()
   174  	grubCfgFile := filepath.Join(mntDir, "boot/grub/grub.cfg")
   175  	writeFile(grubCfgFile, strings.Replace(slurpFile(grubCfgFile), bootUUID, fsUUID, -1))
   176  	run("rm", filepath.Join(mntDir, "boot/grub/device.map"))
   177  	run("grub-install", "--boot-directory="+filepath.Join(mntDir, "boot"), "/dev/nbd0")
   178  	fstabFile := filepath.Join(mntDir, "etc/fstab")
   179  	writeFile(fstabFile, fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1", fsUUID))
   180  
   181  	// Set some password for testing.
   182  	run("chroot", mntDir, "/bin/bash", "-c", "echo root:r | chpasswd")
   183  
   184  	run("umount", mntDir)
   185  	run("qemu-nbd", "-d", "/dev/nbd0")
   186  	if *justRaw {
   187  		log.Printf("Stopping, and leaving %s alone.\nRun with:\n\n$ qemu-system-x86_64 -machine accel=kvm -nographic -curses -nodefconfig -smp 2 -drive if=virtio,file=%s -net nic,model=virtio -net user -boot once=d\n\n", *rawFile, *rawFile)
   188  		os.Exit(0)
   189  	}
   190  
   191  	// Write out a sparse tarball. GCE creates images from sparse
   192  	// tarballs on Google Cloud Storage.
   193  	run("tar", "-Szcf", *outFile, *rawFile)
   194  
   195  	os.Remove(*rawFile)
   196  	os.Exit(0)
   197  }
   198  
   199  func checkDeps() {
   200  	var missing []string
   201  	for _, cmd := range []string{
   202  		"docker",
   203  		"dumpe2fs",
   204  		"fallocate",
   205  		"grub-install",
   206  		"mkfs.ext4",
   207  		"modprobe",
   208  		"mount",
   209  		"qemu-nbd",
   210  		"rm",
   211  		"tar",
   212  		"umount",
   213  	} {
   214  		if _, err := exec.LookPath(cmd); err != nil {
   215  			missing = append(missing, cmd)
   216  		}
   217  	}
   218  	if len(missing) > 0 {
   219  		failf("Missing dependency programs: %v", missing)
   220  	}
   221  }
   222  
   223  func filesystemUUID() string {
   224  	e2fs, err := exec.Command("dumpe2fs", "/dev/nbd0p1").Output()
   225  	if err != nil {
   226  		failf("dumpe2fs: %v", err)
   227  	}
   228  	m := regexp.MustCompile(`Filesystem UUID:\s+(\S+)`).FindStringSubmatch(string(e2fs))
   229  	if m == nil || m[1] == "" {
   230  		failf("failed to find filesystem UUID")
   231  	}
   232  	return m[1]
   233  }
   234  
   235  // failf is like log.Fatalf, but runs deferred functions.
   236  func failf(msg string, args ...interface{}) {
   237  	log.Printf(msg, args...)
   238  	runtime.Goexit()
   239  }
   240  
   241  func httpGet(u string) io.Reader {
   242  	res, err := http.Get(u)
   243  	if err != nil {
   244  		failf("Get %s: %v", u, err)
   245  	}
   246  	if res.StatusCode != 200 {
   247  		failf("Get %s: %v", u, res.Status)
   248  	}
   249  	// Yeah, not closing it. This program is short-lived.
   250  	return res.Body
   251  }
   252  
   253  func slurpFile(file string) string {
   254  	v, err := os.ReadFile(file)
   255  	if err != nil {
   256  		failf("Failed to read %s: %v", file, err)
   257  	}
   258  	return string(v)
   259  }
   260  
   261  func writeFile(file, contents string) {
   262  	if err := os.WriteFile(file, []byte(contents), 0644); err != nil {
   263  		failf("writeFile %s: %v", file, err)
   264  	}
   265  }
   266  
   267  func run(cmd string, args ...string) {
   268  	log.Printf("Running %s %s", cmd, args)
   269  	out, err := exec.Command(cmd, args...).CombinedOutput()
   270  	if err != nil {
   271  		failf("Error running %s %v: %v, %s", cmd, args, err, out)
   272  	}
   273  }
   274  
   275  func pipeInto(stdin io.Reader, cmd string, args ...string) {
   276  	log.Printf("Running %s %s", cmd, args)
   277  	c := exec.Command(cmd, args...)
   278  	c.Stdin = stdin
   279  	out, err := c.CombinedOutput()
   280  	if err != nil {
   281  		failf("Error running %s %v: %v, %s", cmd, args, err, out)
   282  	}
   283  }