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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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.

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

Loading

0 comments on commit c3eb715

Please sign in to comment.