Skip to content

Commit 1deddef

Browse files
authored
feat: add basic network management (#188)
Related #163 This PR adds three things (in order from smaller to bigger): * Getting a container descriptor inside container. This is useful when tests already running inside container with mounted docker socket for container management. * Execution commands inside running container. This is useful "in general". I.e. we have project with test case that simulates RabbitMQ restarts using `rabbitmqctl stop_app` and `rabbitmqctl start_app`. Also it's used in new tests. * Basic network management. Docker networks can be created and deleted using `Pool`, containers can be connected to network using `BuildAndRunWithOptions` and `RunWithOptions`, already running containers can be connected to network using `ConnectToNetwork`. Currrently it's working pretty good but requires too much boilerplate with `docker` package for network management and commands execution. Also it helps in cases metioned in #163 when you need to run some end-to-end or integration tests with multiple linked containers.
1 parent 74d0e5f commit 1deddef

File tree

2 files changed

+308
-0
lines changed

2 files changed

+308
-0
lines changed

dockertest.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dockertest
22

33
import (
44
"fmt"
5+
"io"
56
"io/ioutil"
67
"net"
78
"os"
@@ -16,12 +17,27 @@ import (
1617
"github.com/pkg/errors"
1718
)
1819

20+
var (
21+
ErrNotInContainer = errors.New("not running in container")
22+
)
23+
1924
// Pool represents a connection to the docker API and is used to create and remove docker images.
2025
type Pool struct {
2126
Client *dc.Client
2227
MaxWait time.Duration
2328
}
2429

30+
// Network represents a docker network.
31+
type Network struct {
32+
pool *Pool
33+
Network *dc.Network
34+
}
35+
36+
// Close removes network by calling pool.RemoveNetwork.
37+
func (n *Network) Close() error {
38+
return n.pool.RemoveNetwork(n)
39+
}
40+
2541
// Resource represents a docker container.
2642
type Resource struct {
2743
pool *Pool
@@ -74,6 +90,118 @@ func (r *Resource) GetHostPort(portID string) string {
7490
return net.JoinHostPort(ip, m[0].HostPort)
7591
}
7692

93+
type ExecOptions struct {
94+
// Command environment, optional.
95+
Env []string
96+
97+
// StdIn will be attached as command stdin if provided.
98+
StdIn io.Reader
99+
100+
// StdOut will be attached as command stdout if provided.
101+
StdOut io.Writer
102+
103+
// StdErr will be attached as command stdout if provided.
104+
StdErr io.Writer
105+
106+
// Allocate TTY for command or not.
107+
TTY bool
108+
}
109+
110+
// Exec executes command within container.
111+
func (r *Resource) Exec(cmd []string, opts ExecOptions) (exitCode int, err error) {
112+
exec, err := r.pool.Client.CreateExec(dc.CreateExecOptions{
113+
Container: r.Container.ID,
114+
Cmd: cmd,
115+
Env: opts.Env,
116+
AttachStderr: opts.StdErr != nil,
117+
AttachStdout: opts.StdOut != nil,
118+
AttachStdin: opts.StdIn != nil,
119+
Tty: opts.TTY,
120+
})
121+
if err != nil {
122+
return -1, errors.Wrap(err, "Create exec failed")
123+
}
124+
125+
err = r.pool.Client.StartExec(exec.ID, dc.StartExecOptions{
126+
InputStream: opts.StdIn,
127+
OutputStream: opts.StdOut,
128+
ErrorStream: opts.StdErr,
129+
Tty: opts.TTY,
130+
})
131+
if err != nil {
132+
return -1, errors.Wrap(err, "Start exec failed")
133+
}
134+
135+
inspectExec, err := r.pool.Client.InspectExec(exec.ID)
136+
if err != nil {
137+
return -1, errors.Wrap(err, "Inspect exec failed")
138+
}
139+
140+
return inspectExec.ExitCode, nil
141+
}
142+
143+
// GetIPInNetwork returns container IP address in network.
144+
func (r *Resource) GetIPInNetwork(network *Network) string {
145+
if r.Container == nil || r.Container.NetworkSettings == nil {
146+
return ""
147+
}
148+
149+
netCfg, ok := r.Container.NetworkSettings.Networks[network.Network.Name]
150+
if !ok {
151+
return ""
152+
}
153+
154+
return netCfg.IPAddress
155+
}
156+
157+
// ConnectToNetwork connects container to network.
158+
func (r *Resource) ConnectToNetwork(network *Network) error {
159+
err := r.pool.Client.ConnectNetwork(
160+
network.Network.ID,
161+
dc.NetworkConnectionOptions{Container: r.Container.ID},
162+
)
163+
if err != nil {
164+
return errors.Wrap(err, "Failed to connect container to network")
165+
}
166+
167+
// refresh internal representation
168+
r.Container, err = r.pool.Client.InspectContainer(r.Container.ID)
169+
if err != nil {
170+
return errors.Wrap(err, "Failed to refresh container information")
171+
}
172+
173+
network.Network, err = r.pool.Client.NetworkInfo(network.Network.ID)
174+
if err != nil {
175+
return errors.Wrap(err, "Failed to refresh network information")
176+
}
177+
178+
return nil
179+
}
180+
181+
// DisconnectFromNetwork disconnects container from network.
182+
func (r *Resource) DisconnectFromNetwork(network *Network) error {
183+
err := r.pool.Client.DisconnectNetwork(
184+
network.Network.ID,
185+
dc.NetworkConnectionOptions{Container: r.Container.ID},
186+
)
187+
if err != nil {
188+
return errors.Wrap(err, "Failed to connect container to network")
189+
}
190+
191+
// refresh internal representation
192+
r.Container, err = r.pool.Client.InspectContainer(r.Container.ID)
193+
if err != nil {
194+
return errors.Wrap(err, "Failed to refresh container information")
195+
}
196+
197+
network.Network, err = r.pool.Client.NetworkInfo(network.Network.ID)
198+
if err != nil {
199+
return errors.Wrap(err, "Failed to refresh network information")
200+
}
201+
202+
return nil
203+
}
204+
77205
// Close removes a container and linked volumes from docker by calling pool.Purge.
78206
func (r *Resource) Close() error {
79207
return r.pool.Purge(r)
@@ -167,6 +295,7 @@ type RunOptions struct {
167295
DNS []string
168296
WorkingDir string
169297
NetworkID string
298+
Networks []*Network // optional networks to join
170299
Labels map[string]string
171300
Auth dc.AuthConfiguration
172301
PortBindings map[dc.Port][]dc.PortBinding
@@ -259,6 +388,9 @@ func (d *Pool) RunWithOptions(opts *RunOptions, hcOpts ...func(*dc.HostConfig))
259388
if opts.NetworkID != "" {
260389
networkingConfig.EndpointsConfig[opts.NetworkID] = &dc.EndpointConfig{}
261390
}
391+
for _, network := range opts.Networks {
392+
networkingConfig.EndpointsConfig[network.Network.ID] = &dc.EndpointConfig{}
393+
}
262394

263395
_, err := d.Client.InspectImage(fmt.Sprintf("%s:%s", repository, tag))
264396
if err != nil {
@@ -316,6 +448,13 @@ func (d *Pool) RunWithOptions(opts *RunOptions, hcOpts ...func(*dc.HostConfig))
316448
return nil, errors.Wrap(err, "")
317449
}
318450

451+
for _, network := range opts.Networks {
452+
network.Network, err = d.Client.NetworkInfo(network.Network.ID)
453+
if err != nil {
454+
return nil, errors.Wrap(err, "")
455+
}
456+
}
457+
319458
return &Resource{
320459
pool: d,
321460
Container: c,
@@ -404,3 +543,57 @@ func (d *Pool) Retry(op func() error) error {
404543
bo.MaxElapsedTime = d.MaxWait
405544
return backoff.Retry(op, bo)
406545
}
546+
547+
// CurrentContainer returns current container descriptor if this function called within running container.
548+
// It returns ErrNotInContainer as error if this function running not in container.
549+
func (d *Pool) CurrentContainer() (*Resource, error) {
550+
// docker daemon puts short container id into hostname
551+
hostname, err := os.Hostname()
552+
if err != nil {
553+
return nil, errors.Wrap(err, "Get hostname failed")
554+
}
555+
556+
container, err := d.Client.InspectContainer(hostname)
557+
switch err.(type) {
558+
case nil:
559+
return &Resource{
560+
pool: d,
561+
Container: container,
562+
}, nil
563+
case *dc.NoSuchContainer:
564+
return nil, ErrNotInContainer
565+
default:
566+
return nil, errors.Wrap(err, "")
567+
}
568+
}
569+
570+
// CreateNetwork creates docker network. It's useful for linking multiple containers.
571+
func (d *Pool) CreateNetwork(name string, opts ...func(config *dc.CreateNetworkOptions)) (*Network, error) {
572+
var cfg dc.CreateNetworkOptions
573+
cfg.Name = name
574+
for _, opt := range opts {
575+
opt(&cfg)
576+
}
577+
578+
network, err := d.Client.CreateNetwork(cfg)
579+
if err != nil {
580+
return nil, errors.Wrap(err, "")
581+
}
582+
583+
return &Network{
584+
pool: d,
585+
Network: network,
586+
}, nil
587+
}
588+
589+
// RemoveNetwork disconnects containers and removes provided network.
590+
func (d *Pool) RemoveNetwork(network *Network) error {
591+
for container := range network.Network.Containers {
592+
_ = d.Client.DisconnectNetwork(
593+
network.Network.ID,
594+
dc.NetworkConnectionOptions{Container: container, Force: true},
595+
)
596+
}
597+
598+
return d.Client.RemoveNetwork(network.Network.ID)
599+
}

dockertest_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package dockertest
22

33
import (
4+
"bytes"
45
"database/sql"
56
"fmt"
67
"io/ioutil"
78
"log"
89
"net/http"
910
"os"
11+
"strings"
1012
"testing"
1113
"time"
1214

@@ -221,3 +223,116 @@ func TestRemoveContainerByName(t *testing.T) {
221223
require.Nil(t, err)
222224
require.Nil(t, pool.Purge(resource))
223225
}
226+
227+
func TestExec(t *testing.T) {
228+
resource, err := pool.Run("postgres", "9.5", nil)
229+
require.Nil(t, err)
230+
assert.NotEmpty(t, resource.GetPort("5432/tcp"))
231+
assert.NotEmpty(t, resource.GetBoundIP("5432/tcp"))
232+
233+
defer resource.Close()
234+
235+
var pgVersion string
236+
err = pool.Retry(func() error {
237+
db, err := sql.Open("postgres", fmt.Sprintf("postgres://postgres:secret@localhost:%s/postgres?sslmode=disable", resource.GetPort("5432/tcp")))
238+
if err != nil {
239+
return err
240+
}
241+
return db.QueryRow("SHOW server_version").Scan(&pgVersion)
242+
})
243+
require.Nil(t, err)
244+
245+
var stdout bytes.Buffer
246+
exitCode, err := resource.Exec(
247+
[]string{"psql", "-qtAX", "-U", "postgres", "-c", "SHOW server_version"},
248+
ExecOptions{StdOut: &stdout},
249+
)
250+
require.Nil(t, err)
251+
require.Zero(t, exitCode)
252+
253+
require.Equal(t, pgVersion, strings.TrimRight(stdout.String(), "\n"))
254+
}
255+
256+
func TestNetworking_on_start(t *testing.T) {
257+
network, err := pool.CreateNetwork("test-on-start")
258+
require.Nil(t, err)
259+
defer network.Close()
260+
261+
resourceFirst, err := pool.RunWithOptions(&RunOptions{
262+
Repository: "postgres",
263+
Tag: "9.5",
264+
Networks: []*Network{network},
265+
})
266+
require.Nil(t, err)
267+
defer resourceFirst.Close()
268+
269+
resourceSecond, err := pool.RunWithOptions(&RunOptions{
270+
Repository: "postgres",
271+
Tag: "11",
272+
Networks: []*Network{network},
273+
})
274+
require.Nil(t, err)
275+
defer resourceSecond.Close()
276+
277+
var expectedVersion string
278+
err = pool.Retry(func() error {
279+
db, err := sql.Open(
280+
"postgres",
281+
fmt.Sprintf(
282+
"postgres://postgres:secret@localhost:%s/postgres?sslmode=disable",
283+
resourceSecond.GetPort("5432/tcp"),
284+
),
285+
)
286+
if err != nil {
287+
return err
288+
}
289+
return db.QueryRow("SHOW server_version").Scan(&expectedVersion)
290+
})
291+
require.Nil(t, err)
292+
}
293+
294+
func TestNetworking_after_start(t *testing.T) {
295+
network, err := pool.CreateNetwork("test-after-start")
296+
require.Nil(t, err)
297+
defer network.Close()
298+
299+
resourceFirst, err := pool.Run("postgres", "9.6", nil)
300+
require.Nil(t, err)
301+
defer resourceFirst.Close()
302+
303+
err = resourceFirst.ConnectToNetwork(network)
304+
require.Nil(t, err)
305+
306+
resourceSecond, err := pool.Run("postgres", "11", nil)
307+
require.Nil(t, err)
308+
defer resourceSecond.Close()
309+
310+
err = resourceSecond.ConnectToNetwork(network)
311+
require.Nil(t, err)
312+
313+
var expectedVersion string
314+
err = pool.Retry(func() error {
315+
db, err := sql.Open(
316+
"postgres",
317+
fmt.Sprintf(
318+
"postgres://postgres:secret@localhost:%s/postgres?sslmode=disable",
319+
resourceSecond.GetPort("5432/tcp"),
320+
),
321+
)
322+
if err != nil {
323+
return err
324+
}
325+
return db.QueryRow("SHOW server_version").Scan(&expectedVersion)
326+
})
327+
require.Nil(t, err)
328+
329+
var stdout bytes.Buffer
330+
exitCode, err := resourceFirst.Exec(
331+
[]string{"psql", "-qtAX", "-h", resourceSecond.GetIPInNetwork(network), "-U", "postgres", "-c", "SHOW server_version"},
332+
ExecOptions{StdOut: &stdout},
333+
)
334+
require.Nil(t, err)
335+
require.Zero(t, exitCode)
336+
337+
require.Equal(t, expectedVersion, strings.TrimRight(stdout.String(), "\n"))
338+
}

0 commit comments

Comments
 (0)