Repository: lima-vm/lima
Pull request #3578 (2025-05-25 06:16:42)

Move `identifierutil` → `instance/hostname` package
Part of the crusade against gratuituous use of `util` package suffix.

The package only contains a single `HostnameFromInstName()` function.

It can't be moved into the `instance` package because that would create a circular dependency between `instance` and `cidata`.

AkihiroSuda: CI failing


Patch:
diff --git a/cmd/limactl/edit.go b/cmd/limactl/edit.go
index a411df71..b24e6141 100644
--- a/cmd/limactl/edit.go
+++ b/cmd/limactl/edit.go
@@ -13,6 +13,7 @@ import (
 	"github.com/lima-vm/lima/cmd/limactl/editflags"
 	"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"
@@ -45,28 +46,33 @@ func editAction(cmd *cobra.Command, args []string) error {
 	var filePath string
 	var err error
 	var inst *store.Instance
+	switch {
+	case limatmpl.SeemsYAMLPath(arg):
+		// absolute path is required for `limayaml.Validate`
+		filePath, err = filepath.Abs(arg)
+		if err != nil {
+			return err
+		}
+	default:
+		var instName string
+		if arg != "" {
+			instName = arg
+		} else {
+			instName = DefaultInstanceName
+		}
 
-	if arg == "" {
-		arg = DefaultInstanceName
-	}
-	if err := store.ValidateInstName(arg); err == nil {
-		inst, err = store.Inspect(arg)
+		inst, err = store.Inspect(instName)
 		if err != nil {
 			if errors.Is(err, os.ErrNotExist) {
-				return fmt.Errorf("instance %q not found", arg)
+				return fmt.Errorf("instance %q not found", instName)
 			}
 			return err
 		}
+
 		if inst.Status == store.StatusRunning {
 			return errors.New("cannot edit a running instance")
 		}
 		filePath = filepath.Join(inst.Dir, filenames.LimaYAML)
-	} else {
-		// absolute path is required for `limayaml.Validate`
-		filePath, err = filepath.Abs(arg)
-		if err != nil {
-			return err
-		}
 	}
 
 	yContent, err := os.ReadFile(filePath)
diff --git a/cmd/limactl/start.go b/cmd/limactl/start.go
index 5187de60..c09aaa85 100644
--- a/cmd/limactl/start.go
+++ b/cmd/limactl/start.go
@@ -7,10 +7,12 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"path"
 	"path/filepath"
 	"runtime"
 	"strings"
 
+	"github.com/containerd/containerd/identifiers"
 	"github.com/lima-vm/lima/cmd/limactl/editflags"
 	"github.com/lima-vm/lima/pkg/editutil"
 	"github.com/lima-vm/lima/pkg/instance"
@@ -116,13 +118,8 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
 	if err != nil {
 		return nil, err
 	}
-	if name != "" {
-		err := store.ValidateInstName(name)
-		if err != nil {
-			return nil, err
-		}
-	}
-	if isTemplateURL, templateName := limatmpl.SeemsTemplateURL(arg); isTemplateURL {
+	if isTemplateURL, templateURL := limatmpl.SeemsTemplateURL(arg); isTemplateURL {
+		templateName := path.Join(templateURL.Host, templateURL.Path)
 		switch templateName {
 		case "experimental/vz":
 			logrus.Warn("template://experimental/vz was merged into the default template in Lima v1.0. See also <https://lima-vm.io/docs/config/vmtype/>.")
@@ -147,25 +144,35 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
 		// 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 when reading template from stdin")
+			return nil, errors.New("cannot use --tty=true and read template from stdin together")
 		}
 		tty = false
 	}
-	var tmpl *limatmpl.Template
-	if err := store.ValidateInstName(arg); arg == "" || err == nil {
-		tmpl = &limatmpl.Template{Name: name}
+	tmpl, err := limatmpl.Read(cmd.Context(), name, arg)
+	if err != nil {
+		return nil, err
+	}
+	if len(tmpl.Bytes) > 0 {
+		if createOnly {
+			if _, err := store.Inspect(tmpl.Name); err == nil {
+				return nil, fmt.Errorf("instance %q already exists", tmpl.Name)
+			}
+		}
+	} else {
 		if arg == "" {
-			if name == "" {
+			if tmpl.Name == "" {
 				tmpl.Name = DefaultInstanceName
 			}
 		} else {
 			logrus.Debugf("interpreting argument %q as an instance name", arg)
-			if name != "" && name != arg {
+			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)
 			}
 			tmpl.Name = arg
 		}
-		// store.Inspect() will validate the template name (in case it has been set to arg)
+		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(tmpl.Name)
 		if err == nil {
 			if createOnly {
@@ -197,21 +204,7 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
 		if err != nil {
 			return nil, err
 		}
-	} else {
-		tmpl, err = limatmpl.Read(cmd.Context(), name, arg)
-		if err != nil {
-			return nil, err
-		}
-		if createOnly {
-			// store.Inspect() will also validate the instance name
-			if _, err := store.Inspect(tmpl.Name); err == nil {
-				return nil, fmt.Errorf("instance %q already exists", tmpl.Name)
-			}
-		} else if err := store.ValidateInstName(tmpl.Name); err != nil {
-			return nil, err
-		}
 	}
-
 	if err := tmpl.Embed(cmd.Context(), true, true); err != nil {
 		return nil, err
 	}
diff --git a/go.mod b/go.mod
index 9faca1a4..ad48c336 100644
--- a/go.mod
+++ b/go.mod
@@ -23,7 +23,7 @@ require (
 	github.com/foxcpp/go-mockdns v1.1.0
 	github.com/goccy/go-yaml v1.17.1
 	github.com/google/go-cmp v0.7.0
-	github.com/google/yamlfmt v0.17.0
+	github.com/google/yamlfmt v0.16.0
 	github.com/invopop/jsonschema v0.13.0
 	github.com/lima-vm/go-qcow2reader v0.6.0
 	github.com/lima-vm/sshocker v0.3.8 // gomodjail:unconfined
@@ -36,7 +36,7 @@ require (
 	github.com/opencontainers/go-digest v1.0.0
 	github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
 	github.com/rjeczalik/notify v0.9.3
-	github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
+	github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
 	github.com/sethvargo/go-password v0.3.1
 	github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af
 	github.com/spf13/cobra v1.9.1 // gomodjail:unconfined
@@ -62,6 +62,7 @@ require (
 	github.com/alecthomas/participle/v2 v2.1.4 // indirect
 	github.com/bahlo/generic-list-go v0.2.0 // indirect
 	github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect
+	github.com/braydonk/yaml v0.9.0 // indirect
 	github.com/buger/jsonparser v1.1.1 // indirect
 	github.com/containerd/errdefs v1.0.0 // indirect
 	github.com/containerd/log v0.1.0 // indirect
diff --git a/go.sum b/go.sum
index 73daa18e..30c2aadf 100644
--- a/go.sum
+++ b/go.sum
@@ -28,6 +28,8 @@ github.com/balajiv113/fd v0.0.0-20230330094840-143eec500f3e h1:IdMhFPEfTZQU971tI
 github.com/balajiv113/fd v0.0.0-20230330094840-143eec500f3e/go.mod h1:aXGMJsd3XrnUFTuyf/pTGg5jG6CY8JMZ5juywvShjgQ=
 github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q=
 github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
+github.com/braydonk/yaml v0.9.0 h1:ewGMrVmEVpsm3VwXQDR388sLg5+aQ8Yihp6/hc4m+h4=
+github.com/braydonk/yaml v0.9.0/go.mod h1:hcm3h581tudlirk8XEUPDBAimBPbmnL0Y45hCRl47N4=
 github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/cheggaaa/pb/v3 v3.1.7 h1:2FsIW307kt7A/rz/ZI2lvPO+v3wKazzE4K/0LtTWsOI=
@@ -127,8 +129,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/yamlfmt v0.17.0 h1:/tdp01rIlvLz3LgJ2NtMLnqgAadZm33P7GcPU680b+w=
-github.com/google/yamlfmt v0.17.0/go.mod h1:gs0UEklJOYkUJ+OOCG0hg9n+DzucKDPlJElTUasVNK8=
+github.com/google/yamlfmt v0.16.0 h1:5auoxqdx2CxOb022XGBElFFVH8uE/lAJDCWKRMq4mT8=
+github.com/google/yamlfmt v0.16.0/go.mod h1:/fF8jQmFopG3InQoWYG3gTORPXqLwNkcBqAT4UA4ab0=
 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
 github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
 github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
@@ -239,8 +241,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
-github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
-github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
 github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU=
 github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs=
 github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
diff --git a/pkg/cidata/cidata.go b/pkg/cidata/cidata.go
index a0e4370f..0b52e26f 100644
--- a/pkg/cidata/cidata.go
+++ b/pkg/cidata/cidata.go
@@ -21,7 +21,7 @@ import (
 
 	"github.com/docker/go-units"
 	"github.com/lima-vm/lima/pkg/debugutil"
-	"github.com/lima-vm/lima/pkg/identifierutil"
+	instance "github.com/lima-vm/lima/pkg/instance/hostname"
 	"github.com/lima-vm/lima/pkg/iso9660util"
 	"github.com/lima-vm/lima/pkg/limayaml"
 	"github.com/lima-vm/lima/pkg/localpathutil"
@@ -123,7 +123,7 @@ func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.L
 		Debug:              debugutil.Debug,
 		BootScripts:        bootScripts,
 		Name:               name,
-		Hostname:           identifierutil.HostnameFromInstName(name), // TODO: support customization
+		Hostname:           instance.HostnameFromInstName(name), // TODO: support customization
 		User:               *instConfig.User.Name,
 		Comment:            *instConfig.User.Comment,
 		Home:               *instConfig.User.Home,
diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go
index 36e6d243..921caa12 100644
--- a/pkg/hostagent/hostagent.go
+++ b/pkg/hostagent/hostagent.go
@@ -31,7 +31,7 @@ import (
 	hostagentapi "github.com/lima-vm/lima/pkg/hostagent/api"
 	"github.com/lima-vm/lima/pkg/hostagent/dns"
 	"github.com/lima-vm/lima/pkg/hostagent/events"
-	"github.com/lima-vm/lima/pkg/identifierutil"
+	instance "github.com/lima-vm/lima/pkg/instance/hostname"
 	"github.com/lima-vm/lima/pkg/limayaml"
 	"github.com/lima-vm/lima/pkg/networks"
 	"github.com/lima-vm/lima/pkg/osutil"
@@ -288,7 +288,7 @@ func (a *HostAgent) Run(ctx context.Context) error {
 	if limayaml.FirstUsernetIndex(a.instConfig) == -1 && *a.instConfig.HostResolver.Enabled {
 		hosts := a.instConfig.HostResolver.Hosts
 		hosts["host.lima.internal"] = networks.SlirpGateway
-		hostname := identifierutil.HostnameFromInstName(a.instName) // TODO: support customization
+		hostname := instance.HostnameFromInstName(a.instName) // TODO: support customization
 		hosts[hostname] = networks.SlirpIPAddress
 		srvOpts := dns.ServerOptions{
 			UDPPort: a.udpDNSLocalPort,
diff --git a/pkg/identifierutil/identifierutil.go b/pkg/instance/hostname/hostname.go
similarity index 54%
rename from pkg/identifierutil/identifierutil.go
rename to pkg/instance/hostname/hostname.go
index 8f950fce..38756c85 100644
--- a/pkg/identifierutil/identifierutil.go
+++ b/pkg/instance/hostname/hostname.go
@@ -1,10 +1,13 @@
 // SPDX-FileCopyrightText: Copyright The Lima Authors
 // SPDX-License-Identifier: Apache-2.0
 
-package identifierutil
+package instance
 
 import "strings"
 
+// HostnameFromInstName generates a hostname from an instance name by prefixing
+// it with "lima-" and replacing all dots and underscores with dashes.
+// E.g. "my_example.com" becomes "lima-my-example-com".
 func HostnameFromInstName(instName string) string {
 	s := strings.ReplaceAll(instName, ".", "-")
 	s = strings.ReplaceAll(s, "_", "-")
diff --git a/pkg/limatmpl/locator.go b/pkg/limatmpl/locator.go
index 218311af..3f2f2674 100644
--- a/pkg/limatmpl/locator.go
+++ b/pkg/limatmpl/locator.go
@@ -15,6 +15,7 @@ import (
 	"regexp"
 	"runtime"
 	"strings"
+	"unicode"
 
 	"github.com/containerd/containerd/identifiers"
 	"github.com/lima-vm/lima/pkg/ioutilx"
@@ -36,9 +37,10 @@ func Read(ctx context.Context, name, locator string) (*Template, error) {
 		return tmpl, nil
 	}
 
-	isTemplateURL, templateName := SeemsTemplateURL(locator)
+	isTemplateURL, templateURL := SeemsTemplateURL(locator)
 	switch {
 	case isTemplateURL:
+		templateName := path.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.yaml" , tmpl.Name = "centos-7"
@@ -93,12 +95,7 @@ func Read(ctx context.Context, name, locator string) (*Template, error) {
 		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)
-		}
-	default:
+	case SeemsFilePath(locator):
 		if tmpl.Name == "" {
 			tmpl.Name, err = InstNameFromYAMLPath(locator)
 			if err != nil {
@@ -119,6 +116,11 @@ func Read(ctx context.Context, name, locator string) (*Template, error) {
 		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)
+		}
 	}
 	// The only reason not to call tmpl.UseAbsLocators() here is that `limactl tmpl copy --verbatim …`
 	// should create an unmodified copy of the template.
@@ -231,20 +233,14 @@ func InstNameFromImageURL(locator, imageArch string) string {
 	return name
 }
 
-// SeemsTemplateURL returns true if the arg is a URL using the template scheme.
-// When it returns true, it also returns the template name.
-func SeemsTemplateURL(arg string) (isTemplate bool, templateName string) {
+func SeemsTemplateURL(arg string) (bool, *url.URL) {
 	u, err := url.Parse(arg)
 	if err != nil {
-		return false, ""
+		return false, u
 	}
-	if u.Scheme == "template" {
-		return true, path.Join(u.Host, u.Path)
-	}
-	return false, ""
+	return u.Scheme == "template", u
 }
 
-// SeemsHTTPURL returns true if the arg is a URL using the http or https scheme.
 func SeemsHTTPURL(arg string) bool {
 	u, err := url.Parse(arg)
 	if err != nil {
@@ -256,7 +252,6 @@ func SeemsHTTPURL(arg string) bool {
 	return true
 }
 
-// SeemsFileURL returns true if the arg is a URL using the file scheme.
 func SeemsFileURL(arg string) bool {
 	u, err := url.Parse(arg)
 	if err != nil {
@@ -265,6 +260,28 @@ func SeemsFileURL(arg string) bool {
 	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")
+}
+
+// SeemsFilePath returns true if arg either contains a path separator or has a file extension that
+// does not start with a digit. `my.yaml` is a file path, `ubuntu-20.10` is not.
+func SeemsFilePath(arg string) bool {
+	// Single-letter schemes will be drive names on Windows, e.g. "c:/foo.yaml"
+	if u, err := url.Parse(arg); err == nil && len(u.Scheme) > 1 {
+		return false
+	}
+	if strings.ContainsRune(arg, '/') || strings.ContainsRune(arg, filepath.Separator) {
+		return true
+	}
+	ext := filepath.Ext(arg)
+	return len(ext) > 1 && !unicode.IsDigit(rune(ext[1]))
+}
+
 func InstNameFromURL(urlStr string) (string, error) {
 	u, err := url.Parse(urlStr)
 	if err != nil {
diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go
index 1736b96f..047e1d1f 100644
--- a/pkg/limayaml/defaults.go
+++ b/pkg/limayaml/defaults.go
@@ -24,13 +24,8 @@ import (
 	"github.com/coreos/go-semver/semver"
 	"github.com/docker/go-units"
 	"github.com/goccy/go-yaml"
+	instance "github.com/lima-vm/lima/pkg/instance/hostname"
 	"github.com/lima-vm/lima/pkg/ioutilx"
-	"github.com/lima-vm/lima/pkg/version"
-	"github.com/pbnjay/memory"
-	"github.com/sirupsen/logrus"
-	"golang.org/x/sys/cpu"
-
-	"github.com/lima-vm/lima/pkg/identifierutil"
 	"github.com/lima-vm/lima/pkg/localpathutil"
 	. "github.com/lima-vm/lima/pkg/must"
 	"github.com/lima-vm/lima/pkg/networks"
@@ -38,7 +33,11 @@ import (
 	"github.com/lima-vm/lima/pkg/ptr"
 	"github.com/lima-vm/lima/pkg/store/dirnames"
 	"github.com/lima-vm/lima/pkg/store/filenames"
+	"github.com/lima-vm/lima/pkg/version"
 	"github.com/lima-vm/lima/pkg/version/versionutil"
+	"github.com/pbnjay/memory"
+	"github.com/sirupsen/logrus"
+	"golang.org/x/sys/cpu"
 )
 
 const (
@@ -944,7 +943,7 @@ func executeGuestTemplate(format, instDir string, user User, param map[string]st
 		name := filepath.Base(instDir)
 		data := map[string]any{
 			"Name":     name,
-			"Hostname": identifierutil.HostnameFromInstName(name), // TODO: support customization
+			"Hostname": instance.HostnameFromInstName(name), // TODO: support customization
 			"UID":      *user.UID,
 			"User":     *user.Name,
 			"Home":     *user.Home,
diff --git a/pkg/sshutil/format.go b/pkg/sshutil/format.go
index c2efda18..652b5429 100644
--- a/pkg/sshutil/format.go
+++ b/pkg/sshutil/format.go
@@ -8,7 +8,7 @@ import (
 	"io"
 	"strings"
 
-	"github.com/lima-vm/lima/pkg/identifierutil"
+	instance "github.com/lima-vm/lima/pkg/instance/hostname"
 )
 
 // FormatT specifies the format type.
@@ -42,11 +42,11 @@ const (
 	//	  Port 60022
 	FormatConfig = FormatT("config")
 
-// TODO: consider supporting "url" format (ssh://USER@HOSTNAME:PORT)
-//
-// TODO: consider supporting "json" format
-// It is unclear whether we can just map ssh "config" into JSON, as "config" has duplicated keys.
-// (JSON supports duplicated keys too, but not all JSON implementations expect JSON with duplicated keys)
+	// TODO: consider supporting "url" format (ssh://USER@HOSTNAME:PORT).
+	//
+	// TODO: consider supporting "json" format.
+	// It is unclear whether we can just map ssh "config" into JSON, as "config" has duplicated keys.
+	// (JSON supports duplicated keys too, but not all JSON implementations expect JSON with duplicated keys).
 )
 
 // Formats is the list of the supported formats.
@@ -62,7 +62,7 @@ func quoteOption(o string) string {
 
 // Format formats the ssh options.
 func Format(w io.Writer, sshPath, instName string, format FormatT, opts []string) error {
-	fakeHostname := identifierutil.HostnameFromInstName(instName) // TODO: support customization
+	fakeHostname := instance.HostnameFromInstName(instName) // TODO: support customization
 	switch format {
 	case FormatCmd:
 		args := []string{sshPath}
diff --git a/pkg/store/instance.go b/pkg/store/instance.go
index 2af8f814..c8f71b8a 100644
--- a/pkg/store/instance.go
+++ b/pkg/store/instance.go
@@ -20,7 +20,7 @@ import (
 
 	"github.com/docker/go-units"
 	hostagentclient "github.com/lima-vm/lima/pkg/hostagent/api/client"
-	"github.com/lima-vm/lima/pkg/identifierutil"
+	instancehostname "github.com/lima-vm/lima/pkg/instance/hostname"
 	"github.com/lima-vm/lima/pkg/limayaml"
 	"github.com/lima-vm/lima/pkg/store/dirnames"
 	"github.com/lima-vm/lima/pkg/store/filenames"
@@ -73,7 +73,7 @@ func Inspect(instName string) (*Instance, error) {
 	inst := &Instance{
 		Name: instName,
 		// TODO: support customizing hostname
-		Hostname: identifierutil.HostnameFromInstName(instName),
+		Hostname: instancehostname.HostnameFromInstName(instName),
 		Status:   StatusUnknown,
 	}
 	// InstanceDir validates the instName but does not check whether the instance exists
diff --git a/pkg/store/store.go b/pkg/store/store.go
index 58669dd7..60732d10 100644
--- a/pkg/store/store.go
+++ b/pkg/store/store.go
@@ -5,7 +5,6 @@ package store
 
 import (
 	"errors"
-	"fmt"
 	"os"
 	"path/filepath"
 	"strings"
@@ -46,19 +45,6 @@ func Validate() error {
 	return nil
 }
 
-// ValidateInstName checks if the name is a valid instance name. For this it needs to
-// be a valid identifier, and not end in .yml or .yaml (case insensitively).
-func ValidateInstName(name string) error {
-	if err := identifiers.Validate(name); err != nil {
-		return fmt.Errorf("instance name %q is not a valid identifier: %w", name, err)
-	}
-	lower := strings.ToLower(name)
-	if strings.HasSuffix(lower, ".yml") || strings.HasSuffix(lower, ".yaml") {
-		return fmt.Errorf("instance name %q must not end with .yml or .yaml suffix", name)
-	}
-	return nil
-}
-
 // Instances returns the names of the instances under LimaDir.
 func Instances() ([]string, error) {
 	limaDir, err := dirnames.LimaDir()
@@ -107,7 +93,7 @@ func Disks() ([]string, error) {
 // InstanceDir returns the instance dir.
 // InstanceDir does not check whether the instance exists.
 func InstanceDir(name string) (string, error) {
-	if err := ValidateInstName(name); err != nil {
+	if err := identifiers.Validate(name); err != nil {
 		return "", err
 	}
 	limaDir, err := dirnames.LimaDir()


Test patch:
diff --git a/pkg/identifierutil/identifierutil_test.go b/pkg/identifierutil/identifierutil_test.go
deleted file mode 100644
index a8ec2a74..00000000
--- a/pkg/identifierutil/identifierutil_test.go
+++ /dev/null
@@ -1,16 +0,0 @@
-// SPDX-FileCopyrightText: Copyright The Lima Authors
-// SPDX-License-Identifier: Apache-2.0
-
-package identifierutil
-
-import (
-	"testing"
-
-	"gotest.tools/v3/assert"
-)
-
-func TestHostnameFromInstName(t *testing.T) {
-	assert.Equal(t, "lima-default", HostnameFromInstName("default"))
-	assert.Equal(t, "lima-ubuntu-24-04", HostnameFromInstName("ubuntu-24.04"))
-	assert.Equal(t, "lima-foo-bar-baz", HostnameFromInstName("foo_bar.baz"))
-}
diff --git a/pkg/instance/hostname/hostname_test.go b/pkg/instance/hostname/hostname_test.go
new file mode 100644
index 00000000..69620638
--- /dev/null
+++ b/pkg/instance/hostname/hostname_test.go
@@ -0,0 +1,17 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package instance_test
+
+import (
+	"testing"
+
+	instance "github.com/lima-vm/lima/pkg/instance/hostname"
+	"gotest.tools/v3/assert"
+)
+
+func TestHostnameFromInstName(t *testing.T) {
+	assert.Equal(t, instance.HostnameFromInstName("default"), "lima-default")
+	assert.Equal(t, instance.HostnameFromInstName("ubuntu-24.04"), "lima-ubuntu-24-04")
+	assert.Equal(t, instance.HostnameFromInstName("foo_bar.baz"), "lima-foo-bar-baz")
+}
diff --git a/pkg/limatmpl/locator_test.go b/pkg/limatmpl/locator_test.go
index 6c2c03d8..dcf510c2 100644
--- a/pkg/limatmpl/locator_test.go
+++ b/pkg/limatmpl/locator_test.go
@@ -61,64 +61,3 @@ func TestInstNameFromImageURL(t *testing.T) {
 		assert.Equal(t, name, "rocky-8-9.10")
 	})
 }
-
-func TestSeemsTemplateURL(t *testing.T) {
-	arg := "template://foo/bar"
-	t.Run(arg, func(t *testing.T) {
-		is, name := limatmpl.SeemsTemplateURL(arg)
-		assert.Equal(t, is, true)
-		assert.Equal(t, name, "foo/bar")
-	})
-	notTemplateURLs := []string{
-		"file:///foo",
-		"http://foo",
-		"https://foo",
-		"foo",
-	}
-	for _, arg := range notTemplateURLs {
-		t.Run(arg, func(t *testing.T) {
-			is, _ := limatmpl.SeemsTemplateURL(arg)
-			assert.Equal(t, is, false)
-		})
-	}
-}
-
-func TestSeemsHTTPURL(t *testing.T) {
-	httpURLs := []string{
-		"http://foo/",
-		"https://foo/",
-	}
-	for _, arg := range httpURLs {
-		t.Run(arg, func(t *testing.T) {
-			assert.Equal(t, limatmpl.SeemsHTTPURL(arg), true)
-		})
-	}
-	notHTTPURLs := []string{
-		"file:///foo",
-		"template://foo",
-		"foo",
-	}
-	for _, arg := range notHTTPURLs {
-		t.Run(arg, func(t *testing.T) {
-			assert.Equal(t, limatmpl.SeemsHTTPURL(arg), false)
-		})
-	}
-}
-
-func TestSeemsFileURL(t *testing.T) {
-	arg := "file:///foo"
-	t.Run(arg, func(t *testing.T) {
-		assert.Equal(t, limatmpl.SeemsFileURL(arg), true)
-	})
-	notFileURLs := []string{
-		"http://foo",
-		"https://foo",
-		"template://foo",
-		"foo",
-	}
-	for _, arg := range notFileURLs {
-		t.Run(arg, func(t *testing.T) {
-			assert.Equal(t, limatmpl.SeemsFileURL(arg), false)
-		})
-	}
-}
diff --git a/pkg/store/store_test.go b/pkg/store/store_test.go
deleted file mode 100644
index 4dfe1eea..00000000
--- a/pkg/store/store_test.go
+++ /dev/null
@@ -1,61 +0,0 @@
-// SPDX-FileCopyrightText: Copyright The Lima Authors
-// SPDX-License-Identifier: Apache-2.0
-
-package store
-
-import (
-	"testing"
-
-	"gotest.tools/v3/assert"
-)
-
-func TestValidateInstName(t *testing.T) {
-	instNames := []string{
-		"default",
-		"Ubuntu-20.04",
-		"example.com",
-		"under_score",
-		"1-2_3.4",
-		"yml",
-		"yaml",
-		"foo.yaml.com",
-	}
-	for _, arg := range instNames {
-		t.Run(arg, func(t *testing.T) {
-			err := ValidateInstName(arg)
-			assert.NilError(t, err)
-		})
-	}
-	invalidIdentifiers := []string{
-		"",
-		"my/instance",
-		"my\\instance",
-		"c:default",
-		"dot.",
-		".dot",
-		"dot..dot",
-		"underscore_",
-		"_underscore",
-		"underscore__underscore",
-		"dash-",
-		"-dash",
-		"dash--dash",
-	}
-	for _, arg := range invalidIdentifiers {
-		t.Run(arg, func(t *testing.T) {
-			err := ValidateInstName(arg)
-			assert.ErrorContains(t, err, "not a valid identifier")
-		})
-	}
-	yamlNames := []string{
-		"default.yaml",
-		"MY.YAML",
-		"My.YmL",
-	}
-	for _, arg := range yamlNames {
-		t.Run(arg, func(t *testing.T) {
-			err := ValidateInstName(arg)
-			assert.ErrorContains(t, err, "must not end with .y")
-		})
-	}
-}