image_routes.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. package image // import "github.com/docker/docker/api/server/router/image"
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "net/url"
  8. "strconv"
  9. "strings"
  10. "time"
  11. "github.com/containerd/containerd/platforms"
  12. "github.com/distribution/reference"
  13. "github.com/docker/docker/api"
  14. "github.com/docker/docker/api/server/httputils"
  15. "github.com/docker/docker/api/types"
  16. "github.com/docker/docker/api/types/filters"
  17. opts "github.com/docker/docker/api/types/image"
  18. "github.com/docker/docker/api/types/registry"
  19. "github.com/docker/docker/api/types/versions"
  20. "github.com/docker/docker/builder/remotecontext"
  21. "github.com/docker/docker/dockerversion"
  22. "github.com/docker/docker/errdefs"
  23. "github.com/docker/docker/image"
  24. "github.com/docker/docker/pkg/ioutils"
  25. "github.com/docker/docker/pkg/progress"
  26. "github.com/docker/docker/pkg/streamformatter"
  27. "github.com/opencontainers/go-digest"
  28. ocispec "github.com/opencontainers/image-spec/specs-go/v1"
  29. "github.com/pkg/errors"
  30. )
  31. // Creates an image from Pull or from Import
  32. func (ir *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
  33. if err := httputils.ParseForm(r); err != nil {
  34. return err
  35. }
  36. var (
  37. img = r.Form.Get("fromImage")
  38. repo = r.Form.Get("repo")
  39. tag = r.Form.Get("tag")
  40. comment = r.Form.Get("message")
  41. progressErr error
  42. output = ioutils.NewWriteFlusher(w)
  43. platform *ocispec.Platform
  44. )
  45. defer output.Close()
  46. w.Header().Set("Content-Type", "application/json")
  47. version := httputils.VersionFromContext(ctx)
  48. if versions.GreaterThanOrEqualTo(version, "1.32") {
  49. if p := r.FormValue("platform"); p != "" {
  50. sp, err := platforms.Parse(p)
  51. if err != nil {
  52. return err
  53. }
  54. platform = &sp
  55. }
  56. }
  57. if img != "" { // pull
  58. metaHeaders := map[string][]string{}
  59. for k, v := range r.Header {
  60. if strings.HasPrefix(k, "X-Meta-") {
  61. metaHeaders[k] = v
  62. }
  63. }
  64. // Special case: "pull -a" may send an image name with a
  65. // trailing :. This is ugly, but let's not break API
  66. // compatibility.
  67. image := strings.TrimSuffix(img, ":")
  68. ref, err := reference.ParseNormalizedNamed(image)
  69. if err != nil {
  70. return errdefs.InvalidParameter(err)
  71. }
  72. // TODO(thaJeztah) this could use a WithTagOrDigest() utility
  73. if tag != "" {
  74. // The "tag" could actually be a digest.
  75. var dgst digest.Digest
  76. dgst, err = digest.Parse(tag)
  77. if err == nil {
  78. ref, err = reference.WithDigest(reference.TrimNamed(ref), dgst)
  79. } else {
  80. ref, err = reference.WithTag(ref, tag)
  81. }
  82. if err != nil {
  83. return errdefs.InvalidParameter(err)
  84. }
  85. }
  86. if err := validateRepoName(ref); err != nil {
  87. return errdefs.Forbidden(err)
  88. }
  89. // For a pull it is not an error if no auth was given. Ignore invalid
  90. // AuthConfig to increase compatibility with the existing API.
  91. authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader))
  92. progressErr = ir.backend.PullImage(ctx, ref, platform, metaHeaders, authConfig, output)
  93. } else { // import
  94. src := r.Form.Get("fromSrc")
  95. tagRef, err := httputils.RepoTagReference(repo, tag)
  96. if err != nil {
  97. return errdefs.InvalidParameter(err)
  98. }
  99. if len(comment) == 0 {
  100. comment = "Imported from " + src
  101. }
  102. var layerReader io.ReadCloser
  103. defer r.Body.Close()
  104. if src == "-" {
  105. layerReader = r.Body
  106. } else {
  107. if len(strings.Split(src, "://")) == 1 {
  108. src = "http://" + src
  109. }
  110. u, err := url.Parse(src)
  111. if err != nil {
  112. return errdefs.InvalidParameter(err)
  113. }
  114. resp, err := remotecontext.GetWithStatusError(u.String())
  115. if err != nil {
  116. return err
  117. }
  118. output.Write(streamformatter.FormatStatus("", "Downloading from %s", u))
  119. progressOutput := streamformatter.NewJSONProgressOutput(output, true)
  120. layerReader = progress.NewProgressReader(resp.Body, progressOutput, resp.ContentLength, "", "Importing")
  121. defer layerReader.Close()
  122. }
  123. var id image.ID
  124. id, progressErr = ir.backend.ImportImage(ctx, tagRef, platform, comment, layerReader, r.Form["changes"])
  125. if progressErr == nil {
  126. output.Write(streamformatter.FormatStatus("", id.String()))
  127. }
  128. }
  129. if progressErr != nil {
  130. if !output.Flushed() {
  131. return progressErr
  132. }
  133. _, _ = output.Write(streamformatter.FormatError(progressErr))
  134. }
  135. return nil
  136. }
  137. func (ir *imageRouter) postImagesPush(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
  138. metaHeaders := map[string][]string{}
  139. for k, v := range r.Header {
  140. if strings.HasPrefix(k, "X-Meta-") {
  141. metaHeaders[k] = v
  142. }
  143. }
  144. if err := httputils.ParseForm(r); err != nil {
  145. return err
  146. }
  147. var authConfig *registry.AuthConfig
  148. if authEncoded := r.Header.Get(registry.AuthHeader); authEncoded != "" {
  149. // the new format is to handle the authConfig as a header. Ignore invalid
  150. // AuthConfig to increase compatibility with the existing API.
  151. authConfig, _ = registry.DecodeAuthConfig(authEncoded)
  152. } else {
  153. // the old format is supported for compatibility if there was no authConfig header
  154. var err error
  155. authConfig, err = registry.DecodeAuthConfigBody(r.Body)
  156. if err != nil {
  157. return errors.Wrap(err, "bad parameters and missing X-Registry-Auth")
  158. }
  159. }
  160. output := ioutils.NewWriteFlusher(w)
  161. defer output.Close()
  162. w.Header().Set("Content-Type", "application/json")
  163. img := vars["name"]
  164. tag := r.Form.Get("tag")
  165. var ref reference.Named
  166. // Tag is empty only in case ImagePushOptions.All is true.
  167. if tag != "" {
  168. r, err := httputils.RepoTagReference(img, tag)
  169. if err != nil {
  170. return errdefs.InvalidParameter(err)
  171. }
  172. ref = r
  173. } else {
  174. r, err := reference.ParseNormalizedNamed(img)
  175. if err != nil {
  176. return errdefs.InvalidParameter(err)
  177. }
  178. ref = r
  179. }
  180. if err := ir.backend.PushImage(ctx, ref, metaHeaders, authConfig, output); err != nil {
  181. if !output.Flushed() {
  182. return err
  183. }
  184. _, _ = output.Write(streamformatter.FormatError(err))
  185. }
  186. return nil
  187. }
  188. func (ir *imageRouter) getImagesGet(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
  189. if err := httputils.ParseForm(r); err != nil {
  190. return err
  191. }
  192. w.Header().Set("Content-Type", "application/x-tar")
  193. output := ioutils.NewWriteFlusher(w)
  194. defer output.Close()
  195. var names []string
  196. if name, ok := vars["name"]; ok {
  197. names = []string{name}
  198. } else {
  199. names = r.Form["names"]
  200. }
  201. if err := ir.backend.ExportImage(ctx, names, output); err != nil {
  202. if !output.Flushed() {
  203. return err
  204. }
  205. _, _ = output.Write(streamformatter.FormatError(err))
  206. }
  207. return nil
  208. }
  209. func (ir *imageRouter) postImagesLoad(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
  210. if err := httputils.ParseForm(r); err != nil {
  211. return err
  212. }
  213. quiet := httputils.BoolValueOrDefault(r, "quiet", true)
  214. w.Header().Set("Content-Type", "application/json")
  215. output := ioutils.NewWriteFlusher(w)
  216. defer output.Close()
  217. if err := ir.backend.LoadImage(ctx, r.Body, output, quiet); err != nil {
  218. _, _ = output.Write(streamformatter.FormatError(err))
  219. }
  220. return nil
  221. }
  222. type missingImageError struct{}
  223. func (missingImageError) Error() string {
  224. return "image name cannot be blank"
  225. }
  226. func (missingImageError) InvalidParameter() {}
  227. func (ir *imageRouter) deleteImages(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
  228. if err := httputils.ParseForm(r); err != nil {
  229. return err
  230. }
  231. name := vars["name"]
  232. if strings.TrimSpace(name) == "" {
  233. return missingImageError{}
  234. }
  235. force := httputils.BoolValue(r, "force")
  236. prune := !httputils.BoolValue(r, "noprune")
  237. list, err := ir.backend.ImageDelete(ctx, name, force, prune)
  238. if err != nil {
  239. return err
  240. }
  241. return httputils.WriteJSON(w, http.StatusOK, list)
  242. }
  243. func (ir *imageRouter) getImagesByName(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
  244. img, err := ir.backend.GetImage(ctx, vars["name"], opts.GetImageOpts{Details: true})
  245. if err != nil {
  246. return err
  247. }
  248. imageInspect, err := ir.toImageInspect(img)
  249. if err != nil {
  250. return err
  251. }
  252. version := httputils.VersionFromContext(ctx)
  253. if versions.LessThan(version, "1.44") {
  254. imageInspect.VirtualSize = imageInspect.Size //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.44.
  255. }
  256. return httputils.WriteJSON(w, http.StatusOK, imageInspect)
  257. }
  258. func (ir *imageRouter) toImageInspect(img *image.Image) (*types.ImageInspect, error) {
  259. var repoTags, repoDigests []string
  260. for _, ref := range img.Details.References {
  261. switch ref.(type) {
  262. case reference.NamedTagged:
  263. repoTags = append(repoTags, reference.FamiliarString(ref))
  264. case reference.Canonical:
  265. repoDigests = append(repoDigests, reference.FamiliarString(ref))
  266. }
  267. }
  268. comment := img.Comment
  269. if len(comment) == 0 && len(img.History) > 0 {
  270. comment = img.History[len(img.History)-1].Comment
  271. }
  272. // Make sure we output empty arrays instead of nil.
  273. if repoTags == nil {
  274. repoTags = []string{}
  275. }
  276. if repoDigests == nil {
  277. repoDigests = []string{}
  278. }
  279. var created string
  280. if img.Created != nil {
  281. created = img.Created.Format(time.RFC3339Nano)
  282. }
  283. return &types.ImageInspect{
  284. ID: img.ID().String(),
  285. RepoTags: repoTags,
  286. RepoDigests: repoDigests,
  287. Parent: img.Parent.String(),
  288. Comment: comment,
  289. Created: created,
  290. Container: img.Container,
  291. ContainerConfig: &img.ContainerConfig,
  292. DockerVersion: img.DockerVersion,
  293. Author: img.Author,
  294. Config: img.Config,
  295. Architecture: img.Architecture,
  296. Variant: img.Variant,
  297. Os: img.OperatingSystem(),
  298. OsVersion: img.OSVersion,
  299. Size: img.Details.Size,
  300. GraphDriver: types.GraphDriverData{
  301. Name: img.Details.Driver,
  302. Data: img.Details.Metadata,
  303. },
  304. RootFS: rootFSToAPIType(img.RootFS),
  305. Metadata: opts.Metadata{
  306. LastTagTime: img.Details.LastUpdated,
  307. },
  308. }, nil
  309. }
  310. func rootFSToAPIType(rootfs *image.RootFS) types.RootFS {
  311. var layers []string
  312. for _, l := range rootfs.DiffIDs {
  313. layers = append(layers, l.String())
  314. }
  315. return types.RootFS{
  316. Type: rootfs.Type,
  317. Layers: layers,
  318. }
  319. }
  320. func (ir *imageRouter) getImagesJSON(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
  321. if err := httputils.ParseForm(r); err != nil {
  322. return err
  323. }
  324. imageFilters, err := filters.FromJSON(r.Form.Get("filters"))
  325. if err != nil {
  326. return err
  327. }
  328. version := httputils.VersionFromContext(ctx)
  329. if versions.LessThan(version, "1.41") {
  330. // NOTE: filter is a shell glob string applied to repository names.
  331. filterParam := r.Form.Get("filter")
  332. if filterParam != "" {
  333. imageFilters.Add("reference", filterParam)
  334. }
  335. }
  336. var sharedSize bool
  337. if versions.GreaterThanOrEqualTo(version, "1.42") {
  338. // NOTE: Support for the "shared-size" parameter was added in API 1.42.
  339. sharedSize = httputils.BoolValue(r, "shared-size")
  340. }
  341. images, err := ir.backend.Images(ctx, types.ImageListOptions{
  342. All: httputils.BoolValue(r, "all"),
  343. Filters: imageFilters,
  344. SharedSize: sharedSize,
  345. })
  346. if err != nil {
  347. return err
  348. }
  349. useNone := versions.LessThan(version, "1.43")
  350. withVirtualSize := versions.LessThan(version, "1.44")
  351. for _, img := range images {
  352. if useNone {
  353. if len(img.RepoTags) == 0 && len(img.RepoDigests) == 0 {
  354. img.RepoTags = append(img.RepoTags, "<none>:<none>")
  355. img.RepoDigests = append(img.RepoDigests, "<none>@<none>")
  356. }
  357. } else {
  358. if img.RepoTags == nil {
  359. img.RepoTags = []string{}
  360. }
  361. if img.RepoDigests == nil {
  362. img.RepoDigests = []string{}
  363. }
  364. }
  365. if withVirtualSize {
  366. img.VirtualSize = img.Size //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.44.
  367. }
  368. }
  369. return httputils.WriteJSON(w, http.StatusOK, images)
  370. }
  371. func (ir *imageRouter) getImagesHistory(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
  372. history, err := ir.backend.ImageHistory(ctx, vars["name"])
  373. if err != nil {
  374. return err
  375. }
  376. return httputils.WriteJSON(w, http.StatusOK, history)
  377. }
  378. func (ir *imageRouter) postImagesTag(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
  379. if err := httputils.ParseForm(r); err != nil {
  380. return err
  381. }
  382. ref, err := httputils.RepoTagReference(r.Form.Get("repo"), r.Form.Get("tag"))
  383. if ref == nil || err != nil {
  384. return errdefs.InvalidParameter(err)
  385. }
  386. refName := reference.FamiliarName(ref)
  387. if refName == string(digest.Canonical) {
  388. return errdefs.InvalidParameter(errors.New("refusing to create an ambiguous tag using digest algorithm as name"))
  389. }
  390. img, err := ir.backend.GetImage(ctx, vars["name"], opts.GetImageOpts{})
  391. if err != nil {
  392. return errdefs.NotFound(err)
  393. }
  394. if err := ir.backend.TagImage(ctx, img.ID(), ref); err != nil {
  395. return err
  396. }
  397. w.WriteHeader(http.StatusCreated)
  398. return nil
  399. }
  400. func (ir *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
  401. if err := httputils.ParseForm(r); err != nil {
  402. return err
  403. }
  404. var limit int
  405. if r.Form.Get("limit") != "" {
  406. var err error
  407. limit, err = strconv.Atoi(r.Form.Get("limit"))
  408. if err != nil || limit < 0 {
  409. return errdefs.InvalidParameter(errors.Wrap(err, "invalid limit specified"))
  410. }
  411. }
  412. searchFilters, err := filters.FromJSON(r.Form.Get("filters"))
  413. if err != nil {
  414. return err
  415. }
  416. // For a search it is not an error if no auth was given. Ignore invalid
  417. // AuthConfig to increase compatibility with the existing API.
  418. authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader))
  419. headers := http.Header{}
  420. for k, v := range r.Header {
  421. k = http.CanonicalHeaderKey(k)
  422. if strings.HasPrefix(k, "X-Meta-") {
  423. headers[k] = v
  424. }
  425. }
  426. headers.Set("User-Agent", dockerversion.DockerUserAgent(ctx))
  427. res, err := ir.searcher.Search(ctx, searchFilters, r.Form.Get("term"), limit, authConfig, headers)
  428. if err != nil {
  429. return err
  430. }
  431. return httputils.WriteJSON(w, http.StatusOK, res)
  432. }
  433. func (ir *imageRouter) postImagesPrune(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
  434. if err := httputils.ParseForm(r); err != nil {
  435. return err
  436. }
  437. pruneFilters, err := filters.FromJSON(r.Form.Get("filters"))
  438. if err != nil {
  439. return err
  440. }
  441. pruneReport, err := ir.backend.ImagesPrune(ctx, pruneFilters)
  442. if err != nil {
  443. return err
  444. }
  445. return httputils.WriteJSON(w, http.StatusOK, pruneReport)
  446. }
  447. // validateRepoName validates the name of a repository.
  448. func validateRepoName(name reference.Named) error {
  449. familiarName := reference.FamiliarName(name)
  450. if familiarName == api.NoBaseImageSpecifier {
  451. return fmt.Errorf("'%s' is a reserved name", familiarName)
  452. }
  453. return nil
  454. }