k8s.io/registry.k8s.io@v0.3.1/cmd/archeio/internal/app/handlers.go (about) 1 /* 2 Copyright 2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package app 18 19 import ( 20 "net/http" 21 "path" 22 "regexp" 23 "strings" 24 25 "k8s.io/klog/v2" 26 27 "k8s.io/registry.k8s.io/pkg/net/clientip" 28 "k8s.io/registry.k8s.io/pkg/net/cloudcidrs" 29 ) 30 31 type RegistryConfig struct { 32 UpstreamRegistryEndpoint string 33 UpstreamRegistryPath string 34 InfoURL string 35 PrivacyURL string 36 DefaultAWSBaseURL string 37 } 38 39 // MakeHandler returns the root archeio HTTP handler 40 // 41 // upstream registry should be the url to the primary registry 42 // archeio is fronting. 43 // 44 // Exact behavior should be documented in docs/request-handling.md 45 func MakeHandler(rc RegistryConfig) http.Handler { 46 blobs := newCachedBlobChecker() 47 doV2 := makeV2Handler(rc, blobs) 48 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 49 // only allow GET, HEAD 50 // this is all a client needs to pull images 51 // we do *not* support mutation 52 if r.Method != http.MethodGet && r.Method != http.MethodHead { 53 http.Error(w, "Only GET and HEAD are allowed.", http.StatusMethodNotAllowed) 54 return 55 } 56 // all valid registry requests should be at /v2/ 57 // v1 API is super old and not supported by GCR anymore. 58 path := r.URL.Path 59 switch { 60 case strings.HasPrefix(path, "/v2"): 61 doV2(w, r) 62 case path == "/": 63 http.Redirect(w, r, rc.InfoURL, http.StatusTemporaryRedirect) 64 case strings.HasPrefix(path, "/privacy"): 65 http.Redirect(w, r, rc.PrivacyURL, http.StatusTemporaryRedirect) 66 default: 67 klog.V(2).InfoS("unknown request", "path", path) 68 http.NotFound(w, r) 69 } 70 }) 71 } 72 73 func makeV2Handler(rc RegistryConfig, blobs blobChecker) func(w http.ResponseWriter, r *http.Request) { 74 // matches blob requests, captures the requested blob hash 75 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pull 76 // Blobs are at `/v2/<name>/blobs/<digest>` 77 // Note that ':' cannot be contained in <name> but *must* be contained in <digest> 78 // <digest> also cannot contain `/` so we can use a relatively simple and cheap regex 79 // to match blob requests and capture the digest 80 reBlob := regexp.MustCompile("^/v2/.*/blobs/([^/]+:[a-zA-Z0-9=_-]+)$") 81 // initialize map of clientIP to AWS region 82 regionMapper := cloudcidrs.NewIPMapper() 83 // capture these in a http handler lambda 84 return func(w http.ResponseWriter, r *http.Request) { 85 rPath := r.URL.Path 86 87 // we only care about publicly readable GCR as the backing registry 88 // or publicly readable blob storage 89 // 90 // when the client attempts to probe the API for auth, we always return 91 // 200 OK so it will not attempt to request an auth token 92 // 93 // this makes it easier to redirect to backends with different 94 // repo namespacing without worrying about incorrect token scope 95 // 96 // it turns out publicly readable GCR repos do not actually care about 97 // the presence of a token for any API calls, despite the /v2/ API call 98 // returning 401, prompting token auth 99 if rPath == "/v2/" || rPath == "/v2" { 100 klog.V(2).InfoS("serving 200 OK for /v2/ check", "path", rPath) 101 // NOTE: OCI does not require this, but the docker v2 spec include it, and GCR sets this 102 // Docker distribution v2 clients may fallback to an older version if this is not set. 103 w.Header().Set("Docker-Distribution-Api-Version", "registry/2.0") 104 w.WriteHeader(http.StatusOK) 105 return 106 } 107 // we don't support the non-standard _catalog API 108 // https://github.com/kubernetes/registry.k8s.io/issues/162 109 if rPath == "/v2/_catalog" { 110 http.Error(w, "_catalog is not supported", http.StatusNotFound) 111 return 112 } 113 114 // check if blob request 115 matches := reBlob.FindStringSubmatch(rPath) 116 if len(matches) != 2 { 117 // not a blob request so forward it to the main upstream registry 118 redirectURL := upstreamRedirectURL(rc, rPath) 119 klog.V(2).InfoS("redirecting manifest request to upstream registry", "path", rPath, "redirect", redirectURL) 120 http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect) 121 return 122 } 123 // it is a blob request, grab the hash for later 124 digest := matches[1] 125 126 // for blob requests, check the client IP and determine the best backend 127 clientIP, err := clientip.Get(r) 128 if err != nil { 129 // this should not happen 130 klog.ErrorS(err, "failed to get client IP") 131 http.Error(w, err.Error(), http.StatusBadRequest) 132 return 133 } 134 135 // if client is coming from GCP, stay in GCP 136 ipInfo, ipIsKnown := regionMapper.GetIP(clientIP) 137 if ipIsKnown && ipInfo.Cloud == cloudcidrs.GCP { 138 redirectURL := upstreamRedirectURL(rc, rPath) 139 klog.V(2).InfoS("redirecting GCP blob request to upstream registry", "path", rPath, "redirect", redirectURL) 140 http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect) 141 return 142 } 143 144 // check if blob is available in our AWS layer storage for the region 145 region := "" 146 if ipIsKnown { 147 region = ipInfo.Region 148 } 149 bucketURL := awsRegionToHostURL(region, rc.DefaultAWSBaseURL) 150 // this matches GCR's GCS layout, which we will use for other buckets 151 blobURL := bucketURL + "/containers/images/" + digest 152 if blobs.BlobExists(blobURL) { 153 // blob known to be available in AWS, redirect client there 154 klog.V(2).InfoS("redirecting blob request to AWS", "path", rPath) 155 http.Redirect(w, r, blobURL, http.StatusTemporaryRedirect) 156 return 157 } 158 159 // fall back to redirect to upstream 160 redirectURL := upstreamRedirectURL(rc, rPath) 161 klog.V(2).InfoS("redirecting blob request to upstream registry", "path", rPath, "redirect", redirectURL) 162 http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect) 163 } 164 } 165 166 func upstreamRedirectURL(rc RegistryConfig, originalPath string) string { 167 return rc.UpstreamRegistryEndpoint + path.Join("/v2/", rc.UpstreamRegistryPath, strings.TrimPrefix(originalPath, "/v2")) 168 }