github.com/tonistiigi/docker@v0.10.1-0.20240229224939-974013b0dc6a/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 return httputils.WriteJSON(w, http.StatusOK, imageInspect) 310 } 311 312 func (ir *imageRouter) toImageInspect(img *image.Image) (*types.ImageInspect, error) { 313 var repoTags, repoDigests []string 314 for _, ref := range img.Details.References { 315 switch ref.(type) { 316 case reference.NamedTagged: 317 repoTags = append(repoTags, reference.FamiliarString(ref)) 318 case reference.Canonical: 319 repoDigests = append(repoDigests, reference.FamiliarString(ref)) 320 } 321 } 322 323 comment := img.Comment 324 if len(comment) == 0 && len(img.History) > 0 { 325 comment = img.History[len(img.History)-1].Comment 326 } 327 328 // Make sure we output empty arrays instead of nil. 329 if repoTags == nil { 330 repoTags = []string{} 331 } 332 if repoDigests == nil { 333 repoDigests = []string{} 334 } 335 336 var created string 337 if img.Created != nil { 338 created = img.Created.Format(time.RFC3339Nano) 339 } 340 341 return &types.ImageInspect{ 342 ID: img.ID().String(), 343 RepoTags: repoTags, 344 RepoDigests: repoDigests, 345 Parent: img.Parent.String(), 346 Comment: comment, 347 Created: created, 348 Container: img.Container, //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.45. 349 ContainerConfig: &img.ContainerConfig, //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.45. 350 DockerVersion: img.DockerVersion, 351 Author: img.Author, 352 Config: img.Config, 353 Architecture: img.Architecture, 354 Variant: img.Variant, 355 Os: img.OperatingSystem(), 356 OsVersion: img.OSVersion, 357 Size: img.Details.Size, 358 GraphDriver: types.GraphDriverData{ 359 Name: img.Details.Driver, 360 Data: img.Details.Metadata, 361 }, 362 RootFS: rootFSToAPIType(img.RootFS), 363 Metadata: imagetypes.Metadata{ 364 LastTagTime: img.Details.LastUpdated, 365 }, 366 }, nil 367 } 368 369 func rootFSToAPIType(rootfs *image.RootFS) types.RootFS { 370 var layers []string 371 for _, l := range rootfs.DiffIDs { 372 layers = append(layers, l.String()) 373 } 374 return types.RootFS{ 375 Type: rootfs.Type, 376 Layers: layers, 377 } 378 } 379 380 func (ir *imageRouter) getImagesJSON(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 381 if err := httputils.ParseForm(r); err != nil { 382 return err 383 } 384 385 imageFilters, err := filters.FromJSON(r.Form.Get("filters")) 386 if err != nil { 387 return err 388 } 389 390 version := httputils.VersionFromContext(ctx) 391 if versions.LessThan(version, "1.41") { 392 // NOTE: filter is a shell glob string applied to repository names. 393 filterParam := r.Form.Get("filter") 394 if filterParam != "" { 395 imageFilters.Add("reference", filterParam) 396 } 397 } 398 399 var sharedSize bool 400 if versions.GreaterThanOrEqualTo(version, "1.42") { 401 // NOTE: Support for the "shared-size" parameter was added in API 1.42. 402 sharedSize = httputils.BoolValue(r, "shared-size") 403 } 404 405 images, err := ir.backend.Images(ctx, imagetypes.ListOptions{ 406 All: httputils.BoolValue(r, "all"), 407 Filters: imageFilters, 408 SharedSize: sharedSize, 409 }) 410 if err != nil { 411 return err 412 } 413 414 useNone := versions.LessThan(version, "1.43") 415 withVirtualSize := versions.LessThan(version, "1.44") 416 for _, img := range images { 417 if useNone { 418 if len(img.RepoTags) == 0 && len(img.RepoDigests) == 0 { 419 img.RepoTags = append(img.RepoTags, "<none>:<none>") 420 img.RepoDigests = append(img.RepoDigests, "<none>@<none>") 421 } 422 } else { 423 if img.RepoTags == nil { 424 img.RepoTags = []string{} 425 } 426 if img.RepoDigests == nil { 427 img.RepoDigests = []string{} 428 } 429 } 430 if withVirtualSize { 431 img.VirtualSize = img.Size //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.44. 432 } 433 } 434 435 return httputils.WriteJSON(w, http.StatusOK, images) 436 } 437 438 func (ir *imageRouter) getImagesHistory(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 439 history, err := ir.backend.ImageHistory(ctx, vars["name"]) 440 if err != nil { 441 return err 442 } 443 444 return httputils.WriteJSON(w, http.StatusOK, history) 445 } 446 447 func (ir *imageRouter) postImagesTag(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 448 if err := httputils.ParseForm(r); err != nil { 449 return err 450 } 451 452 ref, err := httputils.RepoTagReference(r.Form.Get("repo"), r.Form.Get("tag")) 453 if ref == nil || err != nil { 454 return errdefs.InvalidParameter(err) 455 } 456 457 refName := reference.FamiliarName(ref) 458 if refName == string(digest.Canonical) { 459 return errdefs.InvalidParameter(errors.New("refusing to create an ambiguous tag using digest algorithm as name")) 460 } 461 462 img, err := ir.backend.GetImage(ctx, vars["name"], backend.GetImageOpts{}) 463 if err != nil { 464 return errdefs.NotFound(err) 465 } 466 467 if err := ir.backend.TagImage(ctx, img.ID(), ref); err != nil { 468 return err 469 } 470 w.WriteHeader(http.StatusCreated) 471 return nil 472 } 473 474 func (ir *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 475 if err := httputils.ParseForm(r); err != nil { 476 return err 477 } 478 479 var limit int 480 if r.Form.Get("limit") != "" { 481 var err error 482 limit, err = strconv.Atoi(r.Form.Get("limit")) 483 if err != nil || limit < 0 { 484 return errdefs.InvalidParameter(errors.Wrap(err, "invalid limit specified")) 485 } 486 } 487 searchFilters, err := filters.FromJSON(r.Form.Get("filters")) 488 if err != nil { 489 return err 490 } 491 492 // For a search it is not an error if no auth was given. Ignore invalid 493 // AuthConfig to increase compatibility with the existing API. 494 authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader)) 495 496 headers := http.Header{} 497 for k, v := range r.Header { 498 k = http.CanonicalHeaderKey(k) 499 if strings.HasPrefix(k, "X-Meta-") { 500 headers[k] = v 501 } 502 } 503 headers.Set("User-Agent", dockerversion.DockerUserAgent(ctx)) 504 res, err := ir.searcher.Search(ctx, searchFilters, r.Form.Get("term"), limit, authConfig, headers) 505 if err != nil { 506 return err 507 } 508 return httputils.WriteJSON(w, http.StatusOK, res) 509 } 510 511 func (ir *imageRouter) postImagesPrune(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 512 if err := httputils.ParseForm(r); err != nil { 513 return err 514 } 515 516 pruneFilters, err := filters.FromJSON(r.Form.Get("filters")) 517 if err != nil { 518 return err 519 } 520 521 pruneReport, err := ir.backend.ImagesPrune(ctx, pruneFilters) 522 if err != nil { 523 return err 524 } 525 return httputils.WriteJSON(w, http.StatusOK, pruneReport) 526 } 527 528 // validateRepoName validates the name of a repository. 529 func validateRepoName(name reference.Named) error { 530 familiarName := reference.FamiliarName(name) 531 if familiarName == api.NoBaseImageSpecifier { 532 return fmt.Errorf("'%s' is a reserved name", familiarName) 533 } 534 return nil 535 }