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  }