Skip to content

fix: use fake local network for port-forward tests #11119

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

Merged
merged 1 commit into from
Dec 11, 2023

Conversation

spikecurtis
Copy link
Contributor

Fixes #10979

Testing code that listens on a specific port has created a long battle with flakes. Previous attempts to deal with this include opening a listener on a port chosen by the OS, then closing the listener, noting the port and starting the test with that port.
This still flakes, notably in macOS which has a proclivity to reuse ports quickly.

Instead of fighting with the chaos that is an OS networking stack, this PR fakes the host networking in tests.

I've taken a small step here, only faking out the Listen() calls that port-forward makes, but I think over time we should be transitioning all networking the CLI does to an abstract interface so we can fake it. This allows us to run in parallel without flakes and
presents an opportunity to test error paths as well.

Copy link
Contributor Author

Current dependencies on/for this PR:

This stack of pull requests is managed by Graphite.

Copy link
Member

@mafredri mafredri left a comment

Choose a reason for hiding this comment

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

Other than a few concerns and renaming suggestions, I think this looks fine.

Another way we might've been able to remove collisions is to use new ranges for localhost, like 127.0.1.1, 127.0.2.1, etc, each test could reserve one and be quite sure that a freed port isn't re-used.

"net"
"strconv"

"github.com/pion/udp"
Copy link
Member

Choose a reason for hiding this comment

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

This package is no longer maintained (repo archived). We should consider switching to github.com/pion/transport/v2/udp if we still need the library.

// Use pion here so that we get a stream-style net.Conn listener, instead
// of a packet-oriented connection that can read and write to multiple
// addresses.
return udp.Listen(network, &net.UDPAddr{
Copy link
Member

Choose a reason for hiding this comment

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

Are we sure we want to use pion for UDP in all CLI usage going forward? (I understand this is mainly used by port-forward, so this is mainly a future concern about making assumptions based on one use-case).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure, no. We can always refactor later if this doesn't meet the future use case.

Putting UDP into its own method makes faking this interface harder/more complex.

// osNet is an implementation that call the real OS for networking.
type osNet struct{}

func (osNet) Listen(network, address string) (net.Listener, error) {
Copy link
Member

Choose a reason for hiding this comment

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

How would we handle situations where you might need to configure dialing via net.Dialer?

Maybe we don't have such use-cases at this time but I'm concerned since we are introducing the field on clibase, which suggests it's the only way one should establish network connections (compared to injecting it into the specific command that needs it).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Add a GetDialer(options) net.Dialer method to the Net interface, or somesuch.

(compared to injecting it into the specific command that needs it)

I'm not sure what you mean here. How would that suggestion be different than what I've implemented?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The idea here is that Net is like the stdio and environment fields on Invocation. When you actually run the command from a command line, you get the OS-provided functions, but in testing we can hook various things up to intercept the command interacting with the OS, just like how today we hook stdio into the tests.

Copy link
Member

Choose a reason for hiding this comment

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

Add a GetDialer(options) net.Dialer method to the Net interface, or somesuch.

I suppose that could work, injecting resolver/dialer for the in-mem stuff 👍🏻.

I'm not sure what you mean here. How would that suggestion be different than what I've implemented?

With that parenthesis, I basically meant not putting the field in clibase/inv until there are more use-cases, i.e. keeping it port-forwarding only for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure how we could keep it port-forwarding only without a major refactor. The command handler only gets an Invocation, which is also the only thing we have access to from clitest.New(). There literally isn't anything else to hang it on besides Invocation (module-level vars will wreak havoc in parallel testing).

Copy link
Member

Choose a reason for hiding this comment

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

There's also context.Context, but I hear you. That's perhaps a bit hidden away.

@spikecurtis
Copy link
Contributor Author

Another way we might've been able to remove collisions is to use new ranges for localhost, like 127.0.1.1, 127.0.2.1, etc, each test could reserve one and be quite sure that a freed port isn't re-used.

On macOS you can't bind to these addresses: listen tcp 127.0.10.1:5000: bind: can't assign requested address

@spikecurtis spikecurtis merged commit 50575e1 into main Dec 11, 2023
@spikecurtis spikecurtis deleted the spike/10979-port-forward-flakes branch December 11, 2023 10:51
@github-actions github-actions bot locked and limited conversation to collaborators Dec 11, 2023
@@ -160,21 +124,27 @@ func TestPortForward(t *testing.T) {
inv.Stdin = pty.Input()
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()

iNet := newInProcNet()
inv.Net = iNet
Copy link
Member

Choose a reason for hiding this comment

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

Looks like GitHub lost one of my comments. 😔

I had asked about what i in iNet means (perhaps testNet, fakeNet, memNet, etc would be more descriptive).

I also suggested a name change => s/Proc/Mem/ to better match our other implementations.

Since this got merged already, I won't demand you make the changes (just a small wish 😄)

Copy link
Contributor Author

Looks like GitHub lost one of my comments. 😔

I had asked about what i in iNet means (perhaps testNet, fakeNet, memNet, etc would be more descriptive).

I also suggested a name change => s/Proc/Mem/ to better match our other implementations.

Since this got merged already, I won't demand you make the changes (just a small wish 😄)

Soz! When I dip back into this for other networking stuff I'll keep it in mind.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

flake: test-go-macos: TestPortForward/UDP_OnePort: match deadline exceeded: context deadline exceeded
2 participants