Skip to content

Commit 6809c31

Browse files
AkihiroSudaolamilekan000
authored andcommitted
limactl help and info flag should show available plugins
Signed-off-by: olalekan odukoya <[email protected]>
2 parents 562d5c9 + b9c1684 commit 6809c31

File tree

7 files changed

+345
-35
lines changed

7 files changed

+345
-35
lines changed

cmd/limactl/list.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,14 @@ func fieldNames() []string {
3131
f := t.Field(i)
3232
if f.Anonymous {
3333
for j := range f.Type.NumField() {
34-
names = append(names, f.Type.Field(j).Name)
34+
if tag := f.Tag.Get("lima"); tag != "deprecated" {
35+
names = append(names, f.Type.Field(j).Name)
36+
}
3537
}
3638
} else {
37-
names = append(names, t.Field(i).Name)
39+
if tag := f.Tag.Get("lima"); tag != "deprecated" {
40+
names = append(names, t.Field(i).Name)
41+
}
3842
}
3943
}
4044
return names

cmd/limactl/main.go

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/lima-vm/lima/v2/pkg/fsutil"
2424
"github.com/lima-vm/lima/v2/pkg/limatype/dirnames"
2525
"github.com/lima-vm/lima/v2/pkg/osutil"
26+
"github.com/lima-vm/lima/v2/pkg/plugin"
2627
"github.com/lima-vm/lima/v2/pkg/version"
2728
)
2829

