Skip to content

Commit

Permalink
feat(server): Added server command
Browse files Browse the repository at this point in the history
  • Loading branch information
micahhausler committed Dec 7, 2017
1 parent 99aa936 commit c3eb715
Show file tree
Hide file tree
Showing 14 changed files with 673 additions and 4 deletions.
12 changes: 9 additions & 3 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Makefile
Expand Up @@ -32,7 +32,7 @@ completion: build
cp out.sh /usr/local/etc/bash_completion.d/$(REPO)

docker:
docker run --rm -v $$(pwd):/go/src/github.com/skuid/$(REPO) -w /go/src/github.com/skuid/$(REPO) golang:1.9-alpine sh -c "apk -U add gcc linux-headers musl-dev && go build -v -a -tags netgo -installsuffix netgo -ldflags '-w'"
docker run --rm -v $$(pwd):/go/src/github.com/skuid/$(REPO) -w /go/src/github.com/skuid/$(REPO) golang:1.9-alpine sh -c "apk -U add gcc linux-headers musl-dev && go build -v -ldflags '-w -X github.com/skuid/helm-value-store/vendor/github.com/skuid/spec/metrics.commit=$(SHA)'"
docker build -t skuid/$(REPO) .

clean:
Expand Down
37 changes: 37 additions & 0 deletions README.md
Expand Up @@ -150,6 +150,43 @@ If this is your first time using helm-value-store, you will need to create a Dyn
helm-value-store load --setup --file <(echo "[]")
```

## Server

Helm value store ships with a `server` subcommand that runs an HTTP server for
applying charts into a cluster.

The server only has one endpoint, `/apply` that accepts the following input:

```
HTTP1.1 POST /apply
{
"uuid": "6fad4903-58ec-446f-bda4-bd39c4ff96aa"
}
```

The response structure is:

```json
{
"status": "success",
"message": "Successfully installed alertmanager"
}
```


By default, the server accepts a Google Oauth2 ID token in the Authorization
header for verifying a user against Google and ensuring their email is in a
given domain.

The server also listens on an alternate port (default `3001`) for the following
endpoints.

```
/metrics - Prometheus metrics
/live - for liveness checks
/ready - for readiness checks
```

## Usage

```
Expand Down
88 changes: 88 additions & 0 deletions cmd/serve.go
@@ -0,0 +1,88 @@
package cmd

