Skip to content

add cp and port-forward commands for VM file copying and port forwarding#369

Open
darioajr wants to merge 5 commits intocrc-org:mainfrom
darioajr:feature/cp-and-portforward
Open

add cp and port-forward commands for VM file copying and port forwarding#369
darioajr wants to merge 5 commits intocrc-org:mainfrom
darioajr:feature/cp-and-portforward

Conversation

@darioajr
Copy link
Copy Markdown

@darioajr darioajr commented Apr 24, 2026

Title: add cp and port-forward commands

Description:

This PR adds two new commands that make it easier to interact with running VMs managed by macadam, bringing the CLI closer to the ergonomics of podman machine, docker, and kubectl.

macadam cp
Copies files between the host and a running VM using SCP under the hood. Follows the same [VMNAME:]PATH convention used by podman cp, docker cp, and kubectl cp — no separate VM name argument needed.

macadam cp ./file.txt myvm:/tmp/
macadam cp myvm:/tmp/file.txt ./
macadam port-forward (alias: pf)
Forwards one or more local ports to a running VM over SSH. Follows the [LOCAL:]REMOTE port spec convention used by kubectl port-forward, hiding the raw SSH -L syntax from the user. Supports multiple port specs as positional arguments and a --address flag to bind on a specific local interface.

macadam port-forward myvm 8080
macadam port-forward myvm 8080:8080 9090:9090
macadam port-forward --address 0.0.0.0 myvm 8080:8080
macadam pf myvm 8080:8080
Design notes
Both commands resolve SSH credentials (username, identity file, port) directly from macadam inspect, so no manual configuration is required.
cp validates that at least one path uses the VMNAME: prefix and errors early if src and dst reference different VMs.
port-forward runs in the foreground (Ctrl+C to stop), consistent with kubectl port-forward. Background execution can be achieved with the shell & operator.
Both commands use LocalhostSSHArgs() for consistent SSH options across all macadam commands.

Summary by CodeRabbit

  • New Features
    • Added macadam cp to copy files between host and VM with VM-qualified paths and recursive --recursive/-r support; requires the target VM to be running.
    • Added macadam port-forward (alias pf) to forward one or more local ports to a running VM via SSH; accepts REMOTE or LOCAL:REMOTE specs and a bind address option.

Copilot AI review requested due to automatic review settings April 24, 2026 14:34
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Warning

Rate limit exceeded

@darioajr has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 34 minutes and 38 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a523987f-2d0b-49e0-b533-011556f6d350

📥 Commits

Reviewing files that changed from the base of the PR and between b03b40d and ed38fee.

📒 Files selected for processing (1)
  • cmd/macadam/cp.go

Walkthrough

Adds two new macadam CLI commands: cp (copies between host and VM using scp, with optional -r) and port-forward/pf (establishes SSH local port forwards). Both parse VM-qualified operands, ensure a single VM in Running state, and invoke system scp/ssh with the VM’s SSH details.

Changes

File copy command

Layer / File(s) Summary
CLI wiring & flags
cmd/macadam/cp.go
Adds cp Cobra command, registers it, and adds --recursive/-r flag.
Argument parsing / validation
cmd/macadam/cp.go
Parses SRC DST via pcopy.ParseSourceAndDestination, requires at least one VMNAME: operand, rejects mismatched VM names.
Provider & VM resolution
cmd/macadam/cp.go
Resolves default provider, looks up VM, loads VM state, errors unless VM is Running.
scp invocation construction
cmd/macadam/cp.go
Builds scp args: -i identity, -P port, optional -r, machine.LocalhostSSHArgs(), and converts remote paths to user@address:path via resolveCpPath.
Execution
cmd/macadam/cp.go
Execs scp with stdin/stdout/stderr wired to the parent process.

Port forwarding command

Layer / File(s) Summary
CLI wiring & flags
cmd/macadam/port_forward.go
Adds port-forward (pf) Cobra command, registers it, and adds --address bind flag (default localhost).
Argument parsing / validation
cmd/macadam/port_forward.go
Validates NAME and at least one port spec; parses port specs of form REMOTE or LOCAL:REMOTE.
Provider & VM resolution
cmd/macadam/port_forward.go
Resolves default provider, looks up VM, loads VM state, errors unless VM is Running.
SSH forwarding construction
cmd/macadam/port_forward.go
Converts specs to -L flags (BIND:LOCAL:127.0.0.1:REMOTE), validates ports in [1,65535], adds -i identity, -p/-P port and machine.LocalhostSSHArgs(), and sets -N.
Execution
cmd/macadam/port_forward.go
Execs ssh -N with forwarding rules and stdio wired to the parent process.

