github.com/fibonacci1729/draft@v0.3.0/api/server.go (about)

     1  package api
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/sha1"
     7  	"encoding/base64"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"net"
    12  	"net/http"
    13  	"os"
    14  	"strconv"
    15  	"strings"
    16  	"syscall"
    17  	"time"
    18  
    19  	log "github.com/Sirupsen/logrus"
    20  	"github.com/docker/docker/api/types"
    21  	docker "github.com/docker/docker/client"
    22  	"github.com/docker/docker/pkg/jsonmessage"
    23  	"github.com/docker/docker/pkg/term"
    24  	"github.com/ghodss/yaml"
    25  	"github.com/gorilla/websocket"
    26  	"github.com/julienschmidt/httprouter"
    27  	"k8s.io/helm/pkg/chartutil"
    28  	"k8s.io/helm/pkg/helm"
    29  	"k8s.io/helm/pkg/proto/hapi/release"
    30  	"k8s.io/helm/pkg/storage/driver"
    31  	"k8s.io/helm/pkg/strvals"
    32  
    33  	"github.com/Azure/draft/pkg/version"
    34  )
    35  
    36  // WebsocketUpgrader represents the default websocket.Upgrader that Draft employs
    37  var WebsocketUpgrader = websocket.Upgrader{
    38  	EnableCompression: true,
    39  	// reduce the WriteBufferSize so `docker build` and `docker push` responses aren't internally
    40  	// buffered by gorilla/websocket, but smaller, more informative messages can still be buffered.
    41  	// https://github.com/gorilla/websocket/blob/9bc973af0682dc73a22553a08bfe00ee6255f56f/conn.go#L586-L593
    42  	WriteBufferSize: 128,
    43  }
    44  
    45  // Server is an API Server which listens and responds to HTTP requests.
    46  type Server struct {
    47  	HTTPServer   *http.Server
    48  	Listener     net.Listener
    49  	DockerClient *docker.Client
    50  	HelmClient   *helm.Client
    51  	// RegistryAuth is the authorization token used to push images up to the registry.
    52  	//
    53  	// This field follows the format of the X-Registry-Auth header.
    54  	RegistryAuth string
    55  	// RegistryOrg is the organization (e.g. your DockerHub account) used to push images
    56  	// up to the registry.
    57  	RegistryOrg string
    58  	// RegistryURL is the URL of the registry (e.g. quay.io, docker.io, gcr.io)
    59  	RegistryURL string
    60  	// Basedomain is the basedomain used to construct the ingress rules
    61  	Basedomain string
    62  }
    63  
    64  // Serve starts the HTTP server, accepting all new connections.
    65  func (s *Server) Serve() error {
    66  	return s.HTTPServer.Serve(s.Listener)
    67  }
    68  
    69  // Close shuts down the HTTP server, dropping all current connections.
    70  func (s *Server) Close() error {
    71  	return s.Listener.Close()
    72  }
    73  
    74  // ServeRequest processes a single HTTP request.
    75  func (s *Server) ServeRequest(w http.ResponseWriter, req *http.Request) {
    76  	s.HTTPServer.Handler.ServeHTTP(w, req)
    77  }
    78  
    79  func (s *Server) createRouter() {
    80  	r := httprouter.New()
    81  
    82  	routerMap := map[string]map[string]httprouter.Handle{
    83  		"GET": {
    84  			"/ping":    ping,
    85  			"/version": getVersion,
    86  		},
    87  		"POST": {
    88  			"/apps/:id": buildApp,
    89  		},
    90  	}
    91  
    92  	for method, routes := range routerMap {
    93  		for route, funct := range routes {
    94  			// disable logging on /ping requests
    95  			if route != "/ping" {
    96  				funct = logRequestMiddleware(funct)
    97  			}
    98  			r.Handle(method, route, s.Middleware(funct))
    99  		}
   100  	}
   101  	s.HTTPServer.Handler = r
   102  }
   103  
   104  type contextKey string
   105  
   106  func (c contextKey) String() string {
   107  	return "api context key " + string(c)
   108  }
   109  
   110  // Middleware adds additional context before handling requests
   111  func (s *Server) Middleware(h httprouter.Handle) httprouter.Handle {
   112  	return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
   113  		// attach the API server to the request params so that it can retrieve info about itself
   114  		ctx := context.WithValue(r.Context(), contextKey("server"), s)
   115  		// Delegate request to the given handle
   116  		h(w, r.WithContext(ctx), p)
   117  	}
   118  }
   119  
   120  // NewServer sets up the required Server and does protocol specific checking.
   121  func NewServer(proto, addr string) (*Server, error) {
   122  	var (
   123  		a   *Server
   124  		err error
   125  	)
   126  	switch proto {
   127  	case "tcp":
   128  		a, err = setupTCPHTTP(addr)
   129  	case "unix":
   130  		a, err = setupUnixHTTP(addr)
   131  	default:
   132  		a, err = nil, fmt.Errorf("invalid protocol format")
   133  	}
   134  	a.createRouter()
   135  	return a, err
   136  }
   137  
   138  func setupTCPHTTP(addr string) (*Server, error) {
   139  	l, err := net.Listen("tcp", addr)
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  
   144  	a := &Server{
   145  		HTTPServer: &http.Server{Addr: addr},
   146  		Listener:   l,
   147  	}
   148  	return a, nil
   149  }
   150  
   151  func setupUnixHTTP(addr string) (*Server, error) {
   152  	if err := syscall.Unlink(addr); err != nil && !os.IsNotExist(err) {
   153  		return nil, err
   154  	}
   155  	mask := syscall.Umask(0777)
   156  	defer syscall.Umask(mask)
   157  
   158  	l, err := net.Listen("unix", addr)
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  
   163  	if err := os.Chmod(addr, 0660); err != nil {
   164  		return nil, err
   165  	}
   166  
   167  	a := &Server{
   168  		HTTPServer: &http.Server{Addr: addr},
   169  		Listener:   l,
   170  	}
   171  	return a, nil
   172  }
   173  
   174  func logRequestMiddleware(h httprouter.Handle) httprouter.Handle {
   175  	return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
   176  		log.Infof("%s %s", r.Method, r.RequestURI)
   177  		// Delegate request to the given handle
   178  		h(w, r, p)
   179  	}
   180  }
   181  
   182  // writeJSON writes the value v to the http response stream as json with standard
   183  // json encoding.
   184  func writeJSON(w http.ResponseWriter, v interface{}, code int) error {
   185  	w.Header().Set("Content-Type", "application/json")
   186  	w.WriteHeader(code)
   187  	return json.NewEncoder(w).Encode(v)
   188  }
   189  
   190  func ping(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
   191  	w.Write([]byte{'P', 'O', 'N', 'G'})
   192  }
   193  
   194  func getVersion(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
   195  	if err := writeJSON(w, version.New(), http.StatusOK); err != nil {
   196  		http.Error(w, err.Error(), http.StatusInternalServerError)
   197  	}
   198  }
   199  
   200  func buildApp(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
   201  	var imagePrefix string
   202  	baseValues := map[string]interface{}{}
   203  	appName := p.ByName("id")
   204  	server := r.Context().Value(contextKey("server")).(*Server)
   205  	namespace := r.Header.Get("Kubernetes-Namespace")
   206  	flagWait := r.Header.Get("Helm-Flag-Wait")
   207  
   208  	// load client values as the base config
   209  	log.Debugf("Helm-Flag-Set: %s", r.Header.Get("Helm-Flag-Set"))
   210  
   211  	userVals, err := base64.StdEncoding.DecodeString(r.Header.Get("Helm-Flag-Set"))
   212  	if err != nil {
   213  		http.Error(w, fmt.Sprintf("error while parsing header 'Helm-Flag-Set': %v\n", err), http.StatusBadRequest)
   214  	}
   215  	if err := yaml.Unmarshal([]byte(userVals), &baseValues); err != nil {
   216  		http.Error(w, fmt.Sprintf("error while unmarshalling header 'Helm-Flag-Set' to yaml: %v\n", err), http.StatusBadRequest)
   217  		return
   218  	}
   219  
   220  	// NOTE(bacongobbler): If no header was set, we default back to the default namespace.
   221  	if namespace == "" {
   222  		namespace = "default"
   223  	}
   224  
   225  	if r.Method != "POST" {
   226  		w.WriteHeader(http.StatusMethodNotAllowed)
   227  		return
   228  	}
   229  
   230  	optionWait, err := strconv.ParseBool(flagWait)
   231  	if err != nil {
   232  		http.Error(w, fmt.Sprintf("error while parsing header 'Helm-Flag-Wait': %v\n", err), http.StatusBadRequest)
   233  		return
   234  	}
   235  
   236  	// this is just a buffer of 32MB. Everything is piped over to docker's build context and to
   237  	// Helm so this is just a sane default in case docker or helm's backed up somehow.
   238  	r.ParseMultipartForm(32 << 20)
   239  	buildContext, _, err := r.FormFile("release-tar")
   240  	if err != nil {
   241  		http.Error(w, fmt.Sprintf("error while reading release-tar: %v\n", err), http.StatusBadRequest)
   242  		return
   243  	}
   244  	defer buildContext.Close()
   245  
   246  	chartFile, _, err := r.FormFile("chart-tar")
   247  	if err != nil {
   248  		http.Error(w, fmt.Sprintf("error while reading chart-tar: %v\n", err), http.StatusBadRequest)
   249  		return
   250  	}
   251  	defer chartFile.Close()
   252  
   253  	conn, err := WebsocketUpgrader.Upgrade(w, r, nil)
   254  	if err != nil {
   255  		http.Error(w, fmt.Sprintf("error when upgrading connection: %v\n", err), http.StatusInternalServerError)
   256  		return
   257  	}
   258  	defer conn.Close()
   259  
   260  	conn.SetCloseHandler(func(code int, text string) error {
   261  		// Note https://tools.ietf.org/html/rfc6455#section-5.5 which specifies control
   262  		// frames MUST be less than 125 bytes (This includes Close, Ping and Pong)
   263  		// Hence, sending text as TextMessage and then sending control message.
   264  		conn.WriteMessage(websocket.TextMessage, []byte(text))
   265  		conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(code, ""), time.Now().Add(time.Second))
   266  		return nil
   267  	})
   268  
   269  	// write build context to a buffer so we can also write to the sha1 hash
   270  	buf := new(bytes.Buffer)
   271  	buildContextChecksum := sha1.New()
   272  	mw := io.MultiWriter(buf, buildContextChecksum)
   273  	io.Copy(mw, buildContext)
   274  
   275  	// truncate checksum to the first 40 characters (20 bytes)
   276  	// this is the equivalent of `shasum build.tar.gz | awk '{print $1}'`
   277  	tag := fmt.Sprintf("%.20x", buildContextChecksum.Sum(nil))
   278  	if server.RegistryOrg != "" {
   279  		imagePrefix = server.RegistryOrg + "/"
   280  	}
   281  	imageName := fmt.Sprintf("%s/%s%s:%s",
   282  		server.RegistryURL,
   283  		imagePrefix,
   284  		appName,
   285  		tag,
   286  	)
   287  
   288  	// inject certain values into the chart such as the registry location, the application name
   289  	// and the version
   290  	imageVals := fmt.Sprintf("image.name=%s,image.org=%s,image.registry=%s,image.tag=%s",
   291  		appName,
   292  		server.RegistryOrg,
   293  		server.RegistryURL,
   294  		tag)
   295  
   296  	if err := strvals.ParseInto(imageVals, baseValues); err != nil {
   297  		handleClosingError(conn, "Could not inject registry data into values", err)
   298  	}
   299  
   300  	rawVals, err := yaml.Marshal(baseValues)
   301  	if err != nil {
   302  		handleClosingError(conn, "Could not marshal values", err)
   303  	}
   304  
   305  	// send uploaded tar to docker as the build context
   306  	conn.WriteMessage(websocket.TextMessage, []byte("--> Building Dockerfile\n"))
   307  	buildResp, err := server.DockerClient.ImageBuild(
   308  		context.Background(),
   309  		buf,
   310  		types.ImageBuildOptions{
   311  			Tags: []string{imageName},
   312  		})
   313  	if err != nil {
   314  		handleClosingError(conn, "Could not build image from build context", err)
   315  	}
   316  	defer buildResp.Body.Close()
   317  	writer, err := conn.NextWriter(websocket.TextMessage)
   318  	if err != nil {
   319  		handleClosingError(conn, "There was an error fetching a text message writer", err)
   320  	}
   321  	outFd, isTerm := term.GetFdInfo(writer)
   322  	if err := jsonmessage.DisplayJSONMessagesStream(buildResp.Body, writer, outFd, isTerm, nil); err != nil {
   323  		handleClosingError(conn, "Error encountered streaming JSON response", err)
   324  	}
   325  
   326  	_, _, err = server.DockerClient.ImageInspectWithRaw(
   327  		context.Background(),
   328  		imageName)
   329  	if err != nil {
   330  		if docker.IsErrImageNotFound(err) {
   331  			handleClosingError(conn, fmt.Sprintf("Could not locate image for %s", appName), err)
   332  		} else {
   333  			handleClosingError(conn, "ImageInspectWithRaw error", err)
   334  		}
   335  	}
   336  
   337  	conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("--> Pushing %s\n", imageName)))
   338  	pushResp, err := server.DockerClient.ImagePush(
   339  		context.Background(),
   340  		imageName,
   341  		types.ImagePushOptions{RegistryAuth: server.RegistryAuth})
   342  	if err != nil {
   343  		handleClosingError(conn, fmt.Sprintf("Could not push %s to registry", imageName), err)
   344  	}
   345  	defer pushResp.Close()
   346  	writer, err = conn.NextWriter(websocket.TextMessage)
   347  	if err != nil {
   348  		handleClosingError(conn, "There was an error fetching a text message writer", err)
   349  	}
   350  	outFd, isTerm = term.GetFdInfo(writer)
   351  	if err := jsonmessage.DisplayJSONMessagesStream(pushResp, writer, outFd, isTerm, nil); err != nil {
   352  		handleClosingError(conn, "Error encountered streaming JSON response", err)
   353  	}
   354  
   355  	conn.WriteMessage(websocket.TextMessage, []byte("--> Deploying to Kubernetes\n"))
   356  	chart, err := chartutil.LoadArchive(chartFile)
   357  	if err != nil {
   358  		handleClosingError(conn, "Could not load chart archive", err)
   359  	}
   360  
   361  	// combinedVars takes the basedomain configured in draftd and appends that to the rawVals
   362  	combinedVars := append([]byte(fmt.Sprintf("basedomain: %s\n", server.Basedomain))[:], []byte(rawVals)[:]...)
   363  
   364  	// If a release does not exist, install it. If another error occurs during
   365  	// the check, ignore the error and continue with the upgrade.
   366  	//
   367  	// The returned error is a grpc.rpcError that wraps the message from the original error.
   368  	// So we're stuck doing string matching against the wrapped error, which is nested somewhere
   369  	// inside of the grpc.rpcError message.
   370  	_, err = server.HelmClient.ReleaseContent(appName, helm.ContentReleaseVersion(1))
   371  	if err != nil && strings.Contains(err.Error(), driver.ErrReleaseNotFound.Error()) {
   372  		conn.WriteMessage(
   373  			websocket.TextMessage,
   374  			[]byte(fmt.Sprintf("    Release %q does not exist. Installing it now.\n", appName)))
   375  		releaseResp, err := server.HelmClient.InstallReleaseFromChart(
   376  			chart,
   377  			namespace,
   378  			helm.ReleaseName(appName),
   379  			helm.ValueOverrides(combinedVars),
   380  			helm.InstallWait(optionWait))
   381  		if err != nil {
   382  			handleClosingError(conn, "Could not install release", err)
   383  		}
   384  		conn.WriteMessage(
   385  			websocket.TextMessage,
   386  			formatReleaseStatus(releaseResp.Release))
   387  	} else {
   388  		releaseResp, err := server.HelmClient.UpdateReleaseFromChart(
   389  			appName,
   390  			chart,
   391  			helm.UpdateValueOverrides(combinedVars),
   392  			helm.UpgradeWait(optionWait))
   393  		if err != nil {
   394  			handleClosingError(conn, "Could not upgrade release", err)
   395  		}
   396  		conn.WriteMessage(
   397  			websocket.TextMessage,
   398  			formatReleaseStatus(releaseResp.Release))
   399  	}
   400  
   401  	// gently tell the client that we are closing the connection
   402  	conn.WriteControl(
   403  		websocket.CloseMessage,
   404  		websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""),
   405  		time.Now().Add(time.Second))
   406  }
   407  
   408  // handleClosingError formats the err and corresponding verbiage and invokes
   409  // conn.CloseHandler() as set by conn.SetCloseHandler()
   410  func handleClosingError(conn *websocket.Conn, verbiage string, err error) {
   411  	conn.CloseHandler()(
   412  		websocket.CloseInternalServerErr,
   413  		fmt.Sprintf("%s: %v\n", verbiage, err))
   414  }
   415  
   416  // formatReleaseStatus returns a byte slice of formatted release status information
   417  func formatReleaseStatus(release *release.Release) []byte {
   418  	output := fmt.Sprintf("--> Status: %s\n", release.Info.Status.Code.String())
   419  	if release.Info.Status.Notes != "" {
   420  		output += fmt.Sprintf("--> Notes:\n     %s\n", release.Info.Status.Notes)
   421  	}
   422  	return []byte(output)
   423  }