Skip to content

Plain mode: support port forwarding #3699

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
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
68 changes: 68 additions & 0 deletions cmd/limactl/editflags/editflags.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ func RegisterCreate(cmd *cobra.Command, commentPrefix string) {
})

flags.Bool("plain", false, commentPrefix+"Plain mode. Disables mounts, port forwarding, containerd, etc.")

flags.StringArray("port-forward", nil, commentPrefix+"Port forwards (host:guest), e.g., '8080:80' or '9090:9090,static=true' for static port-forwards")
_ = cmd.RegisterFlagCompletionFunc("port-forward", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
return []string{"8080:80", "3000:3000", "8080:80,static=true"}, cobra.ShellCompDirectiveNoFileComp
})
}

func defaultExprFunc(expr string) func(v *flag.Flag) (string, error) {
Expand All @@ -104,6 +109,56 @@ func defaultExprFunc(expr string) func(v *flag.Flag) (string, error) {
}
}

func ParsePortForward(spec string) (hostPort, guestPort string, isStatic bool, err error) {
parts := strings.Split(spec, ",")
if len(parts) > 2 {
return "", "", false, fmt.Errorf("invalid port forward format %q, expected HOST:GUEST or HOST:GUEST,static=true", spec)
}

portParts := strings.Split(strings.TrimSpace(parts[0]), ":")
if len(portParts) != 2 {
return "", "", false, fmt.Errorf("invalid port forward format %q, expected HOST:GUEST", parts[0])
}

hostPort = strings.TrimSpace(portParts[0])
guestPort = strings.TrimSpace(portParts[1])

if len(parts) == 2 {
staticPart := strings.TrimSpace(parts[1])
if strings.HasPrefix(staticPart, "static=") {
staticValue := strings.TrimPrefix(staticPart, "static=")
isStatic, err = strconv.ParseBool(staticValue)
if err != nil {
return "", "", false, fmt.Errorf("invalid value for static parameter: %q", staticValue)
}
} else {
return "", "", false, fmt.Errorf("invalid parameter %q, expected 'static=' followed by a boolean value", staticPart)
}
}

return hostPort, guestPort, isStatic, nil
}

func BuildPortForwardExpression(portForwards []string) (string, error) {
if len(portForwards) == 0 {
return "", nil
}

expr := `.portForwards += [`
for i, spec := range portForwards {
hostPort, guestPort, isStatic, err := ParsePortForward(spec)
if err != nil {
return "", err
}
expr += fmt.Sprintf(`{"guestPort": %q, "hostPort": %q, "static": %v}`, guestPort, hostPort, isStatic)
if i < len(portForwards)-1 {
expr += ","
}
}
expr += `]`
return expr, nil
}

