Skip to content

Commit

Permalink
Make guest username, uid, home directory configurable
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
jandubois committed Nov 1, 2024
1 parent 361fe23 commit f5c2776
Show file tree
Hide file tree
Showing 19 changed files with 289 additions and 175 deletions.
3 changes: 3 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 8 additions & 13 deletions cmd/limactl/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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("%[email protected]:%s", u.Username, path[1]))
scpArgs = append(scpArgs, fmt.Sprintf("%[email protected]:%s", *inst.Config.User.Username, path[1]))
} else {
scpArgs = append(scpArgs, fmt.Sprintf("scp://%[email protected]:%d/%s", u.Username, inst.SSHLocalPort, path[1]))
scpArgs = append(scpArgs, fmt.Sprintf("scp://%[email protected]:%d/%s", *inst.Config.User.Username, 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.Username, false, false, false, false)
if err != nil {
return err
}
Expand Down
1 change: 1 addition & 0 deletions cmd/limactl/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ func shellAction(cmd *cobra.Command, args []string) error {

sshOpts, err := sshutil.SSHOpts(
inst.Dir,
*inst.Config.User.Username,
*inst.Config.SSH.LoadDotSSHPubKeys,
*inst.Config.SSH.ForwardAgent,
*inst.Config.SSH.ForwardX11,
Expand Down
1 change: 1 addition & 0 deletions cmd/limactl/show-ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.Username,
*inst.Config.SSH.LoadDotSSHPubKeys,
*inst.Config.SSH.ForwardAgent,
*inst.Config.SSH.ForwardX11,
Expand Down
1 change: 1 addition & 0 deletions cmd/limactl/tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ func tunnelAction(cmd *cobra.Command, args []string) error {

sshOpts, err := sshutil.SSHOpts(
inst.Dir,
*inst.Config.User.Username,
*inst.Config.SSH.LoadDotSSHPubKeys,
*inst.Config.SSH.ForwardAgent,
*inst.Config.SSH.ForwardX11,
Expand Down
10 changes: 3 additions & 7 deletions pkg/cidata/cidata.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,7 @@ 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)
uid, err := strconv.Atoi(*instConfig.User.UID)
if err != nil {
return nil, err
}
Expand All @@ -130,9 +126,9 @@ func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.L
BootScripts: bootScripts,
Name: name,
Hostname: identifierutil.HostnameFromInstName(name), // TODO: support customization
User: u.Username,
UID: uid,
Home: fmt.Sprintf("/home/%s.linux", u.Username),
User: *instConfig.User.Username,
Home: *instConfig.User.HomeDir,
GuestInstallPrefix: *instConfig.GuestInstallPrefix,
UpgradePackages: *instConfig.UpgradePackages,
Containerd: Containerd{System: *instConfig.Containerd.System, User: *instConfig.Containerd.User},
Expand Down
6 changes: 2 additions & 4 deletions pkg/cidata/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -96,9 +95,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\"")
}
Expand Down
5 changes: 3 additions & 2 deletions pkg/hostagent/hostagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.Username,
*inst.Config.SSH.LoadDotSSHPubKeys,
*inst.Config.SSH.ForwardAgent,
*inst.Config.SSH.ForwardX11,
Expand Down Expand Up @@ -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{
Expand Down
2 changes: 1 addition & 1 deletion pkg/instance/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
98 changes: 72 additions & 26 deletions pkg/limayaml/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"net"
"os"
"os/user"
"path/filepath"
"runtime"
"slices"
Expand All @@ -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"
Expand All @@ -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{
Expand Down Expand Up @@ -171,17 +179,58 @@ 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 warn {
// If warnings are enabled, then a new instance is being created using the current Lima version
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.Username == nil {
y.User.Username = d.User.Username
}
if y.User.HomeDir == nil {
y.User.HomeDir = d.User.HomeDir
}
if y.User.UID == nil {
y.User.UID = d.User.UID
}
if o.User.Username != nil {
y.User.Username = o.User.Username
}
if o.User.HomeDir != nil {
y.User.HomeDir = o.User.HomeDir
}
if o.User.UID != nil {
y.User.UID = o.User.UID
}
if y.User.Username == nil {
y.User.Username = ptr.Of(osutil.LimaUser(existingLimaVersion, warn).Username)
warn = false
}
if y.User.HomeDir == nil {
y.User.HomeDir = ptr.Of(osutil.LimaUser(existingLimaVersion, warn).HomeDir)
warn = false
}
if y.User.UID == nil {
y.User.UID = ptr.Of(osutil.LimaUser(existingLimaVersion, warn).Uid)
// warn = false
}
if out, err := executeGuestTemplate(*y.User.HomeDir, instDir, y.User, y.Param); err == nil {
y.User.HomeDir = ptr.Of(out.String())
} else {
logrus.WithError(err).Warnf("Couldn't process `user.homeDir` value %q as a template", *y.User.HomeDir)
}

if y.VMType == nil {
Expand Down Expand Up @@ -406,7 +455,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)
Expand Down Expand Up @@ -477,7 +526,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)
Expand All @@ -486,13 +535,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 {
Expand Down Expand Up @@ -621,7 +670,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)
Expand Down Expand Up @@ -811,17 +860,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.Username,
"Home": *user.HomeDir,
"Param": param,
}
var out bytes.Buffer
Expand All @@ -835,16 +883,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}}`
Expand All @@ -858,7 +904,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
}
Expand Down Expand Up @@ -890,7 +936,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)
Expand All @@ -908,9 +954,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)
Expand Down
Loading

0 comments on commit f5c2776

Please sign in to comment.