Golang Docker API: get events - docker

I want to get all new events from docker via the golang integration.
The problem is that it returns two channels and I couldn't figure out how to subscribe to them.
cli, err := client.NewClientWithOpts(client.WithVersion("1.37"))
if err != nil {
panic(err)
}
ctx, _ := context.WithCancel(context.Background())
msg, err := <- cli.Events(ctx, types.EventsOptions{})

There are many solutions. A solution could be:
msgs, errs := cli.Events(ctx, types.EventsOptions{})
for {
select {
case err := <-errs:print(err)
case msg := <-msgs:print(msg)
}
}

Related

How can I get Container Logs using Golang? (Error)

I am trying to code a Docker Monitoring software in Golang.
my Code looks as followed:
package main
import (
"bytes"
"context"
"fmt"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)
func main() {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
panic(err)
}
containers, err := cli.ContainerList(ctx, types.ContainerListOptions{})
if err != nil {
panic(err)
}
for _, container := range containers {
out, err := cli.ContainerLogs(ctx, container.ID, types.ContainerLogsOptions{
ShowStderr: true,
ShowStdout: true,
Timestamps: false,
Follow: true,
Tail: "40"})
if err != nil {
panic(err)
}
fmt.Println("The \"" + container.Image + "\" container, with the ID \"" + container.ID + "\" logged: ")
fmt.Println()
buf := new(bytes.Buffer)
fmt.Println(buf.ReadFrom(out))
fmt.Println(buf.String())
}
time.Sleep(time.Second * 3)
}
The problem is that the execution of the above code stops on the fmt.Println(buf.ReadFrom(out)) statement. The code used to work, but it suddenly just doesn't anymore. Either it stops without an error, or it returns an empty String.
The client I am trying to collect the logs from is also coded by myself, and it looks like follows:
package main
import (
"log"
"time"
)
func main() {
for i := 0; i > -1; i++ {
log.Output(1, "Hello World logged!")
time.Sleep(time.Minute)
}
}
I already tried debugging and checking Variables, but I just can't get to the source of the Problem.
I am really not sure as I don't have any error logs to confirm my hypothesis.
But could it be the case that as the ContainerLogs returns a stream (io.ReadCloser), maybe the stream itself hasn't closed yet?
If this is a possibility, you can either do a dry run first to test out this theory by adding a timeout and logging it after every small duration ?
one possible way to do this is
select {
case <-time.After(5 * time.Second):
fmt.Println("Timeout exceeded while reading container logs")
case <-ctx.Done():
fmt.Println("Context cancelled while reading container logs")
case b := <-out:
if b != nil {
buf.Write(b)
}
}

Programmatically check if Docker container process ended with non-zero status

I'm working on a Go application which starts some Docker containers using Go Docker SDK. I need to check if containers' processes exit with zero (success) status code.
Here's the minimal working example:
package main
import (
"context"
"io"
"log"
"os"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
)
func main() {
ctx := context.Background()
cli, err := client.NewEnvClient()
if err != nil {
log.Fatal(err)
}
reader, err := cli.ImagePull(
ctx,
"docker.io/library/alpine",
types.ImagePullOptions{},
)
if err != nil {
log.Fatal(err)
}
io.Copy(os.Stdout, reader)
resp, err := cli.ContainerCreate(ctx, &container.Config{
Image: "alpine",
Cmd: []string{"sh", "-c", "echo hello world; return 1"},
Tty: true,
}, nil, nil, "")
if err != nil {
log.Fatal(err)
}
err = cli.ContainerStart(
ctx,
resp.ID,
types.ContainerStartOptions{},
)
if err != nil {
log.Fatal(err)
}
statusCh, errCh := cli.ContainerWait(
ctx,
resp.ID,
container.WaitConditionNotRunning,
)
select {
case err := <-errCh:
if err != nil {
log.Fatal(err)
}
case <-statusCh:
}
out, err := cli.ContainerLogs(
ctx,
resp.ID,
types.ContainerLogsOptions{ShowStdout: true},
)
if err != nil {
log.Fatal(err)
}
io.Copy(os.Stdout, out)
}
As you can see, the process in the container ends with non-zero status (sh -c "echo hello world; return 1"). However, it doesn't log any fatal errors and simply displays hello world when built and executed:
{"status":"Pulling from library/alpine","id":"latest"}
{"status":"Digest: sha256:7043076348bf5040220df6ad703798fd8593a0918d06d3ce30c6c93be117e430"}
{"status":"Status: Image is up to date for alpine:latest"}
hello world
How can I check that container process exited with non-zero status using Docker Go SDK?
I think you should use the status channel to get the exit code. The error channel seems to be used to signal if there was an error while talking to the docker daemon, see https://godoc.org/github.com/docker/docker/client#Client.ContainerWait.
This works for me:
select {
case err := <-errCh:
if err != nil {
log.Fatal(err)
}
case status := <-statusCh:
log.Printf("status.StatusCode: %#+v\n", status.StatusCode)
}

