istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/fakes/imageregistry/main.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package main 16 17 import ( 18 "crypto/tls" 19 "encoding/base64" 20 "encoding/json" 21 "flag" 22 "fmt" 23 "net/http" 24 "regexp" 25 "strings" 26 27 "github.com/google/go-containerregistry/pkg/name" 28 "github.com/google/go-containerregistry/pkg/v1/partial" 29 "github.com/google/go-containerregistry/pkg/v1/remote" 30 31 "istio.io/istio/pkg/log" 32 ) 33 34 var ( 35 port = flag.Int("port", 1338, "port to run registry on") 36 registry = flag.String("registry", "gcr.io", "name of registry to redirect registry request to") 37 regexForManifest = regexp.MustCompile(`(?P<Prefix>/v\d+)?/(?P<ImageName>.+)/manifests/(?P<Tag>[^:]*)$`) 38 regexForLayer = regexp.MustCompile(`/layer/v1/(?P<ImageName>[^:]+):(?P<Tag>[^:]+)`) 39 ) 40 41 const ( 42 User = "user" 43 Passwd = "passwd" 44 ) 45 46 type Handler struct { 47 // Mapping table for support conversion of tag. 48 // The key is combination of the image name and tag with `:` separator, 49 // and the value is the target tag or sha. 50 // For example, 51 // 1) Convert from a tag to a digest 52 // istio-testing/awesomedocker:latest -> sha256:abcedf0123456789 53 // 2) Convert from a tag to another tag 54 // istio-testing/awesomedocker:v1.0.0 -> v1.0.1 55 tagMap map[string]string 56 } 57 58 // Convert tag based on the tag map. 59 // If the given path does not have tagged form or the image name and tag is not the registered one, 60 // just return the path without modification. 61 func (h *Handler) convertTag(path string) string { 62 matches := regexForManifest.FindStringSubmatch(path) 63 if matches == nil { 64 return path 65 } 66 67 prefix := matches[regexForManifest.SubexpIndex("Prefix")] 68 imageName := matches[regexForManifest.SubexpIndex("ImageName")] 69 tag := matches[regexForManifest.SubexpIndex("Tag")] 70 key := imageName + ":" + tag 71 72 log.Infof("key: %v", key) 73 74 if converted, found := h.tagMap[imageName+":"+tag]; found { 75 return prefix + "/" + imageName + "/manifests/" + converted 76 } 77 return path 78 } 79 80 // getFirstLayerURL returns the URL for the first layer of the given image. 81 // `tag` will be converted by using `tagMap` 82 func (h *Handler) getFirstLayerURL(imageName string, tag string) (string, error) { 83 convertedTag := tag 84 if converted, found := h.tagMap[imageName+":"+tag]; found { 85 convertedTag = converted 86 } 87 88 u := fmt.Sprintf("%v/%v:%v", *registry, imageName, convertedTag) 89 ref, err := name.ParseReference(u) 90 if err != nil { 91 return "", fmt.Errorf("could not parse url in image reference: %v", err) 92 } 93 94 t := remote.DefaultTransport.(*http.Transport).Clone() 95 t.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} // nolint: gosec // test only code 96 desc, err := remote.Get(ref, remote.WithTransport(t)) 97 if err != nil { 98 return "", fmt.Errorf("could not get the description: %v", err) 99 } 100 101 manifest, err := partial.Manifest(desc) 102 if err != nil { 103 return "", fmt.Errorf("failed to get manifest: %v", err) 104 } 105 if len(manifest.Layers) != 1 { 106 return "", fmt.Errorf("docker image does not have one layer (got %v)", len(manifest.Layers)) 107 } 108 109 return fmt.Sprintf("https://%v/v2/%v/blobs/%v", *registry, imageName, manifest.Layers[0].Digest.String()), nil 110 } 111 112 func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 113 switch p := r.URL.Path; { 114 case p == "/ready": 115 w.WriteHeader(http.StatusOK) 116 case p == "/admin/v1/tagmap": 117 // convert the requested tag to the other tag or sha of the real registry 118 switch r.Method { 119 case http.MethodPost: 120 m := map[string]string{} 121 err := json.NewDecoder(r.Body).Decode(&m) 122 if err != nil { 123 http.Error(w, err.Error(), http.StatusBadRequest) 124 return 125 } 126 h.tagMap = m 127 w.WriteHeader(http.StatusOK) 128 case http.MethodGet: 129 if jsEncodedMap, err := json.Marshal(h.tagMap); err == nil { 130 w.Header().Set("Content-Type", "application/json") 131 w.WriteHeader(http.StatusOK) 132 fmt.Fprintf(w, "%s", jsEncodedMap) 133 } else { 134 http.Error(w, err.Error(), http.StatusInternalServerError) 135 } 136 default: 137 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 138 } 139 case strings.HasPrefix(p, "/layer/v1/"): 140 // returns the blob URL of the first layer in the given OCI image 141 // URL path would have the form of /layer/v1/<image name>:<tag> 142 matches := regexForLayer.FindStringSubmatch(p) 143 if matches == nil { 144 http.Error(w, fmt.Sprintf("Malformed URL Path: %q", p), http.StatusBadRequest) 145 return 146 } 147 imageName := matches[regexForLayer.SubexpIndex("ImageName")] 148 tag := matches[regexForLayer.SubexpIndex("Tag")] 149 rurl, err := h.getFirstLayerURL(imageName, tag) 150 if err != nil { 151 http.Error(w, err.Error(), http.StatusInternalServerError) 152 return 153 } 154 log.Infof("Get %q, send redirect to %q", r.URL, rurl) 155 http.Redirect(w, r, rurl, http.StatusMovedPermanently) 156 case !strings.Contains(p, "/v2/") || !strings.Contains(p, "/blobs/"): 157 // only requires authentication for getting manifests, not blobs 158 encoded := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%v:%v", User, Passwd))) 159 authHdr := r.Header.Get("Authorization") 160 wantHdr := fmt.Sprintf("Basic %s", encoded) 161 if authHdr != wantHdr { 162 log.Infof("Unauthorized: " + r.URL.Path) 163 log.Infof("Got header %q want header %q", authHdr, wantHdr) 164 w.Header().Set("WWW-Authenticate", "Basic") 165 http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 166 return 167 } 168 fallthrough 169 default: 170 rurl := fmt.Sprintf("https://%v%v", *registry, h.convertTag(r.URL.Path)) 171 log.Infof("Get %q, send redirect to %q", r.URL, rurl) 172 http.Redirect(w, r, rurl, http.StatusMovedPermanently) 173 } 174 } 175 176 func main() { 177 flag.Parse() 178 s := &http.Server{ 179 Addr: fmt.Sprintf(":%d", *port), 180 Handler: &Handler{ 181 tagMap: make(map[string]string), 182 }, 183 } 184 log.Infof("registryredirector server is starting at %d", *port) 185 if err := s.ListenAndServe(); err != nil { 186 log.Error(err) 187 } 188 }