// YQExpressions returns YQ expressions.
func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) {
type def struct {
Expand Down Expand Up @@ -206,6 +261,7 @@ func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) {
false,
false,
},

{
"rosetta",
func(_ *flag.Flag) (string, error) {
Expand Down Expand Up @@ -261,6 +317,18 @@ func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) {
{"disk", d(".disk= \"%sGiB\""), false, false},
{"vm-type", d(".vmType = %q"), true, false},
{"plain", d(".plain = %s"), true, false},
{
"port-forward",
func(_ *flag.Flag) (string, error) {
ss, err := flags.GetStringArray("port-forward")
if err != nil {
return "", err
}
return BuildPortForwardExpression(ss)
},
false,
false,
},
}
var exprs []string
for _, def := range defs {
Expand Down
134 changes: 134 additions & 0 deletions cmd/limactl/editflags/editflags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,137 @@ func TestCompleteMemoryGiB(t *testing.T) {
assert.DeepEqual(t, []float32{1, 2, 4}, completeMemoryGiB(8<<30))
assert.DeepEqual(t, []float32{1, 2, 4, 8, 10}, completeMemoryGiB(20<<30))
}

func TestBuildPortForwardExpression(t *testing.T) {
tests := []struct {
name string
portForwards []string
expected string
expectError bool
}{
{
name: "empty port forwards",
portForwards: []string{},
expected: "",
},
{
name: "single dynamic port forward",
portForwards: []string{"8080:80"},
expected: `.portForwards += [{"guestPort": "80", "hostPort": "8080", "static": false}]`,
},
{
name: "single static port forward",
portForwards: []string{"8080:80,static=true"},
expected: `.portForwards += [{"guestPort": "80", "hostPort": "8080", "static": true}]`,
},
{
name: "multiple mixed port forwards",
portForwards: []string{"8080:80", "2222:22,static=true", "3000:3000"},
expected: `.portForwards += [{"guestPort": "80", "hostPort": "8080", "static": false},{"guestPort": "22", "hostPort": "2222", "static": true},{"guestPort": "3000", "hostPort": "3000", "static": false}]`,
},
{
name: "invalid format - missing colon",
portForwards: []string{"8080"},
expectError: true,
},
{
name: "invalid format - too many colons",
portForwards: []string{"8080:80:extra"},
expectError: true,
},
{
name: "invalid static parameter",
portForwards: []string{"8080:80,invalid=true"},
expectError: true,
},
{
name: "too many parameters",
portForwards: []string{"8080:80,static=true,extra=value"},
expectError: true,
},
{
name: "whitespace handling",
portForwards: []string{" 8080 : 80 , static=true "},
expected: `.portForwards += [{"guestPort": "80", "hostPort": "8080", "static": true}]`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := BuildPortForwardExpression(tt.portForwards)
if tt.expectError {
assert.Check(t, err != nil)
} else {
assert.NilError(t, err)
assert.Equal(t, tt.expected, result)
}
})
}
}

func TestParsePortForward(t *testing.T) {
tests := []struct {
name string
spec string
hostPort string
guestPort string
isStatic bool
expectError bool
}{
{
name: "dynamic port forward",
spec: "8080:80",
hostPort: "8080",
guestPort: "80",
isStatic: false,
},
{
name: "static port forward",
spec: "8080:80,static=true",
hostPort: "8080",
guestPort: "80",
isStatic: true,
},
{
name: "whitespace handling",
spec: " 8080 : 80 , static=true ",
hostPort: "8080",
guestPort: "80",
isStatic: true,
},
{
name: "invalid format - missing colon",
spec: "8080",
expectError: true,
},
{
name: "invalid format - too many colons",
spec: "8080:80:extra",
expectError: true,
},
{
name: "invalid parameter",
spec: "8080:80,invalid=true",
expectError: true,
},
{
name: "too many parameters",
spec: "8080:80,static=true,extra=value",
expectError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hostPort, guestPort, isStatic, err := ParsePortForward(tt.spec)
if tt.expectError {
assert.Check(t, err != nil)
} else {
assert.NilError(t, err)
assert.Equal(t, tt.hostPort, hostPort)
assert.Equal(t, tt.guestPort, guestPort)
assert.Equal(t, tt.isStatic, isStatic)
}
})
}
}
25 changes: 25 additions & 0 deletions hack/test-nonplain-static-port-forward.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash

# SPDX-FileCopyrightText: Copyright The Lima Authors
# SPDX-License-Identifier: Apache-2.0

set -euxo pipefail

INSTANCE=nonplain-static-port-forward
TEMPLATE=hack/test-templates/static-port-forward.yaml

limactl delete -f $INSTANCE || true

limactl start --name=$INSTANCE --tty=false $TEMPLATE

limactl shell $INSTANCE -- bash -c 'until [ -e /run/nginx.pid ]; do sleep 1; done'
limactl shell $INSTANCE -- bash -c 'until systemctl is-active --quiet test-server-9080; do sleep 1; done'
limactl shell $INSTANCE -- bash -c 'until systemctl is-active --quiet test-server-9070; do sleep 1; done'

curl -sSf http://127.0.0.1:9090 | grep -i 'nginx' && echo 'Static port forwarding (9090) works in normal mode!'
curl -sSf http://127.0.0.1:9080 | grep -i 'Dynamic port 9080' && echo 'Dynamic port forwarding (9080) works in normal mode!'
curl -sSf http://127.0.0.1:9070 | grep -i 'Dynamic port 9070' && echo 'Dynamic port forwarding (9070) works in normal mode!'

limactl delete -f $INSTANCE
echo "All tests passed for normal mode - both static and dynamic ports work!"
# EOF
35 changes: 35 additions & 0 deletions hack/test-plain-static-port-forward.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env bash

# SPDX-FileCopyrightText: Copyright The Lima Authors
# SPDX-License-Identifier: Apache-2.0

set -euxo pipefail

INSTANCE=plain-static-port-forward
TEMPLATE=hack/test-templates/static-port-forward.yaml

limactl delete -f $INSTANCE || true

limactl start --name=$INSTANCE --plain=true --tty=false $TEMPLATE

limactl shell $INSTANCE -- bash -c 'until [ -e /run/nginx.pid ]; do sleep 1; done'

curl -sSf http://127.0.0.1:9090 | grep -i 'nginx' && echo 'Static port forwarding (9090) works in plain mode!'

if curl -sSf http://127.0.0.1:9080 2>/dev/null; then
echo 'ERROR: Dynamic port 9080 should not be forwarded in plain mode!'
exit 1
else
echo 'Dynamic port 9080 is correctly NOT forwarded in plain mode.'
fi

if curl -sSf http://127.0.0.1:9070 2>/dev/null; then
echo 'ERROR: Dynamic port 9070 should not be forwarded in plain mode!'
exit 1
else
echo 'Dynamic port 9070 is correctly NOT forwarded in plain mode.'
fi

limactl delete -f $INSTANCE
echo "All tests passed for plain mode - only static ports work!"
# EOF
14 changes: 14 additions & 0 deletions hack/test-templates.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ declare -A CHECKS=(
["snapshot-online"]=""
["snapshot-offline"]=""
["port-forwards"]="1"
["static-port-forwards"]=""
["vmnet"]=""
["disk"]=""
["user-v2"]=""
Expand Down Expand Up @@ -90,6 +91,12 @@ case "$NAME" in
CHECKS["param-env-variables"]="1"
CHECKS["set-user"]="1"
;;
"static-port-forward")
CHECKS["static-port-forwards"]="1"
CHECKS["port-forwards"]=""
CHECKS["container-engine"]=""
CHECKS["restart"]=""
;;
"docker")
CONTAINER_ENGINE="docker"
;;
Expand Down Expand Up @@ -383,6 +390,13 @@ if [[ -n ${CHECKS["port-forwards"]} ]]; then
set +x
fi