How can I get Docker container output through a websocket?

I am trying to send output from a docker container to the console using fmt, but when trying to do it i get this.
&{0xc0422a65c0 {0 0} false <nil> 0x6415a0 0x641540}
How do I do this? This is my full code.
func main() {
imageName := "hidden/hidden"
ctx := context.Background()
cli, err := client.NewClient("tcp://0.0.0.0:0000", "v0.00", nil, nil)
if err != nil {
panic(err)
}
fmt.Println("Pulling \"" + imageName + "\"")
_, err = cli.ImagePull(ctx, imageName, types.ImagePullOptions{})
if err != nil {
panic(err)
}
containerConfig := &container.Config{
Image: imageName,
Cmd: []string{"./app/start.sh", "--no-wizard"},
}
resp, err := cli.ContainerCreate(ctx, containerConfig, nil, nil, "")
if err != nil {
panic(err)
}
if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
panic(err)
}
timer := time.NewTimer(time.Minute)
go func() {
<-timer.C
if err := cli.ContainerStop(ctx, resp.ID, nil); err != nil {
panic(err)
}
}()
out, err := cli.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true})
if err != nil {
panic(err)
}
io.Copy(os.Stdout, out) // This is what I want to change to print with "fmt".
}
Tried: (but does not display until container is done.)
buf := new(bytes.Buffer)
buf.ReadFrom(out)
fmt.Println(buf.String())
Intention: Allow real-time console output to the web.
This seems to be the answer to my question, I did some searching about scanners as Cerise Limón commented. Anyone else who seems to be having the issue that I did can use this code. Thanks to all that helped.
scanner := bufio.NewScanner(out)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
&{0xc0422a65c0 {0 0} false <nil> 0x6415a0 0x641540} is not a bad output. This is a perfectly fine struct output. I think the the main problem is here just your lack of golang experience.
I'm a beginner as well and I can imagine that when you see an output like above you thought that "I made a mistake"
No you didn't. It's default fmt behavior when u try to print out a struct which contains pointers.
Checkout this instead of your fmt.Println:
fmt.Printf("%+v\n", out)
Well this answer stands on my assumptions but if this is the case just ping me for more info.

400 Bad Request with Golang and Telegram API

I'm building a golang application which performs a POST to a Telegram Channel using a given Bot token but when I do it I get
400 Bad Request
This is my POST:
import (
"fmt"
"net/url"
"net/http"
"strings"
)
. . .
request_url := "https://api.telegram.org/bot{token}/sendMessage?chat_id={channelId}"
urlData := url.Values{}
urlData.Set("text", "Hello!")
client := &http.Client{}
req, _ := http.NewRequest("POST", request_url, strings.NewReader(urlData.Encode()))
req.Header.Set("content-type", "application-json")
res, err := client.Do(req)
if(err != nil){
fmt.Println(err)
} else {
fmt.Println(res.Status)
}
I don't get why it's giving me 400 even thought I'm able to perform the very same POST using Postman
POST https://api.telegram.org/bot{token}/sendMessage?chat_id={channelId}
body : {"text" : "Hello"} Content-Type=application/json
Any Hint on how to fix this?
I've been scratching my head for a while but I wasn't able to solve this.
UPDATE
Trying #old_mountain approach leads to the same result
import (
"fmt"
"net/http"
"bytes"
"encoding/json"
)
request_url := "https://api.telegram.org/bot{token}/sendMessage?chat_id={channelId}"
client := &http.Client{}
values := map[string]string{"text": "Hello!"}
jsonStr, _ := json.Marshal(values)
req, _ := http.NewRequest("POST", request_url, bytes.NewBuffer(jsonStr))
req.Header.Set("content-type", "application-json")
res, err := client.Do(req)
if(err != nil){
fmt.Println(err)
} else {
fmt.Println(res.Status)
}
You need to send a json string.
var jsonStr = []byte(`{"text":"Hello!"}`)
req, _ := http.NewRequest("POST", request_url, bytes.NewBuffer(jsonStr))
Or, if you don't want to directly write it:
values := map[string]string{"text": "Hello!"}
jsonStr, _ := json.Marshal(values)
req, _ := http.NewRequest("POST", request_url, bytes.NewBuffer(jsonStr))
Also, adjust the header Content-Type to:
req.Header.Set("Content-Type", "application/json")

