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  }