Skip to content
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

docs: document the SSHd tunnel #2514

Merged
merged 3 commits into from
Apr 25, 2024
Merged
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
12 changes: 12 additions & 0 deletions docs/features/common_functional_options.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ If you need to either pass additional environment variables to a container or ov
postgres, err = postgresModule.RunContainer(ctx, testcontainers.WithEnv(map[string]string{"POSTGRES_INITDB_ARGS": "--no-sync"}))
```

#### WithHostPortAccess

- Not available until the next release of testcontainers-go <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>

If you need to access a port that is already running in the host, you can use `testcontainers.WithHostPortAccess` for example:

```golang
postgres, err = postgresModule.RunContainer(ctx, testcontainers.WithHostPortAccess(8080))
```

To understand more about this feature, please read the [Exposing host ports to the container](/features/networking/#exposing-host-ports-to-the-container) documentation.

#### WithLogConsumers

- Since testcontainers-go <a href="https://github.com/testcontainers/testcontainers-go/releases/tag/v0.28.0"><span class="tc-version">:material-tag: v0.28.0</span></a>
Expand Down
38 changes: 38 additions & 0 deletions docs/features/networking.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,44 @@ It is normally advisable to use `Host` and `MappedPort` together when constructi
!!! info
Setting the `TC_HOST` environment variable overrides the host of the docker daemon where the container port is exposed. For example, `TC_HOST=172.17.0.1`.

## Exposing host ports to the container

- Not available until the next release of testcontainers-go <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>

In some cases it is necessary to make a network connection from a container to a socket that is listening on the host machine. Natively, Docker has limited support for this model across platforms. Testcontainers, however, makes this possible, allowing your code to access services running on the host machine.

In this example, assume that `freePorts` is an slice of ports on our test host machine where different servers (e.g. a web application) are running.

We can simply create a container and expose these ports to the container using the `ContainerRequest` struct:

<!--codeinclude-->
[Exposing the host ports](../../port_forwarding_test.go) inside_block:hostAccessPorts
<!--/codeinclude-->

!!!warning
Note that the server/s listening on those ports on the host must have been started before the container is created.

From a container's perspective, the hostname will be `host.testcontainers.internal` and the port will be the same value as any in the `freePorts` slice. _Testcontainers for Go_ exposes the host internal name as the `testcontainers.HostInternal` constant, so you can use it to build the address to connect to the host on the exposed port.

<!--codeinclude-->
[Accessing the exposed host port from a container](../../port_forwarding_test.go) inside_block:wgetHostInternal
<!--/codeinclude-->

In the above example we are executing an HTTP request from the command line inside the given container to the host machine.

### How it works

When you expose a host port to a container, _Testcontainers for Go_ creates an SSHD server companion container, which will be used to forward the traffic from the container to the host machine. This is done by creating a tunnel between the container and the host machine through the SSHD server container.

You can find more information about this SSHD server container on its Github repository: [https://github.com/testcontainers/sshd-docker](https://github.com/testcontainers/sshd-docker).

<!--codeinclude-->
[SSHD Server Docker Image](../../port_forwarding.go) inside_block:hubSshdImage
<!--/codeinclude-->

!!!important
At this moment, each container request will use a new SSHD server container. This means that if you create multiple containers with exposed host ports, each one will have its own SSHD server container.

## Docker's host networking mode

From [Docker documentation](https://docs.docker.com/network/drivers/host/):
Expand Down
11 changes: 11 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,17 @@ func WithHostConfigModifier(modifier func(hostConfig *container.HostConfig)) Cus
}
}

// WithHostPortAccess allows to expose the host ports to the container
func WithHostPortAccess(ports ...int) CustomizeRequestOption {
return func(req *GenericContainerRequest) {
if req.HostAccessPorts == nil {
req.HostAccessPorts = []int{}
}

req.HostAccessPorts = append(req.HostAccessPorts, ports...)
}
}

// WithImage sets the image for a container
func WithImage(image string) CustomizeRequestOption {
return func(req *GenericContainerRequest) {
Expand Down
33 changes: 33 additions & 0 deletions options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,36 @@ func TestWithEnv(t *testing.T) {
})
}
}

func TestWithHostPortAccess(t *testing.T) {
tests := []struct {
name string
req *testcontainers.GenericContainerRequest
hostPorts []int
expect []int
}{
{
name: "add to existing",
req: &testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
HostAccessPorts: []int{1, 2},
},
},
hostPorts: []int{3, 4},
expect: []int{1, 2, 3, 4},
},
{
name: "add to nil",
req: &testcontainers.GenericContainerRequest{},
hostPorts: []int{3, 4},
expect: []int{3, 4},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
opt := testcontainers.WithHostPortAccess(tc.hostPorts...)
opt.Customize(tc.req)
require.Equal(t, tc.expect, tc.req.HostAccessPorts)
})
}
}
2 changes: 2 additions & 0 deletions port_forwarding.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import (
)

const (
// hubSshdImage {
image string = "testcontainers/sshd:1.2.0"
// }
// HostInternal is the internal hostname used to reach the host from the container,
// using the SSHD container as a bridge.
HostInternal string = "host.testcontainers.internal"
Expand Down
4 changes: 4 additions & 0 deletions port_forwarding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,13 @@ func TestExposeHostPorts(t *testing.T) {
}

req := testcontainers.GenericContainerRequest{
// hostAccessPorts {
ContainerRequest: testcontainers.ContainerRequest{
Image: "alpine:3.17",
HostAccessPorts: freePorts,
Cmd: []string{"top"},
},
// }
Started: true,
}

Expand Down Expand Up @@ -128,11 +130,13 @@ func TestExposeHostPorts(t *testing.T) {
}

func httpRequest(t *testing.T, c testcontainers.Container, port int) (int, string) {
// wgetHostInternal {
code, reader, err := c.Exec(
context.Background(),
[]string{"wget", "-q", "-O", "-", fmt.Sprintf("http://%s:%d", testcontainers.HostInternal, port)},
tcexec.Multiplexed(),
)
// }
if err != nil {
t.Fatal(err)
}
Expand Down
Loading