github.com/uber/kraken@v0.1.4/build-index/tagserver/server.go (about)

     1  // Copyright (c) 2016-2019 Uber Technologies, Inc.
     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  package tagserver
    15  
    16  import (
    17  	"encoding/json"
    18  	"fmt"
    19  	"io"
    20  	"net/http"
    21  	"net/url"
    22  	"path"
    23  	"strconv"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/uber/kraken/build-index/tagclient"
    28  	"github.com/uber/kraken/build-index/tagmodels"
    29  	"github.com/uber/kraken/build-index/tagstore"
    30  	"github.com/uber/kraken/build-index/tagtype"
    31  	"github.com/uber/kraken/core"
    32  	"github.com/uber/kraken/lib/backend"
    33  	"github.com/uber/kraken/lib/backend/backenderrors"
    34  	"github.com/uber/kraken/lib/hostlist"
    35  	"github.com/uber/kraken/lib/middleware"
    36  	"github.com/uber/kraken/lib/persistedretry"
    37  	"github.com/uber/kraken/lib/persistedretry/tagreplication"
    38  	"github.com/uber/kraken/origin/blobclient"
    39  	"github.com/uber/kraken/utils/handler"
    40  	"github.com/uber/kraken/utils/httputil"
    41  	"github.com/uber/kraken/utils/listener"
    42  	"github.com/uber/kraken/utils/log"
    43  
    44  	"github.com/pressly/chi"
    45  	chimiddleware "github.com/pressly/chi/middleware"
    46  	"github.com/uber-go/tally"
    47  )
    48  
    49  // Server provides tag operations for the build-index.
    50  type Server struct {
    51  	config            Config
    52  	stats             tally.Scope
    53  	backends          *backend.Manager
    54  	localOriginDNS    string
    55  	localOriginClient blobclient.ClusterClient
    56  	neighbors         hostlist.List
    57  	store             tagstore.Store
    58  
    59  	// For async new tag replication.
    60  	remotes               tagreplication.Remotes
    61  	tagReplicationManager persistedretry.Manager
    62  	provider              tagclient.Provider
    63  
    64  	// For checking if a tag has all dependent blobs.
    65  	depResolver tagtype.DependencyResolver
    66  }
    67  
    68  // New creates a new Server.
    69  func New(
    70  	config Config,
    71  	stats tally.Scope,
    72  	backends *backend.Manager,
    73  	localOriginDNS string,
    74  	localOriginClient blobclient.ClusterClient,
    75  	neighbors hostlist.List,
    76  	store tagstore.Store,
    77  	remotes tagreplication.Remotes,
    78  	tagReplicationManager persistedretry.Manager,
    79  	provider tagclient.Provider,
    80  	depResolver tagtype.DependencyResolver) *Server {
    81  
    82  	config = config.applyDefaults()
    83  
    84  	stats = stats.Tagged(map[string]string{
    85  		"module": "tagserver",
    86  	})
    87  
    88  	return &Server{
    89  		config:                config,
    90  		stats:                 stats,
    91  		backends:              backends,
    92  		localOriginDNS:        localOriginDNS,
    93  		localOriginClient:     localOriginClient,
    94  		neighbors:             neighbors,
    95  		store:                 store,
    96  		remotes:               remotes,
    97  		tagReplicationManager: tagReplicationManager,
    98  		provider:              provider,
    99  		depResolver:           depResolver,
   100  	}
   101  }
   102  
   103  // Handler returns an http.Handler for s.
   104  func (s *Server) Handler() http.Handler {
   105  	r := chi.NewRouter()
   106  
   107  	r.Use(middleware.StatusCounter(s.stats))
   108  	r.Use(middleware.LatencyTimer(s.stats))
   109  
   110  	r.Get("/health", handler.Wrap(s.healthHandler))
   111  
   112  	r.Put("/tags/{tag}/digest/{digest}", handler.Wrap(s.putTagHandler))
   113  	r.Head("/tags/{tag}", handler.Wrap(s.hasTagHandler))
   114  	r.Get("/tags/{tag}", handler.Wrap(s.getTagHandler))
   115  
   116  	r.Get("/repositories/{repo}/tags", handler.Wrap(s.listRepositoryHandler))
   117  
   118  	r.Get("/list/*", handler.Wrap(s.listHandler))
   119  
   120  	r.Post("/remotes/tags/{tag}", handler.Wrap(s.replicateTagHandler))
   121  
   122  	r.Get("/origin", handler.Wrap(s.getOriginHandler))
   123  
   124  	r.Post(
   125  		"/internal/duplicate/remotes/tags/{tag}/digest/{digest}",
   126  		handler.Wrap(s.duplicateReplicateTagHandler))
   127  
   128  	r.Put(
   129  		"/internal/duplicate/tags/{tag}/digest/{digest}",
   130  		handler.Wrap(s.duplicatePutTagHandler))
   131  
   132  	r.Mount("/debug", chimiddleware.Profiler())
   133  
   134  	return r
   135  }
   136  
   137  // ListenAndServe is a blocking call which runs s.
   138  func (s *Server) ListenAndServe() error {
   139  	log.Infof("Starting tag server on %s", s.config.Listener)
   140  	return listener.Serve(s.config.Listener, s.Handler())
   141  }
   142  
   143  func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) error {
   144  	fmt.Fprintln(w, "OK")
   145  	return nil
   146  }
   147  
   148  func (s *Server) putTagHandler(w http.ResponseWriter, r *http.Request) error {
   149  	tag, err := httputil.ParseParam(r, "tag")
   150  	if err != nil {
   151  		return err
   152  	}
   153  	d, err := httputil.ParseDigest(r, "digest")
   154  	if err != nil {
   155  		return err
   156  	}
   157  	replicate, err := strconv.ParseBool(httputil.GetQueryArg(r, "replicate", "false"))
   158  	if err != nil {
   159  		return handler.Errorf("parse query arg `replicate`: %s", err)
   160  	}
   161  
   162  	deps, err := s.depResolver.Resolve(tag, d)
   163  	if err != nil {
   164  		return fmt.Errorf("resolve dependencies: %s", err)
   165  	}
   166  	if err := s.putTag(tag, d, deps); err != nil {
   167  		return err
   168  	}
   169  
   170  	if replicate {
   171  		if err := s.replicateTag(tag, d, deps); err != nil {
   172  			return err
   173  		}
   174  	}
   175  	w.WriteHeader(http.StatusOK)
   176  	return nil
   177  }
   178  
   179  func (s *Server) duplicatePutTagHandler(w http.ResponseWriter, r *http.Request) error {
   180  	tag, err := httputil.ParseParam(r, "tag")
   181  	if err != nil {
   182  		return err
   183  	}
   184  	d, err := httputil.ParseDigest(r, "digest")
   185  	if err != nil {
   186  		return err
   187  	}
   188  
   189  	var req tagclient.DuplicatePutRequest
   190  	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
   191  		return handler.Errorf("decode body: %s", err)
   192  	}
   193  	delay := req.Delay
   194  
   195  	if err := s.store.Put(tag, d, delay); err != nil {
   196  		return handler.Errorf("storage: %s", err)
   197  	}
   198  
   199  	w.WriteHeader(http.StatusOK)
   200  	return nil
   201  }
   202  
   203  func (s *Server) getTagHandler(w http.ResponseWriter, r *http.Request) error {
   204  	tag, err := httputil.ParseParam(r, "tag")
   205  	if err != nil {
   206  		return err
   207  	}
   208  
   209  	d, err := s.store.Get(tag)
   210  	if err != nil {
   211  		if err == tagstore.ErrTagNotFound {
   212  			return handler.ErrorStatus(http.StatusNotFound)
   213  		}
   214  		return handler.Errorf("storage: %s", err)
   215  	}
   216  
   217  	if _, err := io.WriteString(w, d.String()); err != nil {
   218  		return handler.Errorf("write digest: %s", err)
   219  	}
   220  	return nil
   221  }
   222  
   223  func (s *Server) hasTagHandler(w http.ResponseWriter, r *http.Request) error {
   224  	tag, err := httputil.ParseParam(r, "tag")
   225  	if err != nil {
   226  		return err
   227  	}
   228  
   229  	client, err := s.backends.GetClient(tag)
   230  	if err != nil {
   231  		return handler.Errorf("backend manager: %s", err)
   232  	}
   233  	if _, err := client.Stat(tag, tag); err != nil {
   234  		if err == backenderrors.ErrBlobNotFound {
   235  			return handler.ErrorStatus(http.StatusNotFound)
   236  		}
   237  		return err
   238  	}
   239  	return nil
   240  }
   241  
   242  // listHandler handles list images request. Response model
   243  // tagmodels.ListResponse.
   244  func (s *Server) listHandler(w http.ResponseWriter, r *http.Request) error {
   245  	prefix := r.URL.Path[len("/list/"):]
   246  
   247  	client, err := s.backends.GetClient(prefix)
   248  	if err != nil {
   249  		return handler.Errorf("backend manager: %s", err)
   250  	}
   251  
   252  	opts, err := buildPaginationOptions(r.URL)
   253  	if err != nil {
   254  		return err
   255  	}
   256  
   257  	result, err := client.List(prefix, opts...)
   258  	if err != nil {
   259  		return handler.Errorf("error listing from backend: %s", err)
   260  	}
   261  
   262  	resp, err := buildPaginationResponse(r.URL, result.ContinuationToken,
   263  		result.Names)
   264  	if err != nil {
   265  		return err
   266  	}
   267  	if err := json.NewEncoder(w).Encode(resp); err != nil {
   268  		return handler.Errorf("json encode: %s", err)
   269  	}
   270  	return nil
   271  }
   272  
   273  // listRepositoryHandler handles list images tag request. Response model
   274  // tagmodels.ListResponse.
   275  // TODO(codyg): Remove this.
   276  func (s *Server) listRepositoryHandler(w http.ResponseWriter, r *http.Request) error {
   277  	repo, err := httputil.ParseParam(r, "repo")
   278  	if err != nil {
   279  		return err
   280  	}
   281  
   282  	client, err := s.backends.GetClient(repo)
   283  	if err != nil {
   284  		return handler.Errorf("backend manager: %s", err)
   285  	}
   286  
   287  	opts, err := buildPaginationOptions(r.URL)
   288  	if err != nil {
   289  		return err
   290  	}
   291  
   292  	result, err := client.List(path.Join(repo, "_manifests/tags"), opts...)
   293  	if err != nil {
   294  		return handler.Errorf("error listing from backend: %s", err)
   295  	}
   296  
   297  	var tags []string
   298  	for _, name := range result.Names {
   299  		// Strip repo prefix.
   300  		parts := strings.Split(name, ":")
   301  		if len(parts) != 2 {
   302  			log.With("name", name).Warn("Repo list skipping name, expected repo:tag format")
   303  			continue
   304  		}
   305  		tags = append(tags, parts[1])
   306  	}
   307  
   308  	resp, err := buildPaginationResponse(r.URL, result.ContinuationToken, tags)
   309  	if err != nil {
   310  		return err
   311  	}
   312  	if err := json.NewEncoder(w).Encode(resp); err != nil {
   313  		return handler.Errorf("json encode: %s", err)
   314  	}
   315  	return nil
   316  }
   317  
   318  func (s *Server) replicateTagHandler(w http.ResponseWriter, r *http.Request) error {
   319  	tag, err := httputil.ParseParam(r, "tag")
   320  	if err != nil {
   321  		return err
   322  	}
   323  
   324  	d, err := s.store.Get(tag)
   325  	if err != nil {
   326  		if err == tagstore.ErrTagNotFound {
   327  			return handler.ErrorStatus(http.StatusNotFound)
   328  		}
   329  		return handler.Errorf("storage: %s", err)
   330  	}
   331  	deps, err := s.depResolver.Resolve(tag, d)
   332  	if err != nil {
   333  		return fmt.Errorf("resolve dependencies: %s", err)
   334  	}
   335  	if err := s.replicateTag(tag, d, deps); err != nil {
   336  		return err
   337  	}
   338  	w.WriteHeader(http.StatusOK)
   339  	return nil
   340  }
   341  
   342  func (s *Server) duplicateReplicateTagHandler(w http.ResponseWriter, r *http.Request) error {
   343  	tag, err := httputil.ParseParam(r, "tag")
   344  	if err != nil {
   345  		return err
   346  	}
   347  	d, err := httputil.ParseDigest(r, "digest")
   348  	if err != nil {
   349  		return handler.Errorf("get dependency resolver: %s", err)
   350  	}
   351  	var req tagclient.DuplicateReplicateRequest
   352  	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
   353  		return handler.Errorf("decode body: %s", err)
   354  	}
   355  
   356  	destinations := s.remotes.Match(tag)
   357  
   358  	for _, dest := range destinations {
   359  		task := tagreplication.NewTask(tag, d, req.Dependencies, dest, req.Delay)
   360  		if err := s.tagReplicationManager.Add(task); err != nil {
   361  			return handler.Errorf("add replicate task: %s", err)
   362  		}
   363  	}
   364  
   365  	return nil
   366  }
   367  
   368  func (s *Server) getOriginHandler(w http.ResponseWriter, r *http.Request) error {
   369  	if _, err := io.WriteString(w, s.localOriginDNS); err != nil {
   370  		return handler.Errorf("write local origin dns: %s", err)
   371  	}
   372  	return nil
   373  }
   374  
   375  func (s *Server) putTag(tag string, d core.Digest, deps core.DigestList) error {
   376  	for _, dep := range deps {
   377  		if _, err := s.localOriginClient.Stat(tag, dep); err == blobclient.ErrBlobNotFound {
   378  			return handler.Errorf("cannot upload tag, missing dependency %s", dep)
   379  		} else if err != nil {
   380  			return handler.Errorf("check blob: %s", err)
   381  		}
   382  	}
   383  
   384  	if err := s.store.Put(tag, d, 0); err != nil {
   385  		return handler.Errorf("storage: %s", err)
   386  	}
   387  
   388  	neighbors := s.neighbors.Resolve()
   389  
   390  	var delay time.Duration
   391  	var successes int
   392  	for addr := range neighbors {
   393  		delay += s.config.DuplicatePutStagger
   394  		client := s.provider.Provide(addr)
   395  		if err := client.DuplicatePut(tag, d, delay); err != nil {
   396  			log.Errorf("Error duplicating put task to %s: %s", addr, err)
   397  		} else {
   398  			successes++
   399  		}
   400  	}
   401  	if len(neighbors) != 0 && successes == 0 {
   402  		s.stats.Counter("duplicate_put_failures").Inc(1)
   403  	}
   404  	return nil
   405  }
   406  
   407  func (s *Server) replicateTag(tag string, d core.Digest, deps core.DigestList) error {
   408  	destinations := s.remotes.Match(tag)
   409  	if len(destinations) == 0 {
   410  		return nil
   411  	}
   412  
   413  	for _, dest := range destinations {
   414  		task := tagreplication.NewTask(tag, d, deps, dest, 0)
   415  		if err := s.tagReplicationManager.Add(task); err != nil {
   416  			return handler.Errorf("add replicate task: %s", err)
   417  		}
   418  	}
   419  
   420  	neighbors := s.neighbors.Resolve()
   421  
   422  	var delay time.Duration
   423  	var successes int
   424  	for addr := range neighbors { // Loops in random order.
   425  		delay += s.config.DuplicateReplicateStagger
   426  		client := s.provider.Provide(addr)
   427  		if err := client.DuplicateReplicate(tag, d, deps, delay); err != nil {
   428  			log.Errorf("Error duplicating replicate task to %s: %s", addr, err)
   429  		} else {
   430  			successes++
   431  		}
   432  	}
   433  	if len(neighbors) != 0 && successes == 0 {
   434  		s.stats.Counter("duplicate_replicate_failures").Inc(1)
   435  	}
   436  	return nil
   437  }
   438  
   439  func buildPaginationOptions(u *url.URL) ([]backend.ListOption, error) {
   440  	var opts []backend.ListOption
   441  	q := u.Query()
   442  	for k, v := range q {
   443  		if len(v) != 1 {
   444  			return nil, handler.Errorf(
   445  				"invalid query %s:%s", k, v).Status(http.StatusBadRequest)
   446  		}
   447  		switch k {
   448  		case tagmodels.LimitQ:
   449  			limitCount, err := strconv.Atoi(v[0])
   450  			if err != nil {
   451  				return nil, handler.Errorf(
   452  					"invalid limit %s: %s", v, err).Status(http.StatusBadRequest)
   453  			}
   454  			if limitCount == 0 {
   455  				return nil, handler.Errorf(
   456  					"invalid limit %d", limitCount).Status(http.StatusBadRequest)
   457  			}
   458  			opts = append(opts, backend.ListWithMaxKeys(limitCount))
   459  		case tagmodels.OffsetQ:
   460  			opts = append(opts, backend.ListWithContinuationToken(v[0]))
   461  		default:
   462  			return nil, handler.Errorf(
   463  				"invalid query %s", k).Status(http.StatusBadRequest)
   464  		}
   465  	}
   466  	if len(opts) > 0 {
   467  		// Enable pagination if either or both of the query param exists.
   468  		opts = append(opts, backend.ListWithPagination())
   469  	}
   470  
   471  	return opts, nil
   472  }
   473  
   474  func buildPaginationResponse(u *url.URL, continuationToken string,
   475  	result []string) (*tagmodels.ListResponse, error) {
   476  
   477  	nextUrlString := ""
   478  	if continuationToken != "" {
   479  		// Deep copy url.
   480  		nextUrl, err := url.Parse(u.String())
   481  		if err != nil {
   482  			return nil, handler.Errorf(
   483  				"invalid url string: %s", err).Status(http.StatusBadRequest)
   484  		}
   485  		v := url.Values{}
   486  		if limit := u.Query().Get(tagmodels.LimitQ); limit != "" {
   487  			v.Add(tagmodels.LimitQ, limit)
   488  		}
   489  		// ContinuationToken cannot be empty here.
   490  		v.Add(tagmodels.OffsetQ, continuationToken)
   491  		nextUrl.RawQuery = v.Encode()
   492  		nextUrlString = nextUrl.String()
   493  	}
   494  
   495  	resp := tagmodels.ListResponse{
   496  		Size:   len(result),
   497  		Result: result,
   498  	}
   499  	resp.Links.Next = nextUrlString
   500  	resp.Links.Self = u.String()
   501  
   502  	return &resp, nil
   503  }