Skip to content

Commit 6032d5f

Browse files
committed
Add a plugin mechanism to provide custom locator URL schemes
The template reader will attempt to call `limactl-url-$SCHEME` to rewrite a custom URL as either a local filename, or a URL with a supported URL like https. Signed-off-by: Jan Dubois <jan.dubois@suse.com>
1 parent e49e987 commit 6032d5f

File tree

4 files changed

+111
-27
lines changed

4 files changed

+111
-27
lines changed

cmd/limactl/main.go

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/lima-vm/lima/v2/cmd/yq"
2121
"github.com/lima-vm/lima/v2/pkg/debugutil"
2222
"github.com/lima-vm/lima/v2/pkg/driver/external/server"
23+
"github.com/lima-vm/lima/v2/pkg/executil"
2324
"github.com/lima-vm/lima/v2/pkg/fsutil"
2425
"github.com/lima-vm/lima/v2/pkg/limatype/dirnames"
2526
"github.com/lima-vm/lima/v2/pkg/osutil"
@@ -221,8 +222,11 @@ func executeWithPluginSupport(rootCmd *cobra.Command, args []string) error {
221222
if len(args) > 0 {
222223
cmd, _, err := rootCmd.Find(args)
223224
if err != nil || cmd == rootCmd {
224-
// Function calls os.Exit() if it found and executed the plugin
225-
runExternalPlugin(rootCmd.Context(), args[0], args[1:])
225+
_ = executil.WithExecutablePath(func() error {
226+
// Function calls os.Exit() if it found and executed the plugin
227+
runExternalPlugin(rootCmd.Context(), args[0], args[1:])
228+
return nil
229+
})
226230
}
227231
}
228232

@@ -235,11 +239,6 @@ func runExternalPlugin(ctx context.Context, name string, args []string) {
235239
ctx = context.Background()
236240
}
237241

238-
if err := updatePathEnv(); err != nil {
239-
logrus.Warnf("failed to update PATH environment: %v", err)
240-
// PATH update failure shouldn't prevent plugin execution
241-
}
242-
243242
externalCmd := "limactl-" + name
244243
execPath, err := exec.LookPath(externalCmd)
245244
if err != nil {
@@ -260,25 +259,6 @@ func runExternalPlugin(ctx context.Context, name string, args []string) {
260259
logrus.Fatalf("external command %q failed: %v", execPath, err)
261260
}
262261

263-
func updatePathEnv() error {
264-
exe, err := os.Executable()
265-
if err != nil {
266-
return fmt.Errorf("failed to get executable path: %w", err)
267-
}
268-
269-
binDir := filepath.Dir(exe)
270-
currentPath := os.Getenv("PATH")
271-
newPath := binDir + string(filepath.ListSeparator) + currentPath
272-
273-
if err := os.Setenv("PATH", newPath); err != nil {
274-
return fmt.Errorf("failed to set PATH environment: %w", err)
275-
}
276-
277-
logrus.Debugf("updated PATH to prioritize %s", binDir)
278-
279-
return nil
280-
}
281-
282262
// WrapArgsError annotates cobra args error with some context, so the error message is more user-friendly.
283263
func WrapArgsError(argFn cobra.PositionalArgs) cobra.PositionalArgs {
284264
return func(cmd *cobra.Command, args []string) error {

cmd/limactl/template.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func newTemplateCommand() *cobra.Command {
3737
}
3838
templateCommand.AddCommand(
3939
newTemplateCopyCommand(),
40+
newTemplateURLCommand(),
4041
newTemplateValidateCommand(),
4142
newTemplateYQCommand(),
4243
)
@@ -95,6 +96,25 @@ func fillDefaults(ctx context.Context, tmpl *limatmpl.Template) error {
9596
return err
9697
}
9798

99+
func newTemplateURLCommand() *cobra.Command {
100+
templateURLCommand := &cobra.Command{
101+
Use: "url CUSTOM_URL",
102+
Short: "Transform custom template URLs to regular file or https URLs",
103+
Args: WrapArgsError(cobra.ExactArgs(1)),
104+
RunE: templateURLAction,
105+
}
106+
return templateURLCommand
107+
}
108+
109+
func templateURLAction(cmd *cobra.Command, args []string) error {
110+
url, err := limatmpl.TransformCustomURL(cmd.Context(), args[0])
111+
if err != nil {
112+
return err
113+
}
114+
_, err = fmt.Fprintln(cmd.OutOrStdout(), url)
115+
return err
116+
}
117+
98118
func templateCopyAction(cmd *cobra.Command, args []string) error {
99119
ctx := cmd.Context()
100120
source := args[0]

pkg/executil/command.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ import (
77
"bytes"
88
"context"
99
"fmt"
10+
"os"
1011
"os/exec"
12+
"path/filepath"
13+
14+
"github.com/sirupsen/logrus"
1115

1216
"github.com/lima-vm/lima/v2/pkg/ioutilx"
17+
"github.com/lima-vm/lima/v2/pkg/usrlocalsharelima"
1318
)
1419

1520
type options struct {
@@ -52,3 +57,35 @@ func RunUTF16leCommand(args []string, opts ...Opt) (string, error) {
5257
}
5358
return outString, err
5459
}
60+
61+
// WithExecutablePath prepends the directory containing the current executable to the PATH
62+
// and appends "/usr/local/libexec/lima" to the end before calling fn().
63+
//
64+
// This can be used to prefer plugins from the same directory over ones on the PATH and also works if the
65+
// directory containing the executable itself is not on the path (e.g. "./_output/bin/limactl").
66+
//
67+
// It means if plugins call limactl they will invoke back the executable that called them.
68+
func WithExecutablePath(fn func() error) error {
69+
exe, err := os.Executable()
70+
if err != nil {
71+
return fmt.Errorf("failed to get executable path: %w", err)
72+
}
73+
74+
currentPath := os.Getenv("PATH")
75+
defer os.Setenv("PATH", currentPath)
76+
newPath := filepath.Dir(exe) + string(filepath.ListSeparator) + currentPath
77+
78+
prefixDir, err := usrlocalsharelima.Prefix()
79+
if err == nil {
80+
newPath += string(filepath.ListSeparator) + filepath.Join(prefixDir, "libexec", "lima")
81+
} else {
82+
// This happens in Go unit tests: https://github.com/lima-vm/lima/issues/3208
83+
logrus.Warnf("Couldn't locate libexec path: %v", err)
84+
}
85+
86+
if err := os.Setenv("PATH", newPath); err != nil {
87+
return fmt.Errorf("failed to set PATH environment: %w", err)
88+
}
89+
90+
return fn()
91+
}

pkg/limatmpl/locator.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ package limatmpl
55

66
import (
77
"context"
8+
"errors"
89
"fmt"
910
"io"
1011
"net/http"
1112
"net/url"
1213
"os"
14+
"os/exec"
1315
"path"
1416
"path/filepath"
1517
"regexp"
@@ -18,6 +20,7 @@ import (
1820

1921
"github.com/sirupsen/logrus"
2022

23+
"github.com/lima-vm/lima/v2/pkg/executil"
2124
"github.com/lima-vm/lima/v2/pkg/identifiers"
2225
"github.com/lima-vm/lima/v2/pkg/ioutilx"
2326
"github.com/lima-vm/lima/v2/pkg/limatype"
@@ -28,12 +31,16 @@ import (
2831
const yBytesLimit = 4 * 1024 * 1024 // 4MiB
2932

3033
func Read(ctx context.Context, name, locator string) (*Template, error) {
31-
var err error
3234
tmpl := &Template{
3335
Name: name,
3436
Locator: locator,
3537
}
3638

39+
locator, err := TransformCustomURL(ctx, locator)
40+
if err != nil {
41+
return nil, err
42+
}
43+
3744
if imageTemplate(tmpl, locator) {
3845
return tmpl, nil
3946
}
@@ -285,3 +292,43 @@ func InstNameFromYAMLPath(yamlPath string) (string, error) {
285292
}
286293
return s, nil
287294
}
295+
296+
func transformCustomURL(ctx context.Context, locator string) (string, error) {
297+
u, err := url.Parse(locator)
298+
if err != nil || len(u.Scheme) <= 1 {
299+
return locator, nil
300+
}
301+
302+
// TODO we should call main.updatePathEnv() here (after moving it to some pkg)
303+
externalCmd := "limactl-url-" + u.Scheme
304+
execPath, err := exec.LookPath(externalCmd)
305+
if err != nil {
306+
return locator, nil
307+
}
308+
309+
cmd := exec.CommandContext(ctx, execPath, strings.TrimPrefix(u.String(), u.Scheme+":"))
310+
cmd.Env = os.Environ()
311+
312+
stdout, err := cmd.Output()
313+
if err != nil {
314+
var exitErr *exec.ExitError
315+
if errors.As(err, &exitErr) {
316+
stderrMsg := string(exitErr.Stderr)
317+
if stderrMsg != "" {
318+
return "", fmt.Errorf("command %q failed: %s", cmd.String(), strings.TrimSpace(stderrMsg))
319+
}
320+
}
321+
return "", fmt.Errorf("command %q failed: %w", cmd.String(), err)
322+
}
323+
324+
return strings.TrimSpace(string(stdout)), nil
325+
}
326+
327+
func TransformCustomURL(ctx context.Context, locator string) (string, error) {
328+
err := executil.WithExecutablePath(func() error {
329+
var err error
330+
locator, err = transformCustomURL(ctx, locator)
331+
return err
332+
})
333+
return locator, err
334+
}

0 commit comments

Comments
 (0)