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 +}