diff --git a/cmd/limactl/edit.go b/cmd/limactl/edit.go
index 3d0dde19935..47e37086ee7 100644
--- a/cmd/limactl/edit.go
+++ b/cmd/limactl/edit.go
@@ -8,9 +8,9 @@ import (
"path/filepath"
"github.com/lima-vm/lima/cmd/limactl/editflags"
- "github.com/lima-vm/lima/cmd/limactl/guessarg"
"github.com/lima-vm/lima/pkg/editutil"
"github.com/lima-vm/lima/pkg/instance"
+ "github.com/lima-vm/lima/pkg/limatmpl"
"github.com/lima-vm/lima/pkg/limayaml"
networks "github.com/lima-vm/lima/pkg/networks/reconcile"
"github.com/lima-vm/lima/pkg/store"
@@ -44,7 +44,7 @@ func editAction(cmd *cobra.Command, args []string) error {
var err error
var inst *store.Instance
switch {
- case guessarg.SeemsYAMLPath(arg):
+ case limatmpl.SeemsYAMLPath(arg):
// absolute path is required for `limayaml.Validate`
filePath, err = filepath.Abs(arg)
if err != nil {
diff --git a/cmd/limactl/guessarg/guessarg.go b/cmd/limactl/guessarg/guessarg.go
deleted file mode 100644
index b0261fc411c..00000000000
--- a/cmd/limactl/guessarg/guessarg.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package guessarg
-
-import (
- "fmt"
- "net/url"
- "path"
- "path/filepath"
- "strings"
-
- "github.com/containerd/containerd/identifiers"
-)
-
-func SeemsTemplateURL(arg string) (bool, *url.URL) {
- u, err := url.Parse(arg)
- if err != nil {
- return false, u
- }
- return u.Scheme == "template", u
-}
-
-func SeemsHTTPURL(arg string) bool {
- u, err := url.Parse(arg)
- if err != nil {
- return false
- }
- if u.Scheme != "http" && u.Scheme != "https" {
- return false
- }
- return true
-}
-
-func SeemsFileURL(arg string) bool {
- u, err := url.Parse(arg)
- if err != nil {
- return false
- }
- return u.Scheme == "file"
-}
-
-func SeemsYAMLPath(arg string) bool {
- if strings.Contains(arg, "/") {
- return true
- }
- lower := strings.ToLower(arg)
- return strings.HasSuffix(lower, ".yml") || strings.HasSuffix(lower, ".yaml")
-}
-
-func InstNameFromURL(urlStr string) (string, error) {
- u, err := url.Parse(urlStr)
- if err != nil {
- return "", err
- }
- return InstNameFromYAMLPath(path.Base(u.Path))
-}
-
-func InstNameFromYAMLPath(yamlPath string) (string, error) {
- s := strings.ToLower(filepath.Base(yamlPath))
- s = strings.TrimSuffix(strings.TrimSuffix(s, ".yml"), ".yaml")
- // "." is allowed in instance names, but replaced to "-" for hostnames.
- // e.g., yaml: "ubuntu-24.04.yaml" , instance name: "ubuntu-24.04", hostname: "lima-ubuntu-24-04"
- if err := identifiers.Validate(s); err != nil {
- return "", fmt.Errorf("filename %q is invalid: %w", yamlPath, err)
- }
- return s, nil
-}
diff --git a/cmd/limactl/main.go b/cmd/limactl/main.go
index 2efad920729..259f622a938 100644
--- a/cmd/limactl/main.go
+++ b/cmd/limactl/main.go
@@ -155,6 +155,7 @@ func newApp() *cobra.Command {
newProtectCommand(),
newUnprotectCommand(),
newTunnelCommand(),
+ newTemplateCommand(),
)
if runtime.GOOS == "darwin" || runtime.GOOS == "linux" {
rootCmd.AddCommand(startAtLoginCommand())
diff --git a/cmd/limactl/start.go b/cmd/limactl/start.go
index 90f329c59b3..9c2e0ae29ce 100644
--- a/cmd/limactl/start.go
+++ b/cmd/limactl/start.go
@@ -3,8 +3,6 @@ package main
import (
"errors"
"fmt"
- "io"
- "net/http"
"os"
"path/filepath"
"runtime"
@@ -12,10 +10,9 @@ import (
"github.com/containerd/containerd/identifiers"
"github.com/lima-vm/lima/cmd/limactl/editflags"
- "github.com/lima-vm/lima/cmd/limactl/guessarg"
"github.com/lima-vm/lima/pkg/editutil"
"github.com/lima-vm/lima/pkg/instance"
- "github.com/lima-vm/lima/pkg/ioutilx"
+ "github.com/lima-vm/lima/pkg/limatmpl"
"github.com/lima-vm/lima/pkg/limayaml"
networks "github.com/lima-vm/lima/pkg/networks/reconcile"
"github.com/lima-vm/lima/pkg/store"
@@ -105,11 +102,6 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
arg = args[0]
}
- var (
- st = &creatorState{}
- err error
- )
-
flags := cmd.Flags()
// Create an instance, with menu TUI when TTY is available
@@ -118,19 +110,13 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
return nil, err
}
- st.instName, err = flags.GetString("name")
+ name, err := flags.GetString("name")
if err != nil {
return nil, err
}
-
- const yBytesLimit = 4 * 1024 * 1024 // 4MiB
-
- isTemplateURL, templateURL := guessarg.SeemsTemplateURL(arg)
- switch {
- case isTemplateURL:
+ if isTemplateURL, templateURL := limatmpl.SeemsTemplateURL(arg); isTemplateURL {
// No need to use SecureJoin here. https://github.com/lima-vm/lima/pull/805#discussion_r853411702
templateName := filepath.Join(templateURL.Host, templateURL.Path)
- logrus.Debugf("interpreting argument %q as a template name %q", arg, templateName)
switch templateName {
case "experimental/vz":
logrus.Warn("template://experimental/vz was merged into the default template in Lima v1.0. See also .")
@@ -147,104 +133,43 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
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 .")
}
- if st.instName == "" {
- // e.g., templateName = "deprecated/centos-7" , st.instName = "centos-7"
- st.instName = filepath.Base(templateName)
- }
- st.yBytes, err = templatestore.Read(templateName)
- if err != nil {
- return nil, err
- }
- case guessarg.SeemsHTTPURL(arg):
- if st.instName == "" {
- st.instName, err = guessarg.InstNameFromURL(arg)
- if err != nil {
- return nil, err
- }
- }
- logrus.Debugf("interpreting argument %q as a http url for instance %q", arg, st.instName)
- req, err := http.NewRequestWithContext(cmd.Context(), http.MethodGet, arg, http.NoBody)
- if err != nil {
- return nil, err
- }
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- st.yBytes, err = ioutilx.ReadAtMaximum(resp.Body, yBytesLimit)
- if err != nil {
- return nil, err
- }
- case guessarg.SeemsFileURL(arg):
- if st.instName == "" {
- st.instName, err = guessarg.InstNameFromURL(arg)
- if err != nil {
- return nil, err
- }
- }
- logrus.Debugf("interpreting argument %q as a file url for instance %q", arg, st.instName)
- r, err := os.Open(strings.TrimPrefix(arg, "file://"))
- if err != nil {
- return nil, err
- }
- defer r.Close()
- st.yBytes, err = ioutilx.ReadAtMaximum(r, yBytesLimit)
- if err != nil {
- return nil, err
- }
- case guessarg.SeemsYAMLPath(arg):
- if st.instName == "" {
- st.instName, err = guessarg.InstNameFromYAMLPath(arg)
- if err != nil {
- return nil, err
- }
- }
- logrus.Debugf("interpreting argument %q as a file path for instance %q", arg, st.instName)
- r, err := os.Open(arg)
- if err != nil {
- return nil, err
- }
- defer r.Close()
- st.yBytes, err = ioutilx.ReadAtMaximum(r, yBytesLimit)
- if err != nil {
- return nil, err
- }
- case arg == "-":
- if st.instName == "" {
+ }
+ if arg == "-" {
+ if name == "" {
return nil, errors.New("must pass instance name with --name when reading template from stdin")
}
- st.yBytes, err = io.ReadAll(os.Stdin)
- if err != nil {
- return nil, fmt.Errorf("unexpected error reading stdin: %w", err)
- }
// see if the tty was set explicitly or not
ttySet := cmd.Flags().Changed("tty")
if ttySet && tty {
return nil, errors.New("cannot use --tty=true and read template from stdin together")
}
tty = false
- default:
+ }
+ tmpl, err := limatmpl.Read(cmd.Context(), name, arg)
+ if err != nil {
+ return nil, err
+ }
+ if len(tmpl.Bytes) == 0 {
if arg == "" {
- if st.instName == "" {
- st.instName = DefaultInstanceName
+ if tmpl.Name == "" {
+ tmpl.Name = DefaultInstanceName
}
} else {
logrus.Debugf("interpreting argument %q as an instance name", arg)
- if st.instName != "" && st.instName != arg {
- return nil, fmt.Errorf("instance name %q and CLI flag --name=%q cannot be specified together", arg, st.instName)
+ if tmpl.Name != "" && tmpl.Name != arg {
+ return nil, fmt.Errorf("instance name %q and CLI flag --name=%q cannot be specified together", arg, tmpl.Name)
}
- st.instName = arg
+ tmpl.Name = arg
}
- if err := identifiers.Validate(st.instName); err != nil {
- return nil, fmt.Errorf("argument must be either an instance name, a YAML file path, or a URL, got %q: %w", st.instName, err)
+ if err := identifiers.Validate(tmpl.Name); err != nil {
+ return nil, fmt.Errorf("argument must be either an instance name, a YAML file path, or a URL, got %q: %w", tmpl.Name, err)
}
- inst, err := store.Inspect(st.instName)
+ inst, err := store.Inspect(tmpl.Name)
if err == nil {
if createOnly {
- return nil, fmt.Errorf("instance %q already exists", st.instName)
+ return nil, fmt.Errorf("instance %q already exists", tmpl.Name)
}
- logrus.Infof("Using the existing instance %q", st.instName)
+ logrus.Infof("Using the existing instance %q", tmpl.Name)
yqExprs, err := editflags.YQExpressions(flags, false)
if err != nil {
return nil, err
@@ -253,7 +178,7 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
yq := yqutil.Join(yqExprs)
inst, err = applyYQExpressionToExistingInstance(inst, yq)
if err != nil {
- return nil, fmt.Errorf("failed to apply yq expression %q to instance %q: %w", yq, st.instName, err)
+ return nil, fmt.Errorf("failed to apply yq expression %q to instance %q: %w", yq, tmpl.Name, err)
}
}
return inst, nil
@@ -262,11 +187,11 @@ 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)", st.instName, st.instName)
- logrus.Warnf("This form is deprecated. Use `limactl create --name=%s template://default` instead", st.instName)
+ 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
- st.yBytes, err = templatestore.Read(templatestore.Default)
+ tmpl.Bytes, err = templatestore.Read(templatestore.Default)
if err != nil {
return nil, err
}
@@ -279,18 +204,18 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
yq := yqutil.Join(yqExprs)
if tty {
var err error
- st, err = chooseNextCreatorState(st, yq)
+ tmpl, err = chooseNextCreatorState(tmpl, yq)
if err != nil {
return nil, err
}
} else {
logrus.Info("Terminal is not available, proceeding without opening an editor")
- if err := modifyInPlace(st, yq); err != nil {
+ if err := modifyInPlace(tmpl, yq); err != nil {
return nil, err
}
}
saveBrokenYAML := tty
- return instance.Create(cmd.Context(), st.instName, st.yBytes, saveBrokenYAML)
+ return instance.Create(cmd.Context(), tmpl.Name, tmpl.Bytes, saveBrokenYAML)
}
func applyYQExpressionToExistingInstance(inst *store.Instance, yq string) (*store.Instance, error) {
@@ -326,17 +251,12 @@ func applyYQExpressionToExistingInstance(inst *store.Instance, yq string) (*stor
return store.Inspect(inst.Name)
}
-type creatorState struct {
- instName string // instance name
- yBytes []byte // yaml bytes
-}
-
-func modifyInPlace(st *creatorState, yq string) error {
- out, err := yqutil.EvaluateExpression(yq, st.yBytes)
+func modifyInPlace(st *limatmpl.Template, yq string) error {
+ out, err := yqutil.EvaluateExpression(yq, st.Bytes)
if err != nil {
return err
}
- st.yBytes = out
+ st.Bytes = out
return nil
}
@@ -355,13 +275,13 @@ func (exitSuccessError) ExitCode() int {
return 0
}
-func chooseNextCreatorState(st *creatorState, yq string) (*creatorState, error) {
+func chooseNextCreatorState(tmpl *limatmpl.Template, yq string) (*limatmpl.Template, error) {
for {
- if err := modifyInPlace(st, yq); err != nil {
+ if err := modifyInPlace(tmpl, yq); err != nil {
logrus.WithError(err).Warn("Failed to evaluate yq expression")
- return st, err
+ return tmpl, err
}
- message := fmt.Sprintf("Creating an instance %q", st.instName)
+ message := fmt.Sprintf("Creating an instance %q", tmpl.Name)
options := []string{
"Proceed with the current configuration",
"Open an editor to review or modify the current configuration",
@@ -374,34 +294,34 @@ func chooseNextCreatorState(st *creatorState, yq string) (*creatorState, error)
logrus.Fatal("Interrupted by user")
}
logrus.WithError(err).Warn("Failed to open TUI")
- return st, nil
+ return tmpl, nil
}
switch ans {
case 0: // "Proceed with the current configuration"
- return st, nil
+ return tmpl, nil
case 1: // "Open an editor ..."
- hdr := fmt.Sprintf("# Review and modify the following configuration for Lima instance %q.\n", st.instName)
- if st.instName == DefaultInstanceName {
+ hdr := fmt.Sprintf("# Review and modify the following configuration for Lima instance %q.\n", tmpl.Name)
+ if tmpl.Name == DefaultInstanceName {
hdr += "# - In most cases, you do not need to modify this file.\n"
}
hdr += "# - To cancel starting Lima, just save this file as an empty file.\n"
hdr += "\n"
hdr += editutil.GenerateEditorWarningHeader()
var err error
- st.yBytes, err = editutil.OpenEditor(st.yBytes, hdr)
+ tmpl.Bytes, err = editutil.OpenEditor(tmpl.Bytes, hdr)
if err != nil {
- return st, err
+ return tmpl, err
}
- if len(st.yBytes) == 0 {
+ if len(tmpl.Bytes) == 0 {
const msg = "Aborting, as requested by saving the file with empty content"
logrus.Info(msg)
return nil, exitSuccessError{Msg: msg}
}
- return st, nil
+ return tmpl, nil
case 2: // "Choose another template..."
templates, err := templatestore.Templates()
if err != nil {
- return st, err
+ return tmpl, err
}
message := "Choose a template"
options := make([]string, len(templates))
@@ -410,19 +330,19 @@ func chooseNextCreatorState(st *creatorState, yq string) (*creatorState, error)
}
ansEx, err := uiutil.Select(message, options)
if err != nil {
- return st, err
+ return tmpl, err
}
if ansEx > len(templates)-1 {
- return st, fmt.Errorf("invalid answer %d for %d entries", ansEx, len(templates))
+ return tmpl, fmt.Errorf("invalid answer %d for %d entries", ansEx, len(templates))
}
yamlPath := templates[ansEx].Location
- if st.instName == "" {
- st.instName, err = guessarg.InstNameFromYAMLPath(yamlPath)
+ if tmpl.Name == "" {
+ tmpl.Name, err = limatmpl.InstNameFromYAMLPath(yamlPath)
if err != nil {
return nil, err
}
}
- st.yBytes, err = os.ReadFile(yamlPath)
+ tmpl.Bytes, err = os.ReadFile(yamlPath)
if err != nil {
return nil, err
}
@@ -430,7 +350,7 @@ func chooseNextCreatorState(st *creatorState, yq string) (*creatorState, error)
case 3: // "Exit"
return nil, exitSuccessError{Msg: "Choosing to exit"}
default:
- return st, fmt.Errorf("unexpected answer %q", ans)
+ return tmpl, fmt.Errorf("unexpected answer %q", ans)
}
}
}
@@ -443,7 +363,7 @@ func createStartActionCommon(cmd *cobra.Command, _ []string) (exit bool, err err
if templates, err := templatestore.Templates(); err == nil {
w := cmd.OutOrStdout()
for _, f := range templates {
- fmt.Fprintln(w, f.Name)
+ _, _ = fmt.Fprintln(w, f.Name)
}
return true, nil
}
diff --git a/cmd/limactl/template.go b/cmd/limactl/template.go
new file mode 100644
index 00000000000..58e712abfa7
--- /dev/null
+++ b/cmd/limactl/template.go
@@ -0,0 +1,129 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/lima-vm/lima/pkg/limatmpl"
+ "github.com/lima-vm/lima/pkg/limayaml"
+ "github.com/sirupsen/logrus"
+ "github.com/spf13/cobra"
+)
+
+func newTemplateCommand() *cobra.Command {
+ templateCommand := &cobra.Command{
+ Use: "template",
+ Aliases: []string{"tmpl"},
+ Short: "Lima template management",
+ SilenceUsage: true,
+ SilenceErrors: true,
+ GroupID: advancedCommand,
+ // The template command is still hidden because the subcommands and options are still under development
+ // and subject to change at any time.
+ Hidden: true,
+ }
+ templateCommand.AddCommand(
+ newTemplateCopyCommand(),
+ newTemplateValidateCommand(),
+ )
+ return templateCommand
+}
+
+// The validate command exists for backwards compatibility, and because the template command is still hidden.
+func newValidateCommand() *cobra.Command {
+ validateCommand := newTemplateValidateCommand()
+ validateCommand.GroupID = advancedCommand
+ return validateCommand
+}
+
+var templateCopyExample = ` Template locators are local files, file://, https://, or template:// URLs
+
+ # Copy default template to STDOUT
+ limactl template copy template://default -
+
+ # Copy template from web location to local file
+ limactl template copy https://templates.r.us/lima.yaml mighty-machine.yaml
+`
+
+func newTemplateCopyCommand() *cobra.Command {
+ templateCopyCommand := &cobra.Command{
+ Use: "copy TEMPLATE DEST",
+ Short: "Copy template",
+ Long: "Copy a template via locator to a local file",
+ Example: templateCopyExample,
+ Args: WrapArgsError(cobra.ExactArgs(2)),
+ RunE: templateCopyAction,
+ }
+ return templateCopyCommand
+}
+
+func templateCopyAction(cmd *cobra.Command, args []string) error {
+ tmpl, err := limatmpl.Read(cmd.Context(), "", args[0])
+ if err != nil {
+ return err
+ }
+ if len(tmpl.Bytes) == 0 {
+ return fmt.Errorf("don't know how to interpret %q as a template locator", args[0])
+ }
+ writer := cmd.OutOrStdout()
+ target := args[1]
+ if target != "-" {
+ file, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+ writer = file
+ }
+ _, err = fmt.Fprint(writer, string(tmpl.Bytes))
+ return err
+}
+
+func newTemplateValidateCommand() *cobra.Command {
+ templateValidateCommand := &cobra.Command{
+ Use: "validate TEMPLATE [TEMPLATE, ...]",
+ Short: "Validate YAML templates",
+ Args: WrapArgsError(cobra.MinimumNArgs(1)),
+ RunE: templateValidateAction,
+ }
+ templateValidateCommand.Flags().Bool("fill", false, "fill defaults")
+ return templateValidateCommand
+}
+
+func templateValidateAction(cmd *cobra.Command, args []string) error {
+ fill, err := cmd.Flags().GetBool("fill")
+ if err != nil {
+ return err
+ }
+
+ for _, arg := range args {
+ tmpl, err := limatmpl.Read(cmd.Context(), "", arg)
+ if err != nil {
+ return err
+ }
+ if len(tmpl.Bytes) == 0 {
+ return fmt.Errorf("don't know how to interpret %q as a template locator", arg)
+ }
+ if arg != "-" && tmpl.Name == "" {
+ return fmt.Errorf("can't determine instance name from template locator %q", arg)
+ }
+ // Load() will merge the template with override.yaml and default.yaml via FillDefaults()
+ y, err := limayaml.Load(tmpl.Bytes, tmpl.Locator)
+ if err != nil {
+ return err
+ }
+ if err := limayaml.Validate(y, false); err != nil {
+ return fmt.Errorf("failed to validate YAML file %q: %w", arg, err)
+ }
+ logrus.Infof("%q: OK", arg)
+ if fill {
+ b, err := limayaml.Marshal(y, len(args) > 1)
+ if err != nil {
+ return fmt.Errorf("failed to marshal template %q again after filling defaults: %w", arg, err)
+ }
+ fmt.Fprint(cmd.OutOrStdout(), string(b))
+ }
+ }
+
+ return nil
+}
diff --git a/cmd/limactl/validate.go b/cmd/limactl/validate.go
deleted file mode 100644
index f62c5db3ee8..00000000000
--- a/cmd/limactl/validate.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package main
-
-import (
- "fmt"
-
- "github.com/lima-vm/lima/cmd/limactl/guessarg"
- "github.com/lima-vm/lima/pkg/limayaml"
- "github.com/lima-vm/lima/pkg/store"
- "github.com/spf13/cobra"
-
- "github.com/sirupsen/logrus"
-)
-
-func newValidateCommand() *cobra.Command {
- validateCommand := &cobra.Command{
- Use: "validate FILE.yaml [FILE.yaml, ...]",
- Short: "Validate YAML files",
- Args: WrapArgsError(cobra.MinimumNArgs(1)),
- RunE: validateAction,
- GroupID: advancedCommand,
- }
- validateCommand.Flags().Bool("fill", false, "fill defaults")
- return validateCommand
-}
-
-func validateAction(cmd *cobra.Command, args []string) error {
- fill, err := cmd.Flags().GetBool("fill")
- if err != nil {
- return err
- }
-
- for _, f := range args {
- y, err := store.LoadYAMLByFilePath(f)
- if err != nil {
- return fmt.Errorf("failed to load YAML file %q: %w", f, err)
- }
- if _, err := guessarg.InstNameFromYAMLPath(f); err != nil {
- return err
- }
- logrus.Infof("%q: OK", f)
- if fill {
- b, err := limayaml.Marshal(y, len(args) > 1)
- if err != nil {
- return err
- }
- fmt.Fprint(cmd.OutOrStdout(), string(b))
- }
- }
-
- return nil
-}
diff --git a/pkg/limatmpl/locator.go b/pkg/limatmpl/locator.go
new file mode 100644
index 00000000000..ddb9b61bab9
--- /dev/null
+++ b/pkg/limatmpl/locator.go
@@ -0,0 +1,166 @@
+package limatmpl
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+
+ "github.com/containerd/containerd/identifiers"
+ "github.com/lima-vm/lima/pkg/ioutilx"
+ "github.com/lima-vm/lima/pkg/templatestore"
+ "github.com/sirupsen/logrus"
+)
+
+type Template struct {
+ Name string
+ Locator string
+ Bytes []byte
+}
+
+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,
+ }
+
+ isTemplateURL, templateURL := SeemsTemplateURL(locator)
+ switch {
+ case isTemplateURL:
+ // No need to use SecureJoin here. https://github.com/lima-vm/lima/pull/805#discussion_r853411702
+ templateName := filepath.Join(templateURL.Host, templateURL.Path)
+ logrus.Debugf("interpreting argument %q as a template name %q", locator, templateName)
+ if tmpl.Name == "" {
+ // e.g., templateName = "deprecated/centos-7" , tmpl.Name = "centos-7"
+ tmpl.Name = filepath.Base(templateName)
+ }
+ tmpl.Bytes, err = templatestore.Read(templateName)
+ if err != nil {
+ return nil, err
+ }
+ case SeemsHTTPURL(locator):
+ if tmpl.Name == "" {
+ tmpl.Name, err = InstNameFromURL(locator)
+ if err != nil {
+ return nil, err
+ }
+ }
+ logrus.Debugf("interpreting argument %q as a http url for instance %q", locator, tmpl.Name)
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, locator, http.NoBody)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ tmpl.Bytes, err = ioutilx.ReadAtMaximum(resp.Body, yBytesLimit)
+ if err != nil {
+ return nil, err
+ }
+ case SeemsFileURL(locator):
+ if tmpl.Name == "" {
+ tmpl.Name, err = InstNameFromURL(locator)
+ if err != nil {
+ return nil, err
+ }
+ }
+ logrus.Debugf("interpreting argument %q as a file url for instance %q", locator, tmpl.Name)
+ r, err := os.Open(strings.TrimPrefix(locator, "file://"))
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+ tmpl.Bytes, err = ioutilx.ReadAtMaximum(r, yBytesLimit)
+ if err != nil {
+ return nil, err
+ }
+ case SeemsYAMLPath(locator):
+ if tmpl.Name == "" {
+ tmpl.Name, err = InstNameFromYAMLPath(locator)
+ if err != nil {
+ return nil, err
+ }
+ }
+ logrus.Debugf("interpreting argument %q as a file path for instance %q", locator, tmpl.Name)
+ r, err := os.Open(locator)
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+ tmpl.Bytes, err = ioutilx.ReadAtMaximum(r, yBytesLimit)
+ if err != nil {
+ return nil, err
+ }
+ case locator == "-":
+ tmpl.Bytes, err = io.ReadAll(os.Stdin)
+ if err != nil {
+ return nil, fmt.Errorf("unexpected error reading stdin: %w", err)
+ }
+ }
+ return tmpl, nil
+}
+
+func SeemsTemplateURL(arg string) (bool, *url.URL) {
+ u, err := url.Parse(arg)
+ if err != nil {
+ return false, u
+ }
+ return u.Scheme == "template", u
+}
+
+func SeemsHTTPURL(arg string) bool {
+ u, err := url.Parse(arg)
+ if err != nil {
+ return false
+ }
+ if u.Scheme != "http" && u.Scheme != "https" {
+ return false
+ }
+ return true
+}
+
+func SeemsFileURL(arg string) bool {
+ u, err := url.Parse(arg)
+ if err != nil {
+ return false
+ }
+ return u.Scheme == "file"
+}
+
+func SeemsYAMLPath(arg string) bool {
+ if strings.Contains(arg, "/") {
+ return true
+ }
+ lower := strings.ToLower(arg)
+ return strings.HasSuffix(lower, ".yml") || strings.HasSuffix(lower, ".yaml")
+}
+
+func InstNameFromURL(urlStr string) (string, error) {
+ u, err := url.Parse(urlStr)
+ if err != nil {
+ return "", err
+ }
+ return InstNameFromYAMLPath(path.Base(u.Path))
+}
+
+func InstNameFromYAMLPath(yamlPath string) (string, error) {
+ s := strings.ToLower(filepath.Base(yamlPath))
+ s = strings.TrimSuffix(strings.TrimSuffix(s, ".yml"), ".yaml")
+ // "." is allowed in instance names, but replaced to "-" for hostnames.
+ // e.g., yaml: "ubuntu-24.04.yaml" , instance name: "ubuntu-24.04", hostname: "lima-ubuntu-24-04"
+ if err := identifiers.Validate(s); err != nil {
+ return "", fmt.Errorf("filename %q is invalid: %w", yamlPath, err)
+ }
+ return s, nil
+}