]>
Commit | Line | Data |
---|---|---|
9b12e4fe JC |
1 | // Copyright 2015 go-dockerclient authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style | |
3 | // license that can be found in the LICENSE file. | |
4 | ||
5 | package docker | |
6 | ||
7 | import ( | |
8 | "bytes" | |
9 | "encoding/base64" | |
10 | "encoding/json" | |
11 | "errors" | |
12 | "fmt" | |
13 | "io" | |
14 | "net/http" | |
15 | "net/url" | |
16 | "os" | |
17 | "time" | |
18 | ) | |
19 | ||
20 | // APIImages represent an image returned in the ListImages call. | |
21 | type APIImages struct { | |
22 | ID string `json:"Id" yaml:"Id"` | |
23 | RepoTags []string `json:"RepoTags,omitempty" yaml:"RepoTags,omitempty"` | |
24 | Created int64 `json:"Created,omitempty" yaml:"Created,omitempty"` | |
25 | Size int64 `json:"Size,omitempty" yaml:"Size,omitempty"` | |
26 | VirtualSize int64 `json:"VirtualSize,omitempty" yaml:"VirtualSize,omitempty"` | |
27 | ParentID string `json:"ParentId,omitempty" yaml:"ParentId,omitempty"` | |
28 | RepoDigests []string `json:"RepoDigests,omitempty" yaml:"RepoDigests,omitempty"` | |
29 | Labels map[string]string `json:"Labels,omitempty" yaml:"Labels,omitempty"` | |
30 | } | |
31 | ||
32 | // RootFS represents the underlying layers used by an image | |
33 | type RootFS struct { | |
34 | Type string `json:"Type,omitempty" yaml:"Type,omitempty"` | |
35 | Layers []string `json:"Layers,omitempty" yaml:"Layers,omitempty"` | |
36 | } | |
37 | ||
38 | // Image is the type representing a docker image and its various properties | |
39 | type Image struct { | |
40 | ID string `json:"Id" yaml:"Id"` | |
41 | RepoTags []string `json:"RepoTags,omitempty" yaml:"RepoTags,omitempty"` | |
42 | Parent string `json:"Parent,omitempty" yaml:"Parent,omitempty"` | |
43 | Comment string `json:"Comment,omitempty" yaml:"Comment,omitempty"` | |
44 | Created time.Time `json:"Created,omitempty" yaml:"Created,omitempty"` | |
45 | Container string `json:"Container,omitempty" yaml:"Container,omitempty"` | |
46 | ContainerConfig Config `json:"ContainerConfig,omitempty" yaml:"ContainerConfig,omitempty"` | |
47 | DockerVersion string `json:"DockerVersion,omitempty" yaml:"DockerVersion,omitempty"` | |
48 | Author string `json:"Author,omitempty" yaml:"Author,omitempty"` | |
49 | Config *Config `json:"Config,omitempty" yaml:"Config,omitempty"` | |
50 | Architecture string `json:"Architecture,omitempty" yaml:"Architecture,omitempty"` | |
51 | Size int64 `json:"Size,omitempty" yaml:"Size,omitempty"` | |
52 | VirtualSize int64 `json:"VirtualSize,omitempty" yaml:"VirtualSize,omitempty"` | |
53 | RepoDigests []string `json:"RepoDigests,omitempty" yaml:"RepoDigests,omitempty"` | |
54 | RootFS *RootFS `json:"RootFS,omitempty" yaml:"RootFS,omitempty"` | |
55 | } | |
56 | ||
57 | // ImagePre012 serves the same purpose as the Image type except that it is for | |
58 | // earlier versions of the Docker API (pre-012 to be specific) | |
59 | type ImagePre012 struct { | |
60 | ID string `json:"id"` | |
61 | Parent string `json:"parent,omitempty"` | |
62 | Comment string `json:"comment,omitempty"` | |
63 | Created time.Time `json:"created"` | |
64 | Container string `json:"container,omitempty"` | |
65 | ContainerConfig Config `json:"container_config,omitempty"` | |
66 | DockerVersion string `json:"docker_version,omitempty"` | |
67 | Author string `json:"author,omitempty"` | |
68 | Config *Config `json:"config,omitempty"` | |
69 | Architecture string `json:"architecture,omitempty"` | |
70 | Size int64 `json:"size,omitempty"` | |
71 | } | |
72 | ||
73 | var ( | |
74 | // ErrNoSuchImage is the error returned when the image does not exist. | |
75 | ErrNoSuchImage = errors.New("no such image") | |
76 | ||
77 | // ErrMissingRepo is the error returned when the remote repository is | |
78 | // missing. | |
79 | ErrMissingRepo = errors.New("missing remote repository e.g. 'github.com/user/repo'") | |
80 | ||
81 | // ErrMissingOutputStream is the error returned when no output stream | |
82 | // is provided to some calls, like BuildImage. | |
83 | ErrMissingOutputStream = errors.New("missing output stream") | |
84 | ||
85 | // ErrMultipleContexts is the error returned when both a ContextDir and | |
86 | // InputStream are provided in BuildImageOptions | |
87 | ErrMultipleContexts = errors.New("image build may not be provided BOTH context dir and input stream") | |
88 | ||
89 | // ErrMustSpecifyNames is the error rreturned when the Names field on | |
90 | // ExportImagesOptions is nil or empty | |
91 | ErrMustSpecifyNames = errors.New("must specify at least one name to export") | |
92 | ) | |
93 | ||
94 | // ListImagesOptions specify parameters to the ListImages function. | |
95 | // | |
96 | // See https://goo.gl/xBe1u3 for more details. | |
97 | type ListImagesOptions struct { | |
98 | All bool | |
99 | Filters map[string][]string | |
100 | Digests bool | |
101 | Filter string | |
102 | } | |
103 | ||
104 | // ListImages returns the list of available images in the server. | |
105 | // | |
106 | // See https://goo.gl/xBe1u3 for more details. | |
107 | func (c *Client) ListImages(opts ListImagesOptions) ([]APIImages, error) { | |
108 | path := "/images/json?" + queryString(opts) | |
109 | resp, err := c.do("GET", path, doOptions{}) | |
110 | if err != nil { | |
111 | return nil, err | |
112 | } | |
113 | defer resp.Body.Close() | |
114 | var images []APIImages | |
115 | if err := json.NewDecoder(resp.Body).Decode(&images); err != nil { | |
116 | return nil, err | |
117 | } | |
118 | return images, nil | |
119 | } | |
120 | ||
121 | // ImageHistory represent a layer in an image's history returned by the | |
122 | // ImageHistory call. | |
123 | type ImageHistory struct { | |
124 | ID string `json:"Id" yaml:"Id"` | |
125 | Tags []string `json:"Tags,omitempty" yaml:"Tags,omitempty"` | |
126 | Created int64 `json:"Created,omitempty" yaml:"Created,omitempty"` | |
127 | CreatedBy string `json:"CreatedBy,omitempty" yaml:"CreatedBy,omitempty"` | |
128 | Size int64 `json:"Size,omitempty" yaml:"Size,omitempty"` | |
129 | } | |
130 | ||
131 | // ImageHistory returns the history of the image by its name or ID. | |
132 | // | |
133 | // See https://goo.gl/8bnTId for more details. | |
134 | func (c *Client) ImageHistory(name string) ([]ImageHistory, error) { | |
135 | resp, err := c.do("GET", "/images/"+name+"/history", doOptions{}) | |
136 | if err != nil { | |
137 | if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { | |
138 | return nil, ErrNoSuchImage | |
139 | } | |
140 | return nil, err | |
141 | } | |
142 | defer resp.Body.Close() | |
143 | var history []ImageHistory | |
144 | if err := json.NewDecoder(resp.Body).Decode(&history); err != nil { | |
145 | return nil, err | |
146 | } | |
147 | return history, nil | |
148 | } | |
149 | ||
150 | // RemoveImage removes an image by its name or ID. | |
151 | // | |
152 | // See https://goo.gl/V3ZWnK for more details. | |
153 | func (c *Client) RemoveImage(name string) error { | |
154 | resp, err := c.do("DELETE", "/images/"+name, doOptions{}) | |
155 | if err != nil { | |
156 | if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { | |
157 | return ErrNoSuchImage | |
158 | } | |
159 | return err | |
160 | } | |
161 | resp.Body.Close() | |
162 | return nil | |
163 | } | |
164 | ||
165 | // RemoveImageOptions present the set of options available for removing an image | |
166 | // from a registry. | |
167 | // | |
168 | // See https://goo.gl/V3ZWnK for more details. | |
169 | type RemoveImageOptions struct { | |
170 | Force bool `qs:"force"` | |
171 | NoPrune bool `qs:"noprune"` | |
172 | } | |
173 | ||
174 | // RemoveImageExtended removes an image by its name or ID. | |
175 | // Extra params can be passed, see RemoveImageOptions | |
176 | // | |
177 | // See https://goo.gl/V3ZWnK for more details. | |
178 | func (c *Client) RemoveImageExtended(name string, opts RemoveImageOptions) error { | |
179 | uri := fmt.Sprintf("/images/%s?%s", name, queryString(&opts)) | |
180 | resp, err := c.do("DELETE", uri, doOptions{}) | |
181 | if err != nil { | |
182 | if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { | |
183 | return ErrNoSuchImage | |
184 | } | |
185 | return err | |
186 | } | |
187 | resp.Body.Close() | |
188 | return nil | |
189 | } | |
190 | ||
191 | // InspectImage returns an image by its name or ID. | |
192 | // | |
193 | // See https://goo.gl/jHPcg6 for more details. | |
194 | func (c *Client) InspectImage(name string) (*Image, error) { | |
195 | resp, err := c.do("GET", "/images/"+name+"/json", doOptions{}) | |
196 | if err != nil { | |
197 | if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { | |
198 | return nil, ErrNoSuchImage | |
199 | } | |
200 | return nil, err | |
201 | } | |
202 | defer resp.Body.Close() | |
203 | ||
204 | var image Image | |
205 | ||
206 | // if the caller elected to skip checking the server's version, assume it's the latest | |
207 | if c.SkipServerVersionCheck || c.expectedAPIVersion.GreaterThanOrEqualTo(apiVersion112) { | |
208 | if err := json.NewDecoder(resp.Body).Decode(&image); err != nil { | |
209 | return nil, err | |
210 | } | |
211 | } else { | |
212 | var imagePre012 ImagePre012 | |
213 | if err := json.NewDecoder(resp.Body).Decode(&imagePre012); err != nil { | |
214 | return nil, err | |
215 | } | |
216 | ||
217 | image.ID = imagePre012.ID | |
218 | image.Parent = imagePre012.Parent | |
219 | image.Comment = imagePre012.Comment | |
220 | image.Created = imagePre012.Created | |
221 | image.Container = imagePre012.Container | |
222 | image.ContainerConfig = imagePre012.ContainerConfig | |
223 | image.DockerVersion = imagePre012.DockerVersion | |
224 | image.Author = imagePre012.Author | |
225 | image.Config = imagePre012.Config | |
226 | image.Architecture = imagePre012.Architecture | |
227 | image.Size = imagePre012.Size | |
228 | } | |
229 | ||
230 | return &image, nil | |
231 | } | |
232 | ||
233 | // PushImageOptions represents options to use in the PushImage method. | |
234 | // | |
235 | // See https://goo.gl/zPtZaT for more details. | |
236 | type PushImageOptions struct { | |
237 | // Name of the image | |
238 | Name string | |
239 | ||
240 | // Tag of the image | |
241 | Tag string | |
242 | ||
243 | // Registry server to push the image | |
244 | Registry string | |
245 | ||
246 | OutputStream io.Writer `qs:"-"` | |
247 | RawJSONStream bool `qs:"-"` | |
248 | InactivityTimeout time.Duration `qs:"-"` | |
249 | } | |
250 | ||
251 | // PushImage pushes an image to a remote registry, logging progress to w. | |
252 | // | |
253 | // An empty instance of AuthConfiguration may be used for unauthenticated | |
254 | // pushes. | |
255 | // | |
256 | // See https://goo.gl/zPtZaT for more details. | |
257 | func (c *Client) PushImage(opts PushImageOptions, auth AuthConfiguration) error { | |
258 | if opts.Name == "" { | |
259 | return ErrNoSuchImage | |
260 | } | |
261 | headers, err := headersWithAuth(auth) | |
262 | if err != nil { | |
263 | return err | |
264 | } | |
265 | name := opts.Name | |
266 | opts.Name = "" | |
267 | path := "/images/" + name + "/push?" + queryString(&opts) | |
268 | return c.stream("POST", path, streamOptions{ | |
269 | setRawTerminal: true, | |
270 | rawJSONStream: opts.RawJSONStream, | |
271 | headers: headers, | |
272 | stdout: opts.OutputStream, | |
273 | inactivityTimeout: opts.InactivityTimeout, | |
274 | }) | |
275 | } | |
276 | ||
277 | // PullImageOptions present the set of options available for pulling an image | |
278 | // from a registry. | |
279 | // | |
280 | // See https://goo.gl/iJkZjD for more details. | |
281 | type PullImageOptions struct { | |
282 | Repository string `qs:"fromImage"` | |
283 | Registry string | |
284 | Tag string | |
285 | ||
286 | OutputStream io.Writer `qs:"-"` | |
287 | RawJSONStream bool `qs:"-"` | |
288 | InactivityTimeout time.Duration `qs:"-"` | |
289 | } | |
290 | ||
291 | // PullImage pulls an image from a remote registry, logging progress to | |
292 | // opts.OutputStream. | |
293 | // | |
294 | // See https://goo.gl/iJkZjD for more details. | |
295 | func (c *Client) PullImage(opts PullImageOptions, auth AuthConfiguration) error { | |
296 | if opts.Repository == "" { | |
297 | return ErrNoSuchImage | |
298 | } | |
299 | ||
300 | headers, err := headersWithAuth(auth) | |
301 | if err != nil { | |
302 | return err | |
303 | } | |
304 | return c.createImage(queryString(&opts), headers, nil, opts.OutputStream, opts.RawJSONStream, opts.InactivityTimeout) | |
305 | } | |
306 | ||
307 | func (c *Client) createImage(qs string, headers map[string]string, in io.Reader, w io.Writer, rawJSONStream bool, timeout time.Duration) error { | |
308 | path := "/images/create?" + qs | |
309 | return c.stream("POST", path, streamOptions{ | |
310 | setRawTerminal: true, | |
311 | headers: headers, | |
312 | in: in, | |
313 | stdout: w, | |
314 | rawJSONStream: rawJSONStream, | |
315 | inactivityTimeout: timeout, | |
316 | }) | |
317 | } | |
318 | ||
319 | // LoadImageOptions represents the options for LoadImage Docker API Call | |
320 | // | |
321 | // See https://goo.gl/JyClMX for more details. | |
322 | type LoadImageOptions struct { | |
323 | InputStream io.Reader | |
324 | } | |
325 | ||
326 | // LoadImage imports a tarball docker image | |
327 | // | |
328 | // See https://goo.gl/JyClMX for more details. | |
329 | func (c *Client) LoadImage(opts LoadImageOptions) error { | |
330 | return c.stream("POST", "/images/load", streamOptions{ | |
331 | setRawTerminal: true, | |
332 | in: opts.InputStream, | |
333 | }) | |
334 | } | |
335 | ||
336 | // ExportImageOptions represent the options for ExportImage Docker API call. | |
337 | // | |
338 | // See https://goo.gl/le7vK8 for more details. | |
339 | type ExportImageOptions struct { | |
340 | Name string | |
341 | OutputStream io.Writer | |
342 | InactivityTimeout time.Duration `qs:"-"` | |
343 | } | |
344 | ||
345 | // ExportImage exports an image (as a tar file) into the stream. | |
346 | // | |
347 | // See https://goo.gl/le7vK8 for more details. | |
348 | func (c *Client) ExportImage(opts ExportImageOptions) error { | |
349 | return c.stream("GET", fmt.Sprintf("/images/%s/get", opts.Name), streamOptions{ | |
350 | setRawTerminal: true, | |
351 | stdout: opts.OutputStream, | |
352 | inactivityTimeout: opts.InactivityTimeout, | |
353 | }) | |
354 | } | |
355 | ||
356 | // ExportImagesOptions represent the options for ExportImages Docker API call | |
357 | // | |
358 | // See https://goo.gl/huC7HA for more details. | |
359 | type ExportImagesOptions struct { | |
360 | Names []string | |
361 | OutputStream io.Writer `qs:"-"` | |
362 | InactivityTimeout time.Duration `qs:"-"` | |
363 | } | |
364 | ||
365 | // ExportImages exports one or more images (as a tar file) into the stream | |
366 | // | |
367 | // See https://goo.gl/huC7HA for more details. | |
368 | func (c *Client) ExportImages(opts ExportImagesOptions) error { | |
369 | if opts.Names == nil || len(opts.Names) == 0 { | |
370 | return ErrMustSpecifyNames | |
371 | } | |
372 | return c.stream("GET", "/images/get?"+queryString(&opts), streamOptions{ | |
373 | setRawTerminal: true, | |
374 | stdout: opts.OutputStream, | |
375 | inactivityTimeout: opts.InactivityTimeout, | |
376 | }) | |
377 | } | |
378 | ||
379 | // ImportImageOptions present the set of informations available for importing | |
380 | // an image from a source file or the stdin. | |
381 | // | |
382 | // See https://goo.gl/iJkZjD for more details. | |
383 | type ImportImageOptions struct { | |
384 | Repository string `qs:"repo"` | |
385 | Source string `qs:"fromSrc"` | |
386 | Tag string `qs:"tag"` | |
387 | ||
388 | InputStream io.Reader `qs:"-"` | |
389 | OutputStream io.Writer `qs:"-"` | |
390 | RawJSONStream bool `qs:"-"` | |
391 | InactivityTimeout time.Duration `qs:"-"` | |
392 | } | |
393 | ||
394 | // ImportImage imports an image from a url, a file or stdin | |
395 | // | |
396 | // See https://goo.gl/iJkZjD for more details. | |
397 | func (c *Client) ImportImage(opts ImportImageOptions) error { | |
398 | if opts.Repository == "" { | |
399 | return ErrNoSuchImage | |
400 | } | |
401 | if opts.Source != "-" { | |
402 | opts.InputStream = nil | |
403 | } | |
404 | if opts.Source != "-" && !isURL(opts.Source) { | |
405 | f, err := os.Open(opts.Source) | |
406 | if err != nil { | |
407 | return err | |
408 | } | |
409 | opts.InputStream = f | |
410 | opts.Source = "-" | |
411 | } | |
412 | return c.createImage(queryString(&opts), nil, opts.InputStream, opts.OutputStream, opts.RawJSONStream, opts.InactivityTimeout) | |
413 | } | |
414 | ||
415 | // BuildImageOptions present the set of informations available for building an | |
416 | // image from a tarfile with a Dockerfile in it. | |
417 | // | |
418 | // For more details about the Docker building process, see | |
419 | // http://goo.gl/tlPXPu. | |
420 | type BuildImageOptions struct { | |
421 | Name string `qs:"t"` | |
422 | Dockerfile string `qs:"dockerfile"` | |
423 | NoCache bool `qs:"nocache"` | |
424 | SuppressOutput bool `qs:"q"` | |
425 | Pull bool `qs:"pull"` | |
426 | RmTmpContainer bool `qs:"rm"` | |
427 | ForceRmTmpContainer bool `qs:"forcerm"` | |
428 | Memory int64 `qs:"memory"` | |
429 | Memswap int64 `qs:"memswap"` | |
430 | CPUShares int64 `qs:"cpushares"` | |
431 | CPUQuota int64 `qs:"cpuquota"` | |
432 | CPUPeriod int64 `qs:"cpuperiod"` | |
433 | CPUSetCPUs string `qs:"cpusetcpus"` | |
434 | InputStream io.Reader `qs:"-"` | |
435 | OutputStream io.Writer `qs:"-"` | |
436 | RawJSONStream bool `qs:"-"` | |
437 | Remote string `qs:"remote"` | |
438 | Auth AuthConfiguration `qs:"-"` // for older docker X-Registry-Auth header | |
439 | AuthConfigs AuthConfigurations `qs:"-"` // for newer docker X-Registry-Config header | |
440 | ContextDir string `qs:"-"` | |
441 | Ulimits []ULimit `qs:"-"` | |
442 | BuildArgs []BuildArg `qs:"-"` | |
443 | InactivityTimeout time.Duration `qs:"-"` | |
444 | } | |
445 | ||
446 | // BuildArg represents arguments that can be passed to the image when building | |
447 | // it from a Dockerfile. | |
448 | // | |
449 | // For more details about the Docker building process, see | |
450 | // http://goo.gl/tlPXPu. | |
451 | type BuildArg struct { | |
452 | Name string `json:"Name,omitempty" yaml:"Name,omitempty"` | |
453 | Value string `json:"Value,omitempty" yaml:"Value,omitempty"` | |
454 | } | |
455 | ||
456 | // BuildImage builds an image from a tarball's url or a Dockerfile in the input | |
457 | // stream. | |
458 | // | |
459 | // See https://goo.gl/xySxCe for more details. | |
460 | func (c *Client) BuildImage(opts BuildImageOptions) error { | |
461 | if opts.OutputStream == nil { | |
462 | return ErrMissingOutputStream | |
463 | } | |
464 | headers, err := headersWithAuth(opts.Auth, c.versionedAuthConfigs(opts.AuthConfigs)) | |
465 | if err != nil { | |
466 | return err | |
467 | } | |
468 | ||
469 | if opts.Remote != "" && opts.Name == "" { | |
470 | opts.Name = opts.Remote | |
471 | } | |
472 | if opts.InputStream != nil || opts.ContextDir != "" { | |
473 | headers["Content-Type"] = "application/tar" | |
474 | } else if opts.Remote == "" { | |
475 | return ErrMissingRepo | |
476 | } | |
477 | if opts.ContextDir != "" { | |
478 | if opts.InputStream != nil { | |
479 | return ErrMultipleContexts | |
480 | } | |
481 | var err error | |
482 | if opts.InputStream, err = createTarStream(opts.ContextDir, opts.Dockerfile); err != nil { | |
483 | return err | |
484 | } | |
485 | } | |
486 | ||
487 | qs := queryString(&opts) | |
488 | if len(opts.Ulimits) > 0 { | |
489 | if b, err := json.Marshal(opts.Ulimits); err == nil { | |
490 | item := url.Values(map[string][]string{}) | |
491 | item.Add("ulimits", string(b)) | |
492 | qs = fmt.Sprintf("%s&%s", qs, item.Encode()) | |
493 | } | |
494 | } | |
495 | ||
496 | if len(opts.BuildArgs) > 0 { | |
497 | v := make(map[string]string) | |
498 | for _, arg := range opts.BuildArgs { | |
499 | v[arg.Name] = arg.Value | |
500 | } | |
501 | if b, err := json.Marshal(v); err == nil { | |
502 | item := url.Values(map[string][]string{}) | |
503 | item.Add("buildargs", string(b)) | |
504 | qs = fmt.Sprintf("%s&%s", qs, item.Encode()) | |
505 | } | |
506 | } | |
507 | ||
508 | return c.stream("POST", fmt.Sprintf("/build?%s", qs), streamOptions{ | |
509 | setRawTerminal: true, | |
510 | rawJSONStream: opts.RawJSONStream, | |
511 | headers: headers, | |
512 | in: opts.InputStream, | |
513 | stdout: opts.OutputStream, | |
514 | inactivityTimeout: opts.InactivityTimeout, | |
515 | }) | |
516 | } | |
517 | ||
518 | func (c *Client) versionedAuthConfigs(authConfigs AuthConfigurations) interface{} { | |
519 | if c.serverAPIVersion == nil { | |
520 | c.checkAPIVersion() | |
521 | } | |
522 | if c.serverAPIVersion != nil && c.serverAPIVersion.GreaterThanOrEqualTo(apiVersion119) { | |
523 | return AuthConfigurations119(authConfigs.Configs) | |
524 | } | |
525 | return authConfigs | |
526 | } | |
527 | ||
528 | // TagImageOptions present the set of options to tag an image. | |
529 | // | |
530 | // See https://goo.gl/98ZzkU for more details. | |
531 | type TagImageOptions struct { | |
532 | Repo string | |
533 | Tag string | |
534 | Force bool | |
535 | } | |
536 | ||
537 | // TagImage adds a tag to the image identified by the given name. | |
538 | // | |
539 | // See https://goo.gl/98ZzkU for more details. | |
540 | func (c *Client) TagImage(name string, opts TagImageOptions) error { | |
541 | if name == "" { | |
542 | return ErrNoSuchImage | |
543 | } | |
544 | resp, err := c.do("POST", fmt.Sprintf("/images/"+name+"/tag?%s", | |
545 | queryString(&opts)), doOptions{}) | |
546 | ||
547 | if err != nil { | |
548 | return err | |
549 | } | |
550 | ||
551 | defer resp.Body.Close() | |
552 | ||
553 | if resp.StatusCode == http.StatusNotFound { | |
554 | return ErrNoSuchImage | |
555 | } | |
556 | ||
557 | return err | |
558 | } | |
559 | ||
560 | func isURL(u string) bool { | |
561 | p, err := url.Parse(u) | |
562 | if err != nil { | |
563 | return false | |
564 | } | |
565 | return p.Scheme == "http" || p.Scheme == "https" | |
566 | } | |
567 | ||
568 | func headersWithAuth(auths ...interface{}) (map[string]string, error) { | |
569 | var headers = make(map[string]string) | |
570 | ||
571 | for _, auth := range auths { | |
572 | switch auth.(type) { | |
573 | case AuthConfiguration: | |
574 | var buf bytes.Buffer | |
575 | if err := json.NewEncoder(&buf).Encode(auth); err != nil { | |
576 | return nil, err | |
577 | } | |
578 | headers["X-Registry-Auth"] = base64.URLEncoding.EncodeToString(buf.Bytes()) | |
579 | case AuthConfigurations, AuthConfigurations119: | |
580 | var buf bytes.Buffer | |
581 | if err := json.NewEncoder(&buf).Encode(auth); err != nil { | |
582 | return nil, err | |
583 | } | |
584 | headers["X-Registry-Config"] = base64.URLEncoding.EncodeToString(buf.Bytes()) | |
585 | } | |
586 | } | |
587 | ||
588 | return headers, nil | |
589 | } | |
590 | ||
591 | // APIImageSearch reflect the result of a search on the Docker Hub. | |
592 | // | |
593 | // See https://goo.gl/AYjyrF for more details. | |
594 | type APIImageSearch struct { | |
595 | Description string `json:"description,omitempty" yaml:"description,omitempty"` | |
596 | IsOfficial bool `json:"is_official,omitempty" yaml:"is_official,omitempty"` | |
597 | IsAutomated bool `json:"is_automated,omitempty" yaml:"is_automated,omitempty"` | |
598 | Name string `json:"name,omitempty" yaml:"name,omitempty"` | |
599 | StarCount int `json:"star_count,omitempty" yaml:"star_count,omitempty"` | |
600 | } | |
601 | ||
602 | // SearchImages search the docker hub with a specific given term. | |
603 | // | |
604 | // See https://goo.gl/AYjyrF for more details. | |
605 | func (c *Client) SearchImages(term string) ([]APIImageSearch, error) { | |
606 | resp, err := c.do("GET", "/images/search?term="+term, doOptions{}) | |
607 | if err != nil { | |
608 | return nil, err | |
609 | } | |
610 | defer resp.Body.Close() | |
611 | var searchResult []APIImageSearch | |
612 | if err := json.NewDecoder(resp.Body).Decode(&searchResult); err != nil { | |
613 | return nil, err | |
614 | } | |
615 | return searchResult, nil | |
616 | } | |
617 | ||
618 | // SearchImagesEx search the docker hub with a specific given term and authentication. | |
619 | // | |
620 | // See https://goo.gl/AYjyrF for more details. | |
621 | func (c *Client) SearchImagesEx(term string, auth AuthConfiguration) ([]APIImageSearch, error) { | |
622 | headers, err := headersWithAuth(auth) | |
623 | if err != nil { | |
624 | return nil, err | |
625 | } | |
626 | ||
627 | resp, err := c.do("GET", "/images/search?term="+term, doOptions{ | |
628 | headers: headers, | |
629 | }) | |
630 | if err != nil { | |
631 | return nil, err | |
632 | } | |
633 | ||
634 | defer resp.Body.Close() | |
635 | ||
636 | var searchResult []APIImageSearch | |
637 | if err := json.NewDecoder(resp.Body).Decode(&searchResult); err != nil { | |
638 | return nil, err | |
639 | } | |
640 | ||
641 | return searchResult, nil | |
642 | } |