diff --git a/cmd/d2vm/build.go b/cmd/d2vm/build.go index 68d3c4f..bcd2019 100644 --- a/cmd/d2vm/build.go +++ b/cmd/d2vm/build.go @@ -15,6 +15,7 @@ package main import ( + "fmt" "os" "path/filepath" "runtime" @@ -40,7 +41,41 @@ var ( RunE: func(cmd *cobra.Command, args []string) error { // TODO(adphi): resolve context path if runtime.GOOS != "linux" { - return docker.RunD2VM(cmd.Context(), d2vm.Image, d2vm.Version, cmd.Name(), os.Args[2:]...) + ctxAbsPath, err := filepath.Abs(args[0]) + if err != nil { + return err + } + dockerFileAbsPath, err := filepath.Abs(file) + if err != nil { + return err + } + if !strings.HasPrefix(dockerFileAbsPath, ctxAbsPath) { + return fmt.Errorf("Dockerfile must be in the context directory path") + } + outputPath, err := filepath.Abs(output) + if err != nil { + return err + } + var ( + in = ctxAbsPath + out = filepath.Dir(outputPath) + ) + dargs := os.Args[2:] + for i, v := range dargs { + switch v { + case file: + rel, err := filepath.Rel(in, file) + if err != nil { + return fmt.Errorf("failed to construct Dockerfile container paths: %w", err) + } + dargs[i] = filepath.Join("/in", rel) + case output: + dargs[i] = filepath.Join("/out", output) + case args[0]: + dargs[i] = "/in" + } + } + return docker.RunD2VM(cmd.Context(), d2vm.Image, d2vm.Version, in, out, cmd.Name(), os.Args[2:]...) } size, err := parseSize(size) if err != nil { @@ -49,11 +84,25 @@ var ( if file == "" { file = filepath.Join(args[0], "Dockerfile") } + 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 } - return d2vm.Convert(cmd.Context(), tag, size, password, output, cmdLineExtra, d2vm.NetworkManager(networkManager)) + return d2vm.Convert( + cmd.Context(), + tag, + d2vm.WithSize(size), + d2vm.WithPassword(password), + d2vm.WithOutput(output), + d2vm.WithCmdLineExtra(cmdLineExtra), + d2vm.WithNetworkManager(d2vm.NetworkManager(networkManager)), + d2vm.WithRaw(raw), + ) }, } ) @@ -70,4 +119,5 @@ func init() { 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") } diff --git a/cmd/d2vm/convert.go b/cmd/d2vm/convert.go index e845136..5c6bb49 100644 --- a/cmd/d2vm/convert.go +++ b/cmd/d2vm/convert.go @@ -17,6 +17,7 @@ package main import ( "fmt" "os" + "path/filepath" "runtime" "strings" @@ -29,6 +30,7 @@ import ( ) var ( + raw bool pull = false cmdLineExtra = "" @@ -39,7 +41,19 @@ var ( SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { if runtime.GOOS != "linux" { - return docker.RunD2VM(cmd.Context(), d2vm.Image, d2vm.Version, cmd.Name(), os.Args[2:]...) + abs, err := filepath.Abs(output) + if err != nil { + return err + } + out := filepath.Dir(abs) + dargs := os.Args[2:] + for i, v := range dargs { + if v == output { + dargs[i] = filepath.Join("/out", filepath.Base(output)) + break + } + } + return docker.RunD2VM(cmd.Context(), d2vm.Image, d2vm.Version, out, out, cmd.Name(), dargs...) } img := args[0] tag := "latest" @@ -55,11 +69,6 @@ var ( return fmt.Errorf("%s already exists", output) } } - if _, err := os.Stat(output); err == nil || !os.IsNotExist(err) { - if !force { - return fmt.Errorf("%s already exists", output) - } - } found := false if !pull { imgs, err := docker.ImageList(cmd.Context(), img) @@ -77,7 +86,16 @@ var ( return err } } - return d2vm.Convert(cmd.Context(), img, size, password, output, cmdLineExtra, d2vm.NetworkManager(networkManager)) + return d2vm.Convert( + cmd.Context(), + img, + d2vm.WithSize(size), + d2vm.WithPassword(password), + d2vm.WithOutput(output), + d2vm.WithCmdLineExtra(cmdLineExtra), + d2vm.WithNetworkManager(d2vm.NetworkManager(networkManager)), + d2vm.WithRaw(raw), + ) }, } ) @@ -98,5 +116,6 @@ func init() { 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") rootCmd.AddCommand(convertCmd) } diff --git a/cmd/d2vm/run/sparsecat-linux-amd64 b/cmd/d2vm/run/sparsecat-linux-amd64 index bdf2fee..90fdeff 100755 Binary files a/cmd/d2vm/run/sparsecat-linux-amd64 and b/cmd/d2vm/run/sparsecat-linux-amd64 differ diff --git a/convert.go b/convert.go index b19c578..a5b881a 100644 --- a/convert.go +++ b/convert.go @@ -28,7 +28,11 @@ import ( "go.linka.cloud/d2vm/pkg/docker" ) -func Convert(ctx context.Context, img string, size int64, password string, output string, cmdLineExtra string, networkManager NetworkManager) error { +func Convert(ctx context.Context, img string, opts ...ConvertOption) error { + o := &convertOptions{} + for _, opt := range opts { + opt(o) + } imgUUID := uuid.New().String() tmpPath := filepath.Join(os.TempDir(), "d2vm", imgUUID) if err := os.MkdirAll(tmpPath, os.ModePerm); err != nil { @@ -41,33 +45,41 @@ func Convert(ctx context.Context, img string, size int64, password string, outpu if err != nil { return err } - d, err := NewDockerfile(r, img, password, networkManager) - if err != nil { - return err + if !o.raw { + d, err := NewDockerfile(r, img, o.password, o.networkManager) + if err != nil { + return err + } + logrus.Infof("docker image based on %s", d.Release.Name) + p := filepath.Join(tmpPath, docker.FormatImgName(img)) + dir := filepath.Dir(p) + f, err := os.Create(p) + if err != nil { + return err + } + defer f.Close() + if err := d.Render(f); err != nil { + return err + } + logrus.Infof("building kernel enabled image") + if err := docker.Build(ctx, imgUUID, p, dir); err != nil { + return err + } + defer docker.Remove(ctx, imgUUID) + } else { + // for raw images, we just tag the image with the uuid + if err := docker.Tag(ctx, img, imgUUID); err != nil { + return err + } + defer docker.Remove(ctx, imgUUID) } - logrus.Infof("docker image based on %s", d.Release.Name) - p := filepath.Join(tmpPath, docker.FormatImgName(img)) - dir := filepath.Dir(p) - f, err := os.Create(p) - if err != nil { - return err - } - defer f.Close() - if err := d.Render(f); err != nil { - return err - } - logrus.Infof("building kernel enabled image") - if err := docker.Build(ctx, imgUUID, p, dir); err != nil { - return err - } - defer docker.Remove(ctx, imgUUID) logrus.Infof("creating vm image") - format := strings.TrimPrefix(filepath.Ext(output), ".") + format := strings.TrimPrefix(filepath.Ext(o.output), ".") if format == "" { format = "raw" } - b, err := NewBuilder(ctx, tmpPath, imgUUID, "", size, r, format, cmdLineExtra) + b, err := NewBuilder(ctx, tmpPath, imgUUID, "", o.size, r, format, o.cmdLineExtra) if err != nil { return err } @@ -75,10 +87,10 @@ func Convert(ctx context.Context, img string, size int64, password string, outpu if err := b.Build(ctx); err != nil { return err } - if err := os.RemoveAll(output); err != nil { + if err := os.RemoveAll(o.output); err != nil { return err } - if err := MoveFile(filepath.Join(tmpPath, "disk0."+format), output); err != nil { + if err := MoveFile(filepath.Join(tmpPath, "disk0."+format), o.output); err != nil { return err } return nil diff --git a/convert_options.go b/convert_options.go new file mode 100644 index 0000000..bdb0cd2 --- /dev/null +++ b/convert_options.go @@ -0,0 +1,62 @@ +// 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 + +type ConvertOption func(o *convertOptions) + +type convertOptions struct { + size int64 + password string + output string + cmdLineExtra string + networkManager NetworkManager + raw bool +} + +func WithSize(size int64) ConvertOption { + return func(o *convertOptions) { + o.size = size + } +} + +func WithPassword(password string) ConvertOption { + return func(o *convertOptions) { + o.password = password + } +} + +func WithOutput(output string) ConvertOption { + return func(o *convertOptions) { + o.output = output + } +} + +func WithCmdLineExtra(cmdLineExtra string) ConvertOption { + return func(o *convertOptions) { + o.cmdLineExtra = cmdLineExtra + } +} + +func WithNetworkManager(networkManager NetworkManager) ConvertOption { + return func(o *convertOptions) { + o.networkManager = networkManager + } +} + +func WithRaw(raw bool) ConvertOption { + return func(o *convertOptions) { + o.raw = raw + } +} diff --git a/pkg/docker/docker.go b/pkg/docker/docker.go index 7b6e3fe..3f6aa66 100644 --- a/pkg/docker/docker.go +++ b/pkg/docker/docker.go @@ -62,6 +62,19 @@ func Build(ctx context.Context, tag, dockerfile, dir string, buildArgs ...string return Cmd(ctx, args...) } +func Tag(ctx context.Context, img string, tags ...string) error { + if len(tags) == 0 { + return fmt.Errorf("no tags specified") + } + args := []string{"image", "tag"} + for _, tag := range tags { + if err := Cmd(ctx, append(args, img, tag)...); err != nil { + return err + } + } + return nil +} + func Remove(ctx context.Context, tag string) error { return Cmd(ctx, "image", "rm", tag) } @@ -97,11 +110,17 @@ func RunAndRemove(ctx context.Context, args ...string) error { return Cmd(ctx, append([]string{"run", "--rm"}, args...)...) } -func RunD2VM(ctx context.Context, image, version, cmd string, args ...string) error { +func RunD2VM(ctx context.Context, image, version, in, out, cmd string, args ...string) error { pwd, err := os.Getwd() if err != nil { return err } + if in == "" { + in = pwd + } + if out == "" { + out = pwd + } if image == "" { image = "linkacloud/d2vm" } @@ -113,7 +132,9 @@ func RunD2VM(ctx context.Context, image, version, cmd string, args ...string) er "-v", fmt.Sprintf("%s:/var/run/docker.sock", dockerSocket()), "-v", - fmt.Sprintf("%s:/d2vm", pwd), + fmt.Sprintf("%s:/in", in), + "-v", + fmt.Sprintf("%s:/out", out), "-w", "/d2vm", fmt.Sprintf("%s:%s", image, version),