Combine URL paths with path.Join()

Is there a way in Go to combine URL paths similarly as we can do with filepaths using path.Join()?
For example see e.g. Combine absolute path and relative path to get a new absolute path.
When I use path.Join("http://foo", "bar"), I get http:/foo/bar.
See in Golang Playground.
The function path.Join expects a path, not a URL. Parse the URL to get a path and join with that path:
u, err := url.Parse("http://foo")
if err != nil { log.Fatal(err) }
u.Path = path.Join(u.Path, "bar.html")
s := u.String()
fmt.Println(s) // prints http://foo/bar.html
Use the url.JoinPath function in Go 1.19 or later:
s, err := url.JoinPath("http://foo", "bar.html")
if err != nil { log.Fatal(err) }
fmt.Println(s) // prints http://foo/bar.html
Use ResolveReference if you are resolving a URI reference from a base URL. This operation is different from a simple path join: an absolute path in the reference replaces the entire base path; the base path is trimmed back to the last slash before the join operation.
base, err := url.Parse("http://foo/quux.html")
if err != nil {
log.Fatal(err)
}
ref, err := url.Parse("bar.html")
if err != nil {
log.Fatal(err)
}
u := base.ResolveReference(ref)
fmt.Println(u.String()) // prints http://foo/bar.html
Notice how quux.html in the base URL does not appear in the resolved URL.
ResolveReference() in net/url package
The accepted answer will not work for relative url paths containing file endings like .html or .img. The ResolveReference() function is the correct way to join url paths in go.
package main
import (
"fmt"
"log"
"net/url"
)
func main() {
u, err := url.Parse("../../..//search?q=dotnet")
if err != nil {
log.Fatal(err)
}
base, err := url.Parse("http://example.com/directory/")
if err != nil {
log.Fatal(err)
}
fmt.Println(base.ResolveReference(u))
}
A simple approach to this would be to trim the /'s you don't want and join. Here is an example func
func JoinURL(base string, paths ...string) string {
p := path.Join(paths...)
return fmt.Sprintf("%s/%s", strings.TrimRight(base, "/"), strings.TrimLeft(p, "/"))
}
Usage would be
b := "http://my.domain.com/api/"
u := JoinURL(b, "/foo", "bar/", "baz")
fmt.Println(u)
This removes the need for checking/returning errors
In 1.19 there will be a new function in the standard library that solves this very neatly.
u, err := url.JoinPath("http://host/foo", "bar/")
https://go.dev/play/p/g422ockBq0q?v=gotip
To join a URL with another URL or a path, there is URL.Parse():
func (u *URL) Parse(ref string) (*URL, error)
Parse parses a URL in the context of the receiver. The provided URL
may be relative or absolute. Parse returns nil, err on parse failure,
otherwise its return value is the same as ResolveReference.
func TestURLParse(t *testing.T) {
baseURL, _ := url.Parse("http://foo/a/b/c")
url1, _ := baseURL.Parse("d/e")
require.Equal(t, "http://foo/a/b/d/e", url1.String())
url2, _ := baseURL.Parse("../d/e")
require.Equal(t, "http://foo/a/d/e", url2.String())
url3, _ := baseURL.Parse("/d/e")
require.Equal(t, "http://foo/d/e", url3.String())
}
I wrote this utility function that works for my purposes:
func Join(basePath string, paths ...string) (*url.URL, error) {
u, err := url.Parse(basePath)
if err != nil {
return nil, fmt.Errorf("invalid url")
}
p2 := append([]string{u.Path}, paths...)
result := path.Join(p2...)
u.Path = result
return u, nil
}
https://play.golang.org/p/-QNVvyzacMM

Resources