-
Notifications
You must be signed in to change notification settings - Fork 687
Implement limactl clone
#3673
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement limactl clone
#3673
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is one issue that is surprising, but not specific to If I use So it would work better if the default was to not start the instance, even when you ask, but you would have to explicitly enter But I guess this is a separate discussion outside the scope of this PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
👍 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
// SPDX-FileCopyrightText: Copyright The Lima Authors | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package main | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
|
||
"github.com/spf13/cobra" | ||
|
||
"github.com/lima-vm/lima/cmd/limactl/editflags" | ||
"github.com/lima-vm/lima/pkg/instance" | ||
"github.com/lima-vm/lima/pkg/limayaml" | ||
networks "github.com/lima-vm/lima/pkg/networks/reconcile" | ||
"github.com/lima-vm/lima/pkg/store" | ||
"github.com/lima-vm/lima/pkg/store/filenames" | ||
"github.com/lima-vm/lima/pkg/yqutil" | ||
) | ||
|
||
func newCloneCommand() *cobra.Command { | ||
cloneCommand := &cobra.Command{ | ||
Use: "clone OLDINST NEWINST", | ||
Short: "Clone an instance of Lima", | ||
Long: `Clone an instance of Lima. | ||
|
||
jandubois marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Not to be confused with 'limactl copy' ('limactl cp'). | ||
`, | ||
Args: WrapArgsError(cobra.ExactArgs(2)), | ||
RunE: cloneAction, | ||
ValidArgsFunction: cloneBashComplete, | ||
GroupID: advancedCommand, | ||
} | ||
editflags.RegisterEdit(cloneCommand, "[limactl edit] ") | ||
return cloneCommand | ||
} | ||
|
||
func cloneAction(cmd *cobra.Command, args []string) error { | ||
ctx := cmd.Context() | ||
flags := cmd.Flags() | ||
tty, err := flags.GetBool("tty") | ||
if err != nil { | ||
return err | ||
} | ||
|
||
oldInstName, newInstName := args[0], args[1] | ||
oldInst, err := store.Inspect(oldInstName) | ||
if err != nil { | ||
if errors.Is(err, os.ErrNotExist) { | ||
return fmt.Errorf("instance %q not found", oldInstName) | ||
} | ||
return err | ||
} | ||
|
||
newInst, err := instance.Clone(ctx, oldInst, newInstName) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
yqExprs, err := editflags.YQExpressions(flags, false) | ||
if err != nil { | ||
return err | ||
} | ||
if len(yqExprs) > 0 { | ||
// TODO: reduce duplicated codes across cloneAction and editAction | ||
yq := yqutil.Join(yqExprs) | ||
filePath := filepath.Join(newInst.Dir, filenames.LimaYAML) | ||
yContent, err := os.ReadFile(filePath) | ||
if err != nil { | ||
return err | ||
} | ||
yBytes, err := yqutil.EvaluateExpression(yq, yContent) | ||
if err != nil { | ||
return err | ||
} | ||
y, err := limayaml.LoadWithWarnings(yBytes, filePath) | ||
if err != nil { | ||
return err | ||
} | ||
if err := limayaml.Validate(y, true); err != nil { | ||
return saveRejectedYAML(yBytes, err) | ||
} | ||
if err := limayaml.ValidateAgainstLatestConfig(yBytes, yContent); err != nil { | ||
return saveRejectedYAML(yBytes, err) | ||
} | ||
if err := os.WriteFile(filePath, yBytes, 0o644); err != nil { | ||
return err | ||
} | ||
newInst, err = store.Inspect(newInst.Name) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
if !tty { | ||
// use "start" to start it | ||
return nil | ||
} | ||
startNow, err := askWhetherToStart() | ||
if err != nil { | ||
return err | ||
} | ||
if !startNow { | ||
return nil | ||
} | ||
err = networks.Reconcile(ctx, newInst.Name) | ||
if err != nil { | ||
return err | ||
} | ||
return instance.Start(ctx, newInst, "", false) | ||
} | ||
|
||
func cloneBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { | ||
return bashCompleteInstanceNames(cmd) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
// SPDX-FileCopyrightText: Copyright The Lima Authors | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package instance | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"io/fs" | ||
"os" | ||
"path/filepath" | ||
"slices" | ||
"strings" | ||
|
||
continuityfs "github.com/containerd/continuity/fs" | ||
|
||
"github.com/lima-vm/lima/pkg/osutil" | ||
"github.com/lima-vm/lima/pkg/store" | ||
"github.com/lima-vm/lima/pkg/store/filenames" | ||
) | ||
|
||
func Clone(_ context.Context, oldInst *store.Instance, newInstName string) (*store.Instance, error) { | ||
if newInstName == "" { | ||
return nil, errors.New("got empty instName") | ||
} | ||
if oldInst.Name == newInstName { | ||
return nil, fmt.Errorf("new instance name %q must be different from %q", newInstName, oldInst.Name) | ||
} | ||
if oldInst.Status == store.StatusRunning { | ||
return nil, errors.New("cannot clone a running instance") | ||
} | ||
|
||
newInstDir, err := store.InstanceDir(newInstName) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if _, err = os.Stat(newInstDir); !errors.Is(err, fs.ErrNotExist) { | ||
return nil, fmt.Errorf("instance %q already exists", newInstName) | ||
} | ||
|
||
// the full path of the socket name must be less than UNIX_PATH_MAX chars. | ||
maxSockName := filepath.Join(newInstDir, filenames.LongestSock) | ||
if len(maxSockName) >= osutil.UnixPathMax { | ||
return nil, fmt.Errorf("instance name %q too long: %q must be less than UNIX_PATH_MAX=%d characters, but is %d", | ||
newInstName, maxSockName, osutil.UnixPathMax, len(maxSockName)) | ||
} | ||
|
||
if err = os.Mkdir(newInstDir, 0o700); err != nil { | ||
return nil, err | ||
} | ||
|
||
walkDirFn := func(path string, d fs.DirEntry, err error) error { | ||
base := filepath.Base(path) | ||
if slices.Contains(filenames.SkipOnClone, base) { | ||
return nil | ||
} | ||
for _, ext := range filenames.TmpFileSuffixes { | ||
if strings.HasSuffix(path, ext) { | ||
return nil | ||
} | ||
} | ||
if err != nil { | ||
return err | ||
} | ||
pathRel, err := filepath.Rel(oldInst.Dir, path) | ||
if err != nil { | ||
return err | ||
} | ||
dst := filepath.Join(newInstDir, pathRel) | ||
if d.IsDir() { | ||
return os.MkdirAll(dst, d.Type().Perm()) | ||
} | ||
// NullifyOnClone contains VzIdentifier. | ||
// VzIdentifier file must not be just removed here, as pkg/limayaml depends on | ||
// the existence of VzIdentifier for resolving the VM type. | ||
if slices.Contains(filenames.NullifyOnClone, base) { | ||
return os.WriteFile(dst, nil, 0o666) | ||
AkihiroSuda marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
// CopyFile attempts copy-on-write when supported by the filesystem | ||
return continuityfs.CopyFile(dst, path) | ||
} | ||
|
||
if err = filepath.WalkDir(oldInst.Dir, walkDirFn); err != nil { | ||
return nil, err | ||
} | ||
|
||
return store.Inspect(newInstName) | ||
} |
Uh oh!
There was an error while loading. Please reload this page.