I am using the go-rod library to do some web automation, this service that I am making will live inside a container, and for local debugging I want to be able to connect to the browser I have running locally. This issue is that --remote-debugging-address flag only works with --headless flag. This is a big issue for me as I need to inspect and look at the browser while developing. I've read that SSH tunneling can be done but I am unable to get it working. I tried all combinations of flags, ports and hosts and all result in some kind of error.
Current setup
Running the chromium instance on my host chromium --remote-debugging-port=9222. Which gets me an address like so DevTools listening on ws://0.0.0.0:9222/devtools/browser/f66524d5-eecb-44c2-a48c-5b14d8e6d998
Running my app via this script
#!/bin/bash
docker build -t rod-test .
docker run --add-host=host.docker.internal:host-gateway --rm rod-test
The docker file
FROM golang:1.16-alpine
WORKDIR /app
COPY go.mod ./
COPY go.sum ./
RUN go mod download
COPY *.go ./
RUN go build -o /rod
CMD [ "/rod" ]
The main.go
package main
import (
"fmt"
"github.com/go-rod/rod"
)
func main() {
browser := rod.New().ControlURL("ws://host.docker.internal:9222/devtools/browser/f66524d5-eecb-44c2-a48c-5b14d8e6d998")
if err := browser.Connect(); err != nil {
fmt.Printf("err while connecting: %v", err)
return
}
fmt.Println(
browser.MustPage("https://mdn.dev/").MustEval("() => document.title"),
)
}
If I use --headless --remote-debugging-address=0.0.0.0 it works, but if I remove the headless part it refuses the connection. The only solution seems to be to use SSH tunneling like it is mentioned here. But these keep erroring out for me as all of the answers are very vague as to what is what and what IP should go where
$ ssh -L 172.17.0.1:9222:localhost:9222 -N localhost
ssh: connect to host localhost port 22: Connection refused
OR
$ ssh -L 172.17.0.1:9222:localhost:9222
usage: ssh [-46AaCfGgKkMNnqsTtVvXxYy] [-B bind_interface]
[-b bind_address] [-c cipher_spec] [-D [bind_address:]port]
[-E log_file] [-e escape_char] [-F configfile] [-I pkcs11]
[-i identity_file] [-J [user#]host[:port]] [-L address]
[-l login_name] [-m mac_spec] [-O ctl_cmd] [-o option] [-p port]
[-Q query_option] [-R address] [-S ctl_path] [-W host:port]
[-w local_tun[:remote_tun]] destination [command]
What I want to happen is to be able to connect from the container to the debugger running on my host machine. Some caveats that I would like to cover are
It works on other platforms not just linux
It doesn't require complex setup from the user
This will be used by other teammates and it would be nice to have an approachable setup
So anyone that might have a similar usecase I'll save you a couple of days of debugging and trial and error. The solution I came upon is the following
Using this docker image with chrome debugger pointing at 9222 port. You can run this as is or put it inside docker compose as I did. Now if you navigate to this url chrome://inspect/#devices while the above mentioned image is running, you'll be able to access the and view the browser inside the container. (If its running on 9222 then you can even do localhost:9222 and it will show you the available pages)
Now the code to connect to it using go-rod
ips, err := net.LookupIP("browser-devtools")
if err != nil {
return nil, fmt.Errorf("failed to get browser service ip: %w", err)
}
if len(ips) == 0 {
return nil, errors.New("ip list empty")
}
ip := ips[0].String()
fmt.Printf("IP is %q\n", ip)
resp, err := http.Get(fmt.Sprintf("http://%s:9222/json/version/", ip))
if err != nil {
return nil, fmt.Errorf("failed to get devtools info: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed read response body: %w", err)
}
println(string(body))
responseMapped := make(map[string]interface{})
if err = json.Unmarshal(body, &responseMapped); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
debuggerUrl, ok := responseMapped["webSocketDebuggerUrl"].(string)
if !ok {
return nil, errors.New("no 'webSocketDebuggerUrl' entry in response map")
}
browser := rod.New().ControlURL(debuggerUrl)
if err := browser.Connect(); err != nil {
return nil, fmt.Errorf("failed connect to browser: %w", err)
}
return browser, nil
Explanation:
ips, err := net.LookupIP("browser-devtools")
There is a slight issue that only localhost host can access the debugger or any (allowed) numerical IP, so basically you cannot simply use the service name in docker compose to access the browser as it will be denied. Here we are resolving the actual IP of the service with a lookup
ip := ips[0].String()
resp, err := http.Get(fmt.Sprintf("http://%s:9222/json/version/", ip))
Here we get the string representation of the IP (something like 127.1.0.0) and pass it into the request. The request in itself is important as we don't have the actual browser debugger url that go-rod expects so this get request will return
{
"Browser": "Chrome/72.0.3601.0",
"Protocol-Version": "1.3",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3601.0 Safari/537.36",
"V8-Version": "7.2.233",
"WebKit-Version": "537.36 (#cfede9db1d154de0468cb0538479f34c0755a0f4)",
"webSocketDebuggerUrl": "ws://localhost:9222/devtools/browser/b0b8a4fb-bb17-4359-9533-a8d9f3908bd8"
}
Something along these lines. And the webSocketDebuggerUrl is the actual url we're connecting to
Related
So the setup is as followed:
I have a host machine on which i run a docker container. From this i want to open a connection to a ftp server on the web and download a test file. The programm is written in go and works when executed directly on the host machine without the container.
The minimum example script looks as follows and I use the ftp package github.com/jlaffaye/ftp: (This should work out of the box as is - at least it does for me)
package main
import (
"fmt"
"io/ioutil"
"log"
"strings"
"time"
"github.com/jlaffaye/ftp"
)
func main() {
URL := "ftp://speedtest.tele2.net/1MB.zip"
User := "anonymous"
Password := "anonymous"
urlElements := strings.Split(URL, "/")
dialTarget := fmt.Sprintf("%s:%s", urlElements[2], "21")
fmt.Println("Attempting FTP connection to ", dialTarget)
c, err := ftp.Dial(dialTarget, ftp.DialWithTimeout(5*time.Second))
if err != nil {
log.Fatalf("TCP dial failed: %v", err)
}
defer c.Quit()
err = c.Login(User, Password)
if err != nil {
log.Fatalf("FTP login failed: %v", err)
}
defer c.Logout()
data, err := c.Retr(strings.Join(urlElements[3:], "/"))
if err != nil {
log.Fatalf("Failed to get file: %v", err)
}
_, err = ioutil.ReadAll(data)
if err != nil {
log.Fatalf("Failed to read file: %v", err)
}
fmt.Println("success!")
}
Since this code part will be part of a larger container later on running this on the host network with --network=host in docker options is not possible. (Apart this did not work for some reason -> lead to timeouts or EOF errors on dial)
Solution I considered:
Open an ssh-bridge to the target machine. I tried this using the package github.com/elliotchance/sshtunnel which provides a (imo) comprehensive example of how to use it: See here for reference.
The idea was to basically tunnel from the container through the host directly onto the target. However while the tunnel returned a connection success, the ftp client still failed with timeouts or EOF errors.
For this reason: Is there a good way to do this? If so which direction would be the best to go in? I am somewhat lost on this, especially since since connecting an samba client to a remote machine works without any issue from within the container for some reason.
Thx in advance for any leads - please let me know if i missed to provide any information/details.
For reference the docker file looks like this:
FROM golang:alpine AS BUILDER
# install certificates to enable usage for http
RUN apk update \
&& apk upgrade \
&& apk add --no-cache \
ca-certificates \
&& update-ca-certificates 2>/dev/null || true
# all prerequsites should be done before this point.
WORKDIR /src
COPY . .
RUN go get -d -v
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=5 go build -o ./out/ftp-test .
FROM scratch
COPY --from=BUILDER /src/out/ftp-test /ftp-test
COPY --from=BUILDER /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Run the executable
ENTRYPOINT ["/ftp-test"]
I'm trying to write unit tests which run both locally using github.com/ory/dockertest and in a CircleCI environment (in which the "CI" environment variable is set) using a Docker executor type. In the container, I'd like to run the Google Pub/Sub emulator using the google/cloud-sdk image.
As a simplified example, I've written this Go program:
package main
import (
"context"
"flag"
"fmt"
"log"
"net"
"os"
"time"
"cloud.google.com/go/pubsub"
"github.com/ory/dockertest"
"github.com/ory/dockertest/docker"
"google.golang.org/api/iterator"
)
var pubsubEmulatorHost string
func main() {
flag.StringVar(&pubsubEmulatorHost, "pubsubEmulatorHost", "localhost:8085", "Google Pub/Sub emulator host")
flag.Parse()
if os.Getenv("CI") == "" {
pool, err := dockertest.NewPool("")
if err != nil {
log.Fatalf("Could not connect to Docker: %v", err)
}
opts := &dockertest.RunOptions{
Hostname: "localhost",
Repository: "google/cloud-sdk",
Cmd: []string{"gcloud", "beta", "emulators", "pubsub", "start", "--host-port", "127.0.0.1:8085"},
ExposedPorts: []string{"8085"},
PortBindings: map[docker.Port][]docker.PortBinding{
"8085/tcp": {{HostIP: "127.0.0.1", HostPort: "8085/tcp"}},
},
}
resource, err := pool.RunWithOptions(opts)
if err != nil {
log.Fatalf("Could not start resource: %v", err)
}
pool.MaxWait = 10 * time.Second
if err := pool.Retry(func() error {
_, err := net.Dial("tcp", "localhost:8085")
return err
}); err != nil {
log.Fatalf("Could not dial the Pub/Sub emulator: %v", err)
}
defer func() {
if err := pool.Purge(resource); err != nil {
log.Fatalf("Could not purge resource: %v", err)
}
}()
}
os.Setenv("PUBSUB_EMULATOR_HOST", pubsubEmulatorHost)
defer os.Unsetenv("PUBSUB_EMULATOR_HOST")
client, err := pubsub.NewClient(context.Background(), "my-project")
if err != nil {
log.Fatalf("NewClient: %v", err)
}
topic, err := client.CreateTopic(context.Background(), "my-topic")
if err != nil {
log.Fatalf("CreateTopic: %v", err)
}
log.Println("Created topic:", topic)
topicIterator := client.Topics(context.Background())
for {
topic, err := topicIterator.Next()
if err == iterator.Done {
break
}
if err != nil {
log.Fatalf("Next: %v", err)
}
fmt.Printf("%s\n", topic)
}
}
Firstly, I've verified that running it with the CI environment variable set to a non-empty value after running the container from the command line yields the expected result:
>
docker run -p "8085:8085" google/cloud-sdk gcloud beta emulators pubsub start --host-port=0.0.0.0:8085
Executing: /usr/lib/google-cloud-sdk/platform/pubsub-emulator/bin/cloud-pubsub-emulator --host=0.0.0.0 --port=8085
[pubsub] This is the Google Pub/Sub fake.
[pubsub] Implementation may be incomplete or differ from the real system.
[pubsub] Jul 16, 2020 9:21:33 PM com.google.cloud.pubsub.testing.v1.Main main
[pubsub] INFO: IAM integration is disabled. IAM policy methods and ACL checks are not supported
[pubsub] Jul 16, 2020 9:21:34 PM io.gapi.emulators.netty.NettyUtil applyJava7LongHostnameWorkaround
[pubsub] INFO: Applied Java 7 long hostname workaround.
[pubsub] Jul 16, 2020 9:21:34 PM com.google.cloud.pubsub.testing.v1.Main main
[pubsub] INFO: Server started, listening on 8085
followed by
> env CI=true go run main.go
2020/07/16 14:22:01 Created topic: projects/my-project/topics/my-topic
projects/my-project/topics/my-topic
Note that at this point, port 8085 on the container is mapped to port 8085 on the host as expected:
> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
76724696f9d9 google/cloud-sdk "gcloud beta emulato…" 55 seconds ago Up 54 seconds 0.0.0.0:8085->8085/tcp epic_ganguly
I would not like to stop the container and run the program without setting the CI environment variable, should should take care of spinning up the container automatically. What I observe, however, is that it times out trying to make a connection:
> go run main.go
2020/07/16 14:23:56 Could not dial the Pub/Sub emulator: dial tcp [::1]:8085: connect: connection refused
exit status 1
Upon inspecting the container, it seems that it is mapped to local port 32778 rather than 8085:
> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0df07ac232d5 google/cloud-sdk:latest "gcloud beta emulato…" 34 seconds ago Up 33 seconds 0.0.0.0:32778->8085/tcp wizardly_ptolemy
I would think that specifying the PortBindings in the RunOptions like they are done above should map port 8085 on the container to port 8085 on the host machine, but it seems that is not the case. Does anyone know the correct run options to make this program work?
Dockertest allows you to retrieve the mapped port for the container using resource.GetPort(), you can use this to set pubsubEmulatorHost to the correct value:
port := "8085"
if os.Getenv("CI") == "" {
// ...
pubsubEmulatorHost = opts.Hostname + resource.GetPort("8085/tcp")
// pubsubEmulatorHost = "localhost:32778"
// ...
}
I am prototyping a Go application that will ultimately talk to a remote Docker host. To this end I am using Docker's Go client package (docs at https://godoc.org/github.com/docker/docker/client).
The environment is Ubuntu 19.10 in VirtualBox 6.1.4, using Docker 19.03.6, and Go 1.14. All Go packages have been installed with go get in the last 72 hours.
For local testing purposes I am trying to connect to a local Docker host at tcp://0.0.0.0:2375. That is, I am running
sudo dockerd -H tcp://0.0.0.0:2375
With this, commands such as
docker -H tcp://0.0.0.0:2375 ps
and
curl -k -v -i http://0.0.0.0:2375/v1.40/containers/json
both work, and I am able to observe the traffic over port 2375 with Wireshark.
However, attempting to do the same thing through the Go client package fails with
Cannot connect to the Docker daemon at tcp://0.0.0.0:2375. Is the docker daemon running?
and nothing shows up in Wireshark.
Here is the example Go code :
package main
import (
"context"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)
func main() {
cli, err := client.NewClientWithOpts(client.WithHost("tcp://0.0.0.0:2375"), client.WithAPIVersionNegotiation())
if err != nil {
fmt.Println("error: could not create docker client handle")
fmt.Println(err)
}
options := types.ContainerListOptions{}
data, err := cli.ContainerList(context.Background(), options)
if err != nil {
fmt.Println("error: could not request containers list")
fmt.Println(err)
} else {
fmt.Println(data)
}
}
Attempts to set the environment variables DOCKER_HOST=tcp://0.0.0.0:2375, DOCKER_CERT_PATH=, and DOCKER_TLS_VERIFY=, then configure the client handle through client.FromEnv() also failed in the exact same way.
What am I doing wrong here ?
I am trying to use the docker go client to connect to the google container registry to list and delete images. My golang application also exists in a docker container.
This is the dockerfile for my golang application:
FROM docker:latest
USER root
RUN apk add --update openssl
ADD ./data /app/data
ADD ./data/docker /app/data/docker
ADD mygolangapp /app
RUN chmod -R a+rwx ./app/data/docker/generate_docker_cert.sh
RUN sh ./app/data/docker/generate_docker_cert.sh
ENV GOOGLE_APPLICATION_CREDENTIALS ./app/data/myserviceaccount.json
ENV DOCKER_CONFIG ./app/data/docker
ENV DOCKER_CERT_PATH .
ENV DOCKER_HOST ????????
ENTRYPOINT ["/app/mygolangapp"]
This is the generate_docker_cert.sh file
(https://gist.github.com/bradrydzewski/a6090115b3fecfc25280)
This is my golang code to create the docker go client and list containers.
jsonBytes, err := ioutil.ReadFile(os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"))
if err != nil {
panic(err)
}
dockercli, err := client.NewEnvClient()
if err != nil {
panic(err)
}
dockercli.RegistryLogin(context.Background(), types.AuthConfig{
Username: "_json_key",
Password: string(jsonBytes),
ServerAddress: "https://eu.gcr.io",
})
containers, err := dockercli.ContainerList(context.Background(), types.ContainerListOptions{})
if err != nil {
panic(err)
}
for _, container := range containers {
fmt.Printf("%s %s\n", container.ID[:10], container.Image)
}
Currently I am getting this error:
error during connect: Get https://%2Fvar%2Frun%2Fdocker.sock/v1.25/containers/json?limit=0: dial tcp: lookup /var/run/docker.sock: no such host
So I added docker.sock as a volume in my docker-compose, but it is not working?
mygolangapp:
build: ./mygolangapp
volumes:
- /var/run/docker.sock:/var/run/docker.sock
My question is: How can I use the docker golang client within a golang application for google container registry. What am I doing wrong or missing here? What should DOCKER_HOST be?
Thank you for any help. Any other approach is more than welcome!
As a potential alternate, you could explore the google/go-containerregistry library: https://github.com/google/go-containerregistry
It looks like you're trying to implement some sort of garbage collection tool. If so, you could also look here for an example of how to use the library: https://github.com/google/go-containerregistry/pull/300
I have a very simple application. Here is the code:
package main
import (
"fmt"
"math/rand"
"time"
"net/http"
"encoding/base64"
"encoding/json"
)
type Message struct {
Text string `json:"text"`
}
var cookieQuotes = []string{
// Skipped all the stuff
}
const COOKIE_NAME = "your_cookie"
func main() {
http.HandleFunc("/set_cookie", setCookie)
http.HandleFunc("/get_cookie", getCookie)
http.Handle("/favicon.ico", http.NotFoundHandler())
http.ListenAndServe(":8080", nil)
}
func setCookie(w http.ResponseWriter, r *http.Request) {
quote := getRandomCookieQuote()
encQuote := base64.StdEncoding.EncodeToString([]byte(quote))
http.SetCookie(w, &http.Cookie{
Name: COOKIE_NAME,
Value: encQuote,
})
}
func getCookie(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(COOKIE_NAME)
if err != nil {
fmt.Fprintln(w, "Cannot get the cookie")
}
message, _ := base64.StdEncoding.DecodeString(cookie.Value)
msg := Message{Text:string(message)}
fmt.Println(msg.Text)
respBody, err := json.Marshal(msg)
fmt.Println(string(respBody))
if err != nil {
fmt.Println("Cannot marshall JSON")
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, string(respBody))
}
func getRandomCookieQuote() string {
source := rand.NewSource(time.Now().UnixNano())
random := rand.New(source)
i := random.Intn(len(cookieQuotes))
return cookieQuotes[i]
}
It was tested locally, and, also I've tried to run a docker container with it on my machine (Ubuntu) and it was working perfectly. But I want to run it on Virtual Machine (I use Oracle Virtual Box).
So, I have installed docker-machine:
docker-machine version 0.12.2, build 9371605
After that, I've switched to it, like it was recommended in official documentation like this:
eval "$(docker-machine env default)"
So I can do now from a perspective of that machine.
Also I've tried to run ngnix from the documentation example:
docker run -d -p 8000:80 nginx
curl $(docker-machine ip default):8000
And I get the result, I can get to ngnix welcome page by accessing my docker machine ip-address which could be accessed by command:
docker-machine ip default
But when I try to run my own docker image, I could not do this. When I try to access it, I get:
curl $(docker-machine ip default):8080
curl: (7) Failed to connect to 192.168.99.100 port 8080: Connection refused
Also I've tried to skip a port, to add protocol (http, and even https for the sake of luck) - nothing works.
Maybe, something wrong with my Dockerfile?
# Go experiments with cookies
FROM golang:1.8-onbuild
MAINTAINER vasyania2#gmail.com
Could you help me please?
This command maps port 8080 from your docker host to port 80 of your container:
docker run -d -p 8080:80 cookie-app
This instruction tells your go application to listen on port 8080, inside the container:
http.ListenAndServe(":8080", nil)
You have a port mismatch in those above lines, your application is not listening on the port you are forwarding to.
To connect to port 8080 of your container, you can run the following:
docker run -d -p 8080:8080 cookie-app