1package fakestorage // import "github.com/docker/docker/internal/test/fakestorage"
2
3import (
4	"context"
5	"fmt"
6	"io"
7	"io/ioutil"
8	"net/http"
9	"net/http/httptest"
10	"net/url"
11	"os"
12	"strings"
13
14	"github.com/docker/docker/api/types"
15	containertypes "github.com/docker/docker/api/types/container"
16	"github.com/docker/docker/client"
17	"github.com/docker/docker/internal/test"
18	"github.com/docker/docker/internal/test/environment"
19	"github.com/docker/docker/internal/test/fakecontext"
20	"github.com/docker/docker/internal/test/request"
21	"github.com/docker/docker/internal/testutil"
22	"github.com/docker/go-connections/nat"
23	"gotest.tools/assert"
24)
25
26var testEnv *environment.Execution
27
28type testingT interface {
29	assert.TestingT
30	logT
31	skipT
32	Fatal(args ...interface{})
33	Fatalf(string, ...interface{})
34}
35
36type logT interface {
37	Logf(string, ...interface{})
38}
39
40type skipT interface {
41	Skip(reason string)
42}
43
44// Fake is a static file server. It might be running locally or remotely
45// on test host.
46type Fake interface {
47	Close() error
48	URL() string
49	CtxDir() string
50}
51
52// SetTestEnvironment sets a static test environment
53// TODO: decouple this package from environment
54func SetTestEnvironment(env *environment.Execution) {
55	testEnv = env
56}
57
58// New returns a static file server that will be use as build context.
59func New(t testingT, dir string, modifiers ...func(*fakecontext.Fake) error) Fake {
60	if ht, ok := t.(test.HelperT); ok {
61		ht.Helper()
62	}
63	if testEnv == nil {
64		t.Fatal("fakstorage package requires SetTestEnvironment() to be called before use.")
65	}
66	ctx := fakecontext.New(t, dir, modifiers...)
67	switch {
68	case testEnv.IsRemoteDaemon() && strings.HasPrefix(request.DaemonHost(), "unix:///"):
69		t.Skip("e2e run : daemon is remote but docker host points to a unix socket")
70	case testEnv.IsLocalDaemon():
71		return newLocalFakeStorage(ctx)
72	default:
73		return newRemoteFileServer(t, ctx, testEnv.APIClient())
74	}
75	return nil
76}
77
78// localFileStorage is a file storage on the running machine
79type localFileStorage struct {
80	*fakecontext.Fake
81	*httptest.Server
82}
83
84func (s *localFileStorage) URL() string {
85	return s.Server.URL
86}
87
88func (s *localFileStorage) CtxDir() string {
89	return s.Fake.Dir
90}
91
92func (s *localFileStorage) Close() error {
93	defer s.Server.Close()
94	return s.Fake.Close()
95}
96
97func newLocalFakeStorage(ctx *fakecontext.Fake) *localFileStorage {
98	handler := http.FileServer(http.Dir(ctx.Dir))
99	server := httptest.NewServer(handler)
100	return &localFileStorage{
101		Fake:   ctx,
102		Server: server,
103	}
104}
105
106// remoteFileServer is a containerized static file server started on the remote
107// testing machine to be used in URL-accepting docker build functionality.
108type remoteFileServer struct {
109	host      string // hostname/port web server is listening to on docker host e.g. 0.0.0.0:43712
110	container string
111	image     string
112	client    client.APIClient
113	ctx       *fakecontext.Fake
114}
115
116func (f *remoteFileServer) URL() string {
117	u := url.URL{
118		Scheme: "http",
119		Host:   f.host}
120	return u.String()
121}
122
123func (f *remoteFileServer) CtxDir() string {
124	return f.ctx.Dir
125}
126
127func (f *remoteFileServer) Close() error {
128	defer func() {
129		if f.ctx != nil {
130			f.ctx.Close()
131		}
132		if f.image != "" {
133			if _, err := f.client.ImageRemove(context.Background(), f.image, types.ImageRemoveOptions{
134				Force: true,
135			}); err != nil {
136				fmt.Fprintf(os.Stderr, "Error closing remote file server : %v\n", err)
137			}
138		}
139		if err := f.client.Close(); err != nil {
140			fmt.Fprintf(os.Stderr, "Error closing remote file server : %v\n", err)
141		}
142	}()
143	if f.container == "" {
144		return nil
145	}
146	return f.client.ContainerRemove(context.Background(), f.container, types.ContainerRemoveOptions{
147		Force:         true,
148		RemoveVolumes: true,
149	})
150}
151
152func newRemoteFileServer(t testingT, ctx *fakecontext.Fake, c client.APIClient) *remoteFileServer {
153	var (
154		image     = fmt.Sprintf("fileserver-img-%s", strings.ToLower(testutil.GenerateRandomAlphaOnlyString(10)))
155		container = fmt.Sprintf("fileserver-cnt-%s", strings.ToLower(testutil.GenerateRandomAlphaOnlyString(10)))
156	)
157
158	ensureHTTPServerImage(t)
159
160	// Build the image
161	if err := ctx.Add("Dockerfile", `FROM httpserver
162COPY . /static`); err != nil {
163		t.Fatal(err)
164	}
165	resp, err := c.ImageBuild(context.Background(), ctx.AsTarReader(t), types.ImageBuildOptions{
166		NoCache: true,
167		Tags:    []string{image},
168	})
169	assert.NilError(t, err)
170	_, err = io.Copy(ioutil.Discard, resp.Body)
171	assert.NilError(t, err)
172
173	// Start the container
174	b, err := c.ContainerCreate(context.Background(), &containertypes.Config{
175		Image: image,
176	}, &containertypes.HostConfig{}, nil, container)
177	assert.NilError(t, err)
178	err = c.ContainerStart(context.Background(), b.ID, types.ContainerStartOptions{})
179	assert.NilError(t, err)
180
181	// Find out the system assigned port
182	i, err := c.ContainerInspect(context.Background(), b.ID)
183	assert.NilError(t, err)
184	newP, err := nat.NewPort("tcp", "80")
185	assert.NilError(t, err)
186	ports, exists := i.NetworkSettings.Ports[newP]
187	if !exists || len(ports) != 1 {
188		t.Fatalf("unable to find port 80/tcp for %s", container)
189	}
190	host := ports[0].HostIP
191	port := ports[0].HostPort
192
193	return &remoteFileServer{
194		container: container,
195		image:     image,
196		host:      fmt.Sprintf("%s:%s", host, port),
197		ctx:       ctx,
198		client:    c,
199	}
200}
201