Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 73 additions & 5 deletions cmd/limactl/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/lima-vm/lima/v2/pkg/limatype/dirnames"
"github.com/lima-vm/lima/v2/pkg/limatype/filenames"
"github.com/lima-vm/lima/v2/pkg/limayaml"
"github.com/lima-vm/lima/v2/pkg/localpathutil"
"github.com/lima-vm/lima/v2/pkg/networks/reconcile"
"github.com/lima-vm/lima/v2/pkg/registry"
"github.com/lima-vm/lima/v2/pkg/store"
Expand All @@ -44,7 +45,7 @@ func registerCreateFlags(cmd *cobra.Command, commentPrefix string) {

func newCreateCommand() *cobra.Command {
createCommand := &cobra.Command{
Use: "create FILE.yaml|URL",
Use: "create [FILE.yaml|URL...]",
Example: `
To create an instance "default" from the default Ubuntu template:
$ limactl create
Expand All @@ -69,9 +70,15 @@ func newCreateCommand() *cobra.Command {

To create an instance "local" from a template passed to stdin (--name parameter is required):
$ cat template.yaml | limactl create --name=local -

To create an instance from a template with local overrides:
$ limactl create template:docker my-overrides.yaml

To create an instance from multiple templates (merged in order):
$ limactl create https://example.com/base.yaml secrets.yaml
`,
Short: "Create an instance of Lima",
Args: WrapArgsError(cobra.MaximumNArgs(1)),
Args: WrapArgsError(cobra.ArbitraryArgs),
ValidArgsFunction: createBashComplete,
RunE: createAction,
GroupID: basicCommand,
Expand All @@ -82,16 +89,19 @@ func newCreateCommand() *cobra.Command {

func newStartCommand() *cobra.Command {
startCommand := &cobra.Command{
Use: "start NAME|FILE.yaml|URL",
Use: "start [NAME|FILE.yaml|URL...]",
Example: `
To create an instance "default" (if not created yet) from the default Ubuntu template, and start it:
$ limactl start

To create an instance "default" from a template "docker", and start it:
$ limactl start --name=default template:docker

To create an instance from a template with local overrides, and start it:
$ limactl start template:docker my-overrides.yaml
`,
Short: "Start an instance of Lima",
Args: WrapArgsError(cobra.MaximumNArgs(1)),
Args: WrapArgsError(cobra.ArbitraryArgs),
ValidArgsFunction: startBashComplete,
RunE: startAction,
GroupID: basicCommand,
Expand Down Expand Up @@ -232,6 +242,7 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
return nil, err
}
}

if isTemplateURL, templateName := limatmpl.SeemsTemplateURL(arg); isTemplateURL {
switch templateName {
case "experimental/vz":
Expand Down Expand Up @@ -281,6 +292,9 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
if createOnly {
return nil, fmt.Errorf("instance %q already exists", tmpl.Name)
}
if len(args) > 1 {
return nil, fmt.Errorf("cannot specify additional templates when starting an existing instance %q", tmpl.Name)
}
logrus.Infof("Using the existing instance %q", tmpl.Name)
yqExprs, err := editflags.YQExpressions(flags, false)
if err != nil {
Expand Down Expand Up @@ -308,7 +322,7 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
return nil, err
}
} else {
tmpl, err = limatmpl.Read(cmd.Context(), name, arg)
tmpl, err = loadMultipleTemplates(ctx, name, args)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -615,6 +629,60 @@ func startAction(cmd *cobra.Command, args []string) error {
return instance.Start(ctx, inst, launchHostAgentForeground, progress)
}

// loadMultipleTemplates creates a template from multiple CLI arguments.
// All arguments are treated as base templates and merged in order.
// Relative and tilde paths are expanded to absolute paths.
func loadMultipleTemplates(_ context.Context, name string, args []string) (*limatmpl.Template, error) {
bases := make(limatype.BaseTemplates, 0, len(args))
for _, a := range args {
absLocator := a
// Expand relative and tilde paths to absolute
// "-" (stdin), URLs, and template: locators are kept as-is
if a != "-" && !limatmpl.SeemsHTTPURL(a) && !limatmpl.SeemsFileURL(a) {
if isTemplate, _ := limatmpl.SeemsTemplateURL(a); !isTemplate {
var err error
absLocator, err = localpathutil.Expand(a)
if err != nil {
return nil, fmt.Errorf("failed to expand path %q: %w", a, err)
}
}
}
bases = append(bases, limatype.LocatorWithDigest{URL: absLocator})
}

// Create a minimal config with just the base templates
config := &limatype.LimaYAML{Base: bases}
bytes, err := limayaml.Marshal(config, false)
if err != nil {
return nil, fmt.Errorf("failed to marshal template: %w", err)
}

cwd, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("failed to get working directory: %w", err)
}

tmpl := &limatmpl.Template{
Bytes: bytes,
Name: name,
Locator: cwd,
}

// Derive instance name from first template if not specified
if tmpl.Name == "" {
tmpl.Name, err = limatmpl.InstNameFromURL(args[0])
if err != nil {
// fallback to InstNameFromYAMLPath if URL parsing fails
tmpl.Name, err = limatmpl.InstNameFromYAMLPath(args[0])
if err != nil {
return nil, fmt.Errorf("cannot derive instance name from %q: %w", args[0], err)
}
}
}

return tmpl, nil
}

func createBashComplete(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return bashCompleteTemplateNames(cmd, toComplete)
}
Expand Down
198 changes: 198 additions & 0 deletions hack/bats/tests/multi-template.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# SPDX-FileCopyrightText: Copyright The Lima Authors
# SPDX-License-Identifier: Apache-2.0

load "../helpers/load"

NAME=multi-template

local_setup_file() {
limactl delete --force "$NAME" || :
}

local_teardown_file() {
limactl delete --force "$NAME" || :
}

local_teardown() {
limactl delete --force "$NAME" || :
}

@test 'create with multiple templates merges values' {
# Create base template file (uses /etc/profile as dummy image like create_dummy_instance)
cat > "${BATS_TEST_TMPDIR}/base.yaml" <<'EOF'
images:
- location: /etc/profile
cpus: 3
EOF

# Create override file with additional settings
cat > "${BATS_TEST_TMPDIR}/override.yaml" <<'EOF'
memory: 5GiB
disk: 37GiB
EOF

run -0 limactl create --name "$NAME" "${BATS_TEST_TMPDIR}/base.yaml" "${BATS_TEST_TMPDIR}/override.yaml"

# Verify the base values were used (cpus should be 3 from first template)
run -0 limactl list --format '{{.CPUs}}' "$NAME"
assert_output "3"

# Verify the override values were merged (memory should be from second template)
# 5GiB = 5368709120 bytes
run -0 limactl list --format '{{.Memory}}' "$NAME"
assert_output "5368709120"
}

@test 'first template values take precedence for scalars' {
cat > "${BATS_TEST_TMPDIR}/base.yaml" <<'EOF'
images:
- location: /etc/profile
cpus: 3
memory: 3GiB
EOF

cat > "${BATS_TEST_TMPDIR}/override.yaml" <<'EOF'
cpus: 7
memory: 7GiB
EOF

run -0 limactl create --name "$NAME" "${BATS_TEST_TMPDIR}/base.yaml" "${BATS_TEST_TMPDIR}/override.yaml"

# cpus from first template should win
run -0 limactl list --format '{{.CPUs}}' "$NAME"
assert_output "3"

# memory from first template should win
# 3GiB = 3221225472 bytes
run -0 limactl list --format '{{.Memory}}' "$NAME"
assert_output "3221225472"
}

@test 'relative paths resolve from current directory' {
mkdir -p "${BATS_TEST_TMPDIR}/testdir"

cat > "${BATS_TEST_TMPDIR}/testdir/base.yaml" <<'EOF'
images:
- location: /etc/profile
cpus: 5
EOF

cat > "${BATS_TEST_TMPDIR}/testdir/config.yaml" <<'EOF'
memory: 7GiB
EOF

cd "${BATS_TEST_TMPDIR}/testdir"
run -0 limactl create --name "$NAME" base.yaml config.yaml

run -0 limactl list --format '{{.CPUs}}' "$NAME"
assert_output "5"

# 7GiB = 7516192768 bytes
run -0 limactl list --format '{{.Memory}}' "$NAME"
assert_output "7516192768"
}

@test 'multiple args with existing instance produces error' {
# Create an instance first using stdin
limactl create --name "$NAME" - <<'EOF'
images:
- location: /etc/profile
EOF

cat > "${BATS_TEST_TMPDIR}/extra.yaml" <<'EOF'
cpus: 3
EOF

# Attempting to start the existing instance with additional templates should error
run ! limactl start "$NAME" "${BATS_TEST_TMPDIR}/extra.yaml"
assert_output --partial "cannot specify additional templates"
}

@test 'instance name derived from first template filename' {
# Clean up any stale myinstance from previous runs
limactl delete --force myinstance || :

cat > "${BATS_TEST_TMPDIR}/myinstance.yaml" <<'EOF'
images:
- location: /etc/profile
EOF

cat > "${BATS_TEST_TMPDIR}/override.yaml" <<'EOF'
cpus: 3
EOF

run -0 limactl create "${BATS_TEST_TMPDIR}/myinstance.yaml" "${BATS_TEST_TMPDIR}/override.yaml"

# Instance should be named after first template
run -0 limactl list --format '{{.Name}}'
assert_output --partial "myinstance"

limactl delete --force myinstance || :
}

@test 'explicit --name flag overrides derived name' {
cat > "${BATS_TEST_TMPDIR}/base.yaml" <<'EOF'
images:
- location: /etc/profile
EOF

cat > "${BATS_TEST_TMPDIR}/override.yaml" <<'EOF'
cpus: 5
EOF

run -0 limactl create --name "$NAME" "${BATS_TEST_TMPDIR}/base.yaml" "${BATS_TEST_TMPDIR}/override.yaml"

run -0 limactl list --format '{{.Name}}' "$NAME"
assert_output "$NAME"
}

@test 'three templates merge correctly' {
cat > "${BATS_TEST_TMPDIR}/t1.yaml" <<'EOF'
images:
- location: /etc/profile
cpus: 3
EOF

cat > "${BATS_TEST_TMPDIR}/t2.yaml" <<'EOF'
memory: 5GiB
EOF

cat > "${BATS_TEST_TMPDIR}/t3.yaml" <<'EOF'
disk: 73GiB
EOF

run -0 limactl create --name "$NAME" "${BATS_TEST_TMPDIR}/t1.yaml" "${BATS_TEST_TMPDIR}/t2.yaml" "${BATS_TEST_TMPDIR}/t3.yaml"

run -0 limactl list --format '{{.CPUs}}' "$NAME"
assert_output "3"

# 5GiB = 5368709120 bytes
run -0 limactl list --format '{{.Memory}}' "$NAME"
assert_output "5368709120"

# 73GiB = 78383153152 bytes
run -0 limactl list --format '{{.Disk}}' "$NAME"
assert_output "78383153152"
}

@test 'stdin can be used as one of the templates' {
cat > "${BATS_TEST_TMPDIR}/override.yaml" <<'EOF'
memory: 5GiB
EOF

# Use stdin as the first template, with a file as the second
run -0 limactl create --name "$NAME" - "${BATS_TEST_TMPDIR}/override.yaml" <<'EOF'
images:
- location: /etc/profile
cpus: 7
EOF

# cpus from stdin template should be used
run -0 limactl list --format '{{.CPUs}}' "$NAME"
assert_output "7"

# memory from override file should be merged
# 5GiB = 5368709120 bytes
run -0 limactl list --format '{{.Memory}}' "$NAME"
assert_output "5368709120"
}
4 changes: 4 additions & 0 deletions pkg/limatmpl/abs.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ func absPath(locator, basePath string) (string, error) {
if locator == "" {
return "", errors.New("locator is empty")
}
// "-" means stdin, return as-is
if locator == "-" {
return locator, nil
}
u, err := url.Parse(locator)
if err == nil && len(u.Scheme) > 1 {
return locator, nil
Expand Down
Loading