// Copyright © 2024 Ory Corp // SPDX-License-Identifier: Apache-2.0 // Copyright 2013 go-dockerclient authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package docker import ( "bytes" "context" "encoding/base64" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) // APIImages represent an image returned in the ListImages call. type APIImages struct { ID string `json:"Id" yaml:"Id" toml:"Id"` RepoTags []string `json:"RepoTags,omitempty" yaml:"RepoTags,omitempty" toml:"RepoTags,omitempty"` Created int64 `json:"Created,omitempty" yaml:"Created,omitempty" toml:"Created,omitempty"` Size int64 `json:"Size,omitempty" yaml:"Size,omitempty" toml:"Size,omitempty"` VirtualSize int64 `json:"VirtualSize,omitempty" yaml:"VirtualSize,omitempty" toml:"VirtualSize,omitempty"` ParentID string `json:"ParentId,omitempty" yaml:"ParentId,omitempty" toml:"ParentId,omitempty"` RepoDigests []string `json:"RepoDigests,omitempty" yaml:"RepoDigests,omitempty" toml:"RepoDigests,omitempty"` Labels map[string]string `json:"Labels,omitempty" yaml:"Labels,omitempty" toml:"Labels,omitempty"` } // RootFS represents the underlying layers used by an image type RootFS struct { Type string `json:"Type,omitempty" yaml:"Type,omitempty" toml:"Type,omitempty"` Layers []string `json:"Layers,omitempty" yaml:"Layers,omitempty" toml:"Layers,omitempty"` } // Image is the type representing a docker image and its various properties type Image struct { ID string `json:"Id" yaml:"Id" toml:"Id"` RepoTags []string `json:"RepoTags,omitempty" yaml:"RepoTags,omitempty" toml:"RepoTags,omitempty"` Parent string `json:"Parent,omitempty" yaml:"Parent,omitempty" toml:"Parent,omitempty"` Comment string `json:"Comment,omitempty" yaml:"Comment,omitempty" toml:"Comment,omitempty"` Created time.Time `json:"Created,omitempty" yaml:"Created,omitempty" toml:"Created,omitempty"` Container string `json:"Container,omitempty" yaml:"Container,omitempty" toml:"Container,omitempty"` ContainerConfig Config `json:"ContainerConfig,omitempty" yaml:"ContainerConfig,omitempty" toml:"ContainerConfig,omitempty"` DockerVersion string `json:"DockerVersion,omitempty" yaml:"DockerVersion,omitempty" toml:"DockerVersion,omitempty"` Author string `json:"Author,omitempty" yaml:"Author,omitempty" toml:"Author,omitempty"` Config *Config `json:"Config,omitempty" yaml:"Config,omitempty" toml:"Config,omitempty"` Architecture string `json:"Architecture,omitempty" yaml:"Architecture,omitempty"` Size int64 `json:"Size,omitempty" yaml:"Size,omitempty" toml:"Size,omitempty"` VirtualSize int64 `json:"VirtualSize,omitempty" yaml:"VirtualSize,omitempty" toml:"VirtualSize,omitempty"` RepoDigests []string `json:"RepoDigests,omitempty" yaml:"RepoDigests,omitempty" toml:"RepoDigests,omitempty"` RootFS *RootFS `json:"RootFS,omitempty" yaml:"RootFS,omitempty" toml:"RootFS,omitempty"` OS string `json:"Os,omitempty" yaml:"Os,omitempty" toml:"Os,omitempty"` } // ImagePre012 serves the same purpose as the Image type except that it is for // earlier versions of the Docker API (pre-012 to be specific) type ImagePre012 struct { ID string `json:"id"` Parent string `json:"parent,omitempty"` Comment string `json:"comment,omitempty"` Created time.Time `json:"created"` Container string `json:"container,omitempty"` ContainerConfig Config `json:"container_config,omitempty"` DockerVersion string `json:"docker_version,omitempty"` Author string `json:"author,omitempty"` Config *Config `json:"config,omitempty"` Architecture string `json:"architecture,omitempty"` Size int64 `json:"size,omitempty"` } var ( // ErrNoSuchImage is the error returned when the image does not exist. ErrNoSuchImage = errors.New("no such image") // ErrMissingRepo is the error returned when the remote repository is // missing. ErrMissingRepo = errors.New("missing remote repository e.g. 'github.com/user/repo'") // ErrMissingOutputStream is the error returned when no output stream // is provided to some calls, like BuildImage. ErrMissingOutputStream = errors.New("missing output stream") // ErrMultipleContexts is the error returned when both a ContextDir and // InputStream are provided in BuildImageOptions ErrMultipleContexts = errors.New("image build may not be provided BOTH context dir and input stream") // ErrMustSpecifyNames is the error rreturned when the Names field on // ExportImagesOptions is nil or empty ErrMustSpecifyNames = errors.New("must specify at least one name to export") ) // ListImagesOptions specify parameters to the ListImages function. // // See https://goo.gl/BVzauZ for more details. type ListImagesOptions struct { Filters map[string][]string All bool Digests bool Filter string Context context.Context } // ListImages returns the list of available images in the server. // // See https://goo.gl/BVzauZ for more details. func (c *Client) ListImages(opts ListImagesOptions) ([]APIImages, error) { path := "/images/json?" + queryString(opts) resp, err := c.do("GET", path, doOptions{context: opts.Context}) if err != nil { return nil, err } defer resp.Body.Close() var images []APIImages if err := json.NewDecoder(resp.Body).Decode(&images); err != nil { return nil, err } return images, nil } // ImageHistory represent a layer in an image's history returned by the // ImageHistory call. type ImageHistory struct { ID string `json:"Id" yaml:"Id" toml:"Id"` Tags []string `json:"Tags,omitempty" yaml:"Tags,omitempty" toml:"Tags,omitempty"` Created int64 `json:"Created,omitempty" yaml:"Created,omitempty" toml:"Tags,omitempty"` CreatedBy string `json:"CreatedBy,omitempty" yaml:"CreatedBy,omitempty" toml:"CreatedBy,omitempty"` Size int64 `json:"Size,omitempty" yaml:"Size,omitempty" toml:"Size,omitempty"` } // ImageHistory returns the history of the image by its name or ID. // // See https://goo.gl/fYtxQa for more details. func (c *Client) ImageHistory(name string) ([]ImageHistory, error) { resp, err := c.do("GET", "/images/"+name+"/history", doOptions{}) if err != nil { if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { return nil, ErrNoSuchImage } return nil, err } defer resp.Body.Close() var history []ImageHistory if err := json.NewDecoder(resp.Body).Decode(&history); err != nil { return nil, err } return history, nil } // RemoveImage removes an image by its name or ID. // // See https://goo.gl/Vd2Pck for more details. func (c *Client) RemoveImage(name string) error { resp, err := c.do("DELETE", "/images/"+name, doOptions{}) if err != nil { if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { return ErrNoSuchImage } return err } resp.Body.Close() return nil } // RemoveImageOptions present the set of options available for removing an image // from a registry. // // See https://goo.gl/Vd2Pck for more details. type RemoveImageOptions struct { Force bool `qs:"force"` NoPrune bool `qs:"noprune"` Context context.Context } // RemoveImageExtended removes an image by its name or ID. // Extra params can be passed, see RemoveImageOptions // // See https://goo.gl/Vd2Pck for more details. func (c *Client) RemoveImageExtended(name string, opts RemoveImageOptions) error { uri := fmt.Sprintf("/images/%s?%s", name, queryString(&opts)) resp, err := c.do("DELETE", uri, doOptions{context: opts.Context}) if err != nil { if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { return ErrNoSuchImage } return err } resp.Body.Close() return nil } // InspectImage returns an image by its name or ID. // // See https://goo.gl/ncLTG8 for more details. func (c *Client) InspectImage(name string) (*Image, error) { resp, err := c.do("GET", "/images/"+name+"/json", doOptions{}) if err != nil { if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { return nil, ErrNoSuchImage } return nil, err } defer resp.Body.Close() var image Image // if the caller elected to skip checking the server's version, assume it's the latest if c.SkipServerVersionCheck || c.expectedAPIVersion.GreaterThanOrEqualTo(apiVersion112) { if err := json.NewDecoder(resp.Body).Decode(&image); err != nil { return nil, err } } else { var imagePre012 ImagePre012 if err := json.NewDecoder(resp.Body).Decode(&imagePre012); err != nil { return nil, err } image.ID = imagePre012.ID image.Parent = imagePre012.Parent image.Comment = imagePre012.Comment image.Created = imagePre012.Created image.Container = imagePre012.Container image.ContainerConfig = imagePre012.ContainerConfig image.DockerVersion = imagePre012.DockerVersion image.Author = imagePre012.Author image.Config = imagePre012.Config image.Architecture = imagePre012.Architecture image.Size = imagePre012.Size } return &image, nil } // PushImageOptions represents options to use in the PushImage method. // // See https://goo.gl/BZemGg for more details. type PushImageOptions struct { // Name of the image Name string // Tag of the image Tag string // Registry server to push the image Registry string OutputStream io.Writer `qs:"-"` RawJSONStream bool `qs:"-"` InactivityTimeout time.Duration `qs:"-"` Context context.Context } // PushImage pushes an image to a remote registry, logging progress to w. // // An empty instance of AuthConfiguration may be used for unauthenticated // pushes. // // See https://goo.gl/BZemGg for more details. func (c *Client) PushImage(opts PushImageOptions, auth AuthConfiguration) error { if opts.Name == "" { return ErrNoSuchImage } headers, err := headersWithAuth(auth) if err != nil { return err } name := opts.Name opts.Name = "" path := "/images/" + name + "/push?" + queryString(&opts) return c.stream("POST", path, streamOptions{ setRawTerminal: true, rawJSONStream: opts.RawJSONStream, headers: headers, stdout: opts.OutputStream, inactivityTimeout: opts.InactivityTimeout, context: opts.Context, }) } // PullImageOptions present the set of options available for pulling an image // from a registry. // // See https://goo.gl/qkoSsn for more details. type PullImageOptions struct { Repository string `qs:"fromImage"` Tag string Platform string // Only required for Docker Engine 1.9 or 1.10 w/ Remote API < 1.21 // and Docker Engine < 1.9 // This parameter was removed in Docker Engine 1.11 Registry string OutputStream io.Writer `qs:"-"` RawJSONStream bool `qs:"-"` InactivityTimeout time.Duration `qs:"-"` Context context.Context } // PullImage pulls an image from a remote registry, logging progress to // opts.OutputStream. // // See https://goo.gl/qkoSsn for more details. func (c *Client) PullImage(opts PullImageOptions, auth AuthConfiguration) error { if opts.Repository == "" { return ErrNoSuchImage } headers, err := headersWithAuth(auth) if err != nil { return err } if opts.Tag == "" && strings.Contains(opts.Repository, "@") { parts := strings.SplitN(opts.Repository, "@", 2) opts.Repository = parts[0] opts.Tag = parts[1] } return c.createImage(queryString(&opts), headers, nil, opts.OutputStream, opts.RawJSONStream, opts.InactivityTimeout, opts.Context) } func (c *Client) createImage(qs string, headers map[string]string, in io.Reader, w io.Writer, rawJSONStream bool, timeout time.Duration, context context.Context) error { path := "/images/create?" + qs return c.stream("POST", path, streamOptions{ setRawTerminal: true, headers: headers, in: in, stdout: w, rawJSONStream: rawJSONStream, inactivityTimeout: timeout, context: context, }) } // LoadImageOptions represents the options for LoadImage Docker API Call // // See https://goo.gl/rEsBV3 for more details. type LoadImageOptions struct { InputStream io.Reader OutputStream io.Writer Context context.Context } // LoadImage imports a tarball docker image // // See https://goo.gl/rEsBV3 for more details. func (c *Client) LoadImage(opts LoadImageOptions) error { return c.stream("POST", "/images/load", streamOptions{ setRawTerminal: true, in: opts.InputStream, stdout: opts.OutputStream, context: opts.Context, }) } // ExportImageOptions represent the options for ExportImage Docker API call. // // See https://goo.gl/AuySaA for more details. type ExportImageOptions struct { Name string OutputStream io.Writer InactivityTimeout time.Duration Context context.Context } // ExportImage exports an image (as a tar file) into the stream. // // See https://goo.gl/AuySaA for more details. func (c *Client) ExportImage(opts ExportImageOptions) error { return c.stream("GET", fmt.Sprintf("/images/%s/get", opts.Name), streamOptions{ setRawTerminal: true, stdout: opts.OutputStream, inactivityTimeout: opts.InactivityTimeout, context: opts.Context, }) } // ExportImagesOptions represent the options for ExportImages Docker API call // // See https://goo.gl/N9XlDn for more details. type ExportImagesOptions struct { Names []string OutputStream io.Writer `qs:"-"` InactivityTimeout time.Duration `qs:"-"` Context context.Context } // ExportImages exports one or more images (as a tar file) into the stream // // See https://goo.gl/N9XlDn for more details. func (c *Client) ExportImages(opts ExportImagesOptions) error { if opts.Names == nil || len(opts.Names) == 0 { return ErrMustSpecifyNames } return c.stream("GET", "/images/get?"+queryString(&opts), streamOptions{ setRawTerminal: true, stdout: opts.OutputStream, inactivityTimeout: opts.InactivityTimeout, }) } // ImportImageOptions present the set of informations available for importing // an image from a source file or the stdin. // // See https://goo.gl/qkoSsn for more details. type ImportImageOptions struct { Repository string `qs:"repo"` Source string `qs:"fromSrc"` Tag string `qs:"tag"` InputStream io.Reader `qs:"-"` OutputStream io.Writer `qs:"-"` RawJSONStream bool `qs:"-"` InactivityTimeout time.Duration `qs:"-"` Context context.Context } // ImportImage imports an image from a url, a file or stdin // // See https://goo.gl/qkoSsn for more details. func (c *Client) ImportImage(opts ImportImageOptions) error { if opts.Repository == "" { return ErrNoSuchImage } if opts.Source != "-" { opts.InputStream = nil } if opts.Source != "-" && !isURL(opts.Source) { f, err := os.Open(opts.Source) if err != nil { return err } opts.InputStream = f opts.Source = "-" } return c.createImage(queryString(&opts), nil, opts.InputStream, opts.OutputStream, opts.RawJSONStream, opts.InactivityTimeout, opts.Context) } // BuildImageOptions present the set of informations available for building an // image from a tarfile with a Dockerfile in it. // // For more details about the Docker building process, see // https://goo.gl/4nYHwV. type BuildImageOptions struct { Name string `qs:"t"` Dockerfile string `qs:"dockerfile"` NoCache bool `qs:"nocache"` CacheFrom []string `qs:"-"` SuppressOutput bool `qs:"q"` Pull bool `qs:"pull"` RmTmpContainer bool `qs:"rm"` ForceRmTmpContainer bool `qs:"forcerm"` RawJSONStream bool `qs:"-"` Memory int64 `qs:"memory"` Memswap int64 `qs:"memswap"` CPUShares int64 `qs:"cpushares"` CPUQuota int64 `qs:"cpuquota"` CPUPeriod int64 `qs:"cpuperiod"` CPUSetCPUs string `qs:"cpusetcpus"` Labels map[string]string `qs:"labels"` InputStream io.Reader `qs:"-"` OutputStream io.Writer `qs:"-"` ErrorStream io.Writer `qs:"-"` Remote string `qs:"remote"` Auth AuthConfiguration `qs:"-"` // for older docker X-Registry-Auth header AuthConfigs AuthConfigurations `qs:"-"` // for newer docker X-Registry-Config header ContextDir string `qs:"-"` Ulimits []ULimit `qs:"-"` BuildArgs []BuildArg `qs:"-"` NetworkMode string `qs:"networkmode"` InactivityTimeout time.Duration `qs:"-"` CgroupParent string `qs:"cgroupparent"` SecurityOpt []string `qs:"securityopt"` Target string `gs:"target"` Platform string `qs:"platform"` Context context.Context } // BuildArg represents arguments that can be passed to the image when building // it from a Dockerfile. // // For more details about the Docker building process, see // https://goo.gl/4nYHwV. type BuildArg struct { Name string `json:"Name,omitempty" yaml:"Name,omitempty" toml:"Name,omitempty"` Value string `json:"Value,omitempty" yaml:"Value,omitempty" toml:"Value,omitempty"` } // BuildImage builds an image from a tarball's url or a Dockerfile in the input // stream. // // See https://goo.gl/4nYHwV for more details. func (c *Client) BuildImage(opts BuildImageOptions) error { if opts.OutputStream == nil { return ErrMissingOutputStream } headers, err := headersWithAuth(opts.Auth, c.versionedAuthConfigs(opts.AuthConfigs)) if err != nil { return err } if opts.Remote != "" && opts.Name == "" { opts.Name = opts.Remote } if opts.InputStream != nil || opts.ContextDir != "" { headers["Content-Type"] = "application/tar" } else if opts.Remote == "" { return ErrMissingRepo } if opts.ContextDir != "" { if opts.InputStream != nil { return ErrMultipleContexts } var err error if opts.InputStream, err = createTarStream(opts.ContextDir, opts.Dockerfile); err != nil { return err } } qs := queryString(&opts) if c.serverAPIVersion.GreaterThanOrEqualTo(apiVersion125) && len(opts.CacheFrom) > 0 { if b, err := json.Marshal(opts.CacheFrom); err == nil { item := url.Values(map[string][]string{}) item.Add("cachefrom", string(b)) qs = fmt.Sprintf("%s&%s", qs, item.Encode()) } } if len(opts.Ulimits) > 0 { if b, err := json.Marshal(opts.Ulimits); err == nil { item := url.Values(map[string][]string{}) item.Add("ulimits", string(b)) qs = fmt.Sprintf("%s&%s", qs, item.Encode()) } } if len(opts.BuildArgs) > 0 { v := make(map[string]string) for _, arg := range opts.BuildArgs { v[arg.Name] = arg.Value } if b, err := json.Marshal(v); err == nil { item := url.Values(map[string][]string{}) item.Add("buildargs", string(b)) qs = fmt.Sprintf("%s&%s", qs, item.Encode()) } } return c.stream("POST", fmt.Sprintf("/build?%s", qs), streamOptions{ setRawTerminal: true, rawJSONStream: opts.RawJSONStream, headers: headers, in: opts.InputStream, stdout: opts.OutputStream, stderr: opts.ErrorStream, inactivityTimeout: opts.InactivityTimeout, context: opts.Context, }) } func (c *Client) versionedAuthConfigs(authConfigs AuthConfigurations) interface{} { if c.serverAPIVersion == nil { c.checkAPIVersion() } if c.serverAPIVersion != nil && c.serverAPIVersion.GreaterThanOrEqualTo(apiVersion119) { return AuthConfigurations119(authConfigs.Configs) } return authConfigs } // TagImageOptions present the set of options to tag an image. // // See https://goo.gl/prHrvo for more details. type TagImageOptions struct { Repo string Tag string Force bool Context context.Context } // TagImage adds a tag to the image identified by the given name. // // See https://goo.gl/prHrvo for more details. func (c *Client) TagImage(name string, opts TagImageOptions) error { if name == "" { return ErrNoSuchImage } resp, err := c.do("POST", "/images/"+name+"/tag?"+queryString(&opts), doOptions{ context: opts.Context, }) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return ErrNoSuchImage } return err } func isURL(u string) bool { p, err := url.Parse(u) if err != nil { return false } return p.Scheme == "http" || p.Scheme == "https" } func headersWithAuth(auths ...interface{}) (map[string]string, error) { var headers = make(map[string]string) for _, auth := range auths { switch auth.(type) { case AuthConfiguration: var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(auth); err != nil { return nil, err } headers["X-Registry-Auth"] = base64.URLEncoding.EncodeToString(buf.Bytes()) case AuthConfigurations, AuthConfigurations119: var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(auth); err != nil { return nil, err } headers["X-Registry-Config"] = base64.URLEncoding.EncodeToString(buf.Bytes()) } } return headers, nil } // APIImageSearch reflect the result of a search on the Docker Hub. // // See https://goo.gl/KLO9IZ for more details. type APIImageSearch struct { Description string `json:"description,omitempty" yaml:"description,omitempty" toml:"description,omitempty"` IsOfficial bool `json:"is_official,omitempty" yaml:"is_official,omitempty" toml:"is_official,omitempty"` IsAutomated bool `json:"is_automated,omitempty" yaml:"is_automated,omitempty" toml:"is_automated,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty" toml:"name,omitempty"` StarCount int `json:"star_count,omitempty" yaml:"star_count,omitempty" toml:"star_count,omitempty"` } // SearchImages search the docker hub with a specific given term. // // See https://goo.gl/KLO9IZ for more details. func (c *Client) SearchImages(term string) ([]APIImageSearch, error) { resp, err := c.do("GET", "/images/search?term="+term, doOptions{}) if err != nil { return nil, err } defer resp.Body.Close() var searchResult []APIImageSearch if err := json.NewDecoder(resp.Body).Decode(&searchResult); err != nil { return nil, err } return searchResult, nil } // SearchImagesEx search the docker hub with a specific given term and authentication. // // See https://goo.gl/KLO9IZ for more details. func (c *Client) SearchImagesEx(term string, auth AuthConfiguration) ([]APIImageSearch, error) { headers, err := headersWithAuth(auth) if err != nil { return nil, err } resp, err := c.do("GET", "/images/search?term="+term, doOptions{ headers: headers, }) if err != nil { return nil, err } defer resp.Body.Close() var searchResult []APIImageSearch if err := json.NewDecoder(resp.Body).Decode(&searchResult); err != nil { return nil, err } return searchResult, nil } // PruneImagesOptions specify parameters to the PruneImages function. // // See https://goo.gl/qfZlbZ for more details. type PruneImagesOptions struct { Filters map[string][]string Context context.Context } // PruneImagesResults specify results from the PruneImages function. // // See https://goo.gl/qfZlbZ for more details. type PruneImagesResults struct { ImagesDeleted []struct{ Untagged, Deleted string } SpaceReclaimed int64 } // PruneImages deletes images which are unused. // // See https://goo.gl/qfZlbZ for more details. func (c *Client) PruneImages(opts PruneImagesOptions) (*PruneImagesResults, error) { path := "/images/prune?" + queryString(opts) resp, err := c.do("POST", path, doOptions{context: opts.Context}) if err != nil { return nil, err } defer resp.Body.Close() var results PruneImagesResults if err := json.NewDecoder(resp.Body).Decode(&results); err != nil { return nil, err } return &results, nil }