github.com/uber/kraken@v0.1.4/agent/agentserver/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 agentserver
    15  
    16  import (
    17  	"context"
    18  	"encoding/json"
    19  	"fmt"
    20  	"io"
    21  	"net/http"
    22  	_ "net/http/pprof" // Registers /debug/pprof endpoints in http.DefaultServeMux.
    23  	"os"
    24  	"strings"
    25  
    26  	"github.com/uber/kraken/build-index/tagclient"
    27  	"github.com/uber/kraken/core"
    28  	"github.com/uber/kraken/lib/dockerdaemon"
    29  	"github.com/uber/kraken/lib/middleware"
    30  	"github.com/uber/kraken/lib/store"
    31  	"github.com/uber/kraken/lib/torrent/scheduler"
    32  	"github.com/uber/kraken/utils/handler"
    33  	"github.com/uber/kraken/utils/httputil"
    34  
    35  	"github.com/pressly/chi"
    36  	"github.com/uber-go/tally"
    37  )
    38  
    39  // Config defines Server configuration.
    40  type Config struct{}
    41  
    42  // Server defines the agent HTTP server.
    43  type Server struct {
    44  	config    Config
    45  	stats     tally.Scope
    46  	cads      *store.CADownloadStore
    47  	sched     scheduler.ReloadableScheduler
    48  	tags      tagclient.Client
    49  	dockerCli dockerdaemon.DockerClient
    50  }
    51  
    52  // New creates a new Server.
    53  func New(
    54  	config Config,
    55  	stats tally.Scope,
    56  	cads *store.CADownloadStore,
    57  	sched scheduler.ReloadableScheduler,
    58  	tags tagclient.Client,
    59  	dockerCli dockerdaemon.DockerClient) *Server {
    60  
    61  	stats = stats.Tagged(map[string]string{
    62  		"module": "agentserver",
    63  	})
    64  
    65  	return &Server{config, stats, cads, sched, tags, dockerCli}
    66  }
    67  
    68  // Handler returns the HTTP handler.
    69  func (s *Server) Handler() http.Handler {
    70  	r := chi.NewRouter()
    71  
    72  	r.Use(middleware.StatusCounter(s.stats))
    73  	r.Use(middleware.LatencyTimer(s.stats))
    74  
    75  	r.Get("/health", handler.Wrap(s.healthHandler))
    76  
    77  	r.Get("/tags/{tag}", handler.Wrap(s.getTagHandler))
    78  
    79  	r.Get("/namespace/{namespace}/blobs/{digest}", handler.Wrap(s.downloadBlobHandler))
    80  
    81  	r.Delete("/blobs/{digest}", handler.Wrap(s.deleteBlobHandler))
    82  
    83  	// Preheat/preload endpoints.
    84  	r.Get("/preload/tags/{tag}", handler.Wrap(s.preloadTagHandler))
    85  
    86  	// Dangerous endpoint for running experiments.
    87  	r.Patch("/x/config/scheduler", handler.Wrap(s.patchSchedulerConfigHandler))
    88  
    89  	r.Get("/x/blacklist", handler.Wrap(s.getBlacklistHandler))
    90  
    91  	// Serves /debug/pprof endpoints.
    92  	r.Mount("/", http.DefaultServeMux)
    93  
    94  	return r
    95  }
    96  
    97  // getTagHandler proxies get tag requests to the build-index.
    98  func (s *Server) getTagHandler(w http.ResponseWriter, r *http.Request) error {
    99  	tag, err := httputil.ParseParam(r, "tag")
   100  	if err != nil {
   101  		return err
   102  	}
   103  	d, err := s.tags.Get(tag)
   104  	if err != nil {
   105  		if err == tagclient.ErrTagNotFound {
   106  			return handler.ErrorStatus(http.StatusNotFound)
   107  		}
   108  		return handler.Errorf("get tag: %s", err)
   109  	}
   110  	io.WriteString(w, d.String())
   111  	return nil
   112  }
   113  
   114  // downloadBlobHandler downloads a blob through p2p.
   115  func (s *Server) downloadBlobHandler(w http.ResponseWriter, r *http.Request) error {
   116  	namespace, err := httputil.ParseParam(r, "namespace")
   117  	if err != nil {
   118  		return err
   119  	}
   120  	d, err := parseDigest(r)
   121  	if err != nil {
   122  		return err
   123  	}
   124  	f, err := s.cads.Cache().GetFileReader(d.Hex())
   125  	if err != nil {
   126  		if os.IsNotExist(err) || s.cads.InDownloadError(err) {
   127  			if err := s.sched.Download(namespace, d); err != nil {
   128  				if err == scheduler.ErrTorrentNotFound {
   129  					return handler.ErrorStatus(http.StatusNotFound)
   130  				}
   131  				return handler.Errorf("download torrent: %s", err)
   132  			}
   133  			f, err = s.cads.Cache().GetFileReader(d.Hex())
   134  			if err != nil {
   135  				return handler.Errorf("store: %s", err)
   136  			}
   137  		} else {
   138  			return handler.Errorf("store: %s", err)
   139  		}
   140  	}
   141  	if _, err := io.Copy(w, f); err != nil {
   142  		return fmt.Errorf("copy file: %s", err)
   143  	}
   144  	return nil
   145  }
   146  
   147  func (s *Server) deleteBlobHandler(w http.ResponseWriter, r *http.Request) error {
   148  	d, err := parseDigest(r)
   149  	if err != nil {
   150  		return err
   151  	}
   152  	if err := s.sched.RemoveTorrent(d); err != nil {
   153  		return handler.Errorf("remove torrent: %s", err)
   154  	}
   155  	return nil
   156  }
   157  
   158  // preloadTagHandler triggers docker daemon to download specified docker image.
   159  func (s *Server) preloadTagHandler(w http.ResponseWriter, r *http.Request) error {
   160  	tag, err := httputil.ParseParam(r, "tag")
   161  	if err != nil {
   162  		return err
   163  	}
   164  	parts := strings.Split(tag, ":")
   165  	if len(parts) != 2 {
   166  		return handler.Errorf("failed to parse docker image tag")
   167  	}
   168  	repo, tag := parts[0], parts[1]
   169  	if err := s.dockerCli.PullImage(
   170  		context.Background(), repo, tag); err != nil {
   171  
   172  		return handler.Errorf("trigger docker pull: %s", err)
   173  	}
   174  	return nil
   175  }
   176  
   177  func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) error {
   178  	if err := s.sched.Probe(); err != nil {
   179  		return handler.Errorf("probe torrent client: %s", err)
   180  	}
   181  	fmt.Fprintln(w, "OK")
   182  	return nil
   183  }
   184  
   185  // patchSchedulerConfigHandler restarts the agent torrent scheduler with
   186  // the config in request body.
   187  func (s *Server) patchSchedulerConfigHandler(w http.ResponseWriter, r *http.Request) error {
   188  	defer r.Body.Close()
   189  	var config scheduler.Config
   190  	if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
   191  		return handler.Errorf("json decode: %s", err).Status(http.StatusBadRequest)
   192  	}
   193  	s.sched.Reload(config)
   194  	return nil
   195  }
   196  
   197  func (s *Server) getBlacklistHandler(w http.ResponseWriter, r *http.Request) error {
   198  	blacklist, err := s.sched.BlacklistSnapshot()
   199  	if err != nil {
   200  		return handler.Errorf("blacklist snapshot: %s", err)
   201  	}
   202  	if err := json.NewEncoder(w).Encode(&blacklist); err != nil {
   203  		return handler.Errorf("json encode: %s", err)
   204  	}
   205  	return nil
   206  }
   207  
   208  func parseDigest(r *http.Request) (core.Digest, error) {
   209  	raw, err := httputil.ParseParam(r, "digest")
   210  	if err != nil {
   211  		return core.Digest{}, err
   212  	}
   213  	// TODO(codyg): Accept only a fully formed digest.
   214  	d, err := core.NewSHA256DigestFromHex(raw)
   215  	if err != nil {
   216  		d, err = core.ParseSHA256Digest(raw)
   217  		if err != nil {
   218  			return core.Digest{}, handler.Errorf("parse digest: %s", err).Status(http.StatusBadRequest)
   219  		}
   220  	}
   221  	return d, nil
   222  }