if [[ -n ${CHECKS["static-port-forwards"]} ]]; then
INFO "Testing static port forwarding functionality"
"${scriptdir}/test-plain-static-port-forward.sh" "$NAME"
"${scriptdir}/test-nonplain-static-port-forward.sh" "$NAME"
INFO "All static port forwarding tests passed!"
fi

if [[ -n ${CHECKS["vmnet"]} ]]; then
INFO "Testing vmnet functionality"
guestip="$(limactl shell "$NAME" ip -4 -j addr show dev lima0 | jq -r '.[0].addr_info[0].local')"
Expand Down
64 changes: 64 additions & 0 deletions hack/test-templates/static-port-forward.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
images:
- location: "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64.img"
arch: "x86_64"

provision:
- mode: system
script: |
apt-get update
apt-get install -y nginx python3
systemctl enable nginx
systemctl start nginx

cat > /etc/systemd/system/test-server-9080.service << 'EOF'
[Unit]
Description=Test Server on Port 9080
After=network.target

[Service]
Type=simple
User=root
ExecStart=/usr/bin/python3 -m http.server 9080 --bind 127.0.0.1
Restart=always

[Install]
WantedBy=multi-user.target
EOF

cat > /etc/systemd/system/test-server-9070.service << 'EOF'
[Unit]
Description=Test Server on Port 9070
After=network.target

[Service]
Type=simple
User=root
ExecStart=/usr/bin/python3 -m http.server 9070 --bind 127.0.0.1
Restart=always

[Install]
WantedBy=multi-user.target
EOF

mkdir -p /var/www/html-9080
mkdir -p /var/www/html-9070

echo '<html><body><h1>Dynamic port 9080</h1></body></html>' > /var/www/html-9080/index.html
echo '<html><body><h1>Dynamic port 9070</h1></body></html>' > /var/www/html-9070/index.html

systemctl daemon-reload
systemctl enable test-server-9080
systemctl enable test-server-9070
systemctl start test-server-9080
systemctl start test-server-9070

portForwards:
- guestPort: 80
hostPort: 9090
static: true
- guestPort: 9080
hostPort: 9080
static: false
- guestPort: 9070
hostPort: 9070
static: false
Loading
Loading