Sequence Diagrams

sequenceDiagram
    participant User
    participant CLI as macadam cp
    participant Provider as VM Provider
    participant SCP
    participant VM

    User->>CLI: macadam cp SRC DST
    CLI->>CLI: Parse operands, ensure single VM
    CLI->>Provider: Resolve provider, lookup VM
    Provider-->>CLI: VM info (state, SSH user, address, port, key)
    CLI->>SCP: Exec scp with SSH options
    SCP->>VM: SSH connection to VM
    SCP->>VM: Transfer files
    SCP-->>User: Exit status
Loading
sequenceDiagram
    participant User
    participant CLI as macadam port-forward
    participant Provider as VM Provider
    participant SSH
    participant VM

    User->>CLI: macadam port-forward NAME [ports...]
    CLI->>CLI: Parse port specs, validate, build -L flags
    CLI->>Provider: Resolve provider, lookup VM
    Provider-->>CLI: VM info (state, SSH user, address, port, key)
    CLI->>SSH: Exec ssh -N with -L rules and SSH options
    SSH->>VM: Establish SSH connection
    SSH->>VM: Create port forward tunnels
    SSH-->>User: Local ports listening
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 I hopped through sockets, keys in paw,
I stitched two worlds with scp and ssh—
A tunnel here, a copy there,
Quiet tunnels, files that flow,
Joyful rabbit, trail aglow.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the two main additions: cp command for file copying and port-forward command for port forwarding to VMs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get your free trial and get 200 agent minutes per Slack user (a $50 value).


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 34 minutes and 38 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds two new macadam CLI commands to improve day-to-day interaction with running VMs: copying files via SCP (macadam cp) and forwarding local ports over SSH (macadam port-forward / macadam pf).

Changes:

  • Add macadam port-forward (pf) command to forward one or more ports to a running VM using SSH -L.
  • Add macadam cp command to copy files between host and VM using VMNAME:-prefixed path syntax and scp.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
