From d652bf41f5c387ed9ff594a708654505bd8cdcf0 Mon Sep 17 00:00:00 2001 From: Adphi Date: Wed, 23 Nov 2022 19:21:58 +0100 Subject: [PATCH] run: fix qemu-img convert path typo build & convert: add kubevirt container disk support Signed-off-by: Adphi --- cmd/d2vm/build.go | 29 +++++----- cmd/d2vm/container_disk.go | 43 ++++++++++++++ cmd/d2vm/convert.go | 24 +++----- cmd/d2vm/flags.go | 50 ++++++++++++++++ cmd/d2vm/main.go | 6 +- cmd/d2vm/run/hetzner.go | 8 ++- cmd/d2vm/run/util.go | 91 ----------------------------- cmd/d2vm/run/vbox.go | 6 +- container_disk.go | 67 ++++++++++++++++++++++ pkg/docker/docker.go | 4 ++ pkg/qemu_img/qemu_img.go | 114 +++++++++++++++++++++++++++++++++++++ version.go | 9 +++ 12 files changed, 319 insertions(+), 132 deletions(-) create mode 100644 cmd/d2vm/container_disk.go create mode 100644 cmd/d2vm/flags.go create mode 100644 container_disk.go create mode 100644 pkg/qemu_img/qemu_img.go diff --git a/cmd/d2vm/build.go b/cmd/d2vm/build.go index 7815aef..0606e60 100644 --- a/cmd/d2vm/build.go +++ b/cmd/d2vm/build.go @@ -30,11 +30,10 @@ import ( ) var ( - file = "Dockerfile" - tag = "d2vm-" + uuid.New().String() - networkManager string - buildArgs []string - buildCmd = &cobra.Command{ + file = "Dockerfile" + tag = "d2vm-" + uuid.New().String() + buildArgs []string + buildCmd = &cobra.Command{ Use: "build [context directory]", Short: "Build a vm image from Dockerfile", Args: cobra.ExactArgs(1), @@ -87,6 +86,9 @@ 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) @@ -108,11 +110,12 @@ var ( ); err != nil { return err } - uid, ok := sudoUser() - if !ok { - return nil + if uid, ok := sudoUser(); ok { + if err := os.Chown(output, uid, uid); err != nil { + return err + } } - return os.Chown(output, uid, uid) + return maybeMakeContainerDisk(cmd.Context()) }, } ) @@ -123,11 +126,5 @@ func init() { buildCmd.Flags().StringVarP(&file, "file", "f", "", "Name of the Dockerfile") buildCmd.Flags().StringArrayVar(&buildArgs, "build-arg", nil, "Set build-time variables") - buildCmd.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(), " ")) - buildCmd.Flags().StringVarP(&password, "password", "p", "", "Optional root user password") - buildCmd.Flags().StringVarP(&size, "size", "s", "10G", "The output image size") - buildCmd.Flags().BoolVar(&force, "force", false, "Override output image") - buildCmd.Flags().StringVar(&cmdLineExtra, "append-to-cmdline", "", "Extra kernel cmdline arguments to append to the generated one") - buildCmd.Flags().StringVar(&networkManager, "network-manager", "", "Network manager to use for the image: none, netplan, ifupdown") - buildCmd.Flags().BoolVar(&raw, "raw", false, "Just convert the container to virtual machine image without installing anything more") + buildCmd.Flags().AddFlagSet(buildFlags()) } diff --git a/cmd/d2vm/container_disk.go b/cmd/d2vm/container_disk.go new file mode 100644 index 0000000..2c607b7 --- /dev/null +++ b/cmd/d2vm/container_disk.go @@ -0,0 +1,43 @@ +// Copyright 2022 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 main + +import ( + "context" + "fmt" + + "github.com/sirupsen/logrus" + + "go.linka.cloud/d2vm" + "go.linka.cloud/d2vm/pkg/docker" +) + +func maybeMakeContainerDisk(ctx context.Context) error { + if containerDiskTag == "" { + return nil + } + logrus.Infof("creating container disk image %s", containerDiskTag) + if err := d2vm.MakeContainerDisk(ctx, output, containerDiskTag); err != nil { + return err + } + if !push { + return nil + } + logrus.Infof("pushing container disk image %s", containerDiskTag) + if err := docker.Push(ctx, containerDiskTag); err != nil { + return fmt.Errorf("failed to push container disk: %w", err) + } + return nil +} diff --git a/cmd/d2vm/convert.go b/cmd/d2vm/convert.go index b6d4bee..a335a3f 100644 --- a/cmd/d2vm/convert.go +++ b/cmd/d2vm/convert.go @@ -30,10 +30,6 @@ import ( ) var ( - raw bool - pull = false - cmdLineExtra = "" - convertCmd = &cobra.Command{ Use: "convert [docker image]", Short: "Convert Docker image to vm image", @@ -65,6 +61,9 @@ var ( 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) @@ -100,11 +99,12 @@ var ( return err } // set user permissions on the output file if the command was run with sudo - uid, ok := sudoUser() - if !ok { - return nil + if uid, ok := sudoUser(); ok { + if err := os.Chown(output, uid, uid); err != nil { + return err + } } - return os.Chown(output, uid, uid) + return maybeMakeContainerDisk(cmd.Context()) }, } ) @@ -119,12 +119,6 @@ func parseSize(s string) (int64, error) { func init() { convertCmd.Flags().BoolVar(&pull, "pull", false, "Always pull docker image") - convertCmd.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(), " ")) - convertCmd.Flags().StringVarP(&password, "password", "p", "", "Optional root user password") - convertCmd.Flags().StringVarP(&size, "size", "s", "10G", "The output image size") - convertCmd.Flags().BoolVarP(&force, "force", "f", false, "Override output qcow2 image") - convertCmd.Flags().StringVar(&cmdLineExtra, "append-to-cmdline", "", "Extra kernel cmdline arguments to append to the generated one") - convertCmd.Flags().StringVar(&networkManager, "network-manager", "", "Network manager to use for the image: none, netplan, ifupdown") - convertCmd.Flags().BoolVar(&raw, "raw", false, "Just convert the container to virtual machine image without installing anything more") + convertCmd.Flags().AddFlagSet(buildFlags()) rootCmd.AddCommand(convertCmd) } diff --git a/cmd/d2vm/flags.go b/cmd/d2vm/flags.go new file mode 100644 index 0000000..46047bf --- /dev/null +++ b/cmd/d2vm/flags.go @@ -0,0 +1,50 @@ +// Copyright 2022 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 main + +import ( + "strings" + + "github.com/spf13/pflag" + + "go.linka.cloud/d2vm" +) + +var ( + output = "disk0.qcow2" + size = "1G" + password = "" + force = false + raw bool + pull = false + cmdLineExtra = "" + containerDiskTag = "" + push bool + networkManager string +) + +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(), " ")) + flags.StringVarP(&password, "password", "p", "", "Optional root user password") + flags.StringVarP(&size, "size", "s", "10G", "The output image size") + flags.BoolVar(&force, "force", false, "Override output qcow2 image") + flags.StringVar(&cmdLineExtra, "append-to-cmdline", "", "Extra kernel cmdline arguments to append to the generated one") + flags.StringVar(&networkManager, "network-manager", "", "Network manager to use for the image: none, netplan, ifupdown") + flags.BoolVar(&raw, "raw", false, "Just convert the container to virtual machine image without installing anything more") + flags.StringVarP(&containerDiskTag, "tag", "t", "", "Container disk Docker image tag") + flags.BoolVar(&push, "push", false, "Push the container disk image to the registry") + return flags +} diff --git a/cmd/d2vm/main.go b/cmd/d2vm/main.go index e73fab6..2ede3d9 100644 --- a/cmd/d2vm/main.go +++ b/cmd/d2vm/main.go @@ -34,10 +34,6 @@ import ( ) var ( - output = "disk0.qcow2" - size = "1G" - password = "" - force = false verbose = false timeFormat = "" format = "qcow2" @@ -86,7 +82,7 @@ func init() { rootCmd.PersistentFlags().BoolVarP(&verbose, "debug", "d", false, "Enable Debug output") rootCmd.PersistentFlags().MarkDeprecated("debug", "use -v instead") rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable Verbose output") - rootCmd.PersistentFlags().StringVarP(&timeFormat, "time", "t", "none", "Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)'") + rootCmd.PersistentFlags().StringVar(&timeFormat, "time", "none", "Enable formated timed output, valide formats: 'relative (rel | r)', 'full (f)'") color.NoColor = false logrus.StandardLogger().Formatter = &logfmtFormatter{start: time.Now()} } diff --git a/cmd/d2vm/run/hetzner.go b/cmd/d2vm/run/hetzner.go index 86b7869..29e0964 100644 --- a/cmd/d2vm/run/hetzner.go +++ b/cmd/d2vm/run/hetzner.go @@ -33,6 +33,8 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/svenwiltink/sparsecat" + + "go.linka.cloud/d2vm/pkg/qemu_img" ) const ( @@ -77,7 +79,7 @@ func Hetzner(cmd *cobra.Command, args []string) { } func runHetzner(ctx context.Context, imgPath string, stdin io.Reader, stderr io.Writer, stdout io.Writer) error { - i, err := QemuImgInfo(ctx, imgPath) + i, err := qemu_img.Info(ctx, imgPath) if err != nil { return err } @@ -89,11 +91,11 @@ func runHetzner(ctx context.Context, imgPath string, stdin io.Reader, stderr io. } defer os.RemoveAll(rawPath) logrus.Infof("converting image to raw: %s", rawPath) - if err := QemuImgConvert(ctx, "raw", imgPath, rawPath); err != nil { + if err := qemu_img.Convert(ctx, "raw", imgPath, rawPath); err != nil { return err } imgPath = rawPath - i, err = QemuImgInfo(ctx, imgPath) + i, err = qemu_img.Info(ctx, imgPath) if err != nil { return err } diff --git a/cmd/d2vm/run/util.go b/cmd/d2vm/run/util.go index a88e5de..9d84e26 100644 --- a/cmd/d2vm/run/util.go +++ b/cmd/d2vm/run/util.go @@ -18,24 +18,16 @@ package run import ( "bufio" - "context" _ "embed" - "encoding/json" "fmt" "io" "os" - "os/exec" - "path/filepath" "strconv" "strings" "sync" "time" "golang.org/x/crypto/ssh" - - "go.linka.cloud/d2vm" - "go.linka.cloud/d2vm/pkg/docker" - exec2 "go.linka.cloud/d2vm/pkg/exec" ) //go:embed sparsecat-linux-amd64 @@ -345,86 +337,3 @@ func (p *pw) Progress() int { defer p.mu.RUnlock() return p.total } - -type QemuInfo struct { - VirtualSize int `json:"virtual-size"` - Filename string `json:"filename"` - Format string `json:"format"` - ActualSize int `json:"actual-size"` - DirtyFlag bool `json:"dirty-flag"` -} - -func QemuImgInfo(ctx context.Context, in string) (*QemuInfo, error) { - var ( - o []byte - err error - ) - if path, _ := exec.LookPath("qemu-img"); path == "" { - inAbs, err := filepath.Abs(in) - if err != nil { - return nil, fmt.Errorf("failed to get absolute path for %q: %v", path, err) - } - inMount := filepath.Dir(inAbs) - in := filepath.Join("/in", filepath.Base(inAbs)) - o, err = exec2.CommandContext( - ctx, - "docker", - "run", - "--rm", - "-v", - inMount+":/in", - "--entrypoint", - "qemu-img", - fmt.Sprintf("%s:%s", d2vm.Image, d2vm.Version), - "info", - in, - "--output", - "json", - ).CombinedOutput() - } else { - o, err = exec2.CommandContext(ctx, "qemu-img", "info", path, "--output", "json").CombinedOutput() - } - if err != nil { - return nil, fmt.Errorf("%v: %s", err, string(o)) - } - var i QemuInfo - if err := json.Unmarshal(o, &i); err != nil { - return nil, err - } - return &i, nil -} - -func QemuImgConvert(ctx context.Context, format, in, out string) error { - if path, _ := exec.LookPath("qemu-img"); path != "" { - return exec2.Run(ctx, "qemu-img", "convert", "-O", format, in, out) - } - inAbs, err := filepath.Abs(in) - if err != nil { - return fmt.Errorf("failed to get absolute path for %q: %v", in, err) - } - inMount := filepath.Dir(inAbs) - in = filepath.Join("/in", filepath.Base(inAbs)) - - outAbs, err := filepath.Abs(out) - if err != nil { - return fmt.Errorf("failed to get absolute path for %q: %v", out, err) - } - outMount := filepath.Dir(outAbs) - out = filepath.Join("/out", filepath.Base(outAbs)) - - return docker.RunAndRemove( - ctx, - "-v", - fmt.Sprintf("%s:/in", inMount), - "-v", - fmt.Sprintf("%s:/out", outMount), - "--entrypoint", - "qemu-img", - fmt.Sprintf("%s:%s", d2vm.Image, d2vm.Version), - "convert", - "-O", - format, - in, - out, - ) -} diff --git a/cmd/d2vm/run/vbox.go b/cmd/d2vm/run/vbox.go index 4ada052..775631d 100644 --- a/cmd/d2vm/run/vbox.go +++ b/cmd/d2vm/run/vbox.go @@ -18,6 +18,8 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "go.linka.cloud/console" + + "go.linka.cloud/d2vm/pkg/qemu_img" ) var ( @@ -72,7 +74,7 @@ func vbox(ctx context.Context, path string) error { if err != nil { return fmt.Errorf("Cannot find management binary %s: %v", vboxmanageFlag, err) } - i, err := QemuImgInfo(ctx, path) + i, err := qemu_img.Info(ctx, path) if err != nil { return fmt.Errorf("failed to get image info: %v", err) } @@ -84,7 +86,7 @@ func vbox(ctx context.Context, path string) error { } defer os.RemoveAll(vdi) logrus.Infof("converting image to raw: %s", vdi) - if err := QemuImgConvert(ctx, "vdi", path, vdi); err != nil { + if err := qemu_img.Convert(ctx, "vdi", path, vdi); err != nil { return err } path = vdi diff --git a/container_disk.go b/container_disk.go new file mode 100644 index 0000000..ea78dae --- /dev/null +++ b/container_disk.go @@ -0,0 +1,67 @@ +// Copyright 2022 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 ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/google/uuid" + "github.com/sirupsen/logrus" + + "go.linka.cloud/d2vm/pkg/docker" + "go.linka.cloud/d2vm/pkg/qemu_img" +) + +const ( + // https://kubevirt.io/user-guide/virtual_machines/disks_and_volumes/#containerdisk-workflow-example + uid = 107 + containerDiskDockerfile = `FROM scratch + +ADD --chown=%[1]d:%[1]d %[2]s /disk/ +` +) + +func MakeContainerDisk(ctx context.Context, path string, tag string) error { + tmpPath := filepath.Join(os.TempDir(), "d2vm", uuid.New().String()) + if err := os.MkdirAll(tmpPath, os.ModePerm); err != nil { + return err + } + defer func() { + if err := os.RemoveAll(tmpPath); err != nil { + logrus.Errorf("failed to remove tmp dir %s: %v", tmpPath, err) + } + }() + if _, err := os.Stat(path); err != nil { + return err + } + // convert may not be needed, but this will also copy the file in the tmp dir + qcow2 := filepath.Join(tmpPath, "disk.qcow2") + if err := qemu_img.Convert(ctx, "qcow2", path, qcow2); err != nil { + return err + } + disk := filepath.Base(qcow2) + dockerfileContent := fmt.Sprintf(containerDiskDockerfile, uid, disk) + dockerfile := filepath.Join(tmpPath, "Dockerfile") + if err := os.WriteFile(dockerfile, []byte(dockerfileContent), os.ModePerm); err != nil { + return fmt.Errorf("failed to write dockerfile: %w", err) + } + if err := docker.Build(ctx, tag, dockerfile, tmpPath); err != nil { + return fmt.Errorf("failed to build container disk: %w", err) + } + return nil +} diff --git a/pkg/docker/docker.go b/pkg/docker/docker.go index 0ba29a6..a27809d 100644 --- a/pkg/docker/docker.go +++ b/pkg/docker/docker.go @@ -96,6 +96,10 @@ func Pull(ctx context.Context, tag string) error { return Cmd(ctx, "image", "pull", tag) } +func Push(ctx context.Context, tag string) error { + return Cmd(ctx, "image", "push", tag) +} + func RunInteractiveAndRemove(ctx context.Context, args ...string) error { cmd := exec.CommandContext(ctx, "docker", append([]string{"run", "--rm", "-it"}, args...)...) cmd.Stdin = os.Stdin diff --git a/pkg/qemu_img/qemu_img.go b/pkg/qemu_img/qemu_img.go new file mode 100644 index 0000000..c364818 --- /dev/null +++ b/pkg/qemu_img/qemu_img.go @@ -0,0 +1,114 @@ +// Copyright 2022 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 qemu_img + +import ( + "context" + "encoding/json" + "fmt" + "os/exec" + "path/filepath" + + "go.linka.cloud/d2vm/pkg/docker" + exec2 "go.linka.cloud/d2vm/pkg/exec" +) + +var ( + DockerImageName string + DockerImageVersion string +) + +type ImgInfo struct { + VirtualSize int `json:"virtual-size"` + Filename string `json:"filename"` + Format string `json:"format"` + ActualSize int `json:"actual-size"` + DirtyFlag bool `json:"dirty-flag"` +} + +func Info(ctx context.Context, in string) (*ImgInfo, error) { + var ( + o []byte + err error + ) + if path, _ := exec.LookPath("qemu-img"); path == "" { + inAbs, err := filepath.Abs(in) + if err != nil { + return nil, fmt.Errorf("failed to get absolute path for %q: %v", path, err) + } + inMount := filepath.Dir(inAbs) + in := filepath.Join("/in", filepath.Base(inAbs)) + o, err = exec2.CommandContext( + ctx, + "docker", + "run", + "--rm", + "-v", + inMount+":/in", + "--entrypoint", + "qemu-img", + fmt.Sprintf("%s:%s", DockerImageName, DockerImageVersion), + "info", + in, + "--output", + "json", + ).CombinedOutput() + } else { + o, err = exec2.CommandContext(ctx, "qemu-img", "info", in, "--output", "json").CombinedOutput() + } + if err != nil { + return nil, fmt.Errorf("%v: %s", err, string(o)) + } + var i ImgInfo + if err := json.Unmarshal(o, &i); err != nil { + return nil, err + } + return &i, nil +} + +func Convert(ctx context.Context, format, in, out string) error { + if path, _ := exec.LookPath("qemu-img"); path != "" { + return exec2.Run(ctx, "qemu-img", "convert", "-O", format, in, out) + } + inAbs, err := filepath.Abs(in) + if err != nil { + return fmt.Errorf("failed to get absolute path for %q: %v", in, err) + } + inMount := filepath.Dir(inAbs) + in = filepath.Join("/in", filepath.Base(inAbs)) + + outAbs, err := filepath.Abs(out) + if err != nil { + return fmt.Errorf("failed to get absolute path for %q: %v", out, err) + } + outMount := filepath.Dir(outAbs) + out = filepath.Join("/out", filepath.Base(outAbs)) + + return docker.RunAndRemove( + ctx, + "-v", + fmt.Sprintf("%s:/in", inMount), + "-v", + fmt.Sprintf("%s:/out", outMount), + "--entrypoint", + "qemu-img", + fmt.Sprintf("%s:%s", DockerImageName, DockerImageVersion), + "convert", + "-O", + format, + in, + out, + ) +} diff --git a/version.go b/version.go index 3ac2da5..d3afbb5 100644 --- a/version.go +++ b/version.go @@ -14,8 +14,17 @@ package d2vm +import ( + "go.linka.cloud/d2vm/pkg/qemu_img" +) + var ( Version = "" BuildDate = "" Image = "linkacloud/d2vm" ) + +func init() { + qemu_img.DockerImageName = Image + qemu_img.DockerImageVersion = Version +}