Skip to content

Commit

Permalink
Add limactl protect <INSTANCE> to prohibit accidental removal
Browse files Browse the repository at this point in the history
Fix issue 1595

Signed-off-by: Akihiro Suda <[email protected]>
  • Loading branch information
AkihiroSuda committed Oct 8, 2023
1 parent b4d4c31 commit cdb64c5
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 0 deletions.
3 changes: 3 additions & 0 deletions cmd/limactl/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ func deleteAction(cmd *cobra.Command, args []string) error {
}

func deleteInstance(ctx context.Context, inst *store.Instance, force bool) error {
if inst.Protected {
return fmt.Errorf("instance is protected to prohibit accidental removal (Hint: use `limactl unprotect`)")
}
if !force && inst.Status != store.StatusStopped {
return fmt.Errorf("expected status %q, got %q", store.StatusStopped, inst.Status)
}
Expand Down
2 changes: 2 additions & 0 deletions cmd/limactl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ func newApp() *cobra.Command {
newUsernetCommand(),
newGenDocCommand(),
newSnapshotCommand(),
newProtectCommand(),
newUnprotectCommand(),
)
return rootCmd
}
Expand Down
48 changes: 48 additions & 0 deletions cmd/limactl/protect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package main

import (
"errors"
"fmt"

"github.com/lima-vm/lima/pkg/store"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

func newProtectCommand() *cobra.Command {
var protectCommand = &cobra.Command{
Use: "protect INSTANCE [INSTANCE, ...]",
Short: "Protect an instance to prohibit accidental removal",
Long: `Protect an instance to prohibit accidental removal via the 'limactl delete' command.
The instance is not being protected against removal via '/bin/rm', Finder, etc.`,
Args: WrapArgsError(cobra.MinimumNArgs(1)),
RunE: protectAction,
ValidArgsFunction: protectBashComplete,
}
return protectCommand
}

func protectAction(_ *cobra.Command, args []string) error {
var errs []error
for _, instName := range args {
inst, err := store.Inspect(instName)
if err != nil {
errs = append(errs, fmt.Errorf("failed to inspect instance %q: %w", instName, err))
continue
}
if inst.Protected {
logrus.Warnf("Instance %q is already protected. Skipping.", instName)
continue
}
if err := inst.Protect(); err != nil {
errs = append(errs, fmt.Errorf("failed to protect instance %q: %w", instName, err))
continue
}
logrus.Infof("Protected %q", instName)
}
return errors.Join(errs...)
}

func protectBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return bashCompleteInstanceNames(cmd)
}
46 changes: 46 additions & 0 deletions cmd/limactl/unprotect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"errors"
"fmt"

"github.com/lima-vm/lima/pkg/store"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

func newUnprotectCommand() *cobra.Command {
var unprotectCommand = &cobra.Command{
Use: "unprotect INSTANCE [INSTANCE, ...]",
Short: "Unprotect an instance",
Args: WrapArgsError(cobra.MinimumNArgs(1)),
RunE: unprotectAction,
ValidArgsFunction: unprotectBashComplete,
}
return unprotectCommand
}

func unprotectAction(_ *cobra.Command, args []string) error {
var errs []error
for _, instName := range args {
inst, err := store.Inspect(instName)
if err != nil {
errs = append(errs, fmt.Errorf("failed to inspect instance %q: %w", instName, err))
continue
}
if !inst.Protected {
logrus.Warnf("Instance %q isn't protected. Skipping.", instName)
continue
}
if err := inst.Unprotect(); err != nil {
errs = append(errs, fmt.Errorf("failed to unprotect instance %q: %w", instName, err))
continue
}
logrus.Infof("Unprotected %q", instName)
}
return errors.Join(errs...)
}

func unprotectBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return bashCompleteInstanceNames(cmd)
}
2 changes: 2 additions & 0 deletions pkg/store/filenames/filenames.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ const (

// SocketDir is the default location for forwarded sockets with a relative paths in HostSocket
SocketDir = "sock"

Protected = "protected" // empty file; used by `limactl protect`
)

// Filenames used under a disk directory
Expand Down
30 changes: 30 additions & 0 deletions pkg/store/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type Instance struct {
Errors []error `json:"errors,omitempty"`
Config *limayaml.LimaYAML `json:"config,omitempty"`
SSHAddress string `json:"sshAddress,omitempty"`
Protected bool `json:"protected"`
}

func (inst *Instance) LoadYAML() (*limayaml.LimaYAML, error) {
Expand Down Expand Up @@ -139,6 +140,11 @@ func Inspect(instName string) (*Instance, error) {
inst.Disk = 0
}

protected := filepath.Join(instDir, filenames.Protected)
if _, err := os.Lstat(protected); !errors.Is(err, os.ErrNotExist) {
inst.Protected = true
}

inspectStatus(instDir, inst, y)

tmpl, err := template.New("format").Parse(y.Message)
Expand Down Expand Up @@ -394,3 +400,27 @@ func PrintInstances(w io.Writer, instances []*Instance, format string, options *
}
return nil
}

// Protect protects the instance to prohibit accidental removal.
// Protect does not return an error even when the instance is already protected.
func (inst *Instance) Protect() error {
protected := filepath.Join(inst.Dir, filenames.Protected)
// TODO: Do an equivalent of `chmod +a "everyone deny delete,delete_child,file_inherit,directory_inherit"`
// https://github.com/lima-vm/lima/issues/1595
if err := os.WriteFile(protected, nil, 0400); err != nil {
return err
}
inst.Protected = true
return nil
}

// Unprotect unprotects the instance.
// Unprotect does not return an error even when the instance is already unprotected.
func (inst *Instance) Unprotect() error {
protected := filepath.Join(inst.Dir, filenames.Protected)
if err := os.RemoveAll(protected); err != nil {
return err
}
inst.Protected = false
return nil
}
1 change: 1 addition & 0 deletions website/content/en/docs/dev/Internals/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ An instance directory contains the following files:

Metadata:
- `lima.yaml`: the YAML
- `protected`: empty file, used by `limactl protect`

cloud-init:
- `cidata.iso`: cloud-init ISO9660 image. See [`cidata.iso`](#cidataiso).
Expand Down

0 comments on commit cdb64c5

Please sign in to comment.