Skip to content

Commit 4930675

Browse files
authored
Merge pull request #1873 from AkihiroSuda/protect
Add `limactl protect <INSTANCE>` to prohibit accidental removal
2 parents ff2cf52 + cdb64c5 commit 4930675

File tree

7 files changed

+132
-0
lines changed

7 files changed

+132
-0
lines changed

cmd/limactl/delete.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ func deleteAction(cmd *cobra.Command, args []string) error {
4949
}
5050

5151
func deleteInstance(ctx context.Context, inst *store.Instance, force bool) error {
52+
if inst.Protected {
53+
return fmt.Errorf("instance is protected to prohibit accidental removal (Hint: use `limactl unprotect`)")
54+
}
5255
if !force && inst.Status != store.StatusStopped {
5356
return fmt.Errorf("expected status %q, got %q", store.StatusStopped, inst.Status)
5457
}

cmd/limactl/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ func newApp() *cobra.Command {
116116
newUsernetCommand(),
117117
newGenDocCommand(),
118118
newSnapshotCommand(),
119+
newProtectCommand(),
120+
newUnprotectCommand(),
119121
)
120122
return rootCmd
121123
}

cmd/limactl/protect.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/lima-vm/lima/pkg/store"
8+
"github.com/sirupsen/logrus"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
func newProtectCommand() *cobra.Command {
13+
var protectCommand = &cobra.Command{
14+
Use: "protect INSTANCE [INSTANCE, ...]",
15+
Short: "Protect an instance to prohibit accidental removal",
16+
Long: `Protect an instance to prohibit accidental removal via the 'limactl delete' command.
17+
The instance is not being protected against removal via '/bin/rm', Finder, etc.`,
18+
Args: WrapArgsError(cobra.MinimumNArgs(1)),
19+
RunE: protectAction,
20+
ValidArgsFunction: protectBashComplete,
21+
}
22+
return protectCommand
23+
}
24+
25+
func protectAction(_ *cobra.Command, args []string) error {
26+
var errs []error
27+
for _, instName := range args {
28+
inst, err := store.Inspect(instName)
29+
if err != nil {
30+
errs = append(errs, fmt.Errorf("failed to inspect instance %q: %w", instName, err))
31+
continue
32+
}
33+
if inst.Protected {
34+
logrus.Warnf("Instance %q is already protected. Skipping.", instName)
35+
continue
36+
}
37+
if err := inst.Protect(); err != nil {
38+
errs = append(errs, fmt.Errorf("failed to protect instance %q: %w", instName, err))
39+
continue
40+
}
41+
logrus.Infof("Protected %q", instName)
42+
}
43+
return errors.Join(errs...)
44+
}
45+
46+
func protectBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
47+
return bashCompleteInstanceNames(cmd)
48+
}

cmd/limactl/unprotect.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/lima-vm/lima/pkg/store"
8+
"github.com/sirupsen/logrus"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
func newUnprotectCommand() *cobra.Command {
13+
var unprotectCommand = &cobra.Command{
14+
Use: "unprotect INSTANCE [INSTANCE, ...]",
15+
Short: "Unprotect an instance",
16+
Args: WrapArgsError(cobra.MinimumNArgs(1)),
17+
RunE: unprotectAction,
18+
ValidArgsFunction: unprotectBashComplete,
19+
}
20+
return unprotectCommand
21+
}
22+
23+
func unprotectAction(_ *cobra.Command, args []string) error {
24+
var errs []error
25+
for _, instName := range args {
26+
inst, err := store.Inspect(instName)
27+
if err != nil {
28+
errs = append(errs, fmt.Errorf("failed to inspect instance %q: %w", instName, err))
29+
continue
30+
}
31+
if !inst.Protected {
32+
logrus.Warnf("Instance %q isn't protected. Skipping.", instName)
33+
continue
34+
}
35+
if err := inst.Unprotect(); err != nil {
36+
errs = append(errs, fmt.Errorf("failed to unprotect instance %q: %w", instName, err))
37+
continue
38+
}
39+
logrus.Infof("Unprotected %q", instName)
40+
}
41+
return errors.Join(errs...)
42+
}
43+
44+
func unprotectBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
45+
return bashCompleteInstanceNames(cmd)
46+
}

pkg/store/filenames/filenames.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ const (
5656

5757
// SocketDir is the default location for forwarded sockets with a relative paths in HostSocket
5858
SocketDir = "sock"
59+
60+
Protected = "protected" // empty file; used by `limactl protect`
5961
)
6062

6163
// Filenames used under a disk directory

pkg/store/instance.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type Instance struct {
5555
Errors []error `json:"errors,omitempty"`
5656
Config *limayaml.LimaYAML `json:"config,omitempty"`
5757
SSHAddress string `json:"sshAddress,omitempty"`
58+
Protected bool `json:"protected"`
5859
}
5960

6061
func (inst *Instance) LoadYAML() (*limayaml.LimaYAML, error) {
@@ -139,6 +140,11 @@ func Inspect(instName string) (*Instance, error) {
139140
inst.Disk = 0
140141
}
141142

143+
protected := filepath.Join(instDir, filenames.Protected)
144+
if _, err := os.Lstat(protected); !errors.Is(err, os.ErrNotExist) {
145+
inst.Protected = true
146+
}
147+
142148
inspectStatus(instDir, inst, y)
143149

144150
tmpl, err := template.New("format").Parse(y.Message)
@@ -394,3 +400,27 @@ func PrintInstances(w io.Writer, instances []*Instance, format string, options *
394400
}
395401
return nil
396402
}
403+
404+
// Protect protects the instance to prohibit accidental removal.
405+
// Protect does not return an error even when the instance is already protected.
406+
func (inst *Instance) Protect() error {
407+
protected := filepath.Join(inst.Dir, filenames.Protected)
408+
// TODO: Do an equivalent of `chmod +a "everyone deny delete,delete_child,file_inherit,directory_inherit"`
409+
// https://github.com/lima-vm/lima/issues/1595
410+
if err := os.WriteFile(protected, nil, 0400); err != nil {
411+
return err
412+
}
413+
inst.Protected = true
414+
return nil
415+
}
416+
417+
// Unprotect unprotects the instance.
418+
// Unprotect does not return an error even when the instance is already unprotected.
419+
func (inst *Instance) Unprotect() error {
420+
protected := filepath.Join(inst.Dir, filenames.Protected)
421+
if err := os.RemoveAll(protected); err != nil {
422+
return err
423+
}
424+
inst.Protected = false
425+
return nil
426+
}

website/content/en/docs/dev/Internals/_index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ An instance directory contains the following files:
3131

3232
Metadata:
3333
- `lima.yaml`: the YAML
34+
- `protected`: empty file, used by `limactl protect`
3435

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

0 commit comments

Comments
 (0)