diff --git a/cmd/apptainer.lima b/cmd/apptainer.lima
index 31ace6d57fc..ac267da0786 100755
--- a/cmd/apptainer.lima
+++ b/cmd/apptainer.lima
@@ -4,7 +4,7 @@ set -eu
: "${APPTAINER_BINDPATH:=}"
if [ "$(limactl ls -q "$LIMA_INSTANCE" 2>/dev/null)" != "$LIMA_INSTANCE" ]; then
- echo "instance \"$LIMA_INSTANCE\" does not exist, run \`limactl create --name=$LIMA_INSTANCE template://apptainer\` to create a new instance" >&2
+ echo "instance \"$LIMA_INSTANCE\" does not exist, run \`limactl create --name=$LIMA_INSTANCE template:apptainer\` to create a new instance" >&2
exit 1
elif [ "$(limactl ls -f '{{ .Status }}' "$LIMA_INSTANCE" 2>/dev/null)" != "Running" ]; then
echo "instance \"$LIMA_INSTANCE\" is not running, run \`limactl start $LIMA_INSTANCE\` to start the existing instance" >&2
diff --git a/cmd/docker.lima b/cmd/docker.lima
index 2c2151cd5df..4340def65bd 100755
--- a/cmd/docker.lima
+++ b/cmd/docker.lima
@@ -8,7 +8,7 @@ set -eu
: "${DOCKER:=docker}"
if [ "$(limactl ls -q "$LIMA_INSTANCE" 2>/dev/null)" != "$LIMA_INSTANCE" ]; then
- echo "instance \"$LIMA_INSTANCE\" does not exist, run \`limactl create --name=$LIMA_INSTANCE template://docker\` to create a new instance" >&2
+ echo "instance \"$LIMA_INSTANCE\" does not exist, run \`limactl create --name=$LIMA_INSTANCE template:docker\` to create a new instance" >&2
exit 1
elif [ "$(limactl ls -f '{{ .Status }}' "$LIMA_INSTANCE" 2>/dev/null)" != "Running" ]; then
echo "instance \"$LIMA_INSTANCE\" is not running, run \`limactl start $LIMA_INSTANCE\` to start the existing instance" >&2
diff --git a/cmd/kubectl.lima b/cmd/kubectl.lima
index 40822e8c973..863a52e43a2 100755
--- a/cmd/kubectl.lima
+++ b/cmd/kubectl.lima
@@ -14,8 +14,8 @@ if [ -z "$LIMA_INSTANCE" ]; then
LIMA_INSTANCE=k8s
else
echo "No k3s or k8s existing instances found. Either create one with" >&2
- echo "limactl create --name=k3s template://k3s" >&2
- echo "limactl create --name=k8s template://k8s" >&2
+ echo "limactl create --name=k3s template:k3s" >&2
+ echo "limactl create --name=k8s template:k8s" >&2
echo "or set LIMA_INSTANCE to the name of your Kubernetes instance" >&2
exit 1
fi
diff --git a/cmd/limactl/completion.go b/cmd/limactl/completion.go
index 9e5b949db6a..b9d961a2eed 100644
--- a/cmd/limactl/completion.go
+++ b/cmd/limactl/completion.go
@@ -28,7 +28,7 @@ func bashCompleteTemplateNames(_ *cobra.Command, toComplete string) ([]string, c
var comp []string
if templates, err := templatestore.Templates(); err == nil {
for _, f := range templates {
- name := "template://" + f.Name
+ name := "template:" + f.Name
if !strings.HasPrefix(name, toComplete) {
continue
}
diff --git a/cmd/limactl/main.go b/cmd/limactl/main.go
index 39b93421a72..874a4a10165 100644
--- a/cmd/limactl/main.go
+++ b/cmd/limactl/main.go
@@ -21,7 +21,7 @@ import (
"github.com/lima-vm/lima/v2/pkg/fsutil"
"github.com/lima-vm/lima/v2/pkg/limatype/dirnames"
"github.com/lima-vm/lima/v2/pkg/osutil"
- "github.com/lima-vm/lima/v2/pkg/plugin"
+ "github.com/lima-vm/lima/v2/pkg/plugins"
"github.com/lima-vm/lima/v2/pkg/version"
)
@@ -47,8 +47,7 @@ func main() {
}
}
}
- rootCmd := newApp()
- err := executeWithPluginSupport(rootCmd, os.Args[1:])
+ err := newApp().Execute()
server.StopAllExternalDrivers()
osutil.HandleExitError(err)
if err != nil {
@@ -56,6 +55,41 @@ func main() {
}
}
+func processGlobalFlags(rootCmd *cobra.Command) error {
+ // --log-level will override --debug, but will not reset debugutil.Debug
+ if debug, _ := rootCmd.Flags().GetBool("debug"); debug {
+ logrus.SetLevel(logrus.DebugLevel)
+ debugutil.Debug = true
+ }
+
+ l, _ := rootCmd.Flags().GetString("log-level")
+ if l != "" {
+ lvl, err := logrus.ParseLevel(l)
+ if err != nil {
+ return err
+ }
+ logrus.SetLevel(lvl)
+ }
+
+ logFormat, _ := rootCmd.Flags().GetString("log-format")
+ switch logFormat {
+ case "json":
+ formatter := new(logrus.JSONFormatter)
+ logrus.StandardLogger().SetFormatter(formatter)
+ case "text":
+ // logrus use text format by default.
+ if runtime.GOOS == "windows" && isatty.IsCygwinTerminal(os.Stderr.Fd()) {
+ formatter := new(logrus.TextFormatter)
+ // the default setting does not recognize cygwin on windows
+ formatter.ForceColors = true
+ logrus.StandardLogger().SetFormatter(formatter)
+ }
+ default:
+ return fmt.Errorf("unsupported log-format: %q", logFormat)
+ }
+ return nil
+}
+
func newApp() *cobra.Command {
templatesDir := "$PREFIX/share/lima/templates"
if exe, err := os.Executable(); err == nil {
@@ -92,30 +126,8 @@ func newApp() *cobra.Command {
rootCmd.PersistentFlags().Bool("tty", isatty.IsTerminal(os.Stdout.Fd()), "Enable TUI interactions such as opening an editor. Defaults to true when stdout is a terminal. Set to false for automation.")
rootCmd.PersistentFlags().BoolP("yes", "y", false, "Alias of --tty=false")
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {
- l, _ := cmd.Flags().GetString("log-level")
- if l != "" {
- lvl, err := logrus.ParseLevel(l)
- if err != nil {
- return err
- }
- logrus.SetLevel(lvl)
- }
-
- logFormat, _ := cmd.Flags().GetString("log-format")
- switch logFormat {
- case "json":
- formatter := new(logrus.JSONFormatter)
- logrus.StandardLogger().SetFormatter(formatter)
- case "text":
- // logrus use text format by default.
- if runtime.GOOS == "windows" && isatty.IsCygwinTerminal(os.Stderr.Fd()) {
- formatter := new(logrus.TextFormatter)
- // the default setting does not recognize cygwin on windows
- formatter.ForceColors = true
- logrus.StandardLogger().SetFormatter(formatter)
- }
- default:
- return fmt.Errorf("unsupported log-format: %q", logFormat)
+ if err := processGlobalFlags(rootCmd); err != nil {
+ return err
}
if osutil.IsBeingRosettaTranslated() && cmd.Parent().Name() != "completion" && cmd.Name() != "generate-doc" && cmd.Name() != "validate" {
@@ -191,47 +203,61 @@ func newApp() *cobra.Command {
newNetworkCommand(),
newCloneCommand(),
)
+ addPluginCommands(rootCmd)
return rootCmd
}
-func executeWithPluginSupport(rootCmd *cobra.Command, args []string) error {
- rootCmd.SetArgs(args)
-
- if err := rootCmd.ParseFlags(args); err == nil {
- if debug, _ := rootCmd.Flags().GetBool("debug"); debug {
- logrus.SetLevel(logrus.DebugLevel)
- debugutil.Debug = true
- }
+func addPluginCommands(rootCmd *cobra.Command) {
+ // The global options are only processed when rootCmd.Execute() is called.
+ // Let's take a sneak peek here to help debug the plugin discovery code.
+ if len(os.Args) > 1 && os.Args[1] == "--debug" {
+ logrus.SetLevel(logrus.DebugLevel)
}
- addPluginCommands(rootCmd)
-
- return rootCmd.Execute()
-}
-
-func addPluginCommands(rootCmd *cobra.Command) {
- plugins, err := plugin.DiscoverPlugins()
+ allPlugins, err := plugins.Discover()
if err != nil {
logrus.Warnf("Failed to discover plugins: %v", err)
return
}
- for _, p := range plugins {
- pluginName := p.Name
+ for _, plugin := range allPlugins {
pluginCmd := &cobra.Command{
- Use: pluginName,
- Short: p.Description,
+ Use: plugin.Name,
+ Short: plugin.Description,
GroupID: "plugin",
DisableFlagParsing: true,
- Run: func(cmd *cobra.Command, args []string) {
- plugin.RunExternalPlugin(cmd.Context(), pluginName, args)
+ SilenceErrors: true,
+ SilenceUsage: true,
+ PreRunE: func(*cobra.Command, []string) error {
+ for i, arg := range os.Args {
+ if arg == plugin.Name {
+ // parse global options but ignore plugin options
+ err := rootCmd.ParseFlags(os.Args[1:i])
+ if err == nil {
+ err = processGlobalFlags(rootCmd)
+ }
+ return err
+ }
+ }
+ // unreachable
+ return nil
+ },
+ Run: func(cmd *cobra.Command, _ []string) {
+ for i, arg := range os.Args {
+ if arg == plugin.Name {
+ // ignore global options
+ plugin.Run(cmd.Context(), os.Args[i+1:])
+ // plugin.Run() never returns because it calls os.Exit()
+ }
+ }
+ // unreachable
},
}
-
- pluginCmd.SilenceUsage = true
- pluginCmd.SilenceErrors = true
-
+ // Don't show the url scheme helper in the help output.
+ if strings.HasPrefix(plugin.Name, "url-") {
+ pluginCmd.Hidden = true
+ }
rootCmd.AddCommand(pluginCmd)
}
}
diff --git a/cmd/limactl/start.go b/cmd/limactl/start.go
index 4b6c2dc016f..982075a3ee0 100644
--- a/cmd/limactl/start.go
+++ b/cmd/limactl/start.go
@@ -49,7 +49,7 @@ func newCreateCommand() *cobra.Command {
$ limactl create
To create an instance "default" from a template "docker":
- $ limactl create --name=default template://docker
+ $ limactl create --name=default template:docker
To create an instance "default" with modified parameters:
$ limactl create --cpus=2 --memory=2
@@ -87,7 +87,7 @@ func newStartCommand() *cobra.Command {
$ limactl start
To create an instance "default" from a template "docker", and start it:
- $ limactl start --name=default template://docker
+ $ limactl start --name=default template:docker
`,
Short: "Start an instance of Lima",
Args: WrapArgsError(cobra.MaximumNArgs(1)),
@@ -234,19 +234,19 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
if isTemplateURL, templateName := limatmpl.SeemsTemplateURL(arg); isTemplateURL {
switch templateName {
case "experimental/vz":
- logrus.Warn("template://experimental/vz was merged into the default template in Lima v1.0. See also .")
+ logrus.Warn("template:experimental/vz was merged into the default template in Lima v1.0. See also .")
case "experimental/riscv64":
- logrus.Warn("template://experimental/riscv64 was merged into the default template in Lima v1.0. Use `limactl create --arch=riscv64 template://default` instead.")
+ logrus.Warn("template:experimental/riscv64 was merged into the default template in Lima v1.0. Use `limactl create --arch=riscv64 template:default` instead.")
case "experimental/armv7l":
- logrus.Warn("template://experimental/armv7l was merged into the default template in Lima v1.0. Use `limactl create --arch=armv7l template://default` instead.")
+ logrus.Warn("template:experimental/armv7l was merged into the default template in Lima v1.0. Use `limactl create --arch=armv7l template:default` instead.")
case "vmnet":
- logrus.Warn("template://vmnet was removed in Lima v1.0. Use `limactl create --network=lima:shared template://default` instead. See also .")
+ logrus.Warn("template:vmnet was removed in Lima v1.0. Use `limactl create --network=lima:shared template:default` instead. See also .")
case "experimental/net-user-v2":
- logrus.Warn("template://experimental/net-user-v2 was removed in Lima v1.0. Use `limactl create --network=lima:user-v2 template://default` instead. See also .")
+ logrus.Warn("template:experimental/net-user-v2 was removed in Lima v1.0. Use `limactl create --network=lima:user-v2 template:default` instead. See also .")
case "experimental/9p":
- logrus.Warn("template://experimental/9p was removed in Lima v1.0. Use `limactl create --vm-type=qemu --mount-type=9p template://default` instead. See also .")
+ logrus.Warn("template:experimental/9p was removed in Lima v1.0. Use `limactl create --vm-type=qemu --mount-type=9p template:default` instead. See also .")
case "experimental/virtiofs-linux":
- logrus.Warn("template://experimental/virtiofs-linux was removed in Lima v1.0. Use `limactl create --mount-type=virtiofs template://default` instead. See also .")
+ logrus.Warn("template:experimental/virtiofs-linux was removed in Lima v1.0. Use `limactl create --mount-type=virtiofs template:default` instead. See also .")
}
}
if arg == "-" {
@@ -298,8 +298,8 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
return nil, err
}
if arg != "" && arg != DefaultInstanceName {
- logrus.Infof("Creating an instance %q from template://default (Not from template://%s)", tmpl.Name, tmpl.Name)
- logrus.Warnf("This form is deprecated. Use `limactl create --name=%s template://default` instead", tmpl.Name)
+ logrus.Infof("Creating an instance %q from template:default (Not from template:%s)", tmpl.Name, tmpl.Name)
+ logrus.Warnf("This form is deprecated. Use `limactl create --name=%s template:default` instead", tmpl.Name)
}
// Read the default template for creating a new instance
tmpl.Bytes, err = templatestore.Read(templatestore.Default)
diff --git a/cmd/limactl/template.go b/cmd/limactl/template.go
index 005829cfb83..318836b2e7b 100644
--- a/cmd/limactl/template.go
+++ b/cmd/limactl/template.go
@@ -40,6 +40,7 @@ func newTemplateCommand() *cobra.Command {
newTemplateCopyCommand(),
newTemplateValidateCommand(),
newTemplateYQCommand(),
+ newTemplateURLCommand(),
)
return templateCommand
}
@@ -51,13 +52,13 @@ func newValidateCommand() *cobra.Command {
return validateCommand
}
-var templateCopyExample = ` Template locators are local files, file://, https://, or template:// URLs
+var templateCopyExample = ` Template locators are local files, file://, https://, or template: URLs
# Copy default template to STDOUT
- limactl template copy template://default -
+ limactl template copy template:default -
# Copy template from web location to local file and embed all external references
- # (this does not embed template:// references)
+ # (this does not embed template: references)
limactl template copy --embed https://example.com/lima.yaml mighty-machine.yaml
`
@@ -176,10 +177,10 @@ External references are embedded and default values are filled in
before the YQ expression is evaluated.
Example:
- limactl template yq template://default '.images[].location'
+ limactl template yq template:default '.images[].location'
The example command is equivalent to using an external yq command like this:
- limactl template copy --fill template://default - | yq '.images[].location'
+ limactl template copy --fill template:default - | yq '.images[].location'
`
func newTemplateYQCommand() *cobra.Command {
@@ -281,3 +282,22 @@ func templateValidateAction(cmd *cobra.Command, args []string) error {
return nil
}
+
+func newTemplateURLCommand() *cobra.Command {
+ templateURLCommand := &cobra.Command{
+ Use: "url CUSTOM_URL",
+ Short: "Transform custom template URLs to regular file or https URLs",
+ Args: WrapArgsError(cobra.ExactArgs(1)),
+ RunE: templateURLAction,
+ }
+ return templateURLCommand
+}
+
+func templateURLAction(cmd *cobra.Command, args []string) error {
+ url, err := limatmpl.TransformCustomURL(cmd.Context(), args[0])
+ if err != nil {
+ return err
+ }
+ _, err = fmt.Fprintln(cmd.OutOrStdout(), url)
+ return err
+}
diff --git a/cmd/podman.lima b/cmd/podman.lima
index b05d23b5d0a..59ab326bc7c 100755
--- a/cmd/podman.lima
+++ b/cmd/podman.lima
@@ -4,7 +4,7 @@ set -eu
: "${PODMAN:=podman}"
if [ "$(limactl ls -q "$LIMA_INSTANCE" 2>/dev/null)" != "$LIMA_INSTANCE" ]; then
- echo "instance \"$LIMA_INSTANCE\" does not exist, run \`limactl create --name=$LIMA_INSTANCE template://podman\` to create a new instance" >&2
+ echo "instance \"$LIMA_INSTANCE\" does not exist, run \`limactl create --name=$LIMA_INSTANCE template:podman\` to create a new instance" >&2
exit 1
elif [ "$(limactl ls -f '{{ .Status }}' "$LIMA_INSTANCE" 2>/dev/null)" != "Running" ]; then
echo "instance \"$LIMA_INSTANCE\" is not running, run \`limactl start $LIMA_INSTANCE\` to start the existing instance" >&2
diff --git a/hack/bats/tests/preserve-env.bats b/hack/bats/tests/preserve-env.bats
index e53257356a5..86db1a14111 100644
--- a/hack/bats/tests/preserve-env.bats
+++ b/hack/bats/tests/preserve-env.bats
@@ -17,7 +17,7 @@ local_setup_file() {
limactl delete --force "$NAME" || :
# Make sure that the host agent doesn't inherit file handles 3 or 4.
# Otherwise bats will not finish until the host agent exits.
- limactl start --yes --name "$NAME" template://default 3>&- 4>&-
+ limactl start --yes --name "$NAME" template:default 3>&- 4>&-
}
local_teardown_file() {
diff --git a/hack/common.inc.sh b/hack/common.inc.sh
index 8e6f94fe22d..109021a7d23 100644
--- a/hack/common.inc.sh
+++ b/hack/common.inc.sh
@@ -31,7 +31,7 @@ _IPERF3=iperf3
: "${IPERF3:=$_IPERF3}"
# Setup LIMA_TEMPLATES_PATH because the templates are not installed, but reference base templates
-# via template://_images/* and template://_default/*.
+# via template:_images/* and template:_default/*.
templates_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../templates" && pwd)"
: "${LIMA_TEMPLATES_PATH:-$templates_dir}"
export LIMA_TEMPLATES_PATH
diff --git a/hack/test-templates/test-misc.yaml b/hack/test-templates/test-misc.yaml
index a365160e387..4dddf23d732 100644
--- a/hack/test-templates/test-misc.yaml
+++ b/hack/test-templates/test-misc.yaml
@@ -3,7 +3,7 @@
# - snapshots
# - (More to come)
#
-base: template://ubuntu-22.04
+base: template:ubuntu-22.04
# 9p is not compatible with `limactl snapshot`
mountTypesUnsupported: ["9p"]
diff --git a/pkg/limainfo/limainfo.go b/pkg/limainfo/limainfo.go
index 93235c846a2..bfeb2ad2258 100644
--- a/pkg/limainfo/limainfo.go
+++ b/pkg/limainfo/limainfo.go
@@ -17,7 +17,7 @@ import (
"github.com/lima-vm/lima/v2/pkg/limatype/dirnames"
"github.com/lima-vm/lima/v2/pkg/limatype/filenames"
"github.com/lima-vm/lima/v2/pkg/limayaml"
- "github.com/lima-vm/lima/v2/pkg/plugin"
+ "github.com/lima-vm/lima/v2/pkg/plugins"
"github.com/lima-vm/lima/v2/pkg/registry"
"github.com/lima-vm/lima/v2/pkg/templatestore"
"github.com/lima-vm/lima/v2/pkg/usrlocalsharelima"
@@ -36,7 +36,7 @@ type LimaInfo struct {
HostOS string `json:"hostOS"` // since Lima v2.0.0
HostArch string `json:"hostArch"` // since Lima v2.0.0
IdentityFile string `json:"identityFile"` // since Lima v2.0.0
- Plugins []plugin.Plugin `json:"plugins"` // since Lima v2.0.0
+ Plugins []plugins.Plugin `json:"plugins"` // since Lima v2.0.0
}
type DriverExt struct {
@@ -48,7 +48,7 @@ type GuestAgent struct {
}
// New returns a LimaInfo object with the Lima version, a list of all Templates and their location,
-// the DefaultTemplate corresponding to template://default with all defaults filled in, the
+// the DefaultTemplate corresponding to template:default with all defaults filled in, the
// LimaHome location, a list of all supported VMTypes, and a map of GuestAgents for each architecture.
func New(ctx context.Context) (*LimaInfo, error) {
b, err := templatestore.Read(templatestore.Default)
@@ -111,13 +111,10 @@ func New(ctx context.Context) (*LimaInfo, error) {
}
}
- plugins, err := plugin.DiscoverPlugins()
+ info.Plugins, err = plugins.Discover()
if err != nil {
- logrus.WithError(err).Warn("Failed to discover plugins")
// Don't fail the entire info command if plugin discovery fails.
- info.Plugins = []plugin.Plugin{}
- } else {
- info.Plugins = plugins
+ logrus.WithError(err).Warn("Failed to discover plugins")
}
return info, nil
diff --git a/pkg/limatmpl/abs.go b/pkg/limatmpl/abs.go
index bfc7060e9e5..cd1ee5047ae 100644
--- a/pkg/limatmpl/abs.go
+++ b/pkg/limatmpl/abs.go
@@ -89,6 +89,9 @@ func basePath(locator string) (string, error) {
// Single-letter schemes will be drive names on Windows, e.g. "c:/foo"
if err == nil && len(u.Scheme) > 1 {
// path.Dir("") returns ".", which must be removed for url.JoinPath() to do the right thing later
+ if u.Opaque != "" {
+ return u.Scheme + ":" + strings.TrimSuffix(path.Dir(u.Opaque), "."), nil
+ }
return u.Scheme + "://" + strings.TrimSuffix(path.Dir(path.Join(u.Host, u.Path)), "."), nil
}
base, err := filepath.Abs(filepath.Dir(locator))
@@ -132,6 +135,10 @@ func absPath(locator, basePath string) (string, error) {
return "", err
}
if len(u.Scheme) > 1 {
+ // Treat empty "template:" URL as opaque
+ if u.Opaque != "" || (u.Scheme == "template" && u.Host == "") {
+ return u.Scheme + ":" + path.Join(u.Opaque, locator), nil
+ }
return u.JoinPath(locator).String(), nil
}
locator = filepath.Join(basePath, locator)
diff --git a/pkg/limatmpl/abs_test.go b/pkg/limatmpl/abs_test.go
index 5b78e9c155b..e033a0364de 100644
--- a/pkg/limatmpl/abs_test.go
+++ b/pkg/limatmpl/abs_test.go
@@ -23,67 +23,73 @@ type useAbsLocatorsTestCase struct {
var useAbsLocatorsTestCases = []useAbsLocatorsTestCase{
{
"Template without base or script file",
- "template://foo",
+ "template:foo",
`arch: aarch64`,
`arch: aarch64`,
},
{
"Single string base template",
+ "template:foo",
+ `base: bar.yaml`,
+ `base: template:bar.yaml`,
+ },
+ {
+ "Legacy template:// base template",
"template://foo",
`base: bar.yaml`,
- `base: template://bar.yaml`,
+ `base: template:bar.yaml`,
},
{
"Flow style array of one base template",
- "template://foo",
+ "template:foo",
`base: [{url: bar.yaml, digest: deadbeef}]`,
// not sure why the quotes around the URL were added; maybe because we don't copy the style from the source
- `base: [{url: 'template://bar.yaml', digest: deadbeef}]`,
+ `base: [{url: 'template:bar.yaml', digest: deadbeef}]`,
},
{
"Flow style array of sequence of two base URLs",
- "template://foo",
+ "template:foo",
`base: [bar.yaml, baz.yaml]`,
- `base: ['template://bar.yaml', 'template://baz.yaml']`,
+ `base: ['template:bar.yaml', 'template:baz.yaml']`,
},
{
"Flow style array of sequence of two base locator objects",
- "template://foo",
+ "template:foo",
`base: [{url: bar.yaml, digest: deadbeef}, {url: baz.yaml, digest: decafbad}]`,
- `base: [{url: 'template://bar.yaml', digest: deadbeef}, {url: 'template://baz.yaml', digest: decafbad}]`,
+ `base: [{url: 'template:bar.yaml', digest: deadbeef}, {url: 'template:baz.yaml', digest: decafbad}]`,
},
{
"Block style array of one base template",
- "template://foo",
+ "template:foo",
`
base:
- bar.yaml
`,
`
base:
-- template://bar.yaml`,
+- template:bar.yaml`,
},
{
"Block style of four base templates",
- "template://foo",
+ "template:foo",
`
base:
- bar.yaml
-- template://my
+- template:my
- https://example.com/my.yaml
- baz.yaml
`,
`
base:
-- template://bar.yaml
-- template://my
+- template:bar.yaml
+- template:my
- https://example.com/my.yaml
-- template://baz.yaml
+- template:baz.yaml
`,
},
{
"Provisioning and probe scripts",
- "template://experimental/foo",
+ "template:experimental/foo",
`
provision:
- mode: user
@@ -101,15 +107,15 @@ probes:
`
provision:
- mode: user
- file: template://experimental/userscript.sh
+ file: template:experimental/userscript.sh
- mode: system
file:
- url: template://experimental/systemscript.sh
+ url: template:experimental/systemscript.sh
digest: abc123
probes:
-- file: template://experimental/probe.sh
+- file: template:experimental/probe.sh
- file:
- url: template://experimental/probe.sh
+ url: template:experimental/probe.sh
digest: digest
`,
},
@@ -153,15 +159,15 @@ func TestBasePath(t *testing.T) {
})
t.Run("", func(t *testing.T) {
- actual, err := basePath("template://foo")
+ actual, err := basePath("template:foo")
assert.NilError(t, err)
- assert.Equal(t, actual, "template://")
+ assert.Equal(t, actual, "template:")
})
t.Run("", func(t *testing.T) {
- actual, err := basePath("template://foo/bar")
+ actual, err := basePath("template:foo/bar")
assert.NilError(t, err)
- assert.Equal(t, actual, "template://foo")
+ assert.Equal(t, actual, "template:foo")
})
t.Run("", func(t *testing.T) {
@@ -216,9 +222,9 @@ func TestAbsPath(t *testing.T) {
})
t.Run("", func(t *testing.T) {
- actual, err := absPath("template://foo", volume+"/root")
+ actual, err := absPath("template:foo", volume+"/root")
assert.NilError(t, err)
- assert.Equal(t, actual, "template://foo")
+ assert.Equal(t, actual, "template:foo")
})
t.Run("", func(t *testing.T) {
@@ -272,15 +278,15 @@ func TestAbsPath(t *testing.T) {
}
t.Run("", func(t *testing.T) {
- actual, err := absPath("foo", "template://")
+ actual, err := absPath("foo", "template:")
assert.NilError(t, err)
- assert.Equal(t, actual, "template://foo")
+ assert.Equal(t, actual, "template:foo")
})
t.Run("", func(t *testing.T) {
- actual, err := absPath("bar", "template://foo")
+ actual, err := absPath("bar", "template:foo")
assert.NilError(t, err)
- assert.Equal(t, actual, "template://foo/bar")
+ assert.Equal(t, actual, "template:foo/bar")
})
t.Run("", func(t *testing.T) {
diff --git a/pkg/limatmpl/embed.go b/pkg/limatmpl/embed.go
index a58712247bf..a505af1d634 100644
--- a/pkg/limatmpl/embed.go
+++ b/pkg/limatmpl/embed.go
@@ -76,7 +76,7 @@ func (tmpl *Template) embedAllBases(ctx context.Context, embedAll, defaultBase b
}
isTemplate, _ := SeemsTemplateURL(baseLocator.URL)
if isTemplate && !embedAll {
- // Once we skip a template:// URL we can no longer embed any other base template
+ // Once we skip a template: URL we can no longer embed any other base template
for i := 1; i < len(tmpl.Config.Base); i++ {
isTemplate, _ = SeemsTemplateURL(tmpl.Config.Base[i].URL)
if !isTemplate {
@@ -84,7 +84,7 @@ func (tmpl *Template) embedAllBases(ctx context.Context, embedAll, defaultBase b
}
}
break
- // TODO should we track embedding of template:// URLs so we can warn if we embed a non-template:// URL afterwards?
+ // TODO should we track embedding of template: URLs so we can warn if we embed a non-template: URL afterwards?
}
if seen[baseLocator.URL] {
diff --git a/pkg/limatmpl/embed_test.go b/pkg/limatmpl/embed_test.go
index 54e4a2fedf2..a1d43b4f1e2 100644
--- a/pkg/limatmpl/embed_test.go
+++ b/pkg/limatmpl/embed_test.go
@@ -181,24 +181,24 @@ mounts:
`mounts: [{location: loc1}, {location: loc1, mountPoint: loc2, writable: true, sshfs: {followSymlinks: true}}, {location: loc1, mountPoint: loc3, writable: true}]`,
},
{
- "template:// URLs are not embedded when embedAll is false",
+ "template: URLs are not embedded when embedAll is false",
// also tests file.url format
``,
`
-base: template://default
+base: template:default
provision:
- file:
- url: template://provision.sh
+ url: template:provision.sh
probes:
- file:
- url: template://probe.sh
+ url: template:probe.sh
`,
`
-base: template://default
+base: template:default
provision:
-- file: template://provision.sh
+- file: template:provision.sh
probes:
-- file: template://probe.sh
+- file: template:probe.sh
`,
},
{
@@ -214,18 +214,18 @@ vmType: qemu
`base template loop detected`,
},
{
- "ERROR All bases following template:// bases must be template:// URLs too when embedAll is false",
+ "ERROR All bases following template: bases must be template: URLs too when embedAll is false",
``,
- `base: [template://default, base1.yaml]`,
+ `base: [template:default, base1.yaml]`,
"after not embedding",
},
{
- "ERROR All bases following template:// bases must be template:// URLs too when embedAll is false",
+ "ERROR All bases following template: bases must be template: URLs too when embedAll is false",
``,
`
base: [base1.yaml, base2.yaml]
---
-base: template://default
+base: template:default
---
base: baseX.yaml`,
"after not embedding",
diff --git a/pkg/limatmpl/locator.go b/pkg/limatmpl/locator.go
index 98e664af711..5627b087dfd 100644
--- a/pkg/limatmpl/locator.go
+++ b/pkg/limatmpl/locator.go
@@ -5,11 +5,13 @@ package limatmpl
import (
"context"
+ "errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
+ "os/exec"
"path"
"path/filepath"
"regexp"
@@ -22,18 +24,23 @@ import (
"github.com/lima-vm/lima/v2/pkg/ioutilx"
"github.com/lima-vm/lima/v2/pkg/limatype"
"github.com/lima-vm/lima/v2/pkg/limayaml"
+ "github.com/lima-vm/lima/v2/pkg/plugins"
"github.com/lima-vm/lima/v2/pkg/templatestore"
)
const yBytesLimit = 4 * 1024 * 1024 // 4MiB
func Read(ctx context.Context, name, locator string) (*Template, error) {
- var err error
tmpl := &Template{
Name: name,
Locator: locator,
}
+ locator, err := TransformCustomURL(ctx, locator)
+ if err != nil {
+ return nil, err
+ }
+
if imageTemplate(tmpl, locator) {
return tmpl, nil
}
@@ -241,7 +248,10 @@ func SeemsTemplateURL(arg string) (isTemplate bool, templateName string) {
return false, ""
}
if u.Scheme == "template" {
- return true, path.Join(u.Host, u.Path)
+ if u.Opaque == "" {
+ return true, path.Join(u.Host, u.Path)
+ }
+ return true, u.Opaque
}
return false, ""
}
@@ -285,3 +295,55 @@ func InstNameFromYAMLPath(yamlPath string) (string, error) {
}
return s, nil
}
+
+func TransformCustomURL(ctx context.Context, locator string) (string, error) {
+ u, err := url.Parse(locator)
+ if err != nil || len(u.Scheme) <= 1 {
+ return locator, nil
+ }
+
+ if u.Scheme == "template" {
+ if u.Opaque != "" {
+ return locator, nil
+ }
+ // Fix malformed "template:" URLs.
+ newLocator := "template:" + path.Join(u.Host, u.Path)
+ logrus.Warnf("Template locator %q should be written %q since Lima v2.0", locator, newLocator)
+ return newLocator, nil
+ }
+
+ plugin, err := plugins.Find("url-" + u.Scheme)
+ if err != nil {
+ return "", err
+ }
+ if plugin == nil {
+ return locator, nil
+ }
+
+ currentPath := os.Getenv("PATH")
+ defer os.Setenv("PATH", currentPath)
+ err = plugins.UpdatePath()
+ if err != nil {
+ return "", err
+ }
+
+ cmd := exec.CommandContext(ctx, plugin.Path, strings.TrimPrefix(u.String(), u.Scheme+":"))
+ cmd.Env = os.Environ()
+
+ stdout, err := cmd.Output()
+ if err != nil {
+ var exitErr *exec.ExitError
+ if errors.As(err, &exitErr) {
+ stderrMsg := string(exitErr.Stderr)
+ if stderrMsg != "" {
+ return "", fmt.Errorf("command %q failed: %s", cmd.String(), strings.TrimSpace(stderrMsg))
+ }
+ }
+ return "", fmt.Errorf("command %q failed: %w", cmd.String(), err)
+ }
+ newLocator := strings.TrimSpace(string(stdout))
+ if newLocator != locator {
+ logrus.Debugf("Custom locator %q replaced with %q", locator, newLocator)
+ }
+ return newLocator, nil
+}
diff --git a/pkg/limatmpl/locator_test.go b/pkg/limatmpl/locator_test.go
index b611cf3bc75..5b566200713 100644
--- a/pkg/limatmpl/locator_test.go
+++ b/pkg/limatmpl/locator_test.go
@@ -64,7 +64,7 @@ func TestInstNameFromImageURL(t *testing.T) {
}
func TestSeemsTemplateURL(t *testing.T) {
- arg := "template://foo/bar"
+ arg := "template:foo/bar"
t.Run(arg, func(t *testing.T) {
is, name := limatmpl.SeemsTemplateURL(arg)
assert.Equal(t, is, true)
@@ -96,7 +96,7 @@ func TestSeemsHTTPURL(t *testing.T) {
}
notHTTPURLs := []string{
"file:///foo",
- "template://foo",
+ "template:foo",
"foo",
}
for _, arg := range notHTTPURLs {
@@ -114,7 +114,7 @@ func TestSeemsFileURL(t *testing.T) {
notFileURLs := []string{
"http://foo",
"https://foo",
- "template://foo",
+ "template:foo",
"foo",
}
for _, arg := range notFileURLs {
diff --git a/pkg/plugin/plugin.go b/pkg/plugins/plugins.go
similarity index 77%
rename from pkg/plugin/plugin.go
rename to pkg/plugins/plugins.go
index 81849db12b9..2f8823b3a9e 100644
--- a/pkg/plugin/plugin.go
+++ b/pkg/plugins/plugins.go
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright The Lima Authors
// SPDX-License-Identifier: Apache-2.0
-package plugin
+package plugins
import (
"cmp"
@@ -13,6 +13,7 @@ import (
"runtime"
"slices"
"strings"
+ "sync"
"github.com/sirupsen/logrus"
@@ -28,7 +29,7 @@ type Plugin struct {
Description string `json:"description,omitempty"`
}
-func DiscoverPlugins() ([]Plugin, error) {
+var Discover = sync.OnceValues(func() ([]Plugin, error) {
var plugins []Plugin
seen := make(map[string]bool)
@@ -47,9 +48,9 @@ func DiscoverPlugins() ([]Plugin, error) {
})
return plugins, nil
-}
+})
-func getPluginDirectories() []string {
+var getPluginDirectories = sync.OnceValue(func() []string {
dirs := usrlocalsharelima.SelfDirs()
pathEnv := os.Getenv("PATH")
@@ -65,7 +66,7 @@ func getPluginDirectories() []string {
}
return dirs
-}
+})
// isWindowsExecutableExt checks if the given extension is a valid Windows executable extension
// according to PATHEXT environment variable.
@@ -74,20 +75,9 @@ func isWindowsExecutableExt(ext string) bool {
return false
}
- pathExt := os.Getenv("PATHEXT")
- if pathExt == "" {
- pathExt = defaultPathExt
- }
-
+ pathExt := cmp.Or(os.Getenv("PATHEXT"), defaultPathExt)
extensions := strings.Split(strings.ToUpper(pathExt), ";")
- extUpper := strings.ToUpper(ext)
-
- for _, validExt := range extensions {
- if validExt == extUpper {
- return true
- }
- }
- return false
+ return slices.Contains(extensions, strings.ToUpper(ext))
}
func isExecutable(path string) bool {
@@ -104,18 +94,7 @@ func isExecutable(path string) bool {
return info.Mode()&0o111 != 0
}
- ext := strings.ToLower(filepath.Ext(path))
- pathExt := os.Getenv("PATHEXT")
- if pathExt == "" {
- pathExt = defaultPathExt
- }
-
- for _, e := range strings.Split(strings.ToLower(pathExt), ";") {
- if e == ext {
- return true
- }
- }
- return false
+ return isWindowsExecutableExt(filepath.Ext(path))
}
func scanDirectory(dir string) []Plugin {
@@ -168,46 +147,24 @@ func scanDirectory(dir string) []Plugin {
return plugins
}
-func RunExternalPlugin(ctx context.Context, name string, args []string) {
- if ctx == nil {
- ctx = context.Background()
- }
-
- if err := UpdatePathForPlugins(); err != nil {
+func (plugin *Plugin) Run(ctx context.Context, args []string) {
+ if err := UpdatePath(); err != nil {
logrus.Warnf("failed to update PATH environment: %v", err)
// PATH update failure shouldn't prevent plugin execution
}
- plugins, err := DiscoverPlugins()
- if err != nil {
- logrus.Warnf("failed to discover plugins: %v", err)
- return
- }
-
- var execPath string
- for _, plugin := range plugins {
- if plugin.Name == name {
- execPath = plugin.Path
- break
- }
- }
-
- if execPath == "" {
- return
- }
-
- cmd := exec.CommandContext(ctx, execPath, args...)
+ cmd := exec.CommandContext(ctx, plugin.Path, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = os.Environ()
- err = cmd.Run()
+ err := cmd.Run()
osutil.HandleExitError(err)
if err == nil {
os.Exit(0) //nolint:revive // it's intentional to call os.Exit in this function
}
- logrus.Fatalf("external command %q failed: %v", execPath, err)
+ logrus.Fatalf("external command %q failed: %v", plugin.Path, err)
}
var descRegex = regexp.MustCompile(`(.*?)`)
@@ -235,7 +192,22 @@ func extractDescFromScript(path string) string {
return desc
}
-func UpdatePathForPlugins() error {
+// Find locates a plugin by name and returns a pointer to a copy.
+func Find(name string) (*Plugin, error) {
+ allPlugins, err := Discover()
+ if err != nil {
+ return nil, err
+ }
+ for _, plugin := range allPlugins {
+ if name == plugin.Name {
+ pluginCopy := plugin
+ return &pluginCopy, nil
+ }
+ }
+ return nil, nil
+}
+
+func UpdatePath() error {
pluginDirs := getPluginDirectories()
newPath := strings.Join(pluginDirs, string(filepath.ListSeparator))
return os.Setenv("PATH", newPath)