-
-
Notifications
You must be signed in to change notification settings - Fork 510
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
feat: forward host ports to a container using an SSH tunnel #2471
feat: forward host ports to a container using an SSH tunnel #2471
Conversation
✅ Deploy Preview for testcontainers-go ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the start!
some comments about where context can be propagated to facilitate an incoming context with deadline (I use one all the time with tc). Also, some notes about a logic problem I think.
Finally, if anything is replicating logic from another project, maybe worthwhile citing it even if in the same org, as not all maintainers will know to look in the java side for congruence or practice updates.
// to allow the container to reach the SSHD container. | ||
hostConfig.ExtraHosts = append(hostConfig.ExtraHosts, fmt.Sprintf("%s:%s", hostInternal, sshdIP)) | ||
|
||
modes := []container.NetworkMode{container.NetworkMode(sshdFirstNetwork), "none", "host"} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the edge case where sshdFirstNetwork
== "" .. is this ok here? If not, I would early exit if len(req.Networks) == 0
a lot higher in this function, as it also reduces indentation and breaks apart some other logic
|
||
// do not override the original HostConfigModifier | ||
originalHCM := req.HostConfigModifier | ||
req.HostConfigModifier = func(hostConfig *container.HostConfig) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: not sure about style in this project, but functions like this are easier to unit test if they are not anonymous.
A big thanks from Microcks team 🙏🏻 |
🙇 I need to focus on this task, as I found a blocker here: I cannot see the communication being produced between the container and the exposed port in the host. The code is trying to mimic what the Java team is doing:
I verified that it's possible to telnet from the container to the sshd server (port 22) using Could anybody give this PR a try and help if possible? |
@joonas-fi I noticed you are doing something similar in your holepunch-client. Though maybe you aren't using testcontainers, yet. If you have some time to spare to help folks get container port forwarding working, much oblidged! https://github.com/function61/holepunch-client/blob/master/cmd/holepunch/client.go |
* chore: only include the dockerignore if it contains ignore files * fix: the inclusions must be relative to the context * docs: document the dockerignore feature * chore: only include the dockerignore file if it exists
…rs#2475) * skip search for CACert if ssl has been turned off * add tests with and without ssl enabled * add all config keys that disable CA gen, restrict check to version 8 * rename test to match content
Bumps [idna](https://github.com/kjd/idna) from 3.6 to 3.7. - [Release notes](https://github.com/kjd/idna/releases) - [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.rst) - [Commits](kjd/idna@v3.6...v3.7) --- updated-dependencies: - dependency-name: idna dependency-type: indirect ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix: remove suspicious filepath.Join * chore: fix lint * fix: handle error * chore: reverse assertion for lint * feat: support generating TLS certificates on the fly * chore: apply to cockroachdb * chore: support saving the cert and priv key files to disk * chore: apply to rabbitmq * chore: simplify * chore: use in redpanda module * chore: lint * chore: set validFrom internally * fix: properly use the new API in redpanda * docs: document the TLS helpers * chore: simplify WithParent to accept the struct directly * chore: use tlscert package instead * fix: use non-deprecated API * docs: update * docs: fix examples * chore: use released version of tlscert * fix: add common name for the node cert
* /modules/dolt: wip, kinda working * /modules/dolt: get tests passing * /{.github,.vscode,docs,mkdocs,modules,sonar-project}: use modulegen tool * /modules/dolt/{dolt.go,examples_test.go}: run linter * /modules/dolt/{dolt.go,examples_test.go}: add methods for cloning * /{docs, modules}: add with creds file * /{docs,modules}: pr feedback, cleanup * /modules/dolt/examples_test.go: remove panics, lint * chore: run mod tidy * chore: include MustConnectionString method * chore: do not use named returns * chore: perform initialisation before the container has started --------- Co-authored-by: Manuel de la Peña <[email protected]>
* Bump default postgres version * Bump to use latest pg * Bump version from non-ancient version --------- Co-authored-by: bstrausser <[email protected]>
* Fix the non-default dbname error The linked issue described in great detail an issue where we assumed everyone would use the default database user, whose home DB defaults to the postgres database. When that was not the case, the snapshots would fail silently as the user would not connect to the right database to take the commands. This PR fixes the issue by adding the dbname by default in the command, and adds a test to validate this works as intended. In addition, it also adds some logic to handle any error that does not cause the exec command to fail, such as database access failures. Run the added test to test this works as intended. Closes testcontainers#2474 * Document the postgres dbname issue in the docs
…e, compose, qdrant, couchbase, k3s, milvus, mockserver, pulsar, kafka) (testcontainers#2505) * chore(deps): bump golang.org/x/net in /modules/kafka Bumps [golang.org/x/net](https://github.com/golang/net) from 0.17.0 to 0.23.0. - [Commits](golang/net@v0.17.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] <[email protected]> * chore(deps): bump golang.org/x/net in /modules/pulsar Bumps [golang.org/x/net](https://github.com/golang/net) from 0.17.0 to 0.23.0. - [Commits](golang/net@v0.17.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] <[email protected]> * chore(deps): bump golang.org/x/net in /modules/mockserver Bumps [golang.org/x/net](https://github.com/golang/net) from 0.17.0 to 0.23.0. - [Commits](golang/net@v0.17.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] <[email protected]> * chore(deps): bump golang.org/x/net in /modules/milvus Bumps [golang.org/x/net](https://github.com/golang/net) from 0.17.0 to 0.23.0. - [Commits](golang/net@v0.17.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] <[email protected]> * chore(deps): bump golang.org/x/net from 0.19.0 to 0.23.0 in /modules/k3s Bumps [golang.org/x/net](https://github.com/golang/net) from 0.19.0 to 0.23.0. - [Commits](golang/net@v0.19.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] <[email protected]> * chore(deps): bump golang.org/x/net in /modules/couchbase Bumps [golang.org/x/net](https://github.com/golang/net) from 0.20.0 to 0.23.0. - [Commits](golang/net@v0.20.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] <[email protected]> * chore(deps): bump golang.org/x/net in /modules/qdrant Bumps [golang.org/x/net](https://github.com/golang/net) from 0.20.0 to 0.23.0. - [Commits](golang/net@v0.20.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] <[email protected]> * chore(deps): bump golang.org/x/net in /modules/compose Bumps [golang.org/x/net](https://github.com/golang/net) from 0.20.0 to 0.23.0. - [Commits](golang/net@v0.20.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] <[email protected]> * chore(deps): bump golang.org/x/net in /modules/weaviate Bumps [golang.org/x/net](https://github.com/golang/net) from 0.20.0 to 0.23.0. - [Commits](golang/net@v0.20.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] <[email protected]> * chore(deps): bump golang.org/x/net in /modules/gcloud Bumps [golang.org/x/net](https://github.com/golang/net) from 0.21.0 to 0.23.0. - [Commits](golang/net@v0.21.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] <[email protected]> * chore(deps): bump golang.org/x/net in /modules/minio Bumps [golang.org/x/net](https://github.com/golang/net) from 0.21.0 to 0.23.0. - [Commits](golang/net@v0.21.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] <[email protected]> --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* feat: add testcontainers labels to compose containers * feat: support reaper for compose * chore: increase ryuk reconnection timeout on CI * chore: cache containers on UP * chore: more tuning for compose * chore: more consistent assertion * chore: the compose stack asks for the reaper, but each container then connects to it * chore: use different error groups the first time wait is called, the context is cancelled * chore: the lookup method include cache checks * chore: update tests to make them deterministic * chore: rename local compose testss * chore: support returning the dynamic port in the helper function * chore: try with default reconnection timeout * feat: support removing networks from compose * chore: support naming test services with local and api It will allow the tests to be more deterministic, as there could be service containers started from the local test suite with the same name as in the API test suite. * Revert "chore: try with default reconnection timeout" This reverts commit 336760c. * fix: typo
…pose instance (testcontainers#2509) * feat: support passing io.Reader when creating a compose instance * docs: change title
…stcontainers#2511) * feat: support overriding the default recreate options for compose * chore: validate recreation values
* fix: don't retry on permanent APIClient errors * fix: add more tests for un-retryable scenarios
* main: fix: don't retry on permanent APIClient errors (testcontainers#2506) feat: support overriding the default recreate options for compose (testcontainers#2511) feat: support passing io.Reader for compose files when creating a compose instance (testcontainers#2509) chore: add funding button for testcontainers (testcontainers#2510) feat: support Ryuk for the compose module (testcontainers#2485) chore(deps): bump golang.org/x/net in modules (minio, gcloud, weaviate, compose, qdrant, couchbase, k3s, milvus, mockserver, pulsar, kafka) (testcontainers#2505) fix: fallback to URL-path when parsing auth config URL without scheme (testcontainers#2488) fix(postgres): Fix the non-default dbname error (testcontainers#2489) feat: Bump default postgres version (testcontainers#2481) support Dolt (testcontainers#2177) chore: create TLS certs in a consistent manner (testcontainers#2478) chore(deps): bump idna from 3.6 to 3.7 (testcontainers#2480) Elasticsearch disable CA retrieval when ssl is disabled (testcontainers#2475) fix: handle dockerignore exclusions properly (testcontainers#2476)
@codefromthecrypt @JulienBreux could you give this one a try? 🙏 I think it's ready for review |
Let's go to testing at 2 PM today. |
Detected with go.uber.org/goleak
Hey @mdelapenya 👋🏻 Actually, I use this file to test, and it works well. 🎉 🎉 🎉 Design explanation :
https://gist.github.com/JulienBreux/3892189373cb246c180a0d8d2d1928c7 |
* main: (34 commits) break: return error from Customize request option (testcontainers#2267) fix: wrong copy paste (testcontainers#2515) docs: add documentation for Exec method (testcontainers#2451) docs: document the SSHd tunnel (testcontainers#2514) fix: enhance host configuration port binding (testcontainers#2512) feat: forward host ports to a container using an SSH tunnel (testcontainers#2471) Update follow_logs.md with adding missing package (testcontainers#2513) fix: don't retry on permanent APIClient errors (testcontainers#2506) feat: support overriding the default recreate options for compose (testcontainers#2511) feat: support passing io.Reader for compose files when creating a compose instance (testcontainers#2509) chore: add funding button for testcontainers (testcontainers#2510) feat: support Ryuk for the compose module (testcontainers#2485) chore(deps): bump golang.org/x/net in modules (minio, gcloud, weaviate, compose, qdrant, couchbase, k3s, milvus, mockserver, pulsar, kafka) (testcontainers#2505) fix: fallback to URL-path when parsing auth config URL without scheme (testcontainers#2488) fix(postgres): Fix the non-default dbname error (testcontainers#2489) feat: Bump default postgres version (testcontainers#2481) support Dolt (testcontainers#2177) chore: create TLS certs in a consistent manner (testcontainers#2478) chore(deps): bump idna from 3.6 to 3.7 (testcontainers#2480) Elasticsearch disable CA retrieval when ssl is disabled (testcontainers#2475) ...
* main: (44 commits) feat: expose JSON representation of a container with Inspect (testcontainers#2534) chore(deps): bump test-summary action to v2.3 (testcontainers#2535) chore(deps): bump jinja2 from 3.1.3 to 3.1.4 (testcontainers#2533) Update devcontainer image (testcontainers#2531) chore(influxdb): include more characters in wait for log regex (testcontainers#2532) fix(compose): avoid race conditions when caching services (testcontainers#2528) chore(deps): bump golangci/golangci-lint-action from 3.7.0 to 5.1.0 (testcontainers#2525) chore(deps): bump mkdocs-material from 8.2.7 to 9.1.21 (testcontainers#2524) chore(compose): return error in options (testcontainers#2520) chore(deps): bump github.com/compose-spec/compose-go/v2 from v2.0.0-rc8 to v2.1.0 (testcontainers#2519) chore(deps): bump github.com/containerd/containerd from 1.7.12 to 1.7.15 (testcontainers#2517) break: return error from Customize request option (testcontainers#2267) fix: wrong copy paste (testcontainers#2515) docs: add documentation for Exec method (testcontainers#2451) docs: document the SSHd tunnel (testcontainers#2514) fix: enhance host configuration port binding (testcontainers#2512) feat: forward host ports to a container using an SSH tunnel (testcontainers#2471) Update follow_logs.md with adding missing package (testcontainers#2513) fix: don't retry on permanent APIClient errors (testcontainers#2506) feat: support overriding the default recreate options for compose (testcontainers#2511) ...
probably just me, but I struggled on this in k3s (wanting a pod to call a service on my host). 1000x thanks if someone has a gist or contributes an integration test to the k3s module for this. |
Do you've a reproducible architecture? I need to test more. |
@JulienBreux (cc @salaboy as he probably is interested in this) So, I was testing coredns dnstap plugin (wanting to have traffic flow out to a listener I setup in the go test). However, since the PR here is about http and should work similar, I've a rough test below (pasted into k3s_test.go). I'm not sure I need an external name, but with or without it, I get "bad address" from alpine. const (
expectedResponse = "Hello, World!"
)
func Test_WithHostPortAccess(t *testing.T) {
ctx := context.Background()
port, err := getFreePort()
if err != nil {
t.Fatal(err)
}
portString := fmt.Sprintf("%d", port)
// Create a local HTTP server
server := http.Server{Addr: ":" + portString, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, expectedResponse)
})}
go server.ListenAndServe()
defer server.Close()
k3sContainer, err := k3s.RunContainer(ctx,
testcontainers.WithImage("docker.io/rancher/k3s:v1.27.1-k3s1"),
testcontainers.WithHostPortAccess(port),
)
if err != nil {
t.Fatal(err)
}
// Clean up the container
defer func() {
if err := k3sContainer.Terminate(ctx); err != nil {
t.Fatal(err)
}
}()
kubeConfigYaml, err := k3sContainer.GetKubeConfig(ctx)
if err != nil {
t.Fatal(err)
}
restcfg, err := clientcmd.RESTConfigFromKubeConfig(kubeConfigYaml)
if err != nil {
t.Fatal(err)
}
k8s, err := kubernetes.NewForConfig(restcfg)
if err != nil {
t.Fatal(err)
}
// Create a CNAME httptest pointing to host.testcontainers.internal
service := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "httptest",
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeExternalName,
ExternalName: "host.testcontainers.internal",
},
}
services := k8s.CoreV1().Services("default")
if _, err = services.Create(ctx, service, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
// Block until the service is ready to avoid race conditions
if err = kwait.PollUntilContextCancel(ctx, time.Second, true, func(ctx context.Context) (bool, error) {
if s, err := services.Get(ctx, service.Name, metav1.GetOptions{}); err != nil {
return false, err
} else if s.Spec.Type != service.Spec.Type || s.Spec.ExternalName != service.Spec.ExternalName {
return false, fmt.Errorf("unexpected service spec: %v", s.Spec)
}
return true, nil
}); err != nil {
t.Fatal(err)
}
// Create a pod that attempts to access the host's http service using wget
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pod",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "alpine",
Image: "alpine:3.17",
Command: []string{"wget", "-qO-", fmt.Sprintf("http://%s:%d", service.Name, port)},
},
},
RestartPolicy: corev1.RestartPolicyNever,
},
}
pods := k8s.CoreV1().Pods("default")
if _, err = pods.Create(ctx, pod, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
// Wait for the wget to complete
if err = kwait.PollUntilContextCancel(ctx, time.Second, true, func(ctx context.Context) (bool, error) {
pod, err := pods.Get(ctx, "test-pod", metav1.GetOptions{})
if err != nil {
return false, err
}
for _, status := range pod.Status.ContainerStatuses {
if status.State.Terminated != nil {
return true, nil
}
}
return false, nil
}); err != nil {
t.Fatal(err)
}
// Check the logs of the Pod to ensure that we read the expected response
logs, err := pods.GetLogs("test-pod", &corev1.PodLogOptions{}).DoRaw(ctx)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(logs), expectedResponse) {
t.Fatalf("wget did not return the expected response, got: %s", logs)
}
}
// getFreePort asks the kernel for a free open port that is ready to use.
func getFreePort() (int, error) {
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
if err != nil {
return 0, err
}
l, err := net.ListenTCP("tcp", addr)
if err != nil {
return 0, err
}
defer l.Close()
return l.Addr().(*net.TCPAddr).Port, nil
} FYI I am running docker via |
What does this PR do?
This PR defines a new field in the container request struct, so that users can define which host ports are needed to be exposed to a given container.
If set, an internal SSHD server container will be spun before the container, forwarding each exposed host port to the container using an SSH tunnel.
All this logic will happen thanks to the lifecycle container hooks:
host.testcontainers.internal
key will land in the container's/etc/hosts
file (Linux).As part of this PR we are adding an
internal/core/network
package to start adding core, network-related logic in it. This is needed because we need to get the Docker network of the container to pass it to the SSHD container.Why is it important?
At the moment it's not possible for a container to access a port in the host, which is handy for multiple use cases (please see #2212)
Related issues
Follow-ups
This PR is in draft for one single reason: I'm pushing this branch to receive feedback from the community. I do not want to add any new library for the SSH tunnel, just using the plain stdlib. I see it not very friendly to use it and would like to have more eyes on this feature.
Both Java and .Net use a library to forward the port, and in Go I'd like to avoid that.
At the same time, there is a TODO in the code, as we are adding the network to the sshd container with a functional option that is an exact clone of the
network.WithNetwork
. If we use it, then a circular dependency will appear, which is not desired. This demonstrates that our efforts for refactoring the API with a more decoupled API is in the right direction.One final follow-up is to consider starting just one SSHD container per test session, ala Ryuk: at the moment, there is one sshd container per container request. I think we can start this way, make progress, and move to that approach once needed. Thoughts?