diff --git a/Dockerfile b/Dockerfile index ef36b3c..bb485a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,6 +35,7 @@ RUN apt-get update && \ parted \ kpartx \ e2fsprogs \ + dosfstools \ mount \ tar \ extlinux \ diff --git a/builder.go b/builder.go index 55e4066..82d865f 100644 --- a/builder.go +++ b/builder.go @@ -67,6 +67,7 @@ type builder struct { splitBoot bool bootSize uint64 + bootFS BootFS loDevice string bootPart string @@ -83,7 +84,7 @@ type builder struct { cmdLineExtra string } -func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64, osRelease OSRelease, format string, cmdLineExtra string, splitBoot bool, bootSize uint64, luksPassword string, bootLoader string) (Builder, error) { +func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64, osRelease OSRelease, format string, cmdLineExtra string, splitBoot bool, bootFS BootFS, bootSize uint64, luksPassword string, bootLoader string) (Builder, error) { if err := checkDependencies(); err != nil { return nil, err } @@ -123,6 +124,19 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64, return nil, err } + if splitBoot { + config.Kernel = strings.TrimPrefix(config.Kernel, "/boot") + config.Initrd = strings.TrimPrefix(config.Initrd, "/boot") + } + + if bootFS == "" { + bootFS = FSExt4 + } + + if err := bootFS.Validate(); err != nil { + return nil, err + } + blp, err := BootloaderByName(bootLoader) if err != nil { return nil, err @@ -164,6 +178,7 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64, cmdLineExtra: cmdLineExtra, splitBoot: splitBoot, bootSize: bootSize, + bootFS: bootFS, luksPassword: luksPassword, } if err := os.MkdirAll(b.mntPoint, os.ModePerm); err != nil { @@ -303,7 +318,12 @@ func (b *builder) mountImg(ctx context.Context) error { if err := os.MkdirAll(filepath.Join(b.mntPoint, "boot"), os.ModePerm); err != nil { return err } - if err := exec.Run(ctx, "mkfs.ext4", b.bootPart); err != nil { + if b.bootFS.IsFat() { + err = exec.Run(ctx, "mkfs.fat", "-F32", b.bootPart) + } else { + err = exec.Run(ctx, "mkfs.ext4", b.bootPart) + } + if err != nil { return err } if err := exec.Run(ctx, "mount", b.bootPart, filepath.Join(b.mntPoint, "boot")); err != nil { @@ -363,7 +383,7 @@ func (b *builder) setupRootFS(ctx context.Context) (err error) { return err } } - fstab = fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1\nUUID=%s /boot ext4 errors=remount-ro 0 2\n", b.rootUUID, b.bootUUID) + fstab = fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1\nUUID=%s /boot %s errors=remount-ro 0 2\n", b.rootUUID, b.bootUUID, b.bootFS.linux()) } else { b.bootUUID = b.rootUUID fstab = fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1\n", b.bootUUID) @@ -387,13 +407,7 @@ func (b *builder) setupRootFS(ctx context.Context) (err error) { if err := os.RemoveAll(b.chPath("/.dockerenv")); err != nil { return err } - // create a symlink to /boot for non-alpine images in order to have a consistent path - // even if the image is not split - if _, err := os.Stat(b.chPath("/boot/boot")); os.IsNotExist(err) { - if err := os.Symlink(".", b.chPath("/boot/boot")); err != nil { - return err - } - } + switch b.osRelease.ID { case ReleaseAlpine: by, err := os.ReadFile(b.chPath("/etc/inittab")) @@ -408,48 +422,26 @@ func (b *builder) setupRootFS(ctx context.Context) (err error) { return err } return nil - case ReleaseUbuntu: - if b.osRelease.VersionID >= "20.04" { - return nil - } - fallthrough - case ReleaseDebian, ReleaseKali: - t, err := os.Readlink(b.chPath("/vmlinuz")) - if err != nil { - return err - } - if err := os.Symlink(t, b.chPath("/boot/vmlinuz")); err != nil { - return err - } - t, err = os.Readlink(b.chPath("/initrd.img")) - if err != nil { - return err - } - if err := os.Symlink(t, b.chPath("/boot/initrd.img")); err != nil { - return err - } - return nil default: return nil } } func (b *builder) cmdline(_ context.Context) string { - if b.isLuksEnabled() { - switch b.osRelease.ID { - case ReleaseAlpine: - return b.config.Cmdline(RootUUID(b.rootUUID), "root=/dev/mapper/root", "cryptdm=root", "cryptroot=UUID="+b.cryptUUID, b.cmdLineExtra) - case ReleaseCentOS: - return b.config.Cmdline(RootUUID(b.rootUUID), "rd.luks.name=UUID="+b.rootUUID+" rd.luks.uuid="+b.cryptUUID+" rd.luks.crypttab=0", b.cmdLineExtra) - default: - // for some versions of debian, the cryptopts parameter MUST contain all the following: target,source,key,opts... - // see https://salsa.debian.org/cryptsetup-team/cryptsetup/-/blob/debian/buster/debian/functions - // and https://cryptsetup-team.pages.debian.net/cryptsetup/README.initramfs.html - return b.config.Cmdline(nil, "root=/dev/mapper/root", "cryptopts=target=root,source=UUID="+b.cryptUUID+",key=none,luks", b.cmdLineExtra) - } - } else { + if !b.isLuksEnabled() { return b.config.Cmdline(RootUUID(b.rootUUID), b.cmdLineExtra) } + switch b.osRelease.ID { + case ReleaseAlpine: + return b.config.Cmdline(RootUUID(b.rootUUID), "root=/dev/mapper/root", "cryptdm=root", "cryptroot=UUID="+b.cryptUUID, b.cmdLineExtra) + case ReleaseCentOS: + return b.config.Cmdline(RootUUID(b.rootUUID), "rd.luks.name=UUID="+b.rootUUID+" rd.luks.uuid="+b.cryptUUID+" rd.luks.crypttab=0", b.cmdLineExtra) + default: + // for some versions of debian, the cryptopts parameter MUST contain all the following: target,source,key,opts... + // see https://salsa.debian.org/cryptsetup-team/cryptsetup/-/blob/debian/buster/debian/functions + // and https://cryptsetup-team.pages.debian.net/cryptsetup/README.initramfs.html + return b.config.Cmdline(nil, "root=/dev/mapper/root", "cryptopts=target=root,source=UUID="+b.cryptUUID+",key=none,luks", b.cmdLineExtra) + } } func (b *builder) installBootloader(ctx context.Context) error { diff --git a/cmd/d2vm/build.go b/cmd/d2vm/build.go index 40a8ea3..8f21845 100644 --- a/cmd/d2vm/build.go +++ b/cmd/d2vm/build.go @@ -79,9 +79,8 @@ var ( } return docker.RunD2VM(cmd.Context(), d2vm.Image, d2vm.Version, in, out, cmd.Name(), os.Args[2:]...) } - if luksPassword != "" && !splitBoot { - logrus.Warnf("luks password is set: enabling split boot") - splitBoot = true + if err := validateFlags(); err != nil { + return err } size, err := parseSize(size) if err != nil { @@ -90,14 +89,6 @@ var ( if file == "" { file = filepath.Join(args[0], "Dockerfile") } - if push && tag == "" { - return fmt.Errorf("tag is required when pushing container disk image") - } - if _, err := os.Stat(output); err == nil || !os.IsNotExist(err) { - if !force { - return fmt.Errorf("%s already exists", output) - } - } logrus.Infof("building docker image from %s", file) if err := docker.Build(cmd.Context(), tag, file, args[0], buildArgs...); err != nil { return err @@ -114,6 +105,7 @@ var ( d2vm.WithRaw(raw), d2vm.WithSplitBoot(splitBoot), d2vm.WithBootSize(bootSize), + d2vm.WithBootFS(d2vm.BootFS(bootFS)), d2vm.WithLuksPassword(luksPassword), d2vm.WithKeepCache(keepCache), ); err != nil { diff --git a/cmd/d2vm/convert.go b/cmd/d2vm/convert.go index 1aea72a..8a97e3e 100644 --- a/cmd/d2vm/convert.go +++ b/cmd/d2vm/convert.go @@ -15,11 +15,9 @@ package main import ( - "fmt" "os" "path/filepath" "runtime" - "strings" "github.com/c2h5oh/datasize" "github.com/sirupsen/logrus" @@ -51,28 +49,14 @@ var ( } return docker.RunD2VM(cmd.Context(), d2vm.Image, d2vm.Version, out, out, cmd.Name(), dargs...) } - if luksPassword != "" && !splitBoot { - logrus.Warnf("luks password is set: enabling split boot") - splitBoot = true + if err := validateFlags(); err != nil { + return err } - img := args[0] - tag := "latest" - if parts := strings.Split(img, ":"); len(parts) > 1 { - img, tag = parts[0], parts[1] - } - img = fmt.Sprintf("%s:%s", img, tag) size, err := parseSize(size) if err != nil { return err } - if push && tag == "" { - return fmt.Errorf("tag is required when pushing container disk image") - } - if _, err := os.Stat(output); err == nil || !os.IsNotExist(err) { - if !force { - return fmt.Errorf("%s already exists", output) - } - } + img := args[0] found := false if !pull { imgs, err := docker.ImageList(cmd.Context(), img) @@ -102,6 +86,7 @@ var ( d2vm.WithRaw(raw), d2vm.WithSplitBoot(splitBoot), d2vm.WithBootSize(bootSize), + d2vm.WithBootFS(d2vm.BootFS(bootFS)), d2vm.WithLuksPassword(luksPassword), d2vm.WithKeepCache(keepCache), ); err != nil { diff --git a/cmd/d2vm/flags.go b/cmd/d2vm/flags.go index ce94e72..8adcd6d 100644 --- a/cmd/d2vm/flags.go +++ b/cmd/d2vm/flags.go @@ -15,8 +15,11 @@ package main import ( + "fmt" + "os" "strings" + "github.com/sirupsen/logrus" "github.com/spf13/pflag" "go.linka.cloud/d2vm" @@ -36,11 +39,35 @@ var ( bootloader string splitBoot bool bootSize uint64 + bootFS string luksPassword string keepCache bool ) +func validateFlags() error { + if luksPassword != "" && !splitBoot { + logrus.Warnf("luks password is set: enabling split boot") + splitBoot = true + } + if bootFS := d2vm.BootFS(bootFS); bootFS != "" && !bootFS.IsSupported() { + return fmt.Errorf("invalid boot filesystem: %s", bootFS) + } + if bootFS != "" && !splitBoot { + logrus.Warnf("boot filesystem is set: enabling split boot") + splitBoot = true + } + if push && tag == "" { + return fmt.Errorf("tag is required when pushing container disk image") + } + if _, err := os.Stat(output); err == nil || !os.IsNotExist(err) { + if !force { + return fmt.Errorf("%s already exists", output) + } + } + return nil +} + func buildFlags() *pflag.FlagSet { flags := pflag.NewFlagSet("build", pflag.ExitOnError) flags.StringVarP(&output, "output", "o", output, "The output image, the extension determine the image format, raw will be used if none. Supported formats: "+strings.Join(d2vm.OutputFormats(), " ")) @@ -54,6 +81,7 @@ func buildFlags() *pflag.FlagSet { flags.BoolVar(&push, "push", false, "Push the container disk image to the registry") flags.BoolVar(&splitBoot, "split-boot", false, "Split the boot partition from the root partition") flags.Uint64Var(&bootSize, "boot-size", 100, "Size of the boot partition in MB") + flags.StringVar(&bootFS, "boot-fs", "", "Filesystem to use for the boot partition, ext4 or fat32") flags.StringVar(&bootloader, "bootloader", "syslinux", "Bootloader to use: syslinux, grub") flags.StringVar(&luksPassword, "luks-password", "", "Password to use for the LUKS encrypted root partition. If not set, the root partition will not be encrypted") flags.BoolVar(&keepCache, "keep-cache", false, "Keep the images after the build") diff --git a/config.go b/config.go index 88aad53..86b4596 100644 --- a/config.go +++ b/config.go @@ -25,8 +25,8 @@ var ( Initrd: "/boot/initrd.img", } configDebian = Config{ - Kernel: "/vmlinuz", - Initrd: "/initrd.img", + Kernel: "/boot/vmlinuz", + Initrd: "/boot/initrd.img", } configAlpine = Config{ Kernel: "/boot/vmlinuz-virt", diff --git a/convert.go b/convert.go index cf9ff35..0651fe8 100644 --- a/convert.go +++ b/convert.go @@ -88,7 +88,7 @@ func Convert(ctx context.Context, img string, opts ...ConvertOption) error { if format == "" { format = "raw" } - b, err := NewBuilder(ctx, tmpPath, imgUUID, "", o.size, r, format, o.cmdLineExtra, o.splitBoot, o.bootSize, o.luksPassword, o.bootLoader) + b, err := NewBuilder(ctx, tmpPath, imgUUID, "", o.size, r, format, o.cmdLineExtra, o.splitBoot, o.bootFS, o.bootSize, o.luksPassword, o.bootLoader) if err != nil { return err } diff --git a/convert_options.go b/convert_options.go index 292251b..1682590 100644 --- a/convert_options.go +++ b/convert_options.go @@ -27,6 +27,7 @@ type convertOptions struct { splitBoot bool bootSize uint64 + bootFS BootFS luksPassword string @@ -87,6 +88,12 @@ func WithBootSize(bootSize uint64) ConvertOption { } } +func WithBootFS(bootFS BootFS) ConvertOption { + return func(o *convertOptions) { + o.bootFS = bootFS + } +} + func WithLuksPassword(password string) ConvertOption { return func(o *convertOptions) { o.luksPassword = password diff --git a/fs.go b/fs.go new file mode 100644 index 0000000..33cc58f --- /dev/null +++ b/fs.go @@ -0,0 +1,58 @@ +// Copyright 2023 Linka Cloud All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package d2vm + +import ( + "fmt" +) + +type BootFS string + +const ( + FSExt4 BootFS = "ext4" + FSFat32 BootFS = "fat32" +) + +func (f BootFS) String() string { + return string(f) +} + +func (f BootFS) IsExt() bool { + return f == FSExt4 +} + +func (f BootFS) IsFat() bool { + return f == FSFat32 +} + +func (f BootFS) IsSupported() bool { + return f.IsExt() || f.IsFat() +} + +func (f BootFS) Validate() error { + if !f.IsSupported() { + fmt.Errorf("invalid boot filesystem: %s valid filesystems are: fat32, ext4", f) + } + return nil +} + +func (f BootFS) linux() string { + switch f { + case FSFat32: + return "vfat" + default: + return "ext4" + } +} diff --git a/templates/alpine.Dockerfile b/templates/alpine.Dockerfile index 843c47d..4a8757d 100644 --- a/templates/alpine.Dockerfile +++ b/templates/alpine.Dockerfile @@ -14,7 +14,8 @@ RUN apk update --no-cache && \ {{- else }} busybox-initscripts \ {{- end }} - openrc + openrc && \ + find /boot -type l -exec rm {} \; RUN for s in bootmisc hostname hwclock modules networking swap sysctl urandom syslog; do rc-update add $s boot; done RUN for s in devfs dmesg hwdrivers mdev; do rc-update add $s sysinit; done diff --git a/templates/centos.Dockerfile b/templates/centos.Dockerfile index bf23f99..d06ca51 100644 --- a/templates/centos.Dockerfile +++ b/templates/centos.Dockerfile @@ -18,10 +18,13 @@ RUN yum install -y \ sudo && \ systemctl enable NetworkManager && \ systemctl unmask systemd-remount-fs.service && \ - systemctl unmask getty.target && \ - cd /boot && \ - ln -s $(find . -name 'vmlinuz-*') vmlinuz && \ - ln -s $(find . -name 'initramfs-*.img') initrd.img + systemctl unmask getty.target + +{{- if not .Grub }} +RUN cd /boot && \ + mv $(find . -name 'vmlinuz-*') /boot/vmlinuz && \ + mv $(find . -name 'initramfs-*.img') /boot/initrd.img +{{- end }} {{ if .Luks }} RUN yum install -y cryptsetup && \ diff --git a/templates/debian.Dockerfile b/templates/debian.Dockerfile index a6ea88c..67df837 100644 --- a/templates/debian.Dockerfile +++ b/templates/debian.Dockerfile @@ -12,7 +12,13 @@ RUN echo "deb http://archive.debian.org/debian stretch main" > /etc/apt/sources. RUN apt-get -y update && \ DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \ - linux-image-amd64 + linux-image-amd64 && \ + find /boot -type l -exec rm {} \; + +{{- if not .Grub }} +RUN mv $(find /boot -name 'vmlinuz-*') /boot/vmlinuz && \ + mv $(find /boot -name 'initrd.img-*') /boot/initrd.img +{{- end }} RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ systemd-sysv \ diff --git a/templates/ubuntu.Dockerfile b/templates/ubuntu.Dockerfile index b826d06..b2b7082 100644 --- a/templates/ubuntu.Dockerfile +++ b/templates/ubuntu.Dockerfile @@ -14,7 +14,13 @@ RUN apt-get update -y && \ dbus \ isc-dhcp-client \ iproute2 \ - iputils-ping + iputils-ping && \ + find /boot -type l -exec rm {} \; + +{{- if not .Grub }} +RUN mv $(find /boot -name 'vmlinuz-*') /boot/vmlinuz && \ + mv $(find /boot -name 'initrd.img-*') /boot/initrd.img +{{- end }} RUN systemctl preset-all