github.com/adevinta/lava@v0.7.2/internal/containers/containers_test.go (about)

     1  // Copyright 2023 Adevinta
     2  
     3  package containers
     4  
     5  import (
     6  	"bytes"
     7  	"context"
     8  	"crypto/tls"
     9  	"encoding/json"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"net"
    14  	"net/http"
    15  	"net/http/httptest"
    16  	"os"
    17  	"regexp"
    18  	"strings"
    19  	"testing"
    20  
    21  	"github.com/docker/docker/api/types/container"
    22  	"github.com/docker/docker/api/types/filters"
    23  	"github.com/docker/docker/api/types/image"
    24  	"github.com/docker/docker/client"
    25  	"github.com/docker/docker/pkg/stdcopy"
    26  	"github.com/google/go-cmp/cmp"
    27  )
    28  
    29  var testRuntime Runtime
    30  
    31  func TestMain(m *testing.M) {
    32  	rt, err := GetenvRuntime()
    33  	if err != nil {
    34  		fmt.Fprintf(os.Stderr, "error: get env runtime: %v", err)
    35  		os.Exit(2)
    36  	}
    37  	testRuntime = rt
    38  
    39  	os.Exit(m.Run())
    40  }
    41  
    42  func TestParseRuntime(t *testing.T) {
    43  	tests := []struct {
    44  		name       string
    45  		rtName     string
    46  		want       Runtime
    47  		wantNilErr bool
    48  	}{
    49  		{
    50  			name:       "valid runtime",
    51  			rtName:     "DockerdDockerDesktop",
    52  			want:       RuntimeDockerdDockerDesktop,
    53  			wantNilErr: true,
    54  		},
    55  		{
    56  			name:       "invalid runtime",
    57  			rtName:     "Invalid",
    58  			want:       Runtime(0),
    59  			wantNilErr: false,
    60  		},
    61  	}
    62  
    63  	for _, tt := range tests {
    64  		t.Run(tt.name, func(t *testing.T) {
    65  			got, err := ParseRuntime(tt.rtName)
    66  
    67  			if (err == nil) != tt.wantNilErr {
    68  				t.Errorf("unexpected error: %v", err)
    69  			}
    70  
    71  			if got != tt.want {
    72  				t.Errorf("unexpected runtime: got: %v, want: %v", tt.want, got)
    73  			}
    74  		})
    75  	}
    76  }
    77  
    78  func TestGetenvRuntime(t *testing.T) {
    79  	tests := []struct {
    80  		name       string
    81  		env        string
    82  		want       Runtime
    83  		wantNilErr bool
    84  	}{
    85  		{
    86  			name:       "empty env var",
    87  			env:        "",
    88  			want:       RuntimeDockerd,
    89  			wantNilErr: true,
    90  		},
    91  		{
    92  			name:       "dockerd podman desktop",
    93  			env:        "DockerdPodmanDesktop",
    94  			want:       RuntimeDockerdPodmanDesktop,
    95  			wantNilErr: true,
    96  		},
    97  		{
    98  			name:       "invalid runtime",
    99  			env:        "Invalid",
   100  			want:       Runtime(0),
   101  			wantNilErr: false,
   102  		},
   103  	}
   104  
   105  	for _, tt := range tests {
   106  		t.Run(tt.name, func(t *testing.T) {
   107  			t.Setenv("LAVA_RUNTIME", tt.env)
   108  
   109  			got, err := GetenvRuntime()
   110  
   111  			if (err == nil) != tt.wantNilErr {
   112  				t.Errorf("unexpected error: %v", err)
   113  			}
   114  
   115  			if got != tt.want {
   116  				t.Errorf("unexpected runtime: got: %v, want: %v", got, tt.want)
   117  			}
   118  		})
   119  	}
   120  }
   121  
   122  func TestRuntime_UnmarshalText(t *testing.T) {
   123  	type JSONData struct {
   124  		Runtime Runtime `json:"runtime"`
   125  	}
   126  
   127  	tests := []struct {
   128  		name       string
   129  		data       string
   130  		want       JSONData
   131  		wantNilErr bool
   132  	}{
   133  		{
   134  			name:       "valid runtime",
   135  			data:       `{"runtime": "DockerdRancherDesktop"}`,
   136  			want:       JSONData{Runtime: RuntimeDockerdRancherDesktop},
   137  			wantNilErr: true,
   138  		},
   139  		{
   140  			name:       "invalid runtime",
   141  			data:       `{"runtime": "Invalid"}`,
   142  			want:       JSONData{},
   143  			wantNilErr: false,
   144  		},
   145  	}
   146  
   147  	for _, tt := range tests {
   148  		t.Run(tt.name, func(t *testing.T) {
   149  			var got JSONData
   150  
   151  			err := json.Unmarshal([]byte(tt.data), &got)
   152  
   153  			if (err == nil) != tt.wantNilErr {
   154  				t.Errorf("unexpected error: %v", err)
   155  			}
   156  
   157  			if got != tt.want {
   158  				t.Errorf("unexpected runtime: got: %v, want: %v", tt.want, got)
   159  			}
   160  		})
   161  	}
   162  }
   163  
   164  var (
   165  	bridgeCfgs = []mockDockerdIPAMConfig{{Subnet: "172.17.0.0/16", Gateway: "172.17.0.1"}}
   166  	bridgeAddr = &net.IPNet{IP: net.ParseIP("172.17.0.1"), Mask: net.CIDRMask(16, 32)}
   167  
   168  	defaultAPITestdata = mockDockerdTestdata{
   169  		networks: map[string]mockDockerdNetworkTestdata{
   170  			defaultDockerBridgeNetwork: {
   171  				cfgs:          bridgeCfgs,
   172  				gateways:      []*net.IPNet{bridgeAddr},
   173  				bridgeGateway: bridgeAddr,
   174  			},
   175  			"multi": {
   176  				cfgs: []mockDockerdIPAMConfig{
   177  					{Subnet: "172.18.0.0/16", Gateway: "172.18.0.1"},
   178  					{Subnet: "172.19.0.0/16", Gateway: "172.19.0.10"},
   179  				},
   180  				gateways: []*net.IPNet{
   181  					{IP: net.ParseIP("172.18.0.1"), Mask: net.CIDRMask(16, 32)},
   182  					{IP: net.ParseIP("172.19.0.10"), Mask: net.CIDRMask(16, 32)},
   183  				},
   184  			},
   185  			"empty": {},
   186  			"mismatch": {
   187  				cfgs: []mockDockerdIPAMConfig{
   188  					{Subnet: "172.17.0.0/16", Gateway: "172.18.0.1"},
   189  				},
   190  			},
   191  			"badgateway": {
   192  				cfgs: []mockDockerdIPAMConfig{
   193  					{Subnet: "172.18.0.0/16", Gateway: "172.18.0.555"},
   194  				},
   195  			},
   196  			"badsubnet": {
   197  				cfgs: []mockDockerdIPAMConfig{
   198  					{Subnet: "172.18.555.0/16", Gateway: "172.18.0.1"},
   199  				},
   200  			},
   201  		},
   202  		system: mockDockerdSystemTestdata{
   203  			id: "dockerutil",
   204  		},
   205  	}
   206  )
   207  
   208  func TestNewDockerdClient_tls(t *testing.T) {
   209  	tests := []struct {
   210  		name       string
   211  		host       string
   212  		wantID     string
   213  		wantNilErr bool
   214  	}{
   215  		{
   216  			name:       "success",
   217  			host:       "127.0.0.1",
   218  			wantID:     defaultAPITestdata.system.id,
   219  			wantNilErr: true,
   220  		},
   221  		{
   222  			name:       "error",
   223  			host:       "localhost",
   224  			wantID:     "",
   225  			wantNilErr: false,
   226  		},
   227  	}
   228  
   229  	for _, tt := range tests {
   230  		t.Run(tt.name, func(t *testing.T) {
   231  			srv := httptest.NewUnstartedServer(mockDockerd{testdata: defaultAPITestdata})
   232  
   233  			cert, err := tls.LoadX509KeyPair("testdata/certs/server-cert.pem", "testdata/certs/server-key.pem")
   234  			if err != nil {
   235  				panic(fmt.Sprintf("httptest: NewTLSServer: %v", err))
   236  			}
   237  			srv.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
   238  
   239  			srv.StartTLS()
   240  			defer srv.Close()
   241  
   242  			addr := srv.Listener.Addr().(*net.TCPAddr)
   243  			dockerHost := fmt.Sprintf("tcp://%v:%v", tt.host, addr.Port)
   244  
   245  			t.Setenv("DOCKER_CONFIG", "testdata")
   246  			t.Setenv("DOCKER_CERT_PATH", "testdata/certs")
   247  			t.Setenv("DOCKER_HOST", dockerHost)
   248  			t.Setenv("DOCKER_TLS_VERIFY", "1")
   249  
   250  			cli, err := NewDockerdClient(RuntimeDockerd)
   251  			if err != nil {
   252  				t.Fatalf("could not create API client: %v", err)
   253  			}
   254  			defer cli.Close()
   255  
   256  			if dh := cli.DaemonHost(); dh != dockerHost {
   257  				t.Errorf("unexpected daemon host: got: %v, want: %v", dh, dockerHost)
   258  			}
   259  
   260  			info, err := cli.Info(context.Background())
   261  
   262  			if err == nil != tt.wantNilErr {
   263  				t.Errorf("unexpected error: %v", err)
   264  			}
   265  
   266  			if err != nil {
   267  				var tlsErr *tls.CertificateVerificationError
   268  				if !errors.As(err, &tlsErr) {
   269  					t.Errorf("error is not a TLS error: %v", err)
   270  				}
   271  			}
   272  
   273  			if info.ID != tt.wantID {
   274  				t.Errorf("unexpected system ID: got: %v, want: %v", info.ID, defaultAPITestdata.system.id)
   275  			}
   276  		})
   277  	}
   278  }
   279  
   280  func TestDockerdClient_DaemonHost(t *testing.T) {
   281  	const dockerHost = "tcp://example.com:1234"
   282  
   283  	t.Setenv("DOCKER_CONFIG", "testdata/certs")
   284  	t.Setenv("DOCKER_HOST", dockerHost)
   285  
   286  	cli, err := NewDockerdClient(RuntimeDockerd)
   287  	if err != nil {
   288  		t.Fatalf("could not create API client: %v", err)
   289  	}
   290  	defer cli.Close()
   291  
   292  	if dh := cli.DaemonHost(); dh != dockerHost {
   293  		t.Errorf("unexpected daemon host: got: %v, want: %v", dh, dockerHost)
   294  	}
   295  }
   296  
   297  func TestDockerdClient_HostGatewayHostname(t *testing.T) {
   298  	tests := []struct {
   299  		name string
   300  		rt   Runtime
   301  		want string
   302  	}{
   303  		{
   304  			name: "dockerd",
   305  			rt:   RuntimeDockerd,
   306  			want: "host.docker.internal",
   307  		},
   308  		{
   309  			name: "dockerd podman desktop",
   310  			rt:   RuntimeDockerdPodmanDesktop,
   311  			want: "host.containers.internal",
   312  		},
   313  		{
   314  			name: "invalid runtime",
   315  			rt:   Runtime(255),
   316  			want: "host.docker.internal",
   317  		},
   318  	}
   319  
   320  	for _, tt := range tests {
   321  		t.Run(tt.name, func(t *testing.T) {
   322  			cli, err := NewDockerdClient(tt.rt)
   323  			if err != nil {
   324  				t.Fatalf("could not create API client: %v", err)
   325  			}
   326  			defer cli.Close()
   327  
   328  			got := cli.HostGatewayHostname()
   329  			if got != tt.want {
   330  				t.Errorf("unexpected hostname: got: %v, want: %v", got, tt.want)
   331  			}
   332  		})
   333  	}
   334  }
   335  
   336  func TestDockerdClient_HostGatewayMapping(t *testing.T) {
   337  	tests := []struct {
   338  		name string
   339  		rt   Runtime
   340  		want string
   341  	}{
   342  		{
   343  			name: "dockerd",
   344  			rt:   RuntimeDockerd,
   345  			want: "host.docker.internal:host-gateway",
   346  		},
   347  		{
   348  			name: "dockerd podman desktop",
   349  			rt:   RuntimeDockerdPodmanDesktop,
   350  			want: "",
   351  		},
   352  		{
   353  			name: "invalid runtime",
   354  			rt:   Runtime(255),
   355  			want: "",
   356  		},
   357  	}
   358  
   359  	for _, tt := range tests {
   360  		t.Run(tt.name, func(t *testing.T) {
   361  			cli, err := NewDockerdClient(tt.rt)
   362  			if err != nil {
   363  				t.Fatalf("could not create API client: %v", err)
   364  			}
   365  			defer cli.Close()
   366  
   367  			got := cli.HostGatewayMapping()
   368  			if got != tt.want {
   369  				t.Errorf("unexpected hostname: got: %v, want: %v", got, tt.want)
   370  			}
   371  		})
   372  	}
   373  }
   374  
   375  func TestDockerdClient_gateways(t *testing.T) {
   376  	tests := []struct {
   377  		name       string
   378  		net        string
   379  		wantNilErr bool
   380  	}{
   381  		{
   382  			name:       "default bridge network",
   383  			net:        defaultDockerBridgeNetwork,
   384  			wantNilErr: true,
   385  		},
   386  		{
   387  			name:       "multiple gateways",
   388  			net:        "multi",
   389  			wantNilErr: true,
   390  		},
   391  		{
   392  			name:       "no gateways",
   393  			net:        "empty",
   394  			wantNilErr: true,
   395  		},
   396  		{
   397  			name:       "subnet mismatch",
   398  			net:        "mismatch",
   399  			wantNilErr: false,
   400  		},
   401  		{
   402  			name:       "malformed subnet",
   403  			net:        "badsubnet",
   404  			wantNilErr: false,
   405  		},
   406  		{
   407  			name:       "malformed gateway",
   408  			net:        "badgateway",
   409  			wantNilErr: false,
   410  		},
   411  		{
   412  			name:       "api error",
   413  			net:        "notfound",
   414  			wantNilErr: false,
   415  		},
   416  	}
   417  
   418  	for _, tt := range tests {
   419  		t.Run(tt.name, func(t *testing.T) {
   420  			cli, err := newMockDockerdClient(t, RuntimeDockerd, defaultAPITestdata)
   421  			if err != nil {
   422  				t.Fatalf("could not create test client: %v", err)
   423  			}
   424  			defer cli.Close()
   425  
   426  			got, err := cli.gateways(context.Background(), tt.net)
   427  
   428  			if (err == nil) != tt.wantNilErr {
   429  				t.Errorf("unexpected error: %v", err)
   430  			}
   431  
   432  			td := defaultAPITestdata.networks[tt.net]
   433  			if diff := cmp.Diff(td.gateways, got); diff != "" {
   434  				t.Errorf("gateways mismatch (-want +got):\n%s", diff)
   435  			}
   436  		})
   437  	}
   438  }
   439  
   440  func TestDockerdClient_bridgeGateway(t *testing.T) {
   441  	tests := []struct {
   442  		name       string
   443  		td         mockDockerdTestdata
   444  		wantNilErr bool
   445  	}{
   446  		{
   447  			name:       "default bridge network",
   448  			td:         defaultAPITestdata,
   449  			wantNilErr: true,
   450  		},
   451  		{
   452  			name: "multiple gateways",
   453  			td: mockDockerdTestdata{
   454  				networks: map[string]mockDockerdNetworkTestdata{
   455  					defaultDockerBridgeNetwork: {
   456  						cfgs: []mockDockerdIPAMConfig{
   457  							{Subnet: "172.18.0.0/16", Gateway: "172.18.0.1"},
   458  							{Subnet: "172.19.0.0/16", Gateway: "172.19.0.10"},
   459  						},
   460  					},
   461  				},
   462  			},
   463  			wantNilErr: false,
   464  		},
   465  		{
   466  			name: "no gateways",
   467  			td: mockDockerdTestdata{
   468  				networks: map[string]mockDockerdNetworkTestdata{
   469  					defaultDockerBridgeNetwork: {},
   470  				},
   471  			},
   472  			wantNilErr: false,
   473  		},
   474  	}
   475  
   476  	for _, tt := range tests {
   477  		t.Run(tt.name, func(t *testing.T) {
   478  			cli, err := newMockDockerdClient(t, RuntimeDockerd, tt.td)
   479  			if err != nil {
   480  				t.Fatalf("could not create test client: %v", err)
   481  			}
   482  			defer cli.Close()
   483  
   484  			got, err := cli.bridgeGateway()
   485  
   486  			if (err == nil) != tt.wantNilErr {
   487  				t.Errorf("unexpected error: %v", err)
   488  			}
   489  
   490  			want := tt.td.networks[defaultDockerBridgeNetwork].bridgeGateway
   491  			if !cmp.Equal(got, want) {
   492  				t.Errorf("unexpected value: got: %v, want: %v", got, want)
   493  			}
   494  		})
   495  	}
   496  }
   497  
   498  func TestDockerdClient_HostGatewayInterfaceAddr(t *testing.T) {
   499  	tests := []struct {
   500  		name string
   501  		rt   Runtime
   502  		want string
   503  	}{
   504  		{
   505  			name: "docker desktop",
   506  			rt:   RuntimeDockerdDockerDesktop,
   507  			want: "127.0.0.1",
   508  		},
   509  		{
   510  			name: "docker engine",
   511  			rt:   RuntimeDockerd,
   512  			want: bridgeAddr.IP.String(),
   513  		},
   514  	}
   515  
   516  	for _, tt := range tests {
   517  		t.Run(tt.name, func(t *testing.T) {
   518  			cli, err := newMockDockerdClient(t, tt.rt, defaultAPITestdata)
   519  			if err != nil {
   520  				t.Fatalf("could not create test client: %v", err)
   521  			}
   522  			defer cli.Close()
   523  
   524  			got, err := cli.HostGatewayInterfaceAddr()
   525  			if err != nil {
   526  				t.Errorf("unexpected error: %v", err)
   527  			}
   528  
   529  			if got != tt.want {
   530  				t.Errorf("unexpected value: got: %v, want: %v", got, tt.want)
   531  			}
   532  		})
   533  	}
   534  }
   535  
   536  func TestDockerdClient_ImageBuild(t *testing.T) {
   537  	cli, err := NewDockerdClient(testRuntime)
   538  	if err != nil {
   539  		t.Fatalf("could not create API client: %v", err)
   540  	}
   541  	defer cli.Close()
   542  
   543  	const imgRef = "lava-internal-containers-test:go-test"
   544  
   545  	imgID, err := cli.ImageBuild(context.Background(), "testdata/image", "Dockerfile", imgRef)
   546  	if err != nil {
   547  		t.Fatalf("image build error: %v", err)
   548  	}
   549  	defer func() {
   550  		rmOpts := image.RemoveOptions{Force: true, PruneChildren: true}
   551  		if _, err := cli.ImageRemove(context.Background(), imgRef, rmOpts); err != nil {
   552  			t.Logf("could not delete test Docker image %q: %v", imgRef, err)
   553  		}
   554  	}()
   555  
   556  	summ, err := cli.ImageList(context.Background(), image.ListOptions{
   557  		Filters: filters.NewArgs(filters.Arg("reference", imgRef)),
   558  	})
   559  	if err != nil {
   560  		t.Fatalf("image list error: %v", err)
   561  	}
   562  
   563  	if len(summ) != 1 {
   564  		t.Errorf("unexpected number of images: %v", len(summ))
   565  	}
   566  
   567  	const want = "image build test"
   568  
   569  	got, err := dockerRun(t, cli.APIClient, imgID, want)
   570  	if err != nil {
   571  		t.Fatalf("docker run error: %v", err)
   572  	}
   573  
   574  	if got != want {
   575  		t.Errorf("unexpected output: got: %q, want: %q", got, want)
   576  	}
   577  }
   578  
   579  func dockerRun(t *testing.T, cli client.APIClient, ref string, cmd ...string) (stdout string, err error) {
   580  	contCfg := &container.Config{
   581  		Image: ref,
   582  		Cmd:   cmd,
   583  		Tty:   false,
   584  	}
   585  	resp, err := cli.ContainerCreate(context.Background(), contCfg, nil, nil, nil, "")
   586  	if err != nil {
   587  		return "", fmt.Errorf("container create: %w", err)
   588  	}
   589  	defer func() {
   590  		rmOpts := container.RemoveOptions{Force: true}
   591  		if err := cli.ContainerRemove(context.Background(), resp.ID, rmOpts); err != nil {
   592  			t.Logf("could not delete test Docker container %q: %v", resp.ID, err)
   593  		}
   594  	}()
   595  
   596  	if err := cli.ContainerStart(context.Background(), resp.ID, container.StartOptions{}); err != nil {
   597  		return "", fmt.Errorf("container start: %w", err)
   598  	}
   599  
   600  	statusCh, errCh := cli.ContainerWait(context.Background(), resp.ID, container.WaitConditionNotRunning)
   601  	select {
   602  	case err := <-errCh:
   603  		if err != nil {
   604  			return "", fmt.Errorf("container wait: %w", err)
   605  		}
   606  	case <-statusCh:
   607  	}
   608  
   609  	logs, err := cli.ContainerLogs(context.Background(), resp.ID, container.LogsOptions{ShowStdout: true})
   610  	if err != nil {
   611  		return "", fmt.Errorf("container logs: %w", err)
   612  	}
   613  
   614  	var out bytes.Buffer
   615  	if _, err := stdcopy.StdCopy(&out, io.Discard, logs); err != nil {
   616  		return "", fmt.Errorf("std copy: %w", err)
   617  	}
   618  
   619  	return out.String(), nil
   620  }
   621  
   622  type mockDockerdClient struct {
   623  	DockerdClient
   624  	srv *httptest.Server
   625  }
   626  
   627  func newMockDockerdClient(t *testing.T, rt Runtime, td mockDockerdTestdata) (mockDockerdClient, error) {
   628  	srv := httptest.NewServer(mockDockerd{testdata: td})
   629  
   630  	t.Setenv("DOCKER_HOST", "tcp://"+srv.Listener.Addr().String())
   631  
   632  	cli, err := NewDockerdClient(rt)
   633  	if err != nil {
   634  		srv.Close()
   635  		return mockDockerdClient{}, fmt.Errorf("new client: %w", err)
   636  	}
   637  
   638  	mockcli := mockDockerdClient{
   639  		DockerdClient: cli,
   640  		srv:           srv,
   641  	}
   642  	return mockcli, nil
   643  }
   644  
   645  func (mockcli mockDockerdClient) Close() error {
   646  	mockcli.srv.Close()
   647  	return mockcli.DockerdClient.Close()
   648  }
   649  
   650  type mockDockerd struct {
   651  	testdata mockDockerdTestdata
   652  }
   653  
   654  type mockDockerdTestdata struct {
   655  	networks map[string]mockDockerdNetworkTestdata
   656  	system   mockDockerdSystemTestdata
   657  }
   658  
   659  type mockDockerdNetworkTestdata struct {
   660  	cfgs          []mockDockerdIPAMConfig
   661  	gateways      []*net.IPNet
   662  	bridgeGateway *net.IPNet
   663  }
   664  
   665  type mockDockerdSystemTestdata struct {
   666  	id string
   667  }
   668  
   669  var routeRegexp = regexp.MustCompile(`^/v\d+\.\d+(/.*)$`)
   670  
   671  func (api mockDockerd) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   672  	m := routeRegexp.FindStringSubmatch(r.URL.Path)
   673  	if m == nil {
   674  		http.Error(w, "bad request", http.StatusBadRequest)
   675  		return
   676  	}
   677  	endpoint := m[1]
   678  
   679  	if r.Method != "GET" {
   680  		http.Error(w, "not implemented", http.StatusNotImplemented)
   681  		return
   682  	}
   683  
   684  	switch {
   685  	case strings.HasPrefix(endpoint, "/networks/"):
   686  		api.handleNetworks(w, r, strings.TrimPrefix(endpoint, "/networks/"))
   687  	case endpoint == "/info":
   688  		api.handleInfo(w, r)
   689  	default:
   690  		http.Error(w, "not found", http.StatusNotFound)
   691  	}
   692  }
   693  
   694  type mockDockerdNetwork struct {
   695  	IPAM mockDockerdIPAM `json:"IPAM"`
   696  }
   697  
   698  type mockDockerdIPAM struct {
   699  	Config []mockDockerdIPAMConfig `json:"Config"`
   700  }
   701  
   702  type mockDockerdIPAMConfig struct {
   703  	Subnet  string `json:"Subnet"`
   704  	Gateway string `json:"Gateway"`
   705  }
   706  
   707  func (api mockDockerd) handleNetworks(w http.ResponseWriter, _ *http.Request, name string) {
   708  	td, ok := api.testdata.networks[name]
   709  	if !ok {
   710  		http.Error(w, "not found", http.StatusNotFound)
   711  		return
   712  	}
   713  
   714  	net := mockDockerdNetwork{IPAM: mockDockerdIPAM{Config: td.cfgs}}
   715  	if err := json.NewEncoder(w).Encode(net); err != nil {
   716  		http.Error(w, fmt.Sprintf("marshal: %v", err), http.StatusInternalServerError)
   717  	}
   718  }
   719  
   720  type mockDockerdInfo struct {
   721  	ID string `json:"ID"`
   722  }
   723  
   724  func (api mockDockerd) handleInfo(w http.ResponseWriter, _ *http.Request) {
   725  	net := mockDockerdInfo{ID: api.testdata.system.id}
   726  	if err := json.NewEncoder(w).Encode(net); err != nil {
   727  		http.Error(w, fmt.Sprintf("marshal: %v", err), http.StatusInternalServerError)
   728  	}
   729  }