github.com/npaton/distribution@v2.3.1-rc.0+incompatible/registry/handlers/images.go (about) 1 package handlers 2 3 import ( 4 "bytes" 5 "fmt" 6 "net/http" 7 8 "github.com/docker/distribution" 9 ctxu "github.com/docker/distribution/context" 10 "github.com/docker/distribution/digest" 11 "github.com/docker/distribution/manifest/manifestlist" 12 "github.com/docker/distribution/manifest/schema1" 13 "github.com/docker/distribution/manifest/schema2" 14 "github.com/docker/distribution/reference" 15 "github.com/docker/distribution/registry/api/errcode" 16 "github.com/docker/distribution/registry/api/v2" 17 "github.com/gorilla/handlers" 18 ) 19 20 // These constants determine which architecture and OS to choose from a 21 // manifest list when downconverting it to a schema1 manifest. 22 const ( 23 defaultArch = "amd64" 24 defaultOS = "linux" 25 ) 26 27 // imageManifestDispatcher takes the request context and builds the 28 // appropriate handler for handling image manifest requests. 29 func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler { 30 imageManifestHandler := &imageManifestHandler{ 31 Context: ctx, 32 } 33 reference := getReference(ctx) 34 dgst, err := digest.ParseDigest(reference) 35 if err != nil { 36 // We just have a tag 37 imageManifestHandler.Tag = reference 38 } else { 39 imageManifestHandler.Digest = dgst 40 } 41 42 mhandler := handlers.MethodHandler{ 43 "GET": http.HandlerFunc(imageManifestHandler.GetImageManifest), 44 "HEAD": http.HandlerFunc(imageManifestHandler.GetImageManifest), 45 } 46 47 if !ctx.readOnly { 48 mhandler["PUT"] = http.HandlerFunc(imageManifestHandler.PutImageManifest) 49 mhandler["DELETE"] = http.HandlerFunc(imageManifestHandler.DeleteImageManifest) 50 } 51 52 return mhandler 53 } 54 55 // imageManifestHandler handles http operations on image manifests. 56 type imageManifestHandler struct { 57 *Context 58 59 // One of tag or digest gets set, depending on what is present in context. 60 Tag string 61 Digest digest.Digest 62 } 63 64 // GetImageManifest fetches the image manifest from the storage backend, if it exists. 65 func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http.Request) { 66 ctxu.GetLogger(imh).Debug("GetImageManifest") 67 manifests, err := imh.Repository.Manifests(imh) 68 if err != nil { 69 imh.Errors = append(imh.Errors, err) 70 return 71 } 72 73 var manifest distribution.Manifest 74 if imh.Tag != "" { 75 tags := imh.Repository.Tags(imh) 76 desc, err := tags.Get(imh, imh.Tag) 77 if err != nil { 78 imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err)) 79 return 80 } 81 imh.Digest = desc.Digest 82 } 83 84 if etagMatch(r, imh.Digest.String()) { 85 w.WriteHeader(http.StatusNotModified) 86 return 87 } 88 89 manifest, err = manifests.Get(imh, imh.Digest) 90 if err != nil { 91 imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err)) 92 return 93 } 94 95 supportsSchema2 := false 96 supportsManifestList := false 97 if acceptHeaders, ok := r.Header["Accept"]; ok { 98 for _, mediaType := range acceptHeaders { 99 if mediaType == schema2.MediaTypeManifest { 100 supportsSchema2 = true 101 } 102 if mediaType == manifestlist.MediaTypeManifestList { 103 supportsManifestList = true 104 } 105 } 106 } 107 108 schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest) 109 manifestList, isManifestList := manifest.(*manifestlist.DeserializedManifestList) 110 111 // Only rewrite schema2 manifests when they are being fetched by tag. 112 // If they are being fetched by digest, we can't return something not 113 // matching the digest. 114 if imh.Tag != "" && isSchema2 && !supportsSchema2 { 115 // Rewrite manifest in schema1 format 116 ctxu.GetLogger(imh).Infof("rewriting manifest %s in schema1 format to support old client", imh.Digest.String()) 117 118 manifest, err = imh.convertSchema2Manifest(schema2Manifest) 119 if err != nil { 120 return 121 } 122 } else if imh.Tag != "" && isManifestList && !supportsManifestList { 123 // Rewrite manifest in schema1 format 124 ctxu.GetLogger(imh).Infof("rewriting manifest list %s in schema1 format to support old client", imh.Digest.String()) 125 126 // Find the image manifest corresponding to the default 127 // platform 128 var manifestDigest digest.Digest 129 for _, manifestDescriptor := range manifestList.Manifests { 130 if manifestDescriptor.Platform.Architecture == defaultArch && manifestDescriptor.Platform.OS == defaultOS { 131 manifestDigest = manifestDescriptor.Digest 132 break 133 } 134 } 135 136 if manifestDigest == "" { 137 imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown) 138 return 139 } 140 141 manifest, err = manifests.Get(imh, manifestDigest) 142 if err != nil { 143 imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err)) 144 return 145 } 146 147 // If necessary, convert the image manifest 148 if schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest); isSchema2 && !supportsSchema2 { 149 manifest, err = imh.convertSchema2Manifest(schema2Manifest) 150 if err != nil { 151 return 152 } 153 } 154 } 155 156 ct, p, err := manifest.Payload() 157 if err != nil { 158 return 159 } 160 161 w.Header().Set("Content-Type", ct) 162 w.Header().Set("Content-Length", fmt.Sprint(len(p))) 163 w.Header().Set("Docker-Content-Digest", imh.Digest.String()) 164 w.Header().Set("Etag", fmt.Sprintf(`"%s"`, imh.Digest)) 165 w.Write(p) 166 } 167 168 func (imh *imageManifestHandler) convertSchema2Manifest(schema2Manifest *schema2.DeserializedManifest) (distribution.Manifest, error) { 169 targetDescriptor := schema2Manifest.Target() 170 blobs := imh.Repository.Blobs(imh) 171 configJSON, err := blobs.Get(imh, targetDescriptor.Digest) 172 if err != nil { 173 imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err)) 174 return nil, err 175 } 176 177 ref := imh.Repository.Name() 178 179 if imh.Tag != "" { 180 ref, err = reference.WithTag(imh.Repository.Name(), imh.Tag) 181 if err != nil { 182 imh.Errors = append(imh.Errors, v2.ErrorCodeTagInvalid.WithDetail(err)) 183 return nil, err 184 } 185 } 186 187 builder := schema1.NewConfigManifestBuilder(imh.Repository.Blobs(imh), imh.Context.App.trustKey, ref, configJSON) 188 for _, d := range schema2Manifest.References() { 189 if err := builder.AppendReference(d); err != nil { 190 imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err)) 191 return nil, err 192 } 193 } 194 manifest, err := builder.Build(imh) 195 if err != nil { 196 imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err)) 197 return nil, err 198 } 199 imh.Digest = digest.FromBytes(manifest.(*schema1.SignedManifest).Canonical) 200 201 return manifest, nil 202 } 203 204 func etagMatch(r *http.Request, etag string) bool { 205 for _, headerVal := range r.Header["If-None-Match"] { 206 if headerVal == etag || headerVal == fmt.Sprintf(`"%s"`, etag) { // allow quoted or unquoted 207 return true 208 } 209 } 210 return false 211 } 212 213 // PutImageManifest validates and stores an image in the registry. 214 func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http.Request) { 215 ctxu.GetLogger(imh).Debug("PutImageManifest") 216 manifests, err := imh.Repository.Manifests(imh) 217 if err != nil { 218 imh.Errors = append(imh.Errors, err) 219 return 220 } 221 222 var jsonBuf bytes.Buffer 223 if err := copyFullPayload(w, r, &jsonBuf, imh, "image manifest PUT", &imh.Errors); err != nil { 224 // copyFullPayload reports the error if necessary 225 return 226 } 227 228 mediaType := r.Header.Get("Content-Type") 229 manifest, desc, err := distribution.UnmarshalManifest(mediaType, jsonBuf.Bytes()) 230 if err != nil { 231 imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err)) 232 return 233 } 234 235 if imh.Digest != "" { 236 if desc.Digest != imh.Digest { 237 ctxu.GetLogger(imh).Errorf("payload digest does match: %q != %q", desc.Digest, imh.Digest) 238 imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid) 239 return 240 } 241 } else if imh.Tag != "" { 242 imh.Digest = desc.Digest 243 } else { 244 imh.Errors = append(imh.Errors, v2.ErrorCodeTagInvalid.WithDetail("no tag or digest specified")) 245 return 246 } 247 248 _, err = manifests.Put(imh, manifest) 249 if err != nil { 250 // TODO(stevvooe): These error handling switches really need to be 251 // handled by an app global mapper. 252 if err == distribution.ErrUnsupported { 253 imh.Errors = append(imh.Errors, errcode.ErrorCodeUnsupported) 254 return 255 } 256 switch err := err.(type) { 257 case distribution.ErrManifestVerification: 258 for _, verificationError := range err { 259 switch verificationError := verificationError.(type) { 260 case distribution.ErrManifestBlobUnknown: 261 imh.Errors = append(imh.Errors, v2.ErrorCodeManifestBlobUnknown.WithDetail(verificationError.Digest)) 262 case distribution.ErrManifestNameInvalid: 263 imh.Errors = append(imh.Errors, v2.ErrorCodeNameInvalid.WithDetail(err)) 264 case distribution.ErrManifestUnverified: 265 imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnverified) 266 default: 267 if verificationError == digest.ErrDigestInvalidFormat { 268 imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid) 269 } else { 270 imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown, verificationError) 271 } 272 } 273 } 274 default: 275 imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) 276 } 277 278 return 279 } 280 281 // Tag this manifest 282 if imh.Tag != "" { 283 tags := imh.Repository.Tags(imh) 284 err = tags.Tag(imh, imh.Tag, desc) 285 if err != nil { 286 imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) 287 return 288 } 289 290 } 291 292 // Construct a canonical url for the uploaded manifest. 293 ref, err := reference.WithDigest(imh.Repository.Name(), imh.Digest) 294 if err != nil { 295 imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) 296 return 297 } 298 299 location, err := imh.urlBuilder.BuildManifestURL(ref) 300 if err != nil { 301 // NOTE(stevvooe): Given the behavior above, this absurdly unlikely to 302 // happen. We'll log the error here but proceed as if it worked. Worst 303 // case, we set an empty location header. 304 ctxu.GetLogger(imh).Errorf("error building manifest url from digest: %v", err) 305 } 306 307 w.Header().Set("Location", location) 308 w.Header().Set("Docker-Content-Digest", imh.Digest.String()) 309 w.WriteHeader(http.StatusCreated) 310 } 311 312 // DeleteImageManifest removes the manifest with the given digest from the registry. 313 func (imh *imageManifestHandler) DeleteImageManifest(w http.ResponseWriter, r *http.Request) { 314 ctxu.GetLogger(imh).Debug("DeleteImageManifest") 315 316 manifests, err := imh.Repository.Manifests(imh) 317 if err != nil { 318 imh.Errors = append(imh.Errors, err) 319 return 320 } 321 322 err = manifests.Delete(imh, imh.Digest) 323 if err != nil { 324 switch err { 325 case digest.ErrDigestUnsupported: 326 case digest.ErrDigestInvalidFormat: 327 imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid) 328 return 329 case distribution.ErrBlobUnknown: 330 imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown) 331 return 332 case distribution.ErrUnsupported: 333 imh.Errors = append(imh.Errors, errcode.ErrorCodeUnsupported) 334 return 335 default: 336 imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown) 337 return 338 } 339 } 340 341 tagService := imh.Repository.Tags(imh) 342 referencedTags, err := tagService.Lookup(imh, distribution.Descriptor{Digest: imh.Digest}) 343 if err != nil { 344 imh.Errors = append(imh.Errors, err) 345 return 346 } 347 348 for _, tag := range referencedTags { 349 if err := tagService.Untag(imh, tag); err != nil { 350 imh.Errors = append(imh.Errors, err) 351 return 352 } 353 } 354 355 w.WriteHeader(http.StatusAccepted) 356 }