mirror of
https://github.com/linka-cloud/d2vm.git
synced 2026-01-25 19:15:04 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
2af13ef626
|
|||
|
|
0d4379946b | ||
|
|
e9f3ac9193 | ||
|
a40b7d3c07
|
|||
|
8538bb0521
|
|||
|
13d913db38
|
|||
|
085e57a07a
|
|||
|
20ba409039
|
|||
|
0c9bfb6dd8
|
|||
|
8c1455b030
|
|||
|
690f697ee0
|
|||
|
fa3a4f6039
|
@@ -16,13 +16,12 @@ FROM ubuntu
|
||||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
|
||||
util-linux \
|
||||
kpartx \
|
||||
udev \
|
||||
parted \
|
||||
e2fsprogs \
|
||||
xfsprogs \
|
||||
mount \
|
||||
tar \
|
||||
extlinux \
|
||||
uuid-runtime \
|
||||
qemu-utils
|
||||
|
||||
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
||||
|
||||
18
README.md
18
README.md
@@ -1,5 +1,10 @@
|
||||
|
||||
# d2vm (Docker to Virtual Machine)
|
||||
|
||||
[](https://golang.org/)
|
||||
[](https://pkg.go.dev/go.linka.cloud/d2vm)
|
||||
[](https://matrix.to/#/#d2vm:linka.cloud)
|
||||
|
||||
*Build virtual machine image from Docker images*
|
||||
|
||||
The project is heavily inspired by the [article](https://iximiuz.com/en/posts/from-docker-container-to-bootable-linux-disk-image/) and the work done by [iximiuz](https://github.com/iximiuz) on [docker-to-linux](https://github.com/iximiuz/docker-to-linux).
|
||||
@@ -8,6 +13,8 @@ Many thanks to him.
|
||||
|
||||
**Status**: *alpha*
|
||||
|
||||
[](https://asciinema.org/a/4WFKxaSNWTMPMeYbZWcSNm2nm)
|
||||
|
||||
## Supported Environments:
|
||||
|
||||
**Only Linux is supported.**
|
||||
@@ -15,7 +22,7 @@ Many thanks to him.
|
||||
If you want to run it on **OSX** or **Windows** (the last one is totally untested) you can do it using Docker:
|
||||
|
||||
```bash
|
||||
alias d2vm="docker run --rm -i -t --privileged -v /var/run/docker.sock:/var/run/docker.sock -v \$PWD:/build -w /build linkacloud/d2vm"
|
||||
alias d2vm='docker run --rm -i -t --privileged -v /var/run/docker.sock:/var/run/docker.sock -v $PWD:/build -w /build linkacloud/d2vm'
|
||||
```
|
||||
|
||||
## Supported VM Linux distributions:
|
||||
@@ -25,12 +32,13 @@ Working and tested:
|
||||
- [x] Ubuntu
|
||||
- [x] Debian
|
||||
- [x] Alpine
|
||||
- [x] CentOS
|
||||
|
||||
Need fix:
|
||||
Unsupported:
|
||||
|
||||
- [ ] CentOS / RHEL
|
||||
- [ ] RHEL
|
||||
|
||||
The program use the `/etc/os-release` file to discover the Linux distribution and install the Kernel,
|
||||
The program uses the `/etc/os-release` file to discover the Linux distribution and install the Kernel,
|
||||
if the file is missing, the build cannot succeed.
|
||||
|
||||
Obviously, **Distroless** images are not supported.
|
||||
@@ -57,7 +65,7 @@ which d2vm
|
||||
Or use an alias to the **docker** image:
|
||||
|
||||
```bash
|
||||
alias d2vm="docker run --rm -i -t --privileged -v /var/run/docker.sock:/var/run/docker.sock -v \$PWD:/build -w /build linkacloud/d2vm"
|
||||
alias d2vm='docker run --rm -i -t --privileged -v /var/run/docker.sock:/var/run/docker.sock -v $PWD:/build -w /build linkacloud/d2vm'
|
||||
which d2vm
|
||||
```
|
||||
```
|
||||
|
||||
95
builder.go
95
builder.go
@@ -15,10 +15,8 @@
|
||||
package d2vm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
exec2 "os/exec"
|
||||
"path/filepath"
|
||||
@@ -58,19 +56,17 @@ ff02::3 ip6-allhosts
|
||||
SAY Now booting the kernel from SYSLINUX...
|
||||
LABEL linux
|
||||
KERNEL /boot/vmlinuz-virt
|
||||
APPEND ro root=UUID=%s rootfstype=ext4 initrd=/boot/initramfs-virt console=ttyS0,115200
|
||||
APPEND ro root=UUID=%s rootfstype=ext4 initrd=/boot/initramfs-virt console=ttyS0,115200
|
||||
`
|
||||
syslinuxCfgCentOS = `DEFAULT linux
|
||||
SAY Now booting the kernel from SYSLINUX...
|
||||
LABEL linux
|
||||
KERNEL /boot/vmlinuz
|
||||
APPEND ro root=UUID=%s initrd=/boot/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8
|
||||
APPEND ro root=UUID=%s initrd=/boot/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8
|
||||
`
|
||||
)
|
||||
|
||||
var (
|
||||
fdiskCmds = []string{"n", "p", "1", "", "", "a", "w"}
|
||||
|
||||
formats = []string{"qcow2", "qed", "raw", "vdi", "vhd", "vmdk"}
|
||||
|
||||
mbrPaths = []string{
|
||||
@@ -82,13 +78,20 @@ var (
|
||||
"/usr/share/syslinux/mbr.bin",
|
||||
// centos path
|
||||
"/usr/share/syslinux/mbr.bin",
|
||||
// archlinux path
|
||||
"/usr/lib/syslinux/bios/mbr.bin",
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
perm os.FileMode = 0644
|
||||
)
|
||||
|
||||
type builder struct {
|
||||
osRelease OSRelease
|
||||
|
||||
src string
|
||||
img *image
|
||||
diskRaw string
|
||||
diskOut string
|
||||
format string
|
||||
@@ -103,7 +106,7 @@ type builder struct {
|
||||
diskUUD string
|
||||
}
|
||||
|
||||
func NewBuilder(workdir, src, disk string, size int64, osRelease OSRelease, format string) (*builder, error) {
|
||||
func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size int64, osRelease OSRelease, format string) (*builder, error) {
|
||||
if err := checkDependencies(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -134,18 +137,22 @@ func NewBuilder(workdir, src, disk string, size int64, osRelease OSRelease, form
|
||||
if disk == "" {
|
||||
disk = "disk0"
|
||||
}
|
||||
i, err := os.Stat(src)
|
||||
img, err := NewImage(ctx, imgTag, workdir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if i.Size() > size {
|
||||
s := datasize.ByteSize(math.Ceil(datasize.ByteSize(i.Size()).GBytes())) * datasize.GB
|
||||
logrus.Warnf("%s is smaller than rootfs size, using %s", datasize.ByteSize(size), s)
|
||||
size = int64(s)
|
||||
}
|
||||
// i, err := os.Stat(imgTar)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// if i.Size() > size {
|
||||
// s := datasize.ByteSize(math.Ceil(datasize.ByteSize(i.Size()).GBytes())) * datasize.GB
|
||||
// logrus.Warnf("%s is smaller than rootfs size, using %s", datasize.ByteSize(size), s)
|
||||
// size = int64(s)
|
||||
// }
|
||||
b := &builder{
|
||||
osRelease: osRelease,
|
||||
src: src,
|
||||
img: img,
|
||||
diskRaw: filepath.Join(workdir, disk+".raw"),
|
||||
diskOut: filepath.Join(workdir, disk+".qcow2"),
|
||||
format: f,
|
||||
@@ -217,18 +224,9 @@ func (b *builder) makeImg(ctx context.Context) error {
|
||||
if err := block(b.diskRaw, b.size); err != nil {
|
||||
return err
|
||||
}
|
||||
c := exec.CommandContext(ctx, "fdisk", b.diskRaw)
|
||||
var i bytes.Buffer
|
||||
for _, v := range fdiskCmds {
|
||||
if _, err := i.Write([]byte(v + "\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var e bytes.Buffer
|
||||
c.Stdin = &i
|
||||
c.Stderr = &e
|
||||
if err := c.Run(); err != nil {
|
||||
return fmt.Errorf("%w: %s", err, e.String())
|
||||
|
||||
if err := exec.Run(ctx, "parted", "-s", b.diskRaw, "mklabel", "msdos", "mkpart", "primary", "1Mib", "100%", "set", "1", "boot", "on"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -240,10 +238,10 @@ func (b *builder) mountImg(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
b.loDevice = strings.TrimSuffix(o, "\n")
|
||||
if err := exec.Run(ctx, "kpartx", "-a", b.loDevice); err != nil {
|
||||
if err := exec.Run(ctx, "partprobe", b.loDevice); err != nil {
|
||||
return err
|
||||
}
|
||||
b.loPart = fmt.Sprintf("/dev/mapper/%sp1", filepath.Base(b.loDevice))
|
||||
b.loPart = fmt.Sprintf("%sp1", b.loDevice)
|
||||
logrus.Infof("creating raw image file system")
|
||||
if err := exec.Run(ctx, "mkfs.ext4", b.loPart); err != nil {
|
||||
return err
|
||||
@@ -260,9 +258,6 @@ func (b *builder) unmountImg(ctx context.Context) error {
|
||||
if err := exec.Run(ctx, "umount", b.mntPoint); err != nil {
|
||||
merr = multierr.Append(merr, err)
|
||||
}
|
||||
if err := exec.Run(ctx, "kpartx", "-d", b.loDevice); err != nil {
|
||||
merr = multierr.Append(merr, err)
|
||||
}
|
||||
if err := exec.Run(ctx, "losetup", "-d", b.loDevice); err != nil {
|
||||
merr = multierr.Append(merr, err)
|
||||
}
|
||||
@@ -271,7 +266,7 @@ func (b *builder) unmountImg(ctx context.Context) error {
|
||||
|
||||
func (b *builder) copyRootFS(ctx context.Context) error {
|
||||
logrus.Infof("copying rootfs to raw image")
|
||||
if err := exec.Run(ctx, "tar", "-xvf", b.src, "-C", b.mntPoint); err != nil {
|
||||
if err := b.img.Flatten(ctx, b.mntPoint); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -285,18 +280,19 @@ func (b *builder) setupRootFS(ctx context.Context) error {
|
||||
}
|
||||
b.diskUUD = strings.TrimSuffix(o, "\n")
|
||||
fstab := fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1\n", b.diskUUD)
|
||||
if err := b.chWriteFile("/etc/fstab", fstab, 0644); err != nil {
|
||||
if err := b.chWriteFile("/etc/fstab", fstab, perm); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.chWriteFile("/etc/resolv.conf", "nameserver 8.8.8.8", 0644); err != nil {
|
||||
if err := b.chWriteFileIfNotExist("/etc/resolv.conf", "nameserver 8.8.8.8", 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.chWriteFile("/etc/hostname", "localhost", 0644); err != nil {
|
||||
if err := b.chWriteFileIfNotExist("/etc/hostname", "localhost", perm); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.chWriteFile("/etc/hosts", hosts, 0644); err != nil {
|
||||
if err := b.chWriteFileIfNotExist("/etc/hosts", hosts, perm); err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO(adphi): is it the righ fix ?
|
||||
if err := os.RemoveAll("/ur/sbin/policy-rc.d"); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -311,10 +307,10 @@ func (b *builder) setupRootFS(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
by = append(by, []byte("\n"+"ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100\n")...)
|
||||
if err := b.chWriteFile("/etc/inittab", string(by), 0644); err != nil {
|
||||
if err := b.chWriteFile("/etc/inittab", string(by), perm); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.chWriteFile("/etc/network/interfaces", "", 0644); err != nil {
|
||||
if err := b.chWriteFileIfNotExist("/etc/network/interfaces", "", perm); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -328,17 +324,21 @@ func (b *builder) installKernel(ctx context.Context) error {
|
||||
var sysconfig string
|
||||
switch b.osRelease.ID {
|
||||
case ReleaseUbuntu:
|
||||
sysconfig = syslinuxCfgUbuntu
|
||||
if b.osRelease.VersionID < "20.04" {
|
||||
sysconfig = syslinuxCfgDebian
|
||||
} else {
|
||||
sysconfig = syslinuxCfgUbuntu
|
||||
}
|
||||
case ReleaseDebian:
|
||||
sysconfig = syslinuxCfgDebian
|
||||
case ReleaseAlpine:
|
||||
sysconfig = syslinuxCfgAlpine
|
||||
case ReleaseCentOS, ReleaseRHEL:
|
||||
case ReleaseCentOS:
|
||||
sysconfig = syslinuxCfgCentOS
|
||||
default:
|
||||
return fmt.Errorf("%s: distribution not supported", b.osRelease.ID)
|
||||
}
|
||||
if err := b.chWriteFile("/boot/syslinux.cfg", fmt.Sprintf(sysconfig, b.diskUUD), 0644); err != nil {
|
||||
if err := b.chWriteFile("/boot/syslinux.cfg", fmt.Sprintf(sysconfig, b.diskUUD), perm); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -361,10 +361,21 @@ func (b *builder) chWriteFile(path string, content string, perm os.FileMode) err
|
||||
return os.WriteFile(b.chPath(path), []byte(content), perm)
|
||||
}
|
||||
|
||||
func (b *builder) chWriteFileIfNotExist(path string, content string, perm os.FileMode) error {
|
||||
if i, err := os.Stat(b.chPath(path)); err == nil && i.Size() != 0 {
|
||||
return nil
|
||||
}
|
||||
return os.WriteFile(b.chPath(path), []byte(content), perm)
|
||||
}
|
||||
|
||||
func (b *builder) chPath(path string) string {
|
||||
return fmt.Sprintf("%s%s", b.mntPoint, path)
|
||||
}
|
||||
|
||||
func (b *builder) Close() error {
|
||||
return b.img.Close()
|
||||
}
|
||||
|
||||
func block(path string, size int64) error {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
@@ -376,7 +387,7 @@ func block(path string, size int64) error {
|
||||
|
||||
func checkDependencies() error {
|
||||
var merr error
|
||||
for _, v := range []string{"mount", "blkid", "tar", "kpartx", "losetup", "qemu-img", "extlinux", "dd", "mkfs", "fdisk"} {
|
||||
for _, v := range []string{"mount", "blkid", "tar", "losetup", "parted", "partprobe", "qemu-img", "extlinux", "dd", "mkfs"} {
|
||||
if _, err := exec2.LookPath(v); err != nil {
|
||||
merr = multierr.Append(merr, err)
|
||||
}
|
||||
|
||||
@@ -39,15 +39,9 @@ var (
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if debug {
|
||||
exec.Run = exec.RunStdout
|
||||
}
|
||||
exec.SetDebug(debug)
|
||||
logrus.Infof("building docker image from %s", file)
|
||||
dargs := []string{"build", "-t", tag, "-f", file, args[0]}
|
||||
for _, v := range buildArgs {
|
||||
dargs = append(dargs, "--build-arg", v)
|
||||
}
|
||||
if err := docker.Cmd(cmd.Context(), dargs...); err != nil {
|
||||
if err := docker.Build(cmd.Context(), tag, file, args[0], buildArgs...); err != nil {
|
||||
return err
|
||||
}
|
||||
return d2vm.Convert(cmd.Context(), tag, size, password, output, format)
|
||||
|
||||
@@ -51,9 +51,7 @@ var (
|
||||
return fmt.Errorf("%s already exists", output)
|
||||
}
|
||||
}
|
||||
if debug {
|
||||
exec.Run = exec.RunStdout
|
||||
}
|
||||
exec.SetDebug(debug)
|
||||
if _, err := os.Stat(output); err == nil || !os.IsNotExist(err) {
|
||||
if !force {
|
||||
return fmt.Errorf("%s already exists", output)
|
||||
@@ -61,18 +59,18 @@ var (
|
||||
}
|
||||
found := false
|
||||
if !pull {
|
||||
o, _, err := docker.CmdOut(cmd.Context(), "image", "ls", "--format={{ .Repository }}:{{ .Tag }}", img)
|
||||
imgs, err := docker.ImageList(cmd.Context(), img)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
found = strings.TrimSuffix(o, "\n") == fmt.Sprintf("%s:%s", img, tag)
|
||||
found = len(imgs) == 1 && imgs[0] == fmt.Sprintf("%s:%s", img, tag)
|
||||
if found {
|
||||
logrus.Infof("using local image %s:%s", img, tag)
|
||||
}
|
||||
}
|
||||
if pull || !found {
|
||||
logrus.Infof("pulling image %s", img)
|
||||
if err := docker.Cmd(cmd.Context(), "image", "pull", img); err != nil {
|
||||
if err := docker.Pull(cmd.Context(), img); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
21
convert.go
21
convert.go
@@ -56,28 +56,17 @@ func Convert(ctx context.Context, img string, size int64, password string, outpu
|
||||
return err
|
||||
}
|
||||
logrus.Infof("building kernel enabled image")
|
||||
if err := docker.Cmd(ctx, "image", "build", "-t", imgUUID, "-f", p, dir); err != nil {
|
||||
if err := docker.Build(ctx, imgUUID, p, dir); err != nil {
|
||||
return err
|
||||
}
|
||||
defer docker.Cmd(ctx, "image", "rm", imgUUID)
|
||||
archive := imgUUID + ".tar"
|
||||
archivePath := filepath.Join(tmpPath, archive)
|
||||
logrus.Infof("creating root file system archive")
|
||||
if err := docker.Cmd(ctx, "run", "-d", "--name", imgUUID, imgUUID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := docker.Cmd(ctx, "export", "--output", archivePath, imgUUID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := docker.Cmd(ctx, "rm", "-f", imgUUID); err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.Infof("creating vm image")
|
||||
defer docker.Remove(ctx, imgUUID)
|
||||
|
||||
b, err := NewBuilder(tmpPath, archivePath, "", size, r, format)
|
||||
logrus.Infof("creating vm image")
|
||||
b, err := NewBuilder(ctx, tmpPath, imgUUID, "", size, r, format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer b.Close()
|
||||
if err := b.Build(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
131
docker_image.go
131
docker_image.go
@@ -15,58 +15,127 @@
|
||||
package d2vm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/google/go-containerregistry/cmd/crane/cmd"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/daemon"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
|
||||
"go.linka.cloud/d2vm/pkg/exec"
|
||||
)
|
||||
|
||||
const (
|
||||
dockerImageRun = `
|
||||
#!/bin/sh
|
||||
|
||||
{{- range .Config.Env }}
|
||||
{{- range .DockerImageConfig.Env }}
|
||||
export {{ . }}
|
||||
{{- end }}
|
||||
|
||||
cd {{- if .Config.WorkingDir }}{{ .Config.WorkingDir }}{{- else }}/{{- end }}
|
||||
{{ if .DockerImageConfig.WorkingDir }}cd {{ .DockerImageConfig.WorkingDir }}{{ end }}
|
||||
|
||||
{{ .Config.Entrypoint }} {{ .Config.Args }}
|
||||
{{ if .DockerImageConfig.User }}su {{ .DockerImageConfig.User }} -p -s /bin/sh -c '{{ end }}{{ if .DockerImageConfig.Entrypoint}}{{ format .DockerImageConfig.Entrypoint }} {{ end}}{{ if .DockerImageConfig.Cmd }}{{ format .DockerImageConfig.Cmd }}{{ end }}{{ if .DockerImageConfig.User }}'{{- end }}
|
||||
`
|
||||
)
|
||||
|
||||
var (
|
||||
dockerImageRunTemplate = template.Must(template.New("docker-run.sh").Parse(dockerImageRun))
|
||||
dockerImageRunTemplate = template.Must(template.New("docker-run.sh").Funcs(map[string]interface{}{"format": func(a []string) string {
|
||||
var o []string
|
||||
for _, v := range a {
|
||||
o = append(o, fmt.Sprintf("\"%s\"", v))
|
||||
}
|
||||
return strings.Join(o, " ")
|
||||
}}).Parse(dockerImageRun))
|
||||
|
||||
_ = cmd.NewCmdFlatten
|
||||
)
|
||||
|
||||
type DockerImage struct {
|
||||
Config struct {
|
||||
Hostname string `json:"Hostname"`
|
||||
Domainname string `json:"Domainname"`
|
||||
User string `json:"User"`
|
||||
AttachStdin bool `json:"AttachStdin"`
|
||||
AttachStdout bool `json:"AttachStdout"`
|
||||
AttachStderr bool `json:"AttachStderr"`
|
||||
ExposedPorts struct {
|
||||
Tcp struct {
|
||||
} `json:"3000/tcp"`
|
||||
} `json:"ExposedPorts"`
|
||||
Tty bool `json:"Tty"`
|
||||
OpenStdin bool `json:"OpenStdin"`
|
||||
StdinOnce bool `json:"StdinOnce"`
|
||||
Env []string `json:"Env"`
|
||||
Cmd []string `json:"Cmd"`
|
||||
Image string `json:"Image"`
|
||||
Volumes interface{} `json:"Volumes"`
|
||||
WorkingDir string `json:"WorkingDir"`
|
||||
Entrypoint []string `json:"Entrypoint"`
|
||||
OnBuild interface{} `json:"OnBuild"`
|
||||
Labels interface{} `json:"Labels"`
|
||||
} `json:"Config"`
|
||||
Architecture string `json:"Architecture"`
|
||||
Os string `json:"Os"`
|
||||
Size int `json:"Size"`
|
||||
VirtualSize int `json:"VirtualSize"`
|
||||
DockerImageConfig `json:"Config"`
|
||||
Architecture string `json:"Architecture"`
|
||||
Os string `json:"Os"`
|
||||
Size int `json:"Size"`
|
||||
}
|
||||
|
||||
type DockerImageConfig struct {
|
||||
Image string `json:"Image"`
|
||||
Hostname string `json:"Hostname"`
|
||||
Domainname string `json:"Domainname"`
|
||||
User string `json:"User"`
|
||||
Env []string `json:"Env"`
|
||||
Cmd []string `json:"Cmd"`
|
||||
WorkingDir string `json:"WorkingDir"`
|
||||
Entrypoint []string `json:"Entrypoint"`
|
||||
}
|
||||
|
||||
func (i DockerImage) AsRunScript(w io.Writer) error {
|
||||
return dockerImageRunTemplate.Execute(w, i)
|
||||
}
|
||||
|
||||
const (
|
||||
whiteoutPrefix = ".wh."
|
||||
manifest = "manifest.json"
|
||||
)
|
||||
|
||||
func NewImage(ctx context.Context, tag string, imageTmpPath string) (*image, error) {
|
||||
ref, err := name.ParseReference(tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
img, err := daemon.Image(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.MkdirAll(imageTmpPath, perm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i := &image{
|
||||
img: img,
|
||||
dir: imageTmpPath,
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
type image struct {
|
||||
tag string
|
||||
img v1.Image
|
||||
dir string
|
||||
Config string `json:"Config"`
|
||||
RepoTags []string `json:"RepoTags"`
|
||||
Layers []string `json:"Layers"`
|
||||
}
|
||||
|
||||
func (i image) Flatten(ctx context.Context, out string) error {
|
||||
if err := os.MkdirAll(out, perm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tar := filepath.Join(i.dir, "img.tar")
|
||||
f, err := os.Create(tar)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := io.Copy(f, mutate.Extract(i.img)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := exec.Run(ctx, "tar", "xvf", tar, "-C", out); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i image) Close() error {
|
||||
return os.RemoveAll(i.dir)
|
||||
}
|
||||
|
||||
168
docker_image_test.go
Normal file
168
docker_image_test.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package d2vm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.linka.cloud/d2vm/pkg/docker"
|
||||
"go.linka.cloud/d2vm/pkg/exec"
|
||||
)
|
||||
|
||||
func TestDockerImageAsRunSript(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
image DockerImage
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "nothing",
|
||||
image: DockerImage{
|
||||
DockerImageConfig: DockerImageConfig{
|
||||
User: "",
|
||||
WorkingDir: "",
|
||||
Env: nil,
|
||||
Entrypoint: nil,
|
||||
Cmd: nil,
|
||||
},
|
||||
},
|
||||
want: `
|
||||
#!/bin/sh
|
||||
|
||||
|
||||
|
||||
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "tail -f /dev/null",
|
||||
image: DockerImage{
|
||||
DockerImageConfig: DockerImageConfig{
|
||||
User: "root",
|
||||
Cmd: []string{"tail", "-f", "/dev/null"},
|
||||
},
|
||||
},
|
||||
want: `
|
||||
#!/bin/sh
|
||||
|
||||
|
||||
|
||||
su root -p -s /bin/sh -c '"tail" "-f" "/dev/null"'
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "tail -f /dev/null inside home",
|
||||
image: DockerImage{
|
||||
DockerImageConfig: DockerImageConfig{
|
||||
User: "root",
|
||||
WorkingDir: "/root",
|
||||
Cmd: []string{"tail", "-f", "/dev/null"},
|
||||
},
|
||||
},
|
||||
want: `
|
||||
#!/bin/sh
|
||||
|
||||
cd /root
|
||||
|
||||
su root -p -s /bin/sh -c '"tail" "-f" "/dev/null"'
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "subshell tail -f /dev/null",
|
||||
image: DockerImage{
|
||||
DockerImageConfig: DockerImageConfig{
|
||||
User: "root",
|
||||
Entrypoint: []string{"/bin/sh", "-c"},
|
||||
Cmd: []string{"tail -f /dev/null"},
|
||||
},
|
||||
},
|
||||
want: `
|
||||
#!/bin/sh
|
||||
|
||||
|
||||
|
||||
su root -p -s /bin/sh -c '"/bin/sh" "-c" "tail -f /dev/null"'
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "www-data with env",
|
||||
image: DockerImage{
|
||||
DockerImageConfig: DockerImageConfig{
|
||||
User: "www-data",
|
||||
Cmd: []string{"tail", "-f", "/dev/null"},
|
||||
Env: []string{"ENV=PROD", "DB=mysql://user:password@localhost"},
|
||||
},
|
||||
},
|
||||
want: `
|
||||
#!/bin/sh
|
||||
export ENV=PROD
|
||||
export DB=mysql://user:password@localhost
|
||||
|
||||
|
||||
|
||||
su www-data -p -s /bin/sh -c '"tail" "-f" "/dev/null"'
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var w bytes.Buffer
|
||||
require.NoError(t, tt.image.AsRunScript(&w))
|
||||
assert.Equal(t, tt.want, w.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageFlatten(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
const (
|
||||
img = "d2vm-flatten-test"
|
||||
dockerfile = `FROM alpine
|
||||
|
||||
COPY resolv.conf /etc/
|
||||
COPY hostname /etc/
|
||||
|
||||
RUN rm -rf /etc/apk
|
||||
`
|
||||
)
|
||||
exec.SetDebug(true)
|
||||
tmp := filepath.Join(os.TempDir(), "d2vm-tests", "image-flatten")
|
||||
require.NoError(t, os.MkdirAll(tmp, perm))
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tmp, "hostname"), []byte("d2vm-flatten-test"), perm))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tmp, "resolv.conf"), []byte("nameserver 8.8.8.8"), perm))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tmp, "Dockerfile"), []byte(dockerfile), perm))
|
||||
require.NoError(t, docker.Build(ctx, img, "", tmp))
|
||||
defer docker.Remove(ctx, img)
|
||||
|
||||
imgTmp := filepath.Join(tmp, "image")
|
||||
|
||||
i, err := NewImage(ctx, img, imgTmp)
|
||||
require.NoError(t, err)
|
||||
|
||||
rootfs := filepath.Join(tmp, "rootfs")
|
||||
require.NoError(t, i.Flatten(ctx, rootfs))
|
||||
|
||||
b, err := os.ReadFile(filepath.Join(rootfs, "etc", "resolv.conf"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "nameserver 8.8.8.8", string(b))
|
||||
|
||||
b, err = os.ReadFile(filepath.Join(rootfs, "etc", "hostname"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "d2vm-flatten-test", string(b))
|
||||
|
||||
_, err = os.Stat(filepath.Join(rootfs, "etc", "apk"))
|
||||
assert.Error(t, err)
|
||||
|
||||
require.NoError(t, i.Close())
|
||||
_, err = os.Stat(imgTmp)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func NewDockerfile(release OSRelease, img, password string) (Dockerfile, error)
|
||||
d.tmpl = ubuntuDockerfileTemplate
|
||||
case ReleaseAlpine:
|
||||
d.tmpl = alpineDockerfileTemplate
|
||||
case ReleaseCentOS, ReleaseRHEL:
|
||||
case ReleaseCentOS:
|
||||
d.tmpl = centOSDockerfileTemplate
|
||||
default:
|
||||
return Dockerfile{}, fmt.Errorf("unsupported distribution: %s", release.ID)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
FROM ubuntu
|
||||
|
||||
# Install netplan sudo ssh-server and dns utils
|
||||
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y \
|
||||
# Install some system packages
|
||||
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \
|
||||
qemu-guest-agent \
|
||||
netplan.io \
|
||||
ca-certificates \
|
||||
dnsutils \
|
||||
sudo \
|
||||
openssh-server
|
||||
@@ -19,7 +20,7 @@ ARG PASSWORD=d2vm
|
||||
ARG SSH_KEY=https://github.com/${USER}.keys
|
||||
|
||||
# Setup user environment
|
||||
RUN DEBIAN_FRONTEND=noninteractive apt install -y \
|
||||
RUN DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \
|
||||
bash-completion \
|
||||
curl \
|
||||
zsh \
|
||||
@@ -45,4 +46,5 @@ USER ${USER}
|
||||
RUN bash -c "$(curl -fsSL https://gist.githubusercontent.com/Adphi/f3ce3cc4b2551c437eb667f3a5873a16/raw/be05553da87f6e9d8b0d290af5aa036d07de2e25/env.setup)"
|
||||
# Setup tmux environment
|
||||
RUN bash -c "$(curl -fsSL https://gist.githubusercontent.com/Adphi/765e9382dd5e547633be567e2eb72476/raw/a3fe4b3f35e598dca90e2dd45d30dc1753447a48/tmux-setup)"
|
||||
|
||||
# Setup auto login serial console
|
||||
RUN sudo sed -i "s|ExecStart=.*|ExecStart=-/sbin/agetty --autologin ${USER} --keep-baud 115200,38400,9600 \%I \$TERM|" /usr/lib/systemd/system/serial-getty@.service
|
||||
|
||||
@@ -6,10 +6,11 @@ This example demonstrate the setup of a ZSH workstation.
|
||||
```dockerfile
|
||||
FROM ubuntu
|
||||
|
||||
# Install netplan sudo ssh-server and dns utils
|
||||
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y \
|
||||
# Install some system packages
|
||||
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \
|
||||
qemu-guest-agent \
|
||||
netplan.io \
|
||||
ca-certificates \
|
||||
dnsutils \
|
||||
sudo \
|
||||
openssh-server
|
||||
@@ -25,7 +26,8 @@ ARG PASSWORD=d2vm
|
||||
ARG SSH_KEY=https://github.com/${USER}.keys
|
||||
|
||||
# Setup user environment
|
||||
RUN DEBIAN_FRONTEND=noninteractive apt install -y \
|
||||
RUN DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \
|
||||
bash-completion \
|
||||
curl \
|
||||
zsh \
|
||||
git \
|
||||
@@ -50,6 +52,8 @@ USER ${USER}
|
||||
RUN bash -c "$(curl -fsSL https://gist.githubusercontent.com/Adphi/f3ce3cc4b2551c437eb667f3a5873a16/raw/be05553da87f6e9d8b0d290af5aa036d07de2e25/env.setup)"
|
||||
# Setup tmux environment
|
||||
RUN bash -c "$(curl -fsSL https://gist.githubusercontent.com/Adphi/765e9382dd5e547633be567e2eb72476/raw/a3fe4b3f35e598dca90e2dd45d30dc1753447a48/tmux-setup)"
|
||||
# Setup auto login serial console
|
||||
RUN sudo sed -i "s|ExecStart=.*|ExecStart=-/sbin/agetty --autologin ${USER} --keep-baud 115200,38400,9600 \%I \$TERM|" /usr/lib/systemd/system/serial-getty@.service
|
||||
```
|
||||
|
||||
*00-netconf.yaml*
|
||||
@@ -82,6 +86,7 @@ Run it using *libvirt's virt-install*:
|
||||
```bash
|
||||
virt-install --name workstation --disk $OUTPUT --import --memory 4096 --vcpus 4 --nographics --cpu host --channel unix,target.type=virtio,target.name='org.qemu.guest_agent.0'
|
||||
```
|
||||
... you should be automatically logged in with a **oh-my-zsh** shell
|
||||
|
||||
From an other terminal you should be able to find the VM ip address using:
|
||||
```bash
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM ubuntu
|
||||
|
||||
RUN apt update && apt install -y openssh-server && \
|
||||
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config \
|
||||
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
|
||||
|
||||
30
go.mod
30
go.mod
@@ -4,16 +4,44 @@ go 1.17
|
||||
|
||||
require (
|
||||
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2
|
||||
github.com/google/go-containerregistry v0.8.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/joho/godotenv v1.4.0
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/spf13/cobra v1.4.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
go.uber.org/multierr v1.8.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.5.1 // indirect
|
||||
github.com/containerd/containerd v1.5.8 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.10.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/cli v20.10.12+incompatible // indirect
|
||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||
github.com/docker/docker v20.10.12+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.6.4 // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.6 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.13.6 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.2-0.20211117181255-693428a734f5 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/vbatts/tar-split v0.11.2 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
|
||||
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
|
||||
google.golang.org/grpc v1.43.0 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
)
|
||||
|
||||
@@ -46,8 +46,10 @@ func (r Release) Supported() bool {
|
||||
return true
|
||||
case ReleaseAlpine:
|
||||
return true
|
||||
case ReleaseCentOS, ReleaseRHEL:
|
||||
case ReleaseCentOS:
|
||||
return true
|
||||
case ReleaseRHEL:
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
_ "embed"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"go.linka.cloud/d2vm/pkg/exec"
|
||||
@@ -35,3 +37,36 @@ func Cmd(ctx context.Context, args ...string) error {
|
||||
func CmdOut(ctx context.Context, args ...string) (string, string, error) {
|
||||
return exec.RunOut(ctx, "docker", args...)
|
||||
}
|
||||
|
||||
func Build(ctx context.Context, tag, dockerfile, dir string, buildArgs ...string) error {
|
||||
if dockerfile == "" {
|
||||
dockerfile = filepath.Join(dir, "Dockerfile")
|
||||
}
|
||||
args := []string{"image", "build", "-t", tag, "-f", dockerfile}
|
||||
for _, v := range buildArgs {
|
||||
args = append(args, "--build-arg", v)
|
||||
}
|
||||
args = append(args, dir)
|
||||
return Cmd(ctx, args...)
|
||||
}
|
||||
|
||||
func Remove(ctx context.Context, tag string) error {
|
||||
return Cmd(ctx, "image", "rm", tag)
|
||||
}
|
||||
|
||||
func ImageList(ctx context.Context, tag string) ([]string, error) {
|
||||
o, _, err := CmdOut(ctx, "image", "ls", "--format={{ .Repository }}:{{ .Tag }}", tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := bufio.NewScanner(strings.NewReader(o))
|
||||
var imgs []string
|
||||
for s.Scan() {
|
||||
imgs = append(imgs, s.Text())
|
||||
}
|
||||
return imgs, s.Err()
|
||||
}
|
||||
|
||||
func Pull(ctx context.Context, tag string) error {
|
||||
return Cmd(ctx, "image", "pull", tag)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,14 @@ var (
|
||||
CommandContext = exec.CommandContext
|
||||
)
|
||||
|
||||
func SetDebug(debug bool) {
|
||||
if debug {
|
||||
Run = RunStdout
|
||||
} else {
|
||||
Run = RunNoOut
|
||||
}
|
||||
}
|
||||
|
||||
func RunStdout(ctx context.Context, c string, args ...string) error {
|
||||
cmd := exec.CommandContext(ctx, c, args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
|
||||
@@ -7,7 +7,10 @@ RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* && \
|
||||
|
||||
RUN yum update -y
|
||||
|
||||
RUN yum install -y kernel systemd sudo
|
||||
RUN yum install -y kernel systemd NetworkManager e2fsprogs sudo && \
|
||||
systemctl enable NetworkManager && \
|
||||
systemctl unmask systemd-remount-fs.service && \
|
||||
systemctl unmask getty.target
|
||||
|
||||
RUN dracut --no-hostonly --regenerate-all --force && \
|
||||
cd /boot && \
|
||||
|
||||
@@ -3,17 +3,15 @@ FROM {{ .Image }}
|
||||
USER root
|
||||
|
||||
RUN apt-get update -y && \
|
||||
apt-get -y install \
|
||||
linux-image-virtual
|
||||
|
||||
|
||||
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
systemd-sysv \
|
||||
systemd \
|
||||
dbus \
|
||||
udhcpc \
|
||||
iproute2 \
|
||||
iputils-ping
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
|
||||
linux-image-virtual \
|
||||
initramfs-tools \
|
||||
systemd-sysv \
|
||||
systemd \
|
||||
dbus \
|
||||
udhcpc \
|
||||
iproute2 \
|
||||
iputils-ping
|
||||
|
||||
RUN systemctl preset-all
|
||||
|
||||
|
||||
2
virtinst
2
virtinst
@@ -3,4 +3,4 @@
|
||||
|
||||
IMG=${1:-disk0.qcow2}
|
||||
|
||||
virt-install --disk $IMG --import --memory 4096 --vcpus 4 --nographics --cpu host --channel unix,target.type=virtio,target.name='org.qemu.guest_agent.0'
|
||||
virt-install --disk $IMG --import --memory 4096 --vcpus 4 --nographics --cpu host --channel unix,target.type=virtio,target.name='org.qemu.guest_agent.0' --transient
|
||||
|
||||
Reference in New Issue
Block a user