diff --git a/README.md b/README.md index 60a76a2..8dc4ad4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ![build and test](https://github.com/thediveo/morbyd/workflows/build%20and%20test/badge.svg?branch=master) ![goroutines](https://img.shields.io/badge/go%20routines-not%20leaking-success) [![Go Report Card](https://goreportcard.com/badge/github.com/thediveo/morbyd)](https://goreportcard.com/report/github.com/thediveo/morbyd) -![Coverage](https://img.shields.io/badge/Coverage-99.1%25-brightgreen) +![Coverage](https://img.shields.io/badge/Coverage-99.2%25-brightgreen) `morbyd` is a thin layer on top of the standard Docker Go client to easily build and run throw-away test Docker images and containers. And to easily run commands @@ -35,11 +35,11 @@ elements, such as names, labels, and options. - [Session.Run](https://pkg.go.dev/github.com/thediveo/morbyd#Session.Run) a new container: options are in the [run](https://pkg.go.dev/github.com/thediveo/morbyd/run) package, such as - [WithCommand](https://pkg.go.dev/github.com/thediveo/morbyd/run#WithCommand)... + [WithCommand](https://pkg.go.dev/github.com/thediveo/morbyd/run#WithCommand), ... - [Container.Exec](https://pkg.go.dev/github.com/thediveo/morbyd#Container.Exec)ute a command inside a container: options are in the [exec](https://pkg.go.dev/github.com/thediveo/morbyd/exec) package, such as - [WithCombinedOutput](https://pkg.go.dev/github.com/thediveo/morbyd/exec#WithCombinedOutput)... + [WithCombinedOutput](https://pkg.go.dev/github.com/thediveo/morbyd/exec#WithCombinedOutput), ... - [Session.CreateNetwork](https://pkg.go.dev/github.com/thediveo/morbyd#Session.CreateNetwork): with options in the [net](https://pkg.go.dev/github.com/thediveo/morbyd/net) and [bridge](https://pkg.go.dev/github.com/thediveo/morbyd/net/bridge), @@ -113,11 +113,15 @@ func main() { sess, _ := morbyd.NewSession(ctx, session.WithAutoCleaning("test.mytest=")) defer sess.Close(ctx) + // run a container and copy the container's combined output + // of stdout and stderr to our stdout. cntr, _ := sess.Run(ctx, "busybox", run.WithCommand("/bin/sh", "-c", "while true; do sleep 1; done"), run.WithAutoRemove(), run.WithCombinedOutput(os.Stdout)) + // run a command inside the container and wait for this command + // to finish. cmd, _ := cntr.Exec(ctx, exec.WithCommand("/bin/sh", "-c", "echo \"Hellorld!\""), exec.WithCombinedOutput(os.Stdout)) @@ -153,7 +157,7 @@ func main() { cntr, _ := sess.Run(ctx, "busybox", run.WithCommand("/bin/sh", "-c", `echo "DOH!" > index.html && httpd -f -p 1234`), run.WithAutoRemove(), - run.WithPublishedPort("127.0.0.1:1234")) + run.WithPublishedPort("127.0.0.1:1234")) svcAddrPort := container.PublishedPort("1234").Any().UnspecifiedAsLoopback().String() req, _ := http.NewRequest(http.MethodGet, "http://"+svcAddrPort+"/", nil) @@ -164,6 +168,39 @@ func main() { } ``` +### Dealing with Container Output + +[safe.Buffer](https://pkg.go.dev/github.com/thediveo/morbyd/safe#Buffer) is the +concurrency-safe drop-in sibling to Go's +[bytes.Buffer](https://pkg.go.dev/bytes#Buffer): it is essential in unit tests +that reason about container output without setting off Go's race detector. The +reason is that container output is handled on background Go routines and +simultaneously polling an unguarded `bytes.Buffer` causes a race. All you need +to do is replace `bytes.Buffer` with `safe.Buffer` (which is just a thin +mutex'ed wrapper), for instance, in this test leveraging +[Gomega](https://onsi.github.io/gomega/): + +```go +var buff safe.Buffer + +// run a container that outputs a magic phrase and then +// keeps sleeping until the container gets terminated. +Expect(cntr.Exec(ctx, + exec.Command("/bin/sh", "-c", "echo \"**FOO!**\" 1>&2; while true; do sleep 1; done"), + exec.WithTTY(), + exec.WithCombinedOutput( + io.MultiWriter( + &buff, timestamper.New(GinkgoWriter))))).To(Succeed()) + +// ensure we got the magic phrase +Eventually(buff.String).Should(Equal("**FOO!**\r\n")) +``` + +[timestamper.New](https://pkg.go.dev/github.com/thediveo/morbyd@v0.13.0/timestamper#New) +returns a writer object implementing [io.Writer](https://pkg.go.dev/io#Writer) +that time stamps each line of output. It has proven useful in debugging tests +involving container output. + ## Alternatives Why `morbyd` when there are already other much bigger and long-time diff --git a/container_published_port_test.go b/container_published_port_test.go index ca5df64..804f0ef 100644 --- a/container_published_port_test.go +++ b/container_published_port_test.go @@ -18,7 +18,9 @@ import ( context "context" io "io" "net/http" + "strings" + "github.com/thediveo/morbyd/net" "github.com/thediveo/morbyd/run" "github.com/thediveo/morbyd/session" "github.com/thediveo/morbyd/timestamper" @@ -42,8 +44,13 @@ var _ = Describe("published container ports", Ordered, func() { `echo "DOH!" > index.html && httpd -v -f -p 1234`), run.WithAutoRemove(), run.WithCombinedOutput(timestamper.New(GinkgoWriter)), + // It's not possible (anymore) to publish the port both at the + // unspecified IP/IPv6 addresses, as well as on the IPv6 loopback + // address: this will fail with a bind error in the "userland + // proxy". Thus, we publish on a fixed port on IPv6 loopback to work + // around this restriction. run.WithPublishedPort("1234"), - run.WithPublishedPort("[::1]:1234/tcp"), + run.WithPublishedPort("[::1]:1234:1234/tcp"), )) svcAddrs := cntr.PublishedPort("1234") @@ -73,4 +80,35 @@ var _ = Describe("published container ports", Ordered, func() { Expect(string(Successful(io.ReadAll(resp.Body)))).To(Equal("DOH!\n")) }) + It("publishes ports on an IPv6 custom Docker network", func(ctx context.Context) { + sess := Successful(NewSession(ctx, + session.WithAutoCleaning("test.morbyd=container.portv6"))) + DeferCleanup(func(ctx context.Context) { sess.Close(ctx) }) + + v6net, err := sess.CreateNetwork(ctx, "morbyd-v6notwork", + net.WithIPv6()) + if err != nil && strings.Contains(err.Error(), "could not find an available, non-overlapping IPv6 address pool among the defaults") { + Skip("needs IPv6 pools for custom Docker networks") + } + + By("spinning up an http serving busybox with published ports") + cntr := Successful(sess.Run(ctx, + "busybox", + run.WithCommand("/bin/sh", "-c", + `echo "DOH!" > index.html && httpd -v -f -p 1235`), + run.WithAutoRemove(), + run.WithCombinedOutput(timestamper.New(GinkgoWriter)), + run.WithNetwork(v6net.ID), + run.WithPublishedPort("[::1]:1235/tcp"), + )) + + svcAddrs := cntr.PublishedPort("1235") + Expect(svcAddrs).To(ConsistOf( + And( + HaveField("Network()", "tcp"), + MatchRegexp(`\[::1\]:\d+`), + ), + )) + }) + }) diff --git a/net/options.go b/net/options.go index 7ce83ee..0834350 100644 --- a/net/options.go +++ b/net/options.go @@ -71,6 +71,15 @@ func WithIPv6() Opt { } } +// WithoutIPv6 disables IPv6 for the custom Docker network ... boo! +func WithoutIPv6() Opt { + return func(o *Options) error { + f := false + o.EnableIPv6 = &f + return nil + } +} + // WithLabel adds a label in “KEY=VALUE” to the custom Docker network. func WithLabel(label string) Opt { return func(o *Options) error { diff --git a/net/options_test.go b/net/options_test.go index 910ed85..315e4c6 100644 --- a/net/options_test.go +++ b/net/options_test.go @@ -57,6 +57,14 @@ var _ = Describe("network options", func() { Expect(netos.Labels).To(HaveKeyWithValue("bar", "baz")) }) + It("enables and disables IPv6", func() { + var opts Options + Expect(WithIPv6()(&opts)).To(Succeed()) + Expect(opts.EnableIPv6).To(HaveValue(BeTrue())) + Expect(WithoutIPv6()(&opts)).To(Succeed()) + Expect(opts.EnableIPv6).To(HaveValue(BeFalse())) + }) + It("rejects invalid net options", func() { var opts Options Expect(WithLabels("=")(&opts)).To(HaveOccurred())