From c3e92706cdc5bb968703c5038f5726f8c72f848f Mon Sep 17 00:00:00 2001 From: Jan Dubois Date: Fri, 1 Nov 2024 16:41:54 -0700 Subject: [PATCH] Make guest username, uid, home directory configurable Also disallows the "admin" username by default (because it is a builtin user in Ubuntu), but can be overridden by setting it explicitly in lima.yaml. Signed-off-by: Jan Dubois --- .golangci.yml | 3 + cmd/limactl/copy.go | 21 +-- cmd/limactl/edit.go | 2 +- cmd/limactl/shell.go | 1 + cmd/limactl/show-ssh.go | 1 + cmd/limactl/tunnel.go | 1 + hack/test-templates.sh | 7 + hack/test-templates/test-misc.yaml | 6 + .../cidata.TEMPLATE.d/boot/02-wsl2-setup.sh | 2 +- pkg/cidata/cidata.TEMPLATE.d/lima.env | 2 +- pkg/cidata/cidata.TEMPLATE.d/user-data | 4 +- pkg/cidata/cidata.go | 18 +- pkg/cidata/template.go | 8 +- pkg/cidata/template_test.go | 20 +-- pkg/hostagent/hostagent.go | 5 +- pkg/instance/create.go | 2 +- pkg/limayaml/defaults.go | 115 ++++++++++--- pkg/limayaml/defaults_test.go | 44 +++-- pkg/limayaml/limayaml.go | 8 + pkg/limayaml/load.go | 13 +- pkg/limayaml/validate.go | 12 +- pkg/must/must.go | 8 + pkg/osutil/user.go | 160 +++++++++--------- pkg/osutil/user_test.go | 49 ++++-- pkg/sshutil/sshutil.go | 8 +- templates/default.yaml | 22 ++- .../content/en/docs/dev/internals/_index.md | 2 +- 27 files changed, 346 insertions(+), 198 deletions(-) create mode 100644 pkg/must/must.go diff --git a/.golangci.yml b/.golangci.yml index 82a57f202fe..4f67d2c1f4d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -121,6 +121,9 @@ linters-settings: - name: context-keys-type - name: deep-exit - name: dot-imports + arguments: + - allowedPackages: + - github.com/lima-vm/lima/pkg/must - name: empty-block - name: error-naming - name: error-return diff --git a/cmd/limactl/copy.go b/cmd/limactl/copy.go index cbee6d21127..ae9a295d2bf 100644 --- a/cmd/limactl/copy.go +++ b/cmd/limactl/copy.go @@ -8,7 +8,6 @@ import ( "strings" "github.com/coreos/go-semver/semver" - "github.com/lima-vm/lima/pkg/osutil" "github.com/lima-vm/lima/pkg/sshutil" "github.com/lima-vm/lima/pkg/store" "github.com/sirupsen/logrus" @@ -48,11 +47,7 @@ func copyAction(cmd *cobra.Command, args []string) error { if err != nil { return err } - u, err := osutil.LimaUser(false) - if err != nil { - return err - } - instDirs := make(map[string]string) + instances := make(map[string]*store.Instance) scpFlags := []string{} scpArgs := []string{} debug, err := cmd.Flags().GetBool("debug") @@ -85,28 +80,28 @@ func copyAction(cmd *cobra.Command, args []string) error { } if legacySSH { scpFlags = append(scpFlags, "-P", fmt.Sprintf("%d", inst.SSHLocalPort)) - scpArgs = append(scpArgs, fmt.Sprintf("%s@127.0.0.1:%s", u.Username, path[1])) + scpArgs = append(scpArgs, fmt.Sprintf("%s@127.0.0.1:%s", *inst.Config.User.Name, path[1])) } else { - scpArgs = append(scpArgs, fmt.Sprintf("scp://%s@127.0.0.1:%d/%s", u.Username, inst.SSHLocalPort, path[1])) + scpArgs = append(scpArgs, fmt.Sprintf("scp://%s@127.0.0.1:%d/%s", *inst.Config.User.Name, inst.SSHLocalPort, path[1])) } - instDirs[instName] = inst.Dir + instances[instName] = inst default: return fmt.Errorf("path %q contains multiple colons", arg) } } - if legacySSH && len(instDirs) > 1 { + if legacySSH && len(instances) > 1 { return fmt.Errorf("more than one (instance) host is involved in this command, this is only supported for openSSH v8.0 or higher") } scpFlags = append(scpFlags, "-3", "--") scpArgs = append(scpFlags, scpArgs...) var sshOpts []string - if len(instDirs) == 1 { + if len(instances) == 1 { // Only one (instance) host is involved; we can use the instance-specific // arguments such as ControlPath. This is preferred as we can multiplex // sessions without re-authenticating (MaxSessions permitting). - for _, instDir := range instDirs { - sshOpts, err = sshutil.SSHOpts(instDir, false, false, false, false) + for _, inst := range instances { + sshOpts, err = sshutil.SSHOpts(inst.Dir, *inst.Config.User.Name, false, false, false, false) if err != nil { return err } diff --git a/cmd/limactl/edit.go b/cmd/limactl/edit.go index 70a54a858f6..3d0dde19935 100644 --- a/cmd/limactl/edit.go +++ b/cmd/limactl/edit.go @@ -115,7 +115,7 @@ func editAction(cmd *cobra.Command, args []string) error { logrus.Info("Aborting, no changes made to the instance") return nil } - y, err := limayaml.Load(yBytes, filePath) + y, err := limayaml.LoadWithWarnings(yBytes, filePath) if err != nil { return err } diff --git a/cmd/limactl/shell.go b/cmd/limactl/shell.go index 694413923d8..65d8fad7b29 100644 --- a/cmd/limactl/shell.go +++ b/cmd/limactl/shell.go @@ -167,6 +167,7 @@ func shellAction(cmd *cobra.Command, args []string) error { sshOpts, err := sshutil.SSHOpts( inst.Dir, + *inst.Config.User.Name, *inst.Config.SSH.LoadDotSSHPubKeys, *inst.Config.SSH.ForwardAgent, *inst.Config.SSH.ForwardX11, diff --git a/cmd/limactl/show-ssh.go b/cmd/limactl/show-ssh.go index 63e203c5c14..04cbf59088a 100644 --- a/cmd/limactl/show-ssh.go +++ b/cmd/limactl/show-ssh.go @@ -90,6 +90,7 @@ func showSSHAction(cmd *cobra.Command, args []string) error { filepath.Join(inst.Dir, filenames.SSHConfig), inst.Hostname) opts, err := sshutil.SSHOpts( inst.Dir, + *inst.Config.User.Name, *inst.Config.SSH.LoadDotSSHPubKeys, *inst.Config.SSH.ForwardAgent, *inst.Config.SSH.ForwardX11, diff --git a/cmd/limactl/tunnel.go b/cmd/limactl/tunnel.go index c84c9b25ed5..446089c65e5 100644 --- a/cmd/limactl/tunnel.go +++ b/cmd/limactl/tunnel.go @@ -107,6 +107,7 @@ func tunnelAction(cmd *cobra.Command, args []string) error { sshOpts, err := sshutil.SSHOpts( inst.Dir, + *inst.Config.User.Name, *inst.Config.SSH.LoadDotSSHPubKeys, *inst.Config.SSH.ForwardAgent, *inst.Config.SSH.ForwardX11, diff --git a/hack/test-templates.sh b/hack/test-templates.sh index 37455a2a1f9..57ff37a450b 100755 --- a/hack/test-templates.sh +++ b/hack/test-templates.sh @@ -37,6 +37,7 @@ declare -A CHECKS=( ["mount-path-with-spaces"]="" ["provision-ansible"]="" ["param-env-variables"]="" + ["set-user"]="" ) case "$NAME" in @@ -63,6 +64,7 @@ case "$NAME" in CHECKS["mount-path-with-spaces"]="1" CHECKS["provision-ansible"]="1" CHECKS["param-env-variables"]="1" + CHECKS["set-user"]="1" ;; "docker") CONTAINER_ENGINE="docker" @@ -172,6 +174,11 @@ if [[ -n ${CHECKS["param-env-variables"]} ]]; then limactl shell "$NAME" test -e /tmp/param-user fi +if [[ -n ${CHECKS["set-user"]} ]]; then + INFO 'Testing that user settings can be provided by lima.yaml' + limactl shell "$NAME" grep "^john:x:4711:4711:John Doe:/home/john-john" /etc/passwd +fi + INFO "Testing proxy settings are imported" got=$(limactl shell "$NAME" env | grep FTP_PROXY) # Expected: FTP_PROXY is set in addition to ftp_proxy, localhost is replaced diff --git a/hack/test-templates/test-misc.yaml b/hack/test-templates/test-misc.yaml index 6284c4acc67..2137c5f3e69 100644 --- a/hack/test-templates/test-misc.yaml +++ b/hack/test-templates/test-misc.yaml @@ -58,3 +58,9 @@ probes: # $ limactl disk create data --size 10G additionalDisks: - "data" + +user: + name: john + comment: John Doe + home: "/home/{{.User}}-{{.User}}" + uid: 4711 diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-setup.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-setup.sh index ab942215a5d..e3059ea2661 100755 --- a/pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-setup.sh +++ b/pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-setup.sh @@ -4,7 +4,7 @@ [ "$LIMA_CIDATA_VMTYPE" = "wsl2" ] || exit 0 # create user -sudo useradd -u "${LIMA_CIDATA_UID}" "${LIMA_CIDATA_USER}" -c "${LIMA_CIDATA_GECOS}" -d "${LIMA_CIDATA_HOME}" +sudo useradd -u "${LIMA_CIDATA_UID}" "${LIMA_CIDATA_USER}" -c "${LIMA_CIDATA_COMMENT}" -d "${LIMA_CIDATA_HOME}" sudo mkdir "${LIMA_CIDATA_HOME}"/.ssh/ sudo cp "${LIMA_CIDATA_MNT}"/ssh_authorized_keys "${LIMA_CIDATA_HOME}"/.ssh/authorized_keys sudo chown "${LIMA_CIDATA_USER}" "${LIMA_CIDATA_HOME}"/.ssh/authorized_keys diff --git a/pkg/cidata/cidata.TEMPLATE.d/lima.env b/pkg/cidata/cidata.TEMPLATE.d/lima.env index 6666b245540..3ff899fda32 100644 --- a/pkg/cidata/cidata.TEMPLATE.d/lima.env +++ b/pkg/cidata/cidata.TEMPLATE.d/lima.env @@ -2,7 +2,7 @@ LIMA_CIDATA_DEBUG={{ .Debug }} LIMA_CIDATA_NAME={{ .Name }} LIMA_CIDATA_USER={{ .User }} LIMA_CIDATA_UID={{ .UID }} -LIMA_CIDATA_GECOS={{ .GECOS }} +LIMA_CIDATA_COMMENT={{ .Comment }} LIMA_CIDATA_HOME={{ .Home}} LIMA_CIDATA_HOSTHOME_MOUNTPOINT={{ .HostHomeMountPoint }} LIMA_CIDATA_MOUNTS={{ len .Mounts }} diff --git a/pkg/cidata/cidata.TEMPLATE.d/user-data b/pkg/cidata/cidata.TEMPLATE.d/user-data index d8ea69e8f20..13a98c78181 100644 --- a/pkg/cidata/cidata.TEMPLATE.d/user-data +++ b/pkg/cidata/cidata.TEMPLATE.d/user-data @@ -30,8 +30,8 @@ timezone: {{.TimeZone}} users: - name: "{{.User}}" uid: "{{.UID}}" -{{- if .GECOS }} - gecos: {{ printf "%q" .GECOS }} +{{- if .Comment }} + gecos: {{ printf "%q" .Comment }} {{- end }} homedir: "{{.Home}}" shell: /bin/bash diff --git a/pkg/cidata/cidata.go b/pkg/cidata/cidata.go index b0e60db8fde..a3e46e8620e 100644 --- a/pkg/cidata/cidata.go +++ b/pkg/cidata/cidata.go @@ -11,7 +11,6 @@ import ( "path" "path/filepath" "slices" - "strconv" "strings" "time" @@ -117,23 +116,15 @@ func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.L if err := limayaml.Validate(instConfig, false); err != nil { return nil, err } - u, err := osutil.LimaUser(true) - if err != nil { - return nil, err - } - uid, err := strconv.Atoi(u.Uid) - if err != nil { - return nil, err - } args := TemplateArgs{ Debug: debugutil.Debug, BootScripts: bootScripts, Name: name, Hostname: identifierutil.HostnameFromInstName(name), // TODO: support customization - User: u.Username, - UID: uid, - GECOS: u.Name, - Home: fmt.Sprintf("/home/%s.linux", u.Username), + User: *instConfig.User.Name, + Comment: *instConfig.User.Comment, + Home: *instConfig.User.Home, + UID: *instConfig.User.UID, GuestInstallPrefix: *instConfig.GuestInstallPrefix, UpgradePackages: *instConfig.UpgradePackages, Containerd: Containerd{System: *instConfig.Containerd.System, User: *instConfig.Containerd.User}, @@ -151,6 +142,7 @@ func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.L firstUsernetIndex := limayaml.FirstUsernetIndex(instConfig) var subnet net.IP + var err error if firstUsernetIndex != -1 { usernetName := instConfig.Networks[firstUsernetIndex].Lima diff --git a/pkg/cidata/template.go b/pkg/cidata/template.go index a927f839509..e035a18f777 100644 --- a/pkg/cidata/template.go +++ b/pkg/cidata/template.go @@ -11,7 +11,6 @@ import ( "github.com/lima-vm/lima/pkg/iso9660util" "github.com/containerd/containerd/identifiers" - "github.com/lima-vm/lima/pkg/osutil" "github.com/lima-vm/lima/pkg/textutil" ) @@ -59,7 +58,7 @@ type TemplateArgs struct { Hostname string // instance hostname IID string // instance id User string // user name - GECOS string // user information + Comment string // user information Home string // home directory UID int SSHPubKeys []string @@ -97,9 +96,8 @@ func ValidateTemplateArgs(args *TemplateArgs) error { if err := identifiers.Validate(args.Name); err != nil { return err } - if !osutil.ValidateUsername(args.User) { - return errors.New("field User must be valid linux username") - } + // args.User is intentionally not validated here; the user can override with any name they want + // limayaml.FillDefault will validate the default (local) username, but not an explicit setting if args.User == "root" { return errors.New("field User must not be \"root\"") } diff --git a/pkg/cidata/template_test.go b/pkg/cidata/template_test.go index dbe848ac06d..e05abd3e7e9 100644 --- a/pkg/cidata/template_test.go +++ b/pkg/cidata/template_test.go @@ -12,11 +12,11 @@ var defaultRemoveDefaults = false func TestConfig(t *testing.T) { args := &TemplateArgs{ - Name: "default", - User: "foo", - UID: 501, - GECOS: "Foo", - Home: "/home/foo.linux", + Name: "default", + User: "foo", + UID: 501, + Comment: "Foo", + Home: "/home/foo.linux", SSHPubKeys: []string{ "ssh-rsa dummy foo@example.com", }, @@ -30,11 +30,11 @@ func TestConfig(t *testing.T) { func TestConfigCACerts(t *testing.T) { args := &TemplateArgs{ - Name: "default", - User: "foo", - UID: 501, - GECOS: "Foo", - Home: "/home/foo.linux", + Name: "default", + User: "foo", + UID: 501, + Comment: "Foo", + Home: "/home/foo.linux", SSHPubKeys: []string{ "ssh-rsa dummy foo@example.com", }, diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index 0706b6cc730..8d7a8d6830e 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -144,6 +144,7 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt sshOpts, err := sshutil.SSHOpts( inst.Dir, + *inst.Config.User.Name, *inst.Config.SSH.LoadDotSSHPubKeys, *inst.Config.SSH.ForwardAgent, *inst.Config.SSH.ForwardX11, @@ -182,13 +183,13 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt // Block ports 22 and sshLocalPort on all IPs for _, port := range []int{sshGuestPort, sshLocalPort} { rule := limayaml.PortForward{GuestIP: net.IPv4zero, GuestPort: port, Ignore: true} - limayaml.FillPortForwardDefaults(&rule, inst.Dir, inst.Param) + limayaml.FillPortForwardDefaults(&rule, inst.Dir, inst.Config.User, inst.Param) rules = append(rules, rule) } rules = append(rules, inst.Config.PortForwards...) // Default forwards for all non-privileged ports from "127.0.0.1" and "::1" rule := limayaml.PortForward{} - limayaml.FillPortForwardDefaults(&rule, inst.Dir, inst.Param) + limayaml.FillPortForwardDefaults(&rule, inst.Dir, inst.Config.User, inst.Param) rules = append(rules, rule) limaDriver := driverutil.CreateTargetDriverInstance(&driver.BaseDriver{ diff --git a/pkg/instance/create.go b/pkg/instance/create.go index 6a3f91d1c01..4941efbcf41 100644 --- a/pkg/instance/create.go +++ b/pkg/instance/create.go @@ -41,7 +41,7 @@ func Create(ctx context.Context, instName string, instConfig []byte, saveBrokenY } // limayaml.Load() needs to pass the store file path to limayaml.FillDefault() to calculate default MAC addresses filePath := filepath.Join(instDir, filenames.LimaYAML) - loadedInstConfig, err := limayaml.Load(instConfig, filePath) + loadedInstConfig, err := limayaml.LoadWithWarnings(instConfig, filePath) if err != nil { return nil, err } diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go index 3d2a89a3296..4c5bd9a414a 100644 --- a/pkg/limayaml/defaults.go +++ b/pkg/limayaml/defaults.go @@ -8,6 +8,7 @@ import ( "fmt" "net" "os" + "os/user" "path/filepath" "runtime" "slices" @@ -18,11 +19,13 @@ import ( "github.com/coreos/go-semver/semver" "github.com/docker/go-units" "github.com/goccy/go-yaml" + "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/must" "github.com/lima-vm/lima/pkg/networks" "github.com/lima-vm/lima/pkg/osutil" "github.com/lima-vm/lima/pkg/ptr" @@ -43,7 +46,12 @@ const ( DefaultVirtiofsQueueSize int = 1024 ) -var IPv4loopback1 = net.IPv4(127, 0, 0, 1) +var ( + IPv4loopback1 = net.IPv4(127, 0, 0, 1) + + userHomeDir = Must(os.UserHomeDir()) + currentUser = Must(user.Current()) +) func defaultCPUType() CPUType { cpuType := map[Arch]string{ @@ -171,17 +179,73 @@ func defaultGuestInstallPrefix() string { // - Networks are appended in d, y, o order // - DNS are picked from the highest priority where DNS is not empty. // - CACertificates Files and Certs are uniquely appended in d, y, o order -func FillDefault(y, d, o *LimaYAML, filePath string) { +func FillDefault(y, d, o *LimaYAML, filePath string, warn bool) { instDir := filepath.Dir(filePath) // existingLimaVersion can be empty if the instance was created with Lima prior to v0.20, - // or, when editing a template file without an instance (`limactl edit foo.yaml`) var existingLimaVersion string - limaVersionFile := filepath.Join(instDir, filenames.LimaVersion) - if b, err := os.ReadFile(limaVersionFile); err == nil { - existingLimaVersion = strings.TrimSpace(string(b)) - } else if !errors.Is(err, os.ErrNotExist) { - logrus.WithError(err).Warnf("Failed to read %q", limaVersionFile) + if !isExistingInstanceDir(instDir) { + existingLimaVersion = version.Version + } else { + limaVersionFile := filepath.Join(instDir, filenames.LimaVersion) + if b, err := os.ReadFile(limaVersionFile); err == nil { + existingLimaVersion = strings.TrimSpace(string(b)) + } else if !errors.Is(err, os.ErrNotExist) { + logrus.WithError(err).Warnf("Failed to read %q", limaVersionFile) + } + } + + if y.User.Name == nil { + y.User.Name = d.User.Name + } + if y.User.Comment == nil { + y.User.Comment = d.User.Comment + } + if y.User.Home == nil { + y.User.Home = d.User.Home + } + if y.User.UID == nil { + y.User.UID = d.User.UID + } + if o.User.Name != nil { + y.User.Name = o.User.Name + } + if o.User.Comment != nil { + y.User.Comment = o.User.Comment + } + if o.User.Home != nil { + y.User.Home = o.User.Home + } + if o.User.UID != nil { + y.User.UID = o.User.UID + } + if y.User.Name == nil { + y.User.Name = ptr.Of(osutil.LimaUser(existingLimaVersion, warn).Username) + warn = false + } + if y.User.Comment == nil { + y.User.Comment = ptr.Of(osutil.LimaUser(existingLimaVersion, warn).Name) + warn = false + } + if y.User.Home == nil { + y.User.Home = ptr.Of(osutil.LimaUser(existingLimaVersion, warn).HomeDir) + warn = false + } + if y.User.UID == nil { + uidString := osutil.LimaUser(existingLimaVersion, warn).Uid + if uid, err := strconv.ParseUint(uidString, 10, 32); err == nil { + y.User.UID = ptr.Of(int(uid)) + } else { + // This should never happen; LimaUser() makes sure that .Uid is numeric + logrus.WithError(err).Warnf("Can't parse `user.uid` %q", uidString) + y.User.UID = ptr.Of(1000) + } + // warn = false + } + if out, err := executeGuestTemplate(*y.User.Home, instDir, y.User, y.Param); err == nil { + y.User.Home = ptr.Of(out.String()) + } else { + logrus.WithError(err).Warnf("Couldn't process `user.home` value %q as a template", *y.User.Home) } if y.VMType == nil { @@ -406,7 +470,7 @@ func FillDefault(y, d, o *LimaYAML, filePath string) { if provision.Mode == ProvisionModeDependency && provision.SkipDefaultDependencyResolution == nil { provision.SkipDefaultDependencyResolution = ptr.Of(false) } - if out, err := executeGuestTemplate(provision.Script, instDir, y.Param); err == nil { + if out, err := executeGuestTemplate(provision.Script, instDir, y.User, y.Param); err == nil { provision.Script = out.String() } else { logrus.WithError(err).Warnf("Couldn't process provisioning script %q as a template", provision.Script) @@ -477,7 +541,7 @@ func FillDefault(y, d, o *LimaYAML, filePath string) { if probe.Description == "" { probe.Description = fmt.Sprintf("user probe %d/%d", i+1, len(y.Probes)) } - if out, err := executeGuestTemplate(probe.Script, instDir, y.Param); err == nil { + if out, err := executeGuestTemplate(probe.Script, instDir, y.User, y.Param); err == nil { probe.Script = out.String() } else { logrus.WithError(err).Warnf("Couldn't process probing script %q as a template", probe.Script) @@ -486,13 +550,13 @@ func FillDefault(y, d, o *LimaYAML, filePath string) { y.PortForwards = append(append(o.PortForwards, y.PortForwards...), d.PortForwards...) for i := range y.PortForwards { - FillPortForwardDefaults(&y.PortForwards[i], instDir, y.Param) + FillPortForwardDefaults(&y.PortForwards[i], instDir, y.User, y.Param) // After defaults processing the singular HostPort and GuestPort values should not be used again. } y.CopyToHost = append(append(o.CopyToHost, y.CopyToHost...), d.CopyToHost...) for i := range y.CopyToHost { - FillCopyToHostDefaults(&y.CopyToHost[i], instDir, y.Param) + FillCopyToHostDefaults(&y.CopyToHost[i], instDir, y.User, y.Param) } if y.HostResolver.Enabled == nil { @@ -621,7 +685,7 @@ func FillDefault(y, d, o *LimaYAML, filePath string) { logrus.WithError(err).Warnf("Couldn't process mount location %q as a template", mount.Location) } if mount.MountPoint != nil { - if out, err := executeGuestTemplate(*mount.MountPoint, instDir, y.Param); err == nil { + if out, err := executeGuestTemplate(*mount.MountPoint, instDir, y.User, y.Param); err == nil { mount.MountPoint = ptr.Of(out.String()) } else { logrus.WithError(err).Warnf("Couldn't process mount point %q as a template", *mount.MountPoint) @@ -811,17 +875,16 @@ func fixUpForPlainMode(y *LimaYAML) { y.TimeZone = ptr.Of("") } -func executeGuestTemplate(format, instDir string, param map[string]string) (bytes.Buffer, error) { +func executeGuestTemplate(format, instDir string, user User, param map[string]string) (bytes.Buffer, error) { tmpl, err := template.New("").Parse(format) if err == nil { - user, _ := osutil.LimaUser(false) name := filepath.Base(instDir) data := map[string]interface{}{ - "Home": fmt.Sprintf("/home/%s.linux", user.Username), "Name": name, "Hostname": identifierutil.HostnameFromInstName(name), // TODO: support customization - "UID": user.Uid, - "User": user.Username, + "UID": *user.UID, + "User": *user.Name, + "Home": *user.Home, "Param": param, } var out bytes.Buffer @@ -835,16 +898,14 @@ func executeGuestTemplate(format, instDir string, param map[string]string) (byte func executeHostTemplate(format, instDir string, param map[string]string) (bytes.Buffer, error) { tmpl, err := template.New("").Parse(format) if err == nil { - user, _ := osutil.LimaUser(false) - home, _ := os.UserHomeDir() limaHome, _ := dirnames.LimaDir() data := map[string]interface{}{ "Dir": instDir, - "Home": home, "Name": filepath.Base(instDir), // TODO: add hostname fields for the host and the guest - "UID": user.Uid, - "User": user.Username, + "UID": currentUser.Uid, + "User": currentUser.Username, + "Home": userHomeDir, "Param": param, "Instance": filepath.Base(instDir), // DEPRECATED, use `{{.Name}}` @@ -858,7 +919,7 @@ func executeHostTemplate(format, instDir string, param map[string]string) (bytes return bytes.Buffer{}, err } -func FillPortForwardDefaults(rule *PortForward, instDir string, param map[string]string) { +func FillPortForwardDefaults(rule *PortForward, instDir string, user User, param map[string]string) { if rule.Proto == "" { rule.Proto = ProtoTCP } @@ -890,7 +951,7 @@ func FillPortForwardDefaults(rule *PortForward, instDir string, param map[string } } if rule.GuestSocket != "" { - if out, err := executeGuestTemplate(rule.GuestSocket, instDir, param); err == nil { + if out, err := executeGuestTemplate(rule.GuestSocket, instDir, user, param); err == nil { rule.GuestSocket = out.String() } else { logrus.WithError(err).Warnf("Couldn't process guestSocket %q as a template", rule.GuestSocket) @@ -908,9 +969,9 @@ func FillPortForwardDefaults(rule *PortForward, instDir string, param map[string } } -func FillCopyToHostDefaults(rule *CopyToHost, instDir string, param map[string]string) { +func FillCopyToHostDefaults(rule *CopyToHost, instDir string, user User, param map[string]string) { if rule.GuestFile != "" { - if out, err := executeGuestTemplate(rule.GuestFile, instDir, param); err == nil { + if out, err := executeGuestTemplate(rule.GuestFile, instDir, user, param); err == nil { rule.GuestFile = out.String() } else { logrus.WithError(err).Warnf("Couldn't process guest %q as a template", rule.GuestFile) diff --git a/pkg/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go index 848dc8036ef..ebd600528a6 100644 --- a/pkg/limayaml/defaults_test.go +++ b/pkg/limayaml/defaults_test.go @@ -7,6 +7,7 @@ import ( "path/filepath" "runtime" "slices" + "strconv" "testing" "github.com/google/go-cmp/cmp" @@ -54,10 +55,14 @@ func TestFillDefault(t *testing.T) { assert.NilError(t, err) limaHome, err := dirnames.LimaDir() assert.NilError(t, err) - user, err := osutil.LimaUser(false) + user := osutil.LimaUser("0.0.0", false) + if runtime.GOOS != "windows" { + // manual template expansion for "/home/{{.User}}.linux" (done by FillDefault) + user.HomeDir = fmt.Sprintf("/home/%s.linux", user.Username) + } + uid, err := strconv.ParseUint(user.Uid, 10, 32) assert.NilError(t, err) - guestHome := fmt.Sprintf("/home/%s.linux", user.Username) instName := "instance" instDir := filepath.Join(limaHome, instName) filePath := filepath.Join(instDir, filenames.LimaYAML) @@ -108,6 +113,12 @@ func TestFillDefault(t *testing.T) { }, NestedVirtualization: ptr.Of(false), Plain: ptr.Of(false), + User: User{ + Name: ptr.Of(user.Username), + Comment: ptr.Of(user.Name), + Home: ptr.Of(user.HomeDir), + UID: ptr.Of(int(uid)), + }, } defaultPortForward := PortForward{ @@ -267,11 +278,11 @@ func TestFillDefault(t *testing.T) { expect.PortForwards[2].HostPort = 8888 expect.PortForwards[2].HostPortRange = [2]int{8888, 8888} - expect.PortForwards[3].GuestSocket = fmt.Sprintf("%s | %s | %s | %s", guestHome, user.Uid, user.Username, y.Param["ONE"]) - expect.PortForwards[3].HostSocket = fmt.Sprintf("%s | %s | %s | %s | %s | %s", hostHome, instDir, instName, user.Uid, user.Username, y.Param["ONE"]) + expect.PortForwards[3].GuestSocket = fmt.Sprintf("%s | %s | %s | %s", user.HomeDir, user.Uid, user.Username, y.Param["ONE"]) + expect.PortForwards[3].HostSocket = fmt.Sprintf("%s | %s | %s | %s | %s | %s", hostHome, instDir, instName, currentUser.Uid, currentUser.Username, y.Param["ONE"]) - expect.CopyToHost[0].GuestFile = fmt.Sprintf("%s | %s | %s | %s", guestHome, user.Uid, user.Username, y.Param["ONE"]) - expect.CopyToHost[0].HostFile = fmt.Sprintf("%s | %s | %s | %s | %s | %s", hostHome, instDir, instName, user.Uid, user.Username, y.Param["ONE"]) + expect.CopyToHost[0].GuestFile = fmt.Sprintf("%s | %s | %s | %s", user.HomeDir, user.Uid, user.Username, y.Param["ONE"]) + expect.CopyToHost[0].HostFile = fmt.Sprintf("%s | %s | %s | %s | %s | %s", hostHome, instDir, instName, currentUser.Uid, currentUser.Username, y.Param["ONE"]) expect.Env = y.Env @@ -296,7 +307,7 @@ func TestFillDefault(t *testing.T) { expect.NestedVirtualization = ptr.Of(false) - FillDefault(&y, &LimaYAML{}, &LimaYAML{}, filePath) + FillDefault(&y, &LimaYAML{}, &LimaYAML{}, filePath, false) assert.DeepEqual(t, &y, &expect, opts...) filledDefaults := y @@ -424,6 +435,12 @@ func TestFillDefault(t *testing.T) { BinFmt: ptr.Of(true), }, NestedVirtualization: ptr.Of(true), + User: User{ + Name: ptr.Of("xxx"), + Comment: ptr.Of("Foo Bar"), + Home: ptr.Of("/tmp"), + UID: ptr.Of(8080), + }, } expect = d @@ -464,7 +481,7 @@ func TestFillDefault(t *testing.T) { expect.Plain = ptr.Of(false) y = LimaYAML{} - FillDefault(&y, &d, &LimaYAML{}, filePath) + FillDefault(&y, &d, &LimaYAML{}, filePath, false) assert.DeepEqual(t, &y, &expect, opts...) dExpect := expect @@ -475,6 +492,7 @@ func TestFillDefault(t *testing.T) { y = filledDefaults y.DNS = []net.IP{net.ParseIP("8.8.8.8")} y.AdditionalDisks = []Disk{{Name: "overridden"}} + y.User.Home = ptr.Of("/root") expect = y @@ -502,7 +520,7 @@ func TestFillDefault(t *testing.T) { t.Logf("d.vmType=%q, y.vmType=%q, expect.vmType=%q", *d.VMType, *y.VMType, *expect.VMType) - FillDefault(&y, &d, &LimaYAML{}, filePath) + FillDefault(&y, &d, &LimaYAML{}, filePath, false) assert.DeepEqual(t, &y, &expect, opts...) // ------------------------------------------------------------------------------------ @@ -639,6 +657,12 @@ func TestFillDefault(t *testing.T) { BinFmt: ptr.Of(false), }, NestedVirtualization: ptr.Of(false), + User: User{ + Name: ptr.Of("foo"), + Comment: ptr.Of("foo bar baz"), + Home: ptr.Of("/override"), + UID: ptr.Of(1122), + }, } y = filledDefaults @@ -697,7 +721,7 @@ func TestFillDefault(t *testing.T) { expect.NestedVirtualization = ptr.Of(false) - FillDefault(&y, &d, &o, filePath) + FillDefault(&y, &d, &o, filePath, false) assert.DeepEqual(t, &y, &expect, opts...) } diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go index dc27723e512..95d76af6ec3 100644 --- a/pkg/limayaml/limayaml.go +++ b/pkg/limayaml/limayaml.go @@ -47,6 +47,7 @@ type LimaYAML struct { Plain *bool `yaml:"plain,omitempty" json:"plain,omitempty" jsonschema:"nullable"` TimeZone *string `yaml:"timezone,omitempty" json:"timezone,omitempty" jsonschema:"nullable"` NestedVirtualization *bool `yaml:"nestedVirtualization,omitempty" json:"nestedVirtualization,omitempty" jsonschema:"nullable"` + User User `yaml:"user,omitempty" json:"user,omitempty"` } type ( @@ -83,6 +84,13 @@ var ( VMTypes = []VMType{QEMU, VZ, WSL2} ) +type User struct { + Name *string `yaml:"name,omitempty" json:"name,omitempty" jsonschema:"nullable"` + Comment *string `yaml:"comment,omitempty" json:"comment,omitempty" jsonschema:"nullable"` + Home *string `yaml:"home,omitempty" json:"home,omitempty" jsonschema:"nullable"` + UID *int `yaml:"uid,omitempty" json:"uid,omitempty" jsonschema:"nullable"` +} + type VMOpts struct { QEMU QEMUOpts `yaml:"qemu,omitempty" json:"qemu,omitempty"` } diff --git a/pkg/limayaml/load.go b/pkg/limayaml/load.go index 511ae2b6307..58a47a0a564 100644 --- a/pkg/limayaml/load.go +++ b/pkg/limayaml/load.go @@ -15,6 +15,17 @@ import ( // // Load does not validate. Use Validate for validation. func Load(b []byte, filePath string) (*LimaYAML, error) { + return load(b, filePath, false) +} + +// LoadWithWarnings will call FillDefaults with warnings enabled (e.g. when +// the username is not valid on Linux and must be replaced by "Lima"). +// It is called when creating or editing an instance. +func LoadWithWarnings(b []byte, filePath string) (*LimaYAML, error) { + return load(b, filePath, true) +} + +func load(b []byte, filePath string, warn bool) (*LimaYAML, error) { var y, d, o LimaYAML if err := Unmarshal(b, &y, fmt.Sprintf("main file %q", filePath)); err != nil { @@ -52,6 +63,6 @@ func Load(b []byte, filePath string) (*LimaYAML, error) { return nil, err } - FillDefault(&y, &d, &o, filePath) + FillDefault(&y, &d, &o, filePath, warn) return &y, nil } diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go index 6c1a5d6d658..56d03c0d22c 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -136,13 +136,6 @@ func Validate(y *LimaYAML, warn bool) error { return fmt.Errorf("field `memory` has an invalid value: %w", err) } - u, err := osutil.LimaUser(false) - if err != nil { - return fmt.Errorf("internal error (not an error of YAML): %w", err) - } - // reservedHome is the home directory defined in "cidata.iso:/user-data" - reservedHome := fmt.Sprintf("/home/%s.linux", u.Username) - for i, f := range y.Mounts { if !filepath.IsAbs(f.Location) && !strings.HasPrefix(f.Location, "~") { return fmt.Errorf("field `mounts[%d].location` must be an absolute path, got %q", @@ -155,8 +148,9 @@ func Validate(y *LimaYAML, warn bool) error { switch loc { case "/", "/bin", "/dev", "/etc", "/home", "/opt", "/sbin", "/tmp", "/usr", "/var": return fmt.Errorf("field `mounts[%d].location` must not be a system path such as /etc or /usr", i) - case reservedHome: - return fmt.Errorf("field `mounts[%d].location` is internally reserved", i) + // home directory defined in "cidata.iso:/user-data" + case *y.User.Home: + return fmt.Errorf("field `mounts[%d].location` is the reserved internal home directory", i) } st, err := os.Stat(loc) diff --git a/pkg/must/must.go b/pkg/must/must.go new file mode 100644 index 00000000000..8dbada99f88 --- /dev/null +++ b/pkg/must/must.go @@ -0,0 +1,8 @@ +package must + +func Must[T any](obj T, err error) T { + if err != nil { + panic(err) + } + return obj +} diff --git a/pkg/osutil/user.go b/pkg/osutil/user.go index 45b2b7e1ab4..d1883c59d6e 100644 --- a/pkg/osutil/user.go +++ b/pkg/osutil/user.go @@ -11,6 +11,8 @@ import ( "strings" "sync" + . "github.com/lima-vm/lima/pkg/must" + "github.com/lima-vm/lima/pkg/version/versionutil" "github.com/sirupsen/logrus" ) @@ -41,10 +43,6 @@ var regexUsername = regexp.MustCompile("^[a-z_][a-z0-9_-]*$") // regexPath detects valid Linux path. var regexPath = regexp.MustCompile("^[/a-zA-Z0-9_-]+$") -func ValidateUsername(name string) bool { - return regexUsername.MatchString(name) -} - func LookupUser(name string) (User, error) { if users == nil { users = make(map[string]User) @@ -95,11 +93,89 @@ const ( fallbackGid = 1000 ) -var cache struct { - sync.Once - u *user.User - err error +var currentUser = Must(user.Current()) + +var ( + once = new(sync.Once) + limaUser *user.User warnings []string +) + +func LimaUser(limaVersion string, warn bool) *user.User { + once.Do(func() { + limaUser = currentUser + if !regexUsername.MatchString(limaUser.Username) { + warning := fmt.Sprintf("local username %q is not a valid Linux username (must match %q); using %q instead", + limaUser.Username, regexUsername.String(), fallbackUser) + warnings = append(warnings, warning) + limaUser.Username = fallbackUser + } + if runtime.GOOS != "windows" { + limaUser.HomeDir = "/home/{{.User}}.linux" + } else { + idu, err := call([]string{"id", "-u"}) + if err != nil { + logrus.Debug(err) + } + uid, err := parseUidGid(idu) + if err != nil { + uid = fallbackUid + } + if _, err := parseUidGid(limaUser.Uid); err != nil { + warning := fmt.Sprintf("local uid %q is not a valid Linux uid (must be integer); using %d uid instead", + limaUser.Uid, uid) + warnings = append(warnings, warning) + limaUser.Uid = formatUidGid(uid) + } + idg, err := call([]string{"id", "-g"}) + if err != nil { + logrus.Debug(err) + } + gid, err := parseUidGid(idg) + if err != nil { + gid = fallbackGid + } + if _, err := parseUidGid(limaUser.Gid); err != nil { + warning := fmt.Sprintf("local gid %q is not a valid Linux gid (must be integer); using %d gid instead", + limaUser.Gid, gid) + warnings = append(warnings, warning) + limaUser.Gid = formatUidGid(gid) + } + home, err := call([]string{"cygpath", limaUser.HomeDir}) + if err != nil { + logrus.Debug(err) + } + if home == "" { + drive := filepath.VolumeName(limaUser.HomeDir) + home = filepath.ToSlash(limaUser.HomeDir) + // replace C: with /c + prefix := strings.ToLower(fmt.Sprintf("/%c", drive[0])) + home = strings.Replace(home, drive, prefix, 1) + } + if !regexPath.MatchString(limaUser.HomeDir) { + warning := fmt.Sprintf("local home %q is not a valid Linux path (must match %q); using %q home instead", + limaUser.HomeDir, regexPath.String(), home) + warnings = append(warnings, warning) + limaUser.HomeDir = home + } + } + }) + if warn { + for _, warning := range warnings { + logrus.Warn(warning) + } + } + // Make sure we return a pointer to a COPY of limaUser + u := *limaUser + if versionutil.GreaterEqual(limaVersion, "1.0.0") { + if u.Username == "admin" { + if warn { + logrus.Warnf("local username %q is reserved; using %q instead", u.Username, fallbackUser) + } + u.Username = fallbackUser + } + } + return &u } func call(args []string) (string, error) { @@ -111,74 +187,6 @@ func call(args []string) (string, error) { return strings.TrimSpace(string(out)), nil } -func LimaUser(warn bool) (*user.User, error) { - cache.warnings = []string{} - cache.Do(func() { - cache.u, cache.err = user.Current() - if cache.err == nil { - if !ValidateUsername(cache.u.Username) { - warning := fmt.Sprintf("local user %q is not a valid Linux username (must match %q); using %q username instead", - cache.u.Username, regexUsername.String(), fallbackUser) - cache.warnings = append(cache.warnings, warning) - cache.u.Username = fallbackUser - } - if runtime.GOOS == "windows" { - idu, err := call([]string{"id", "-u"}) - if err != nil { - logrus.Debug(err) - } - uid, err := parseUidGid(idu) - if err != nil { - uid = fallbackUid - } - if _, err := parseUidGid(cache.u.Uid); err != nil { - warning := fmt.Sprintf("local uid %q is not a valid Linux uid (must be integer); using %d uid instead", - cache.u.Uid, uid) - cache.warnings = append(cache.warnings, warning) - cache.u.Uid = formatUidGid(uid) - } - idg, err := call([]string{"id", "-g"}) - if err != nil { - logrus.Debug(err) - } - gid, err := parseUidGid(idg) - if err != nil { - gid = fallbackGid - } - if _, err := parseUidGid(cache.u.Gid); err != nil { - warning := fmt.Sprintf("local gid %q is not a valid Linux gid (must be integer); using %d gid instead", - cache.u.Gid, gid) - cache.warnings = append(cache.warnings, warning) - cache.u.Gid = formatUidGid(gid) - } - home, err := call([]string{"cygpath", cache.u.HomeDir}) - if err != nil { - logrus.Debug(err) - } - if home == "" { - drive := filepath.VolumeName(cache.u.HomeDir) - home = filepath.ToSlash(cache.u.HomeDir) - // replace C: with /c - prefix := strings.ToLower(fmt.Sprintf("/%c", drive[0])) - home = strings.Replace(home, drive, prefix, 1) - } - if !regexPath.MatchString(cache.u.HomeDir) { - warning := fmt.Sprintf("local home %q is not a valid Linux path (must match %q); using %q home instead", - cache.u.HomeDir, regexPath.String(), home) - cache.warnings = append(cache.warnings, warning) - cache.u.HomeDir = home - } - } - } - }) - if warn && len(cache.warnings) > 0 { - for _, warning := range cache.warnings { - logrus.Warn(warning) - } - } - return cache.u, cache.err -} - // parseUidGid converts string value to Linux uid or gid. func parseUidGid(uidOrGid string) (uint32, error) { res, err := strconv.ParseUint(uidOrGid, 10, 32) diff --git a/pkg/osutil/user_test.go b/pkg/osutil/user_test.go index e68375acddb..fe1aa05428a 100644 --- a/pkg/osutil/user_test.go +++ b/pkg/osutil/user_test.go @@ -3,40 +3,57 @@ package osutil import ( "path" "strconv" + "sync" "testing" "gotest.tools/v3/assert" ) -func TestLimaUserWarn(t *testing.T) { - _, err := LimaUser(true) - assert.NilError(t, err) +const limaVersion = "1.0.0" + +// "admin" is a reserved username in 1.0.0 +func TestLimaUserAdminNew(t *testing.T) { + currentUser.Username = "admin" + once = new(sync.Once) + user := LimaUser(limaVersion, false) + assert.Equal(t, user.Username, fallbackUser) } -func TestLimaUsername(t *testing.T) { - user, err := LimaUser(false) - assert.NilError(t, err) - // check for reasonable unix user name - assert.Assert(t, ValidateUsername(user.Username), user.Username) +// "admin" is allowed in older instances +func TestLimaUserAdminOld(t *testing.T) { + currentUser.Username = "admin" + once = new(sync.Once) + user := LimaUser("0.23.0", false) + assert.Equal(t, user.Username, "admin") +} + +func TestLimaUserInvalid(t *testing.T) { + currentUser.Username = "use@example.com" + once = new(sync.Once) + user := LimaUser(limaVersion, false) + assert.Equal(t, user.Username, fallbackUser) } func TestLimaUserUid(t *testing.T) { - user, err := LimaUser(false) - assert.NilError(t, err) - _, err = strconv.Atoi(user.Uid) + currentUser.Username = fallbackUser + once = new(sync.Once) + user := LimaUser(limaVersion, false) + _, err := strconv.Atoi(user.Uid) assert.NilError(t, err) } func TestLimaUserGid(t *testing.T) { - user, err := LimaUser(false) - assert.NilError(t, err) - _, err = strconv.Atoi(user.Gid) + currentUser.Username = fallbackUser + once = new(sync.Once) + user := LimaUser(limaVersion, false) + _, err := strconv.Atoi(user.Gid) assert.NilError(t, err) } func TestLimaHomeDir(t *testing.T) { - user, err := LimaUser(false) - assert.NilError(t, err) + currentUser.Username = fallbackUser + once = new(sync.Once) + user := LimaUser(limaVersion, false) // check for absolute unix path (/home) assert.Assert(t, path.IsAbs(user.HomeDir), user.HomeDir) } diff --git a/pkg/sshutil/sshutil.go b/pkg/sshutil/sshutil.go index 302ebfdb794..af141e660a6 100644 --- a/pkg/sshutil/sshutil.go +++ b/pkg/sshutil/sshutil.go @@ -223,15 +223,11 @@ func CommonOpts(useDotSSH bool) ([]string, error) { } // SSHOpts adds the following options to CommonOptions: User, ControlMaster, ControlPath, ControlPersist. -func SSHOpts(instDir string, useDotSSH, forwardAgent, forwardX11, forwardX11Trusted bool) ([]string, error) { +func SSHOpts(instDir, username string, useDotSSH, forwardAgent, forwardX11, forwardX11Trusted bool) ([]string, error) { controlSock := filepath.Join(instDir, filenames.SSHSock) if len(controlSock) >= osutil.UnixPathMax { return nil, fmt.Errorf("socket path %q is too long: >= UNIX_PATH_MAX=%d", controlSock, osutil.UnixPathMax) } - u, err := osutil.LimaUser(false) - if err != nil { - return nil, err - } opts, err := CommonOpts(useDotSSH) if err != nil { return nil, err @@ -242,7 +238,7 @@ func SSHOpts(instDir string, useDotSSH, forwardAgent, forwardX11, forwardX11Trus controlPath = fmt.Sprintf(`ControlPath='%s'`, controlSock) } opts = append(opts, - fmt.Sprintf("User=%s", u.Username), // guest and host have the same username, but we should specify the username explicitly (#85) + fmt.Sprintf("User=%s", username), // guest and host have the same username, but we should specify the username explicitly (#85) "ControlMaster=auto", controlPath, "ControlPersist=yes", diff --git a/templates/default.yaml b/templates/default.yaml index a11f51d8a6a..732463b583d 100644 --- a/templates/default.yaml +++ b/templates/default.yaml @@ -56,7 +56,7 @@ disk: null # Expose host directories to the guest, the mount point might be accessible from all UIDs in the guest # "location" can use these template variables: {{.Home}}, {{.Dir}}, {{.Name}}, {{.UID}}, {{.User}}, and {{.Param.Key}}. -# "mountPoint" can use these template variables: {{.Home}}, {{.Name}}, {{.Hostname}}, {{.UID}}, {{.User}}, and {{.Param.Key}} +# "mountPoint" can use these template variables: {{.Home}}, {{.Name}}, {{.Hostname}}, {{.UID}}, {{.User}}, and {{.Param.Key}}. # 🟢 Builtin default: [] (Mount nothing) # 🔵 This file: Mount the home as read-only, /tmp/lima as writable mounts: @@ -210,7 +210,7 @@ containerd: # Provisioning scripts need to be idempotent because they might be called # multiple times, e.g. when the host VM is being restarted. -# The scripts can use the following template variables: {{.Home}}, {{.Name}}, {{.Hostname}}, {{.UID}}, {{.User}}, and {{.Param.Key}} +# The scripts can use the following template variables: {{.Home}}, {{.Name}}, {{.Hostname}}, {{.UID}}, {{.User}}, and {{.Param.Key}}. # 🟢 Builtin default: [] # provision: # # `system` is executed with root privileges @@ -253,7 +253,7 @@ containerd: # Probe scripts to check readiness. # The scripts run in user mode. They must start with a '#!' line. -# The scripts can use the following template variables: {{.Home}}, {{.Name}}, {{.Hostname}}, {{.UID}}, {{.User}}, and {{.Param.Key}} +# The scripts can use the following template variables: {{.Home}}, {{.Name}}, {{.Hostname}}, {{.UID}}, {{.User}}, and {{.Param.Key}}. # 🟢 Builtin default: [] # probes: # # Only `readiness` probes are supported right now. @@ -279,6 +279,22 @@ containerd: # 🟢 Builtin default: not set minimumLimaVersion: null +# User to be used inside the VM +user: + # User name. An explicitly specified username is not validated by Lima. + # 🟢 Builtin default: same as the host username, if it is a valid Linux username, otherwise "lima" + name: null + # Full name or display name of the user. + # 🟢 Builtin default: user information from the host + comment: null + # Numeric user id. It is not currently possible to specify a group id. + # 🟢 Builtin default: same as the host user id of the current user (NOT a lookup of the specified "username"). + uid: null + # Home directory inside the VM, NOT the mounted home directory of the host. + # It can use the following template variables: {{.Name}}, {{.Hostname}}, {{.UID}}, {{.User}}, and {{.Param.Key}}. + # 🟢 Builtin default: "/home/{{.User}}.linux" + home: null + vmOpts: qemu: # Minimum version of QEMU required to create an instance of this template. diff --git a/website/content/en/docs/dev/internals/_index.md b/website/content/en/docs/dev/internals/_index.md index 0d8d6ebb021..53701be3a31 100644 --- a/website/content/en/docs/dev/internals/_index.md +++ b/website/content/en/docs/dev/internals/_index.md @@ -179,7 +179,7 @@ The volume label is "cidata", as defined by [cloud-init NoCloud](https://docs.cl - `LIMA_CIDATA_MNT`: the mount point of the disk. `/mnt/lima-cidata`. - `LIMA_CIDATA_USER`: the username string - `LIMA_CIDATA_UID`: the numeric UID -- `LIMA_CIDATA_GECOS`: the name or comment string +- `LIMA_CIDATA_COMMENT`: the full name or comment string - `LIMA_CIDATA_HOME`: the guest home directory - `LIMA_CIDATA_HOSTHOME_MOUNTPOINT`: the mount point of the host home directory, or empty if not mounted - `LIMA_CIDATA_MOUNTS`: the number of the Lima mounts