Browse code

Add Unit test to daemon.SearchRegistryForImages…

… and refactor a little bit some daemon on the way.

- Move `SearchRegistryForImages` to a new file (`daemon/search.go`) as
`daemon.go` is getting pretty big.
- `registry.Service` is now an interface (allowing us to decouple it a
little bit and thus unit test easily).
- Add some unit test for `SearchRegistryForImages`.
- Use UniqueExactMatch for search filters
- And use empty restore id for now in client.ContainerStart.

Signed-off-by: Vincent Demeester <vincent@sbr.pm>

Vincent Demeester authored on 2016/05/21 23:00:28
Showing 11 changed files
... ...
@@ -234,7 +234,7 @@ func (cli *DockerCli) CmdRun(args ...string) error {
234 234
 	}
235 235
 
236 236
 	//start the container
237
-	if err := cli.client.ContainerStart(ctx, createResponse.ID); err != nil {
237
+	if err := cli.client.ContainerStart(ctx, createResponse.ID, ""); err != nil {
238 238
 		// If we have holdHijackedConnection, we should notify
239 239
 		// holdHijackedConnection we are going to exit and wait
240 240
 		// to avoid the terminal are not restored.
... ...
@@ -113,7 +113,7 @@ func (cli *DockerCli) CmdStart(args ...string) error {
113 113
 		})
114 114
 
115 115
 		// 3. Start the container.
116
-		if err := cli.client.ContainerStart(ctx, container); err != nil {
116
+		if err := cli.client.ContainerStart(ctx, container, ""); err != nil {
117 117
 			cancelFun()
118 118
 			<-cErr
119 119
 			return err
... ...
@@ -147,7 +147,7 @@ func (cli *DockerCli) CmdStart(args ...string) error {
147 147
 func (cli *DockerCli) startContainersWithoutAttachments(ctx context.Context, containers []string) error {
148 148
 	var failedContainers []string
149 149
 	for _, container := range containers {
150
-		if err := cli.client.ContainerStart(ctx, container); err != nil {
150
+		if err := cli.client.ContainerStart(ctx, container, ""); err != nil {
151 151
 			fmt.Fprintf(cli.err, "%s\n", err)
152 152
 			failedContainers = append(failedContainers, container)
153 153
 		} else {
... ...
@@ -15,7 +15,6 @@ import (
15 15
 	"path/filepath"
16 16
 	"regexp"
17 17
 	"runtime"
18
-	"strconv"
19 18
 	"strings"
20 19
 	"sync"
21 20
 	"syscall"
... ...
@@ -31,7 +30,6 @@ import (
31 31
 	"github.com/docker/engine-api/types"
32 32
 	containertypes "github.com/docker/engine-api/types/container"
33 33
 	networktypes "github.com/docker/engine-api/types/network"
34
-	registrytypes "github.com/docker/engine-api/types/registry"
35 34
 	"github.com/docker/engine-api/types/strslice"
36 35
 	// register graph drivers
37 36
 	_ "github.com/docker/docker/daemon/graphdriver/register"
... ...
@@ -63,7 +61,6 @@ import (
63 63
 	volumedrivers "github.com/docker/docker/volume/drivers"
64 64
 	"github.com/docker/docker/volume/local"
65 65
 	"github.com/docker/docker/volume/store"
66
-	"github.com/docker/engine-api/types/filters"
67 66
 	"github.com/docker/go-connections/nat"
68 67
 	"github.com/docker/libnetwork"
69 68
 	nwconfig "github.com/docker/libnetwork/config"
... ...
@@ -93,7 +90,7 @@ type Daemon struct {
93 93
 	configStore               *Config
94 94
 	statsCollector            *statsCollector
95 95
 	defaultLogConfig          containertypes.LogConfig
96
-	RegistryService           *registry.Service
96
+	RegistryService           registry.Service
97 97
 	EventsService             *events.Events
98 98
 	netController             libnetwork.NetworkController
99 99
 	volumes                   *store.VolumeStore
... ...
@@ -614,7 +611,7 @@ func (daemon *Daemon) registerLink(parent, child *container.Container, alias str
614 614
 
615 615
 // NewDaemon sets up everything for the daemon to be able to service
616 616
 // requests from the webserver.
617
-func NewDaemon(config *Config, registryService *registry.Service, containerdRemote libcontainerd.Remote) (daemon *Daemon, err error) {
617
+func NewDaemon(config *Config, registryService registry.Service, containerdRemote libcontainerd.Remote) (daemon *Daemon, err error) {
618 618
 	setDefaultMtu(config)
619 619
 
620 620
 	// Ensure we have compatible and valid configuration options
... ...
@@ -1144,88 +1141,7 @@ func configureVolumes(config *Config, rootUID, rootGID int) (*store.VolumeStore,
1144 1144
 
1145 1145
 // AuthenticateToRegistry checks the validity of credentials in authConfig
1146 1146
 func (daemon *Daemon) AuthenticateToRegistry(ctx context.Context, authConfig *types.AuthConfig) (string, string, error) {
1147
-	return daemon.RegistryService.Auth(authConfig, dockerversion.DockerUserAgent(ctx))
1148
-}
1149
-
1150
-var acceptedSearchFilterTags = map[string]bool{
1151
-	"is-automated": true,
1152
-	"is-official":  true,
1153
-	"stars":        true,
1154
-}
1155
-
1156
-// SearchRegistryForImages queries the registry for images matching
1157
-// term. authConfig is used to login.
1158
-func (daemon *Daemon) SearchRegistryForImages(ctx context.Context, filtersArgs string, term string,
1159
-	authConfig *types.AuthConfig,
1160
-	headers map[string][]string) (*registrytypes.SearchResults, error) {
1161
-
1162
-	searchFilters, err := filters.FromParam(filtersArgs)
1163
-	if err != nil {
1164
-		return nil, err
1165
-	}
1166
-	if err := searchFilters.Validate(acceptedSearchFilterTags); err != nil {
1167
-		return nil, err
1168
-	}
1169
-
1170
-	unfilteredResult, err := daemon.RegistryService.Search(term, authConfig, dockerversion.DockerUserAgent(ctx), headers)
1171
-	if err != nil {
1172
-		return nil, err
1173
-	}
1174
-
1175
-	var isAutomated, isOfficial bool
1176
-	var hasStarFilter = 0
1177
-	if searchFilters.Include("is-automated") {
1178
-		if searchFilters.ExactMatch("is-automated", "true") {
1179
-			isAutomated = true
1180
-		} else if !searchFilters.ExactMatch("is-automated", "false") {
1181
-			return nil, fmt.Errorf("Invalid filter 'is-automated=%s'", searchFilters.Get("is-automated"))
1182
-		}
1183
-	}
1184
-	if searchFilters.Include("is-official") {
1185
-		if searchFilters.ExactMatch("is-official", "true") {
1186
-			isOfficial = true
1187
-		} else if !searchFilters.ExactMatch("is-official", "false") {
1188
-			return nil, fmt.Errorf("Invalid filter 'is-official=%s'", searchFilters.Get("is-official"))
1189
-		}
1190
-	}
1191
-	if searchFilters.Include("stars") {
1192
-		hasStars := searchFilters.Get("stars")
1193
-		for _, hasStar := range hasStars {
1194
-			iHasStar, err := strconv.Atoi(hasStar)
1195
-			if err != nil {
1196
-				return nil, fmt.Errorf("Invalid filter 'stars=%s'", hasStar)
1197
-			}
1198
-			if iHasStar > hasStarFilter {
1199
-				hasStarFilter = iHasStar
1200
-			}
1201
-		}
1202
-	}
1203
-
1204
-	filteredResults := []registrytypes.SearchResult{}
1205
-	for _, result := range unfilteredResult.Results {
1206
-		if searchFilters.Include("is-automated") {
1207
-			if isAutomated != result.IsAutomated {
1208
-				continue
1209
-			}
1210
-		}
1211
-		if searchFilters.Include("is-official") {
1212
-			if isOfficial != result.IsOfficial {
1213
-				continue
1214
-			}
1215
-		}
1216
-		if searchFilters.Include("stars") {
1217
-			if result.StarCount < hasStarFilter {
1218
-				continue
1219
-			}
1220
-		}
1221
-		filteredResults = append(filteredResults, result)
1222
-	}
1223
-
1224
-	return &registrytypes.SearchResults{
1225
-		Query:      unfilteredResult.Query,
1226
-		NumResults: len(filteredResults),
1227
-		Results:    filteredResults,
1228
-	}, nil
1147
+	return daemon.RegistryService.Auth(ctx, authConfig, dockerversion.DockerUserAgent(ctx))
1229 1148
 }
1230 1149
 
1231 1150
 // IsShuttingDown tells whether the daemon is shutting down or not
1232 1151
new file mode 100644
... ...
@@ -0,0 +1,94 @@
0
+package daemon
1
+
2
+import (
3
+	"fmt"
4
+	"strconv"
5
+
6
+	"golang.org/x/net/context"
7
+
8
+	"github.com/docker/docker/dockerversion"
9
+	"github.com/docker/engine-api/types"
10
+	"github.com/docker/engine-api/types/filters"
11
+	registrytypes "github.com/docker/engine-api/types/registry"
12
+)
13
+
14
+var acceptedSearchFilterTags = map[string]bool{
15
+	"is-automated": true,
16
+	"is-official":  true,
17
+	"stars":        true,
18
+}
19
+
20
+// SearchRegistryForImages queries the registry for images matching
21
+// term. authConfig is used to login.
22
+func (daemon *Daemon) SearchRegistryForImages(ctx context.Context, filtersArgs string, term string,
23
+	authConfig *types.AuthConfig,
24
+	headers map[string][]string) (*registrytypes.SearchResults, error) {
25
+
26
+	searchFilters, err := filters.FromParam(filtersArgs)
27
+	if err != nil {
28
+		return nil, err
29
+	}
30
+	if err := searchFilters.Validate(acceptedSearchFilterTags); err != nil {
31
+		return nil, err
32
+	}
33
+
34
+	unfilteredResult, err := daemon.RegistryService.Search(ctx, term, authConfig, dockerversion.DockerUserAgent(ctx), headers)
35
+	if err != nil {
36
+		return nil, err
37
+	}
38
+
39
+	var isAutomated, isOfficial bool
40
+	var hasStarFilter = 0
41
+	if searchFilters.Include("is-automated") {
42
+		if searchFilters.UniqueExactMatch("is-automated", "true") {
43
+			isAutomated = true
44
+		} else if !searchFilters.UniqueExactMatch("is-automated", "false") {
45
+			return nil, fmt.Errorf("Invalid filter 'is-automated=%s'", searchFilters.Get("is-automated"))
46
+		}
47
+	}
48
+	if searchFilters.Include("is-official") {
49
+		if searchFilters.UniqueExactMatch("is-official", "true") {
50
+			isOfficial = true
51
+		} else if !searchFilters.UniqueExactMatch("is-official", "false") {
52
+			return nil, fmt.Errorf("Invalid filter 'is-official=%s'", searchFilters.Get("is-official"))
53
+		}
54
+	}
55
+	if searchFilters.Include("stars") {
56
+		hasStars := searchFilters.Get("stars")
57
+		for _, hasStar := range hasStars {
58
+			iHasStar, err := strconv.Atoi(hasStar)
59
+			if err != nil {
60
+				return nil, fmt.Errorf("Invalid filter 'stars=%s'", hasStar)
61
+			}
62
+			if iHasStar > hasStarFilter {
63
+				hasStarFilter = iHasStar
64
+			}
65
+		}
66
+	}
67
+
68
+	filteredResults := []registrytypes.SearchResult{}
69
+	for _, result := range unfilteredResult.Results {
70
+		if searchFilters.Include("is-automated") {
71
+			if isAutomated != result.IsAutomated {
72
+				continue
73
+			}
74
+		}
75
+		if searchFilters.Include("is-official") {
76
+			if isOfficial != result.IsOfficial {
77
+				continue
78
+			}
79
+		}
80
+		if searchFilters.Include("stars") {
81
+			if result.StarCount < hasStarFilter {
82
+				continue
83
+			}
84
+		}
85
+		filteredResults = append(filteredResults, result)
86
+	}
87
+
88
+	return &registrytypes.SearchResults{
89
+		Query:      unfilteredResult.Query,
90
+		NumResults: len(filteredResults),
91
+		Results:    filteredResults,
92
+	}, nil
93
+}
0 94
new file mode 100644
... ...
@@ -0,0 +1,357 @@
0
+package daemon
1
+
2
+import (
3
+	"fmt"
4
+	"strings"
5
+	"testing"
6
+
7
+	"golang.org/x/net/context"
8
+
9
+	"github.com/docker/docker/registry"
10
+	"github.com/docker/engine-api/types"
11
+	registrytypes "github.com/docker/engine-api/types/registry"
12
+)
13
+
14
+type FakeService struct {
15
+	registry.DefaultService
16
+
17
+	shouldReturnError bool
18
+
19
+	term    string
20
+	results []registrytypes.SearchResult
21
+}
22
+
23
+func (s *FakeService) Search(ctx context.Context, term string, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) {
24
+	if s.shouldReturnError {
25
+		return nil, fmt.Errorf("Search unknown error")
26
+	}
27
+	return &registrytypes.SearchResults{
28
+		Query:      s.term,
29
+		NumResults: len(s.results),
30
+		Results:    s.results,
31
+	}, nil
32
+}
33
+
34
+func TestSearchRegistryForImagesErrors(t *testing.T) {
35
+	errorCases := []struct {
36
+		filtersArgs       string
37
+		shouldReturnError bool
38
+		expectedError     string
39
+	}{
40
+		{
41
+			expectedError:     "Search unknown error",
42
+			shouldReturnError: true,
43
+		},
44
+		{
45
+			filtersArgs:   "invalid json",
46
+			expectedError: "invalid character 'i' looking for beginning of value",
47
+		},
48
+		{
49
+			filtersArgs:   `{"type":{"custom":true}}`,
50
+			expectedError: "Invalid filter 'type'",
51
+		},
52
+		{
53
+			filtersArgs:   `{"is-automated":{"invalid":true}}`,
54
+			expectedError: "Invalid filter 'is-automated=[invalid]'",
55
+		},
56
+		{
57
+			filtersArgs:   `{"is-automated":{"true":true,"false":true}}`,
58
+			expectedError: "Invalid filter 'is-automated",
59
+		},
60
+		{
61
+			filtersArgs:   `{"is-official":{"invalid":true}}`,
62
+			expectedError: "Invalid filter 'is-official=[invalid]'",
63
+		},
64
+		{
65
+			filtersArgs:   `{"is-official":{"true":true,"false":true}}`,
66
+			expectedError: "Invalid filter 'is-official",
67
+		},
68
+		{
69
+			filtersArgs:   `{"stars":{"invalid":true}}`,
70
+			expectedError: "Invalid filter 'stars=invalid'",
71
+		},
72
+		{
73
+			filtersArgs:   `{"stars":{"1":true,"invalid":true}}`,
74
+			expectedError: "Invalid filter 'stars=invalid'",
75
+		},
76
+	}
77
+	for index, e := range errorCases {
78
+		daemon := &Daemon{
79
+			RegistryService: &FakeService{
80
+				shouldReturnError: e.shouldReturnError,
81
+			},
82
+		}
83
+		_, err := daemon.SearchRegistryForImages(context.Background(), e.filtersArgs, "term", nil, map[string][]string{})
84
+		if err == nil {
85
+			t.Errorf("%d: expected an error, got nothing", index)
86
+		}
87
+		if !strings.Contains(err.Error(), e.expectedError) {
88
+			t.Errorf("%d: expected error to contain %s, got %s", index, e.expectedError, err.Error())
89
+		}
90
+	}
91
+}
92
+
93
+func TestSearchRegistryForImages(t *testing.T) {
94
+	term := "term"
95
+	successCases := []struct {
96
+		filtersArgs     string
97
+		registryResults []registrytypes.SearchResult
98
+		expectedResults []registrytypes.SearchResult
99
+	}{
100
+		{
101
+			filtersArgs:     "",
102
+			registryResults: []registrytypes.SearchResult{},
103
+			expectedResults: []registrytypes.SearchResult{},
104
+		},
105
+		{
106
+			filtersArgs: "",
107
+			registryResults: []registrytypes.SearchResult{
108
+				{
109
+					Name:        "name",
110
+					Description: "description",
111
+				},
112
+			},
113
+			expectedResults: []registrytypes.SearchResult{
114
+				{
115
+					Name:        "name",
116
+					Description: "description",
117
+				},
118
+			},
119
+		},
120
+		{
121
+			filtersArgs: `{"is-automated":{"true":true}}`,
122
+			registryResults: []registrytypes.SearchResult{
123
+				{
124
+					Name:        "name",
125
+					Description: "description",
126
+				},
127
+			},
128
+			expectedResults: []registrytypes.SearchResult{},
129
+		},
130
+		{
131
+			filtersArgs: `{"is-automated":{"true":true}}`,
132
+			registryResults: []registrytypes.SearchResult{
133
+				{
134
+					Name:        "name",
135
+					Description: "description",
136
+					IsAutomated: true,
137
+				},
138
+			},
139
+			expectedResults: []registrytypes.SearchResult{
140
+				{
141
+					Name:        "name",
142
+					Description: "description",
143
+					IsAutomated: true,
144
+				},
145
+			},
146
+		},
147
+		{
148
+			filtersArgs: `{"is-automated":{"false":true}}`,
149
+			registryResults: []registrytypes.SearchResult{
150
+				{
151
+					Name:        "name",
152
+					Description: "description",
153
+					IsAutomated: true,
154
+				},
155
+			},
156
+			expectedResults: []registrytypes.SearchResult{},
157
+		},
158
+		{
159
+			filtersArgs: `{"is-automated":{"false":true}}`,
160
+			registryResults: []registrytypes.SearchResult{
161
+				{
162
+					Name:        "name",
163
+					Description: "description",
164
+					IsAutomated: false,
165
+				},
166
+			},
167
+			expectedResults: []registrytypes.SearchResult{
168
+				{
169
+					Name:        "name",
170
+					Description: "description",
171
+					IsAutomated: false,
172
+				},
173
+			},
174
+		},
175
+		{
176
+			filtersArgs: `{"is-official":{"true":true}}`,
177
+			registryResults: []registrytypes.SearchResult{
178
+				{
179
+					Name:        "name",
180
+					Description: "description",
181
+				},
182
+			},
183
+			expectedResults: []registrytypes.SearchResult{},
184
+		},
185
+		{
186
+			filtersArgs: `{"is-official":{"true":true}}`,
187
+			registryResults: []registrytypes.SearchResult{
188
+				{
189
+					Name:        "name",
190
+					Description: "description",
191
+					IsOfficial:  true,
192
+				},
193
+			},
194
+			expectedResults: []registrytypes.SearchResult{
195
+				{
196
+					Name:        "name",
197
+					Description: "description",
198
+					IsOfficial:  true,
199
+				},
200
+			},
201
+		},
202
+		{
203
+			filtersArgs: `{"is-official":{"false":true}}`,
204
+			registryResults: []registrytypes.SearchResult{
205
+				{
206
+					Name:        "name",
207
+					Description: "description",
208
+					IsOfficial:  true,
209
+				},
210
+			},
211
+			expectedResults: []registrytypes.SearchResult{},
212
+		},
213
+		{
214
+			filtersArgs: `{"is-official":{"false":true}}`,
215
+			registryResults: []registrytypes.SearchResult{
216
+				{
217
+					Name:        "name",
218
+					Description: "description",
219
+					IsOfficial:  false,
220
+				},
221
+			},
222
+			expectedResults: []registrytypes.SearchResult{
223
+				{
224
+					Name:        "name",
225
+					Description: "description",
226
+					IsOfficial:  false,
227
+				},
228
+			},
229
+		},
230
+		{
231
+			filtersArgs: `{"stars":{"0":true}}`,
232
+			registryResults: []registrytypes.SearchResult{
233
+				{
234
+					Name:        "name",
235
+					Description: "description",
236
+					StarCount:   0,
237
+				},
238
+			},
239
+			expectedResults: []registrytypes.SearchResult{
240
+				{
241
+					Name:        "name",
242
+					Description: "description",
243
+					StarCount:   0,
244
+				},
245
+			},
246
+		},
247
+		{
248
+			filtersArgs: `{"stars":{"1":true}}`,
249
+			registryResults: []registrytypes.SearchResult{
250
+				{
251
+					Name:        "name",
252
+					Description: "description",
253
+					StarCount:   0,
254
+				},
255
+			},
256
+			expectedResults: []registrytypes.SearchResult{},
257
+		},
258
+		{
259
+			filtersArgs: `{"stars":{"1":true}}`,
260
+			registryResults: []registrytypes.SearchResult{
261
+				{
262
+					Name:        "name0",
263
+					Description: "description0",
264
+					StarCount:   0,
265
+				},
266
+				{
267
+					Name:        "name1",
268
+					Description: "description1",
269
+					StarCount:   1,
270
+				},
271
+			},
272
+			expectedResults: []registrytypes.SearchResult{
273
+				{
274
+					Name:        "name1",
275
+					Description: "description1",
276
+					StarCount:   1,
277
+				},
278
+			},
279
+		},
280
+		{
281
+			filtersArgs: `{"stars":{"1":true}, "is-official":{"true":true}, "is-automated":{"true":true}}`,
282
+			registryResults: []registrytypes.SearchResult{
283
+				{
284
+					Name:        "name0",
285
+					Description: "description0",
286
+					StarCount:   0,
287
+					IsOfficial:  true,
288
+					IsAutomated: true,
289
+				},
290
+				{
291
+					Name:        "name1",
292
+					Description: "description1",
293
+					StarCount:   1,
294
+					IsOfficial:  false,
295
+					IsAutomated: true,
296
+				},
297
+				{
298
+					Name:        "name2",
299
+					Description: "description2",
300
+					StarCount:   1,
301
+					IsOfficial:  true,
302
+					IsAutomated: false,
303
+				},
304
+				{
305
+					Name:        "name3",
306
+					Description: "description3",
307
+					StarCount:   2,
308
+					IsOfficial:  true,
309
+					IsAutomated: true,
310
+				},
311
+			},
312
+			expectedResults: []registrytypes.SearchResult{
313
+				{
314
+					Name:        "name3",
315
+					Description: "description3",
316
+					StarCount:   2,
317
+					IsOfficial:  true,
318
+					IsAutomated: true,
319
+				},
320
+			},
321
+		},
322
+	}
323
+	for index, s := range successCases {
324
+		daemon := &Daemon{
325
+			RegistryService: &FakeService{
326
+				term:    term,
327
+				results: s.registryResults,
328
+			},
329
+		}
330
+		results, err := daemon.SearchRegistryForImages(context.Background(), s.filtersArgs, term, nil, map[string][]string{})
331
+		if err != nil {
332
+			t.Errorf("%d: %v", index, err)
333
+		}
334
+		if results.Query != term {
335
+			t.Errorf("%d: expected Query to be %s, got %s", index, term, results.Query)
336
+		}
337
+		if results.NumResults != len(s.expectedResults) {
338
+			t.Errorf("%d: expected NumResults to be %d, got %d", index, len(s.expectedResults), results.NumResults)
339
+		}
340
+		for _, result := range results.Results {
341
+			found := false
342
+			for _, expectedResult := range s.expectedResults {
343
+				if expectedResult.Name == result.Name &&
344
+					expectedResult.Description == result.Description &&
345
+					expectedResult.IsAutomated == result.IsAutomated &&
346
+					expectedResult.IsOfficial == result.IsOfficial &&
347
+					expectedResult.StarCount == result.StarCount {
348
+					found = true
349
+				}
350
+			}
351
+			if !found {
352
+				t.Errorf("%d: expected results %v, got %v", index, s.expectedResults, results.Results)
353
+			}
354
+		}
355
+	}
356
+}
... ...
@@ -27,7 +27,7 @@ type ImagePullConfig struct {
27 27
 	ProgressOutput progress.Output
28 28
 	// RegistryService is the registry service to use for TLS configuration
29 29
 	// and endpoint lookup.
30
-	RegistryService *registry.Service
30
+	RegistryService registry.Service
31 31
 	// ImageEventLogger notifies events for a given image
32 32
 	ImageEventLogger func(id, name, action string)
33 33
 	// MetadataStore is the storage backend for distribution-specific
... ...
@@ -31,7 +31,7 @@ type ImagePushConfig struct {
31 31
 	ProgressOutput progress.Output
32 32
 	// RegistryService is the registry service to use for TLS configuration
33 33
 	// and endpoint lookup.
34
-	RegistryService *registry.Service
34
+	RegistryService registry.Service
35 35
 	// ImageEventLogger notifies events for a given image
36 36
 	ImageEventLogger func(id, name, action string)
37 37
 	// MetadataStore is the storage backend for distribution-specific
... ...
@@ -661,7 +661,7 @@ func TestMirrorEndpointLookup(t *testing.T) {
661 661
 		}
662 662
 		return false
663 663
 	}
664
-	s := Service{config: makeServiceConfig([]string{"my.mirror"}, nil)}
664
+	s := DefaultService{config: makeServiceConfig([]string{"my.mirror"}, nil)}
665 665
 
666 666
 	imageName, err := reference.WithName(IndexName + "/test/image")
667 667
 	if err != nil {
... ...
@@ -7,35 +7,50 @@ import (
7 7
 	"net/url"
8 8
 	"strings"
9 9
 
10
+	"golang.org/x/net/context"
11
+
10 12
 	"github.com/Sirupsen/logrus"
11 13
 	"github.com/docker/docker/reference"
12 14
 	"github.com/docker/engine-api/types"
13 15
 	registrytypes "github.com/docker/engine-api/types/registry"
14 16
 )
15 17
 
16
-// Service is a registry service. It tracks configuration data such as a list
18
+// Service is the interface defining what a registry service should implement.
19
+type Service interface {
20
+	Auth(ctx context.Context, authConfig *types.AuthConfig, userAgent string) (status, token string, err error)
21
+	LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error)
22
+	LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error)
23
+	ResolveRepository(name reference.Named) (*RepositoryInfo, error)
24
+	ResolveIndex(name string) (*registrytypes.IndexInfo, error)
25
+	Search(ctx context.Context, term string, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error)
26
+	ServiceConfig() *registrytypes.ServiceConfig
27
+	TLSConfig(hostname string) (*tls.Config, error)
28
+}
29
+
30
+// DefaultService is a registry service. It tracks configuration data such as a list
17 31
 // of mirrors.
18
-type Service struct {
32
+type DefaultService struct {
19 33
 	config *serviceConfig
20 34
 }
21 35
 
22
-// NewService returns a new instance of Service ready to be
36
+// NewService returns a new instance of DefaultService ready to be
23 37
 // installed into an engine.
24
-func NewService(options ServiceOptions) *Service {
25
-	return &Service{
38
+func NewService(options ServiceOptions) *DefaultService {
39
+	return &DefaultService{
26 40
 		config: newServiceConfig(options),
27 41
 	}
28 42
 }
29 43
 
30 44
 // ServiceConfig returns the public registry service configuration.
31
-func (s *Service) ServiceConfig() *registrytypes.ServiceConfig {
45
+func (s *DefaultService) ServiceConfig() *registrytypes.ServiceConfig {
32 46
 	return &s.config.ServiceConfig
33 47
 }
34 48
 
35 49
 // Auth contacts the public registry with the provided credentials,
36 50
 // and returns OK if authentication was successful.
37 51
 // It can be used to verify the validity of a client's credentials.
38
-func (s *Service) Auth(authConfig *types.AuthConfig, userAgent string) (status, token string, err error) {
52
+func (s *DefaultService) Auth(ctx context.Context, authConfig *types.AuthConfig, userAgent string) (status, token string, err error) {
53
+	// TODO Use ctx when searching for repositories
39 54
 	serverAddress := authConfig.ServerAddress
40 55
 	if serverAddress == "" {
41 56
 		serverAddress = IndexServer
... ...
@@ -93,7 +108,8 @@ func splitReposSearchTerm(reposName string) (string, string) {
93 93
 
94 94
 // Search queries the public registry for images matching the specified
95 95
 // search terms, and returns the results.
96
-func (s *Service) Search(term string, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) {
96
+func (s *DefaultService) Search(ctx context.Context, term string, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) {
97
+	// TODO Use ctx when searching for repositories
97 98
 	if err := validateNoScheme(term); err != nil {
98 99
 		return nil, err
99 100
 	}
... ...
@@ -130,12 +146,12 @@ func (s *Service) Search(term string, authConfig *types.AuthConfig, userAgent st
130 130
 
131 131
 // ResolveRepository splits a repository name into its components
132 132
 // and configuration of the associated registry.
133
-func (s *Service) ResolveRepository(name reference.Named) (*RepositoryInfo, error) {
133
+func (s *DefaultService) ResolveRepository(name reference.Named) (*RepositoryInfo, error) {
134 134
 	return newRepositoryInfo(s.config, name)
135 135
 }
136 136
 
137 137
 // ResolveIndex takes indexName and returns index info
138
-func (s *Service) ResolveIndex(name string) (*registrytypes.IndexInfo, error) {
138
+func (s *DefaultService) ResolveIndex(name string) (*registrytypes.IndexInfo, error) {
139 139
 	return newIndexInfo(s.config, name)
140 140
 }
141 141
 
... ...
@@ -155,25 +171,25 @@ func (e APIEndpoint) ToV1Endpoint(userAgent string, metaHeaders http.Header) (*V
155 155
 }
156 156
 
157 157
 // TLSConfig constructs a client TLS configuration based on server defaults
158
-func (s *Service) TLSConfig(hostname string) (*tls.Config, error) {
158
+func (s *DefaultService) TLSConfig(hostname string) (*tls.Config, error) {
159 159
 	return newTLSConfig(hostname, isSecureIndex(s.config, hostname))
160 160
 }
161 161
 
162
-func (s *Service) tlsConfigForMirror(mirrorURL *url.URL) (*tls.Config, error) {
162
+func (s *DefaultService) tlsConfigForMirror(mirrorURL *url.URL) (*tls.Config, error) {
163 163
 	return s.TLSConfig(mirrorURL.Host)
164 164
 }
165 165
 
166 166
 // LookupPullEndpoints creates a list of endpoints to try to pull from, in order of preference.
167 167
 // It gives preference to v2 endpoints over v1, mirrors over the actual
168 168
 // registry, and HTTPS over plain HTTP.
169
-func (s *Service) LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
169
+func (s *DefaultService) LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
170 170
 	return s.lookupEndpoints(hostname)
171 171
 }
172 172
 
173 173
 // LookupPushEndpoints creates a list of endpoints to try to push to, in order of preference.
174 174
 // It gives preference to v2 endpoints over v1, and HTTPS over plain HTTP.
175 175
 // Mirrors are not included.
176
-func (s *Service) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
176
+func (s *DefaultService) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
177 177
 	allEndpoints, err := s.lookupEndpoints(hostname)
178 178
 	if err == nil {
179 179
 		for _, endpoint := range allEndpoints {
... ...
@@ -185,7 +201,7 @@ func (s *Service) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint,
185 185
 	return endpoints, err
186 186
 }
187 187
 
188
-func (s *Service) lookupEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
188
+func (s *DefaultService) lookupEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
189 189
 	endpoints, err = s.lookupV2Endpoints(hostname)
190 190
 	if err != nil {
191 191
 		return nil, err
... ...
@@ -6,7 +6,7 @@ import (
6 6
 	"github.com/docker/go-connections/tlsconfig"
7 7
 )
8 8
 
9
-func (s *Service) lookupV1Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
9
+func (s *DefaultService) lookupV1Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
10 10
 	var cfg = tlsconfig.ServerDefault
11 11
 	tlsConfig := &cfg
12 12
 	if hostname == DefaultNamespace {
... ...
@@ -7,7 +7,7 @@ import (
7 7
 	"github.com/docker/go-connections/tlsconfig"
8 8
 )
9 9
 
10
-func (s *Service) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
10
+func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
11 11
 	var cfg = tlsconfig.ServerDefault
12 12
 	tlsConfig := &cfg
13 13
 	if hostname == DefaultNamespace || hostname == DefaultV1Registry.Host {