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