cmd/macadam/port_forward.go Implements port-forward / pf command and port-spec parsing for SSH -L forwarding.
cmd/macadam/cp.go Implements cp command with VMNAME: path parsing and scp execution using VM SSH config.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cmd/macadam/cp.go Outdated
Comment thread cmd/macadam/cp.go Outdated
Comment thread cmd/macadam/cp.go
Comment thread cmd/macadam/port_forward.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/macadam/cp.go`:
- Around line 45-46: splitCpPath currently treats any ':' as a VM separator
which misparses local paths containing colons; change splitCpPath so it only
treats the first ':' as a VM-prefix when the segment before that ':' contains no
path separators ('/' or '\') and is not a Windows drive letter pattern (single
ASCII letter), and optionally validate the prefix against allowed VM name
characters (alnum, '-', '_'); update all call sites (the splitCpPath calls used
in cp.go around the src/dst parsing) to rely on this tightened logic so local
filenames like "C:\foo" or "name:with:colon" are not mistaken for VM:PATH.

In `@cmd/macadam/port_forward.go`:
- Around line 101-114: The toSSHForwardFlag function currently only checks
numeric syntax via strconv.Atoi but allows 0, negatives, and >65535; update
validation to ensure both parsed localPort and remotePort are within the valid
TCP port range (1–65535). After parsing with strconv.Atoi, check each value: if
p < 1 || p > 65535 return an error like fmt.Errorf("invalid port spec %q: local
port must be between 1 and 65535", spec) (and similarly for remote port), and
otherwise continue to format the SSH flag as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2ab8d29c-4fb4-4c6b-b95d-dfb668059eff

📥 Commits

Reviewing files that changed from the base of the PR and between 64c1d1b and e9814db.

📒 Files selected for processing (2)
  • cmd/macadam/cp.go
  • cmd/macadam/port_forward.go

Comment thread cmd/macadam/cp.go Outdated
Comment thread cmd/macadam/port_forward.go
… instructions; add port number validation function
@darioajr
Copy link
Copy Markdown
Author

Currently, I use my extension at https://github.com/darioajr/macadam-ext, but I would like this support to be native, which is why I created this PR.

@darioajr
Copy link
Copy Markdown
Author

All the issues pointed out were fixed in the second commit.

Copy link
Copy Markdown
Collaborator

@lstocchi lstocchi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @darioajr , thanks for the PR 🎉
Looks good, just some small comments.
I think we could try to reuse some code from podman (like moving the scp command there as there is already something similar) but we can do it later

Comment thread cmd/macadam/cp.go Outdated
Comment on lines +86 to +88
if address == "" {
address = "localhost"
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be removed. GetAddress will never return an empty string

func (mc *MachineConfig) GetAddress() string {
	if mc.IPAddress != "" {
		return mc.IPAddress
	}

	// Default to the IPv4 localhost address "127.0.0.1".
	// When using mirrored networking mode in WSL, attempting to connect by the name "localhost"
	// often resolves to the IPv6 address (::1) first. Mirrored networking
	// does not support this IPv6 localhost address resolution, leading to connection timeouts.
	// By using "127.0.0.1" it avoids this resolution issue.
	return "127.0.0.1"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 5775da4d:

Comment thread cmd/macadam/port_forward.go Outdated
Comment on lines +74 to +76
if address == "" {
address = "localhost"
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 5775da4d

Comment thread cmd/macadam/cp.go Outdated
- Remove redundant empty-address fallback: GetAddress() always returns
  a non-empty string (falls back to 127.0.0.1 internally)
- Guard splitCpPath against paths like ./data:backup.txt being
  misidentified as VM names by rejecting any prefix containing a slash
@darioajr darioajr force-pushed the feature/cp-and-portforward branch from 57b8e85 to 5775da4 Compare April 30, 2026 14:09
@darioajr
Copy link
Copy Markdown
Author

darioajr commented May 3, 2026

Hi @lstocchi, thanks for the review! 🙏

I've addressed all your comments in commit 5775da4d:

  1. cmd/macadam/cp.go — redundant address fallback
    Removed the if address == "" { address = "localhost" } block, since GetAddress() already guarantees a non-empty return (defaults to 127.0.0.1 internally).

  2. cmd/macadam/port_forward.go — same redundant fallback
    Removed for the same reason.

  3. cmd/macadam/cp.go — splitCpPath misidentifying paths as VM names
    Added the slash-detection guard you suggested. Paths like ./data:backup.txt are now correctly treated as local paths instead of being parsed as a VM name:

prefix := path[:idx]
// If the prefix contains a slash, it's a path, not a VM name
if strings.ContainsAny(prefix, "/\") {
return "", path
}
Regarding your suggestion to reuse the scp logic from podman — agreed, that makes sense as a follow-up. Happy to open a separate issue/PR for it once this one lands.

Let me know if there's anything else you'd like me to adjust!

Replace the local splitCpPath helper with
pcopy.ParseSourceAndDestination from
github.com/containers/podman/v5/pkg/copy, addressing the reviewer's
suggestion to reuse podman code where possible.

Why:
- Podman's parser uses filepath.IsAbs to detect absolute paths,
  which correctly handles Windows UNC paths (\\server\share\...)
  and other edge cases that our 1-letter-prefix heuristic missed.
- It also rejects empty paths early (e.g. `cp myvm: ./local`),
  surfacing a clear error instead of letting scp fail with a
  confusing message.
- Less custom parsing code for us to maintain, and the behavior
  now matches podman's own `cp` conventions.

What did NOT change, and why:
- scp invocation stays local: machine.LocalhostSSHCopy() is not
  yet available in the vendored podman v5 (only on podman main).
  Once macadam moves to v6, the exec.Command("scp", ...) block
  can be replaced by that helper.
- The "at least one side must be a VM" and "src/dst VMs must
  match" checks remain here, since they are macadam-specific
  rules; podman's parser is generic and does not enforce them.
- resolveCpPath stays as a small local helper for formatting
  user@address:path; no equivalent exists in podman v5.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
cmd/macadam/cp.go (1)

102-106: ⚡ Quick win

Use exec.CommandContext to honour the Cobra command's context.

exec.Command has no knowledge of cmd.Context(), so a programmatic context cancellation (e.g. timeout, test teardown) will not kill the child scp process. For normal terminal Ctrl+C the process group signal saves you, but the idiomatic Go pattern is to propagate the context explicitly.

♻️ Proposed fix
-	scpExec := exec.Command("scp", scpArgs...)
+	scpExec := exec.CommandContext(cmd.Context(), "scp", scpArgs...)
 	scpExec.Stdin = os.Stdin
 	scpExec.Stdout = os.Stdout
 	scpExec.Stderr = os.Stderr
 	return scpExec.Run()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/macadam/cp.go` around lines 102 - 106, Replace the exec.Command call with
