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