@@ -165,6 +166,10 @@ func newApp() *cobra.Command {
165166
}
166167
rootCmd.AddGroup(&cobra.Group{ID: "basic", Title: "Basic Commands:"})
167168
rootCmd.AddGroup(&cobra.Group{ID: "advanced", Title: "Advanced Commands:"})
169+
rootCmd.AddGroup(&cobra.Group{ID: "plugin", Title: "Available Plugins (Experimental):"})
170+
171+
// Discover and add plugins as commands
172+
addPluginCommands(rootCmd)
168173
rootCmd.AddCommand(
169174
newCreateCommand(),
170175
newStartCommand(),
@@ -215,14 +220,6 @@ func handleExitError(err error) {
215220

216221
// executeWithPluginSupport handles command execution with plugin support.
217222
func executeWithPluginSupport(rootCmd *cobra.Command, args []string) error {
218-
if len(args) > 0 {
219-
cmd, _, err := rootCmd.Find(args)
220-
if err != nil || cmd == rootCmd {
221-
// Function calls os.Exit() if it found and executed the plugin
222-
runExternalPlugin(rootCmd.Context(), args[0], args[1:])
223-
}
224-
}
225-
226223
rootCmd.SetArgs(args)
227224
return rootCmd.Execute()
228225
}
@@ -232,7 +229,7 @@ func runExternalPlugin(ctx context.Context, name string, args []string) {
232229
ctx = context.Background()
233230
}
234231

235-
if err := updatePathEnv(); err != nil {
232+
if err := plugin.UpdatePathForPlugins(); err != nil {
236233
logrus.Warnf("failed to update PATH environment: %v", err)
237234
// PATH update failure shouldn't prevent plugin execution
238235
}
@@ -257,23 +254,29 @@ func runExternalPlugin(ctx context.Context, name string, args []string) {
257254
logrus.Fatalf("external command %q failed: %v", execPath, err)
258255
}
259256

260-
func updatePathEnv() error {
261-
exe, err := os.Executable()
257+
func addPluginCommands(rootCmd *cobra.Command) {
258+
plugins, err := plugin.DiscoverPlugins()
262259
if err != nil {
263-
return fmt.Errorf("failed to get executable path: %w", err)
260+
logrus.Debugf("Failed to discover plugins: %v", err)
261+
return
264262
}
265263

266-
binDir := filepath.Dir(exe)
267-
currentPath := os.Getenv("PATH")
268-
newPath := binDir + string(filepath.ListSeparator) + currentPath
269-
270-
if err := os.Setenv("PATH", newPath); err != nil {
271-
return fmt.Errorf("failed to set PATH environment: %w", err)
272-
}
264+
for _, p := range plugins {
265+
pluginCmd := &cobra.Command{
266+
Use: p.Name,
267+
Short: p.Description,
268+
GroupID: "plugin",
269+
Run: func(cmd *cobra.Command, args []string) {
270+
pluginName := cmd.Use
271+
runExternalPlugin(cmd.Context(), pluginName, args)
272+
},
273+
}
273274

274-
logrus.Debugf("updated PATH to prioritize %s", binDir)
275+
pluginCmd.SilenceUsage = true
276+
pluginCmd.SilenceErrors = true
275277

276-
return nil
278+
rootCmd.AddCommand(pluginCmd)
279+
}
277280
}
278281

279282
// WrapArgsError annotates cobra args error with some context, so the error message is more user-friendly.

pkg/limainfo/limainfo.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ import (
77
"context"
88
"errors"
99
"io/fs"
10+
"path/filepath"
11+
"runtime"
1012

1113
"github.com/sirupsen/logrus"
1214

1315
"github.com/lima-vm/lima/v2/pkg/envutil"
1416
"github.com/lima-vm/lima/v2/pkg/limatype"
1517
"github.com/lima-vm/lima/v2/pkg/limatype/dirnames"
18+
"github.com/lima-vm/lima/v2/pkg/limatype/filenames"
1619
"github.com/lima-vm/lima/v2/pkg/limayaml"
20+
"github.com/lima-vm/lima/v2/pkg/plugin"
1721
"github.com/lima-vm/lima/v2/pkg/registry"
1822
"github.com/lima-vm/lima/v2/pkg/templatestore"
1923
"github.com/lima-vm/lima/v2/pkg/usrlocalsharelima"
@@ -29,6 +33,10 @@ type LimaInfo struct {
2933
VMTypesEx map[string]DriverExt `json:"vmTypesEx"` // since Lima v2.0.0
3034
GuestAgents map[limatype.Arch]GuestAgent `json:"guestAgents"` // since Lima v1.1.0
3135
ShellEnvBlock []string `json:"shellEnvBlock"`
36+
HostOS string `json:"hostOS"` // since Lima v2.0.0
37+
HostArch string `json:"hostArch"` // since Lima v2.0.0
38+
IdentityFile string `json:"identityFile"` // since Lima v2.0.0
39+
Plugins []plugin.Plugin `json:"plugins"` // since Lima v2.0.0
3240
}
3341

3442
type DriverExt struct {
@@ -72,6 +80,8 @@ func New(ctx context.Context) (*LimaInfo, error) {
7280
VMTypesEx: vmTypesEx,
7381
GuestAgents: make(map[limatype.Arch]GuestAgent),
7482
ShellEnvBlock: envutil.GetDefaultBlockList(),
83+
HostOS: runtime.GOOS,
84+
HostArch: limatype.NewArch(runtime.GOARCH),
7585
}
7686
info.Templates, err = templatestore.Templates()
7787
if err != nil {
@@ -81,6 +91,11 @@ func New(ctx context.Context) (*LimaInfo, error) {
8191
if err != nil {
8292
return nil, err
8393
}
94+
configDir, err := dirnames.LimaConfigDir()
95+
if err != nil {
96+
return nil, err
97+
}
98+
info.IdentityFile = filepath.Join(configDir, filenames.UserPrivateKey)
8499
for _, arch := range limatype.ArchTypes {
85100
bin, err := usrlocalsharelima.GuestAgentBinary(limatype.LINUX, arch)
86101
if err != nil {
@@ -95,5 +110,15 @@ func New(ctx context.Context) (*LimaInfo, error) {
95110
Location: bin,
96111
}
97112
}
113+
114+
plugins, err := plugin.DiscoverPlugins()
115+
if err != nil {
116+
logrus.WithError(err).Debug("Failed to discover plugins")
117+
// Don't fail the entire info command if plugin discovery fails
118+
info.Plugins = []plugin.Plugin{}
119+
} else {
120+
info.Plugins = plugins
121+
}
122+
98123
return info, nil
99124
}

pkg/plugin/plugin.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// SPDX-FileCopyrightText: Copyright The Lima Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package plugin
5+
6+
import (
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
11+
"github.com/sirupsen/logrus"
12+
13+
"github.com/lima-vm/lima/v2/pkg/usrlocalsharelima"
14+
)
15+
16+
type Plugin struct {
17+
Name string `json:"name"`
18+
Path string `json:"path"`
19+
Description string `json:"description,omitempty"`
20+
}
21+
22+
func DiscoverPlugins() ([]Plugin, error) {
23+
var plugins []Plugin
24+
seen := make(map[string]bool)
25+
26+
dirs := getPluginDirectories()
27+
28+
for _, dir := range dirs {
29+
pluginsInDir, err := scanDirectory(dir)
30+
if err != nil {
31+
logrus.Debugf("Failed to scan directory %s: %v", dir, err)
32+
continue
33+
}
34+
35+
for _, plugin := range pluginsInDir {
36+
if !seen[plugin.Name] {
37+
plugins = append(plugins, plugin)
38+
seen[plugin.Name] = true
39+
}
40+
}
41+
}
42+
43+
return plugins, nil
44+
}
45+
46+
func getPluginDirectories() []string {
47+
var dirs []string
48+
49+
selfPaths := []string{}
50+
51+
selfViaArgs0, err := usrlocalsharelima.ExecutableViaArgs0()
52+
if err != nil {
53+
logrus.WithError(err).Debug("failed to find executable")
54+
} else {
55+
selfPaths = append(selfPaths, selfViaArgs0)
56+
}
57+
58+
selfViaOS, err := os.Executable()
59+
if err != nil {
60+
logrus.WithError(err).Debug("failed to find os.Executable()")
61+
} else {
62+
selfFinalPathViaOS, err := filepath.EvalSymlinks(selfViaOS)
63+
if err != nil {
64+
logrus.WithError(err).Debug("failed to resolve symlinks")
65+
selfFinalPathViaOS = selfViaOS
66+
}
67+
68+
if len(selfPaths) == 0 || selfFinalPathViaOS != selfPaths[0] {
69+
selfPaths = append(selfPaths, selfFinalPathViaOS)
70+
}
71+
}
72+
73+
for _, self := range selfPaths {
74+
binDir := filepath.Dir(self)
75+
dirs = append(dirs, binDir)
76+
}
77+
78+
pathEnv := os.Getenv("PATH")
79+
if pathEnv != "" {
80+
pathDirs := filepath.SplitList(pathEnv)
81+
dirs = append(dirs, pathDirs...)
82+
}
83+
84+
if prefixDir, err := usrlocalsharelima.Prefix(); err == nil {
85+
libexecDir := filepath.Join(prefixDir, "libexec", "lima")
86+
if _, err := os.Stat(libexecDir); err == nil {
87+
dirs = append(dirs, libexecDir)
88+
}
89+
}
90+
91+
return dirs
92+
}
93+
94+
func scanDirectory(dir string) ([]Plugin, error) {
95+
var plugins []Plugin
96+
97+
entries, err := os.ReadDir(dir)
98+
if err != nil {
99+
return nil, err
100+
}
101+
102+
for _, entry := range entries {
103+
if entry.IsDir() {
104+
continue
105+
}
106+
107+
name := entry.Name()
108+
if !strings.HasPrefix(name, "limactl-") {
109+
continue
110+
}
111+
112+
pluginName := strings.TrimPrefix(name, "limactl-")
113+
114+
if strings.Contains(pluginName, ".") {
115+
if filepath.Ext(name) == ".exe" {
116+
pluginName = strings.TrimSuffix(pluginName, ".exe")
117+
} else {
118+
continue
119+
}
120+
}
121+
122+
fullPath := filepath.Join(dir, name)
123+
124+
if !isExecutable(fullPath) {
125+
continue
126+
}
127+
128+
plugin := Plugin{
129+
Name: pluginName,
130+
Path: fullPath,
131+
}
132+
133+
if desc := extractDescFromScript(fullPath); desc != "" {
134+
plugin.Description = desc
135+
}
136+
137+
plugins = append(plugins, plugin)
138+
}
139+
140+
return plugins, nil
141+
}
142+
143+
func isExecutable(path string) bool {
144+
info, err := os.Stat(path)
145+
if err != nil {
146+
return false
147+
}
148+
149+
mode := info.Mode()
150+
if mode&0o111 != 0 {
151+
return true
152+
}
153+
154+
if filepath.Ext(path) == ".exe" {
155+
return true
156+
}
157+
158+
return false
159+
}
160+
161+
func extractDescFromScript(path string) string {
162+
content, err := os.ReadFile(path)
163+
if err != nil {
164+
logrus.Debugf("Failed to read plugin script %s: %v", path, err)
165+
return ""
166+
}
167+
168+
lines := strings.Split(string(content), "\n")
169+
for _, line := range lines {
170+
line = strings.TrimSpace(line)
171+
172+
// Look for pattern: # <limactl-desc>Description text</limactl-desc>
173+
if strings.HasPrefix(line, "#") && strings.Contains(line, "<limactl-desc>") && strings.Contains(line, "</limactl-desc>") {
174+
start := strings.Index(line, "<limactl-desc>") + len("<limactl-desc>")
175+
end := strings.Index(line, "</limactl-desc>")
176+
177+
if start < end {
178+
desc := strings.TrimSpace(line[start:end])
179+
logrus.Debugf("Plugin %s: extracted description from script: %q", filepath.Base(path), desc)
180+
return desc
181+
}
182+
}
183+
}
184+
185+
logrus.Debugf("Plugin %s: no <limactl-desc> found in script", filepath.Base(path))
186+
187+
return ""
188+
}
189+
190+
func UpdatePathForPlugins() error {
191+
pluginDirs := getPluginDirectories()
192+
newPath := strings.Join(pluginDirs, string(filepath.ListSeparator))
193+
return os.Setenv("PATH", newPath)
194+
}

pkg/store/instance.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -225,11 +225,14 @@ func ReadPIDFile(path string) (int, error) {
225225
}
226226

227227
type FormatData struct {
228-
limatype.Instance
229-
HostOS string
230-
HostArch string
231-
LimaHome string
232-
IdentityFile string
228+
limatype.Instance `yaml:",inline"`
229+
230+
// Using these host attributes is deprecated; they will be removed in Lima 3.0
231+
// The values are available from `limactl info` as hostOS, hostArch, limaHome, and identifyFile.
232+
HostOS string `json:"HostOS" yaml:"HostOS" lima:"deprecated"`
233+
HostArch string `json:"HostArch" yaml:"HostArch" lima:"deprecated"`
234+
LimaHome string `json:"LimaHome" yaml:"LimaHome" lima:"deprecated"`
235+
IdentityFile string `json:"IdentityFile" yaml:"IdentityFile" lima:"deprecated"`
233236
}
234237

235238
var FormatHelp = "\n" +

0 commit comments

Comments
 (0)