exec.CommandContext so the child scp process is cancelled when the Cobra command
context is done: use cmd.Context() (the Cobra command's context) as the context
argument when creating the command that currently assigns to scpExec, keep
wiring scpExec.Stdin/Stdout/Stderr and return scpExec.Run() unchanged so the
command honors cancellation/timeouts; update the code around the scpExec
variable in cp.go accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cmd/macadam/cp.go`:
- Around line 102-106: Replace the exec.Command call with exec.CommandContext so
the child scp process is cancelled when the Cobra command context is done: use
cmd.Context() (the Cobra command's context) as the context argument when
creating the command that currently assigns to scpExec, keep wiring
scpExec.Stdin/Stdout/Stderr and return scpExec.Run() unchanged so the command
honors cancellation/timeouts; update the code around the scpExec variable in
cp.go accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5385f6bb-38df-407a-a551-4cef54c0fbb2

📥 Commits

Reviewing files that changed from the base of the PR and between 57b8e85 and b03b40d.

📒 Files selected for processing (2)
  • cmd/macadam/cp.go
  • cmd/macadam/port_forward.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • cmd/macadam/port_forward.go

@darioajr
Copy link
Copy Markdown
Author

darioajr commented May 3, 2026

Hi @lstocchi, just a quick update on your reuse suggestion 🙌

I had a closer look at the podman codebase to see how much we could share. The path-parsing part has now been addressed in this PR:

✅ Done in this PR

Replaced our custom splitCpPath with pcopy.ParseSourceAndDestination from github.com/containers/podman/v5/pkg/copy. This gives us:

Robust Windows path handling (uses filepath.IsAbs, so UNC paths and other edge cases are handled correctly).
Early validation of empty paths (e.g. macadam cp myvm: ./local now fails fast with a clear error instead of leaking through to scp).
Less custom parsing code to maintain, and behavior aligned with podman's own cp conventions.
🔮 Possible follow-ups (not in this PR)

A few things still look reusable but require either an upstream change or a podman version bump:

machine.LocalhostSSHCopy() — exists on podman main but is not yet in the vendored v5. Once macadam moves to v6 (or if a backport is accepted upstream), we can drop our local exec.Command("scp", ...) block and use it directly.
Moving the scp helper into podman — as you mentioned, it would be a good fit alongside the existing image scp logic. Happy to open a follow-up issue/PR upstream to discuss extracting a shared helper.
resolveCpPath — still macadam-local for now (4 lines), but could naturally fold into a future shared helper.
What stays macadam-specific by design: the "at least one side must be a VM" and "src/dst VMs must match" checks, since podman's parser is generic and doesn't enforce these rules.

Let me know if you'd like me to file the upstream issue now, or if you'd rather we revisit it after this PR lands 👍

@lstocchi
Copy link
Copy Markdown
Collaborator

lstocchi commented May 5, 2026

Sorry for the late answer @darioajr , i've been quite busy on other stuff. I'll give it a try tomorrow morning. Looks good at first sight. Could you please squash the commits? I think having one for cp and one for port-forward is good enough

Comment thread cmd/macadam/cp.go
Comment on lines +45 to +47
if len(args) != 2 {
return errors.New("usage: macadam cp [options] SRC DST")
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of this you could add a check at command definition (like https://github.com/containers/podman/blob/main/cmd/podman/machine/cp.go#L31)

Comment on lines +47 to +49
if len(args) < 2 {
return errors.New("VM name and at least one port spec are required")
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as cp. You could use cobra.MinimumNArgs(2)

Comment on lines +86 to +93
sshArgs = append(sshArgs, machine.LocalhostSSHArgs()...)
sshArgs = append(sshArgs, "-N", mc.SSH.RemoteUsername+"@"+address)

sshExec := exec.Command("ssh", sshArgs...)
sshExec.Stdin = os.Stdin
sshExec.Stdout = os.Stdout
sshExec.Stderr = os.Stderr
return sshExec.Run()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before executing the command we could display a message similar to what does kubectl or other tools

Forwarding <local>→<remote>... Press Ctrl+C to stop

Comment thread cmd/macadam/cp.go
scpArgs = append(scpArgs, machine.LocalhostSSHArgs()...)
scpArgs = append(scpArgs, src, dst)

scpExec := exec.CommandContext(cmd.Context(), "scp", scpArgs...)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good approach. Why don't use the same in port-forward? I think it would make sense so the process is killed if the command is cancelled

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants