proxy/registryoverride/server.go (102 lines of code) (raw):

// Copyright (c) 2016-2019 Uber Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package registryoverride import ( "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "strings" "github.com/go-chi/chi" "github.com/uber/kraken/build-index/tagclient" "github.com/uber/kraken/utils/handler" "github.com/uber/kraken/utils/listener" "github.com/uber/kraken/utils/log" "github.com/uber/kraken/utils/stringset" ) // Server overrides Docker registry endpoints. type Server struct { config Config tagClient tagclient.Client } // NewServer creates a new Server. func NewServer(config Config, tagClient tagclient.Client) *Server { return &Server{config, tagClient} } // Handler returns a handler for s. func (s *Server) Handler() http.Handler { r := chi.NewRouter() r.Get("/v2/_catalog", handler.Wrap(s.catalogHandler)) return r } // ListenAndServe is a blocking call which runs s. func (s *Server) ListenAndServe() error { log.Infof("Starting registry override server on %s", s.config.Listener) return listener.Serve(s.config.Listener, s.Handler()) } type catalogResponse struct { Repositories []string `json:"repositories"` } // catalogHandler handles catalog request. // https://docs.docker.com/registry/spec/api/#pagination for more reference. func (s *Server) catalogHandler(w http.ResponseWriter, r *http.Request) error { limitQ := "n" offsetQ := "last" // Build request for ListWithPagination. var filter tagclient.ListFilter u := r.URL q := u.Query() for k, v := range q { if len(v) != 1 { return handler.Errorf( "invalid query %s:%s", k, v).Status(http.StatusBadRequest) } switch k { case limitQ: limitCount, err := strconv.Atoi(v[0]) if err != nil { return handler.Errorf( "invalid limit %s: %s", v, err).Status(http.StatusBadRequest) } if limitCount == 0 { return handler.Errorf( "invalid limit %d", limitCount).Status(http.StatusBadRequest) } filter.Limit = limitCount case offsetQ: filter.Offset = v[0] default: return handler.Errorf("invalid query %s", k).Status(http.StatusBadRequest) } } // List with pagination. listResp, err := s.tagClient.ListWithPagination("", filter) if err != nil { return handler.Errorf("list: %s", err) } repos := stringset.New() for _, tag := range listResp.Result { parts := strings.Split(tag, ":") if len(parts) != 2 { log.With("tag", tag).Errorf("Invalid tag format, expected repo:tag") continue } repos.Add(parts[0]) } // Build Link for response. offset, err := listResp.GetOffset() if err != nil && err != io.EOF { return handler.Errorf("invalid offset %s", err) } if offset != "" { nextUrl, err := url.Parse(u.String()) if err != nil { return handler.Errorf( "invalid url string: %s", err).Status(http.StatusBadRequest) } val, err := url.ParseQuery(nextUrl.RawQuery) if err != nil { return handler.Errorf( "invalid url string: %s", err).Status(http.StatusBadRequest) } val.Set(offsetQ, offset) nextUrl.RawQuery = val.Encode() // Set header (https://docs.docker.com/registry/spec/api/#pagination), // except the host and scheme. // Link: <<url>?n=2&last=b>; rel="next" w.Header().Set("Link", fmt.Sprintf("%s; rel=\"next\"", nextUrl.String())) } resp := catalogResponse{Repositories: repos.ToSlice()} if err := json.NewEncoder(w).Encode(&resp); err != nil { return handler.Errorf("json encode: %s", err) } return nil }