import (
"flag"
"fmt"
"net/http"

"github.com/skuid/go-middlewares/authn/google"
"github.com/skuid/helm-value-store/server"
"github.com/skuid/spec"
"github.com/skuid/spec/lifecycle"
"github.com/skuid/spec/middlewares"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

var level = zapcore.InfoLevel

// serveCmd represents the serve command
var serveCmd = &cobra.Command{
Use: "serve",
Short: "A brief description of your command",
PreRunE: func(cmd *cobra.Command, args []string) error {
l, err := spec.NewStandardLevelLogger(level)
if err != nil {
return fmt.Errorf("Error initializing logger: %q", err)
}
zap.ReplaceGlobals(l)
return nil
},
Run: func(cmd *cobra.Command, args []string) {
middlewareList := []middlewares.Middleware{middlewares.InstrumentRoute()}
loggingClosures := []func(*http.Request) []zapcore.Field{}
serverOpts := []server.ControllerOpt{}

if viper.GetBool("auth-enabled") {
authorizer := google.New(google.WithAuthorizedDomains(viper.GetString("email-domain")))
serverOpts = append(serverOpts, server.WithAuthorizers(authorizer))
middlewareList = append([]middlewares.Middleware{authorizer.Authorize()}, middlewareList...)
loggingClosures = append(loggingClosures, authorizer.LoggingClosure)
}

apiController := server.NewApiController(releaseStore, serverOpts...)
middlewareList = append(middlewareList, middlewares.Logging(loggingClosures...))

authMux := http.NewServeMux()
authMux.HandleFunc("/apply", apiController.ApplyChart)

mux := http.NewServeMux()
mux.Handle("/", middlewares.Apply(authMux, middlewareList...))

go spec.MetricsServer(viper.GetInt("metrics-port"))

hostPort := fmt.Sprintf(":%d", viper.GetInt("port"))
httpServer := &http.Server{Addr: hostPort, Handler: mux}
lifecycle.ShutdownOnTerm(httpServer)

zap.L().Info("Starting helm-value-store server 🃏 ", zap.Int("port", viper.GetInt("port")))
if err := httpServer.ListenAndServe(); err != http.ErrServerClosed {
zap.L().Fatal("Error listening", zap.Error(err))
}
zap.L().Info("Server gracefully stopped")

},
}

func init() {
RootCmd.AddCommand(serveCmd)

// Hack to make level work
set := flag.NewFlagSet("temp", flag.ExitOnError)
set.Var(&level, "level", "Log level")
levelPFlag := pflag.PFlagFromGoFlag(set.Lookup("level"))
levelPFlag.Shorthand = "l"

localFlagSet := serveCmd.Flags()
localFlagSet.AddFlag(levelPFlag)
localFlagSet.Int64P("timeout", "t", 300, "Time in seconds to timeout on installation/update of releases")
localFlagSet.IntP("port", "p", 3000, "The port to listen on")
localFlagSet.Int("metrics-port", 3001, "The port to listen on for metrics/health checks")
localFlagSet.String("email-domain", "", "The email domain to filter on")
localFlagSet.Bool("auth-enabled", true, "Enable authentication/authorization")

viper.BindPFlags(localFlagSet)
}
141 changes: 141 additions & 0 deletions server/apply.go
@@ -0,0 +1,141 @@
package server

import (
"encoding/json"
"fmt"
"net/http"
"strings"

"github.com/skuid/helm-value-store/store"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

type applyRequest struct {
UUID string `json:"uuid"`
}

type applyResponse struct {
Status string `json:"status"`
Message string `json:"message"`
}

type upsertConfig struct {
location string
timeout int64
}

func upsertRelease(r *store.Release, conf upsertConfig) error {
_, err := r.Get()

if err != nil && !strings.Contains(err.Error(), "not found") {
return err
}

if err != nil && strings.Contains(err.Error(), "not found") {
_, err = r.Install(conf.location, false, conf.timeout)
} else if err == nil {
_, err = r.Upgrade(conf.location, false, conf.timeout)
}

return err
}

// ApplyChart applies a chart to a tiller server
func (c ApiController) ApplyChart(w http.ResponseWriter, r *http.Request) {
var err error
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusNotFound)
return
}

// Fields for the autit log
var auditFields []zapcore.Field
for _, a := range c.authorizers {
auditFields = append(auditFields, a.LoggingClosure(r)...)
}

defer func() {
successful := err == nil
auditFields = append(
auditFields,
zap.String("controller", "apply"),
zap.Bool("successful", successful),
)
zap.L().Info("Audit Log", auditFields...)
}()

applyReq := &applyRequest{}

if err = json.NewDecoder(r.Body).Decode(applyReq); err != nil {
w.WriteHeader(http.StatusBadRequest)
zap.L().Error("Error decoding request", zap.Error(err))
return
}
auditFields = append(auditFields, zap.String("uuid", applyReq.UUID))

release := &store.Release{}
release, err = c.releaseStore.Get(r.Context(), applyReq.UUID)

applyResp := &applyResponse{}

if err != nil {
zap.L().Error("Error getting release", zap.Error(err))

applyResp.Status = "error"
applyResp.Message = "Error getting release"
err = json.NewEncoder(w).Encode(applyResp)
if err != nil {
zap.L().Error("Error marshaling response", zap.Error(err))
}
return
}
auditFields = append(auditFields,
zap.String("chart", release.Chart),
zap.String("release", release.Name),
zap.String("version", release.Version),
zap.String("namespace", release.Namespace),
)

var location string
location, err = release.Download()

if err != nil {
zap.L().Error("Error downloading release", zap.Error(err))

w.WriteHeader(http.StatusInternalServerError)
applyResp.Status = "error"
applyResp.Message = "Error downloading release"
err = json.NewEncoder(w).Encode(applyResp)

if err != nil {
zap.L().Error("Error marshaling response", zap.Error(err))
}
return
}

err = upsertRelease(release, upsertConfig{
location: location,
timeout: c.timeout,
})

if err != nil {
zap.L().Error("Error applying release", zap.Error(err))

w.WriteHeader(http.StatusInternalServerError)
applyResp.Status = "error"
applyResp.Message = "Error applying release"
err = json.NewEncoder(w).Encode(applyResp)
if err != nil {
zap.L().Error("Error marshaling response", zap.Error(err))
}
return
}

applyResp.Status = "success"
applyResp.Message = fmt.Sprintf("Successfully installed %s", release.Name)
err = json.NewEncoder(w).Encode(applyResp)
if err != nil {
zap.L().Error("Error marshaling response", zap.Error(err))
}
}
43 changes: 43 additions & 0 deletions server/root.go
@@ -0,0 +1,43 @@
package server

import (
"github.com/skuid/go-middlewares"
"github.com/skuid/helm-value-store/store"
)

// ApiController stores metadata for the API
type ApiController struct {
releaseStore store.ReleaseStore
authorizers []go_middlewares.Authorizer
timeout int64
}

// ControllerOpt is a func that modifies an ApiController
type ControllerOpt func(*ApiController)

// WithAutorizer sets the authorizer for an ApiController
func WithAuthorizers(azs ...go_middlewares.Authorizer) ControllerOpt {
return func(a *ApiController) {
a.authorizers = azs
}
}

// WithTimeout sets the timeout in seconds on an ApiController
func WithTimeout(timeout int64) ControllerOpt {
return func(a *ApiController) {
a.timeout = timeout
}
}

// NewApiController returns a new API controller with a default timeout of 300 seconds
func NewApiController(s store.ReleaseStore, opts ...ControllerOpt) *ApiController {
response := &ApiController{
releaseStore: s,
timeout: 300,
}
for _, opt := range opts {
opt(response)
}

return response
}
1 change: 1 addition & 0 deletions vendor/github.com/skuid/go-middlewares/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions vendor/github.com/skuid/go-middlewares/.travis.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions vendor/github.com/skuid/go-middlewares/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions vendor/github.com/skuid/go-middlewares/authn/google/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c3eb715

Please sign in to comment.