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 }