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