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