Skip to content

Commit

Permalink
- Added support for exposing commands over HTTP by using the internal…
Browse files Browse the repository at this point in the history
… serve command.
  • Loading branch information
Kristoffer Ahl committed May 17, 2019
1 parent b3c8f46 commit c956c2e
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 0 deletions.
4 changes: 4 additions & 0 deletions examples/centry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ commands:
- name: up
path: commands/updown.sh
description: Upserts resources
annotations:
centry.api/serve: "true"
- name: down
path: commands/updown.sh
description: Destroys resources
annotations:
centry.api/serve: "false"
- name: rotate
path: commands/rotate.sh
description: Rotating secrets, hosts etc.
Expand Down
16 changes: 16 additions & 0 deletions pkg/api/contract.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package api

// IndexResponse defines an HTTP response object
type IndexResponse struct {
}

// ExecuteRequest defines an HTTP response object
type ExecuteRequest struct {
Args string `json:"args"`
}

// ExecuteResponse defines an HTTP response object
type ExecuteResponse struct {
Result string `json:"result"`
ExitCode int `json:"exitCode"`
}
83 changes: 83 additions & 0 deletions pkg/api/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package api

import (
"net/http"
"time"

"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
)

// Config defines server configuration
type Config struct {
Log *logrus.Entry
}

// Server holds the configuration, router and http server
type Server struct {
Config Config
Router *mux.Router
Server *http.Server
}

// NewServer creates a new HTTP server
func NewServer(config Config) Server {
s := Server{
Server: nil,
Config: config,
Router: mux.NewRouter(),
}

s.Router.Use(loggingMiddleware(config))

s.Server = &http.Server{
Handler: s.Router,
Addr: ":8113",
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
}

config.Log.WithFields(logrus.Fields{
"service": "HTTP-Server",
"address": s.Server.Addr,
}).Infof("listening on %s", s.Server.Addr)

return s
}

// RunAndBlock starts the HTTP server and blocks
func (s Server) RunAndBlock() error {
return s.Server.ListenAndServe()
}

func loggingMiddleware(config Config) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
config.Log.WithFields(logrus.Fields{
"service": "HTTP-Server",
"method": r.Method,
"path": r.RequestURI,
"direction": "incoming",
}).Debugf("HTTP %s %s", r.Method, r.RequestURI)

scrw := &statusCodeResponseWriter{w, http.StatusOK}
next.ServeHTTP(scrw, r)

config.Log.WithFields(logrus.Fields{
"service": "HTTP-Server",
"status": scrw.statusCode,
"direction": "outgoing",
}).Debugf("HTTP %s %s - %d %s", r.Method, r.RequestURI, scrw.statusCode, http.StatusText(scrw.statusCode))
})
}
}

type statusCodeResponseWriter struct {
http.ResponseWriter
statusCode int
}

func (lrw *statusCodeResponseWriter) WriteHeader(code int) {
lrw.statusCode = code
lrw.ResponseWriter.WriteHeader(code)
}
3 changes: 3 additions & 0 deletions pkg/centry/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ type Executor string
// CLI executor
var CLI Executor = "CLI"

// API Executor
var API Executor = "API"

// Context defines the current context
type Context struct {
executor Executor
Expand Down
12 changes: 12 additions & 0 deletions pkg/centry/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ func Create(inputArgs []string, context *Context) *Runtime {

logger := context.log.GetLogger()

// Register builtin commands
if context.executor == CLI {
c.Commands["serve"] = func() (cli.Command, error) {
return &ServeCommand{
Manifest: context.manifest,
Log: logger.WithFields(logrus.Fields{
"command": "serve",
}),
}, nil
}
}

// Build commands
for _, cmd := range context.manifest.Commands {
cmd := cmd
Expand Down
114 changes: 114 additions & 0 deletions pkg/centry/serve_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package centry

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

api "github.com/kristofferahl/go-centry/pkg/api"
"github.com/kristofferahl/go-centry/pkg/config"
"github.com/kristofferahl/go-centry/pkg/io"
"github.com/sirupsen/logrus"
)

// ServeCommand is a Command implementation that applies stuff
type ServeCommand struct {
Manifest *config.Manifest
Log *logrus.Entry
}

// Run starts an HTTP server and blocks
func (sc *ServeCommand) Run(args []string) int {
sc.Log.Debugf("Serving HTTP api")

s := api.NewServer(api.Config{
Log: sc.Log,
})

s.Router.HandleFunc("/", indexHandler(sc.Manifest)).Methods("GET")
s.Router.HandleFunc("/commands/", executeHandler(sc.Manifest)).Methods("POST")

err := s.RunAndBlock()
if err != nil {
return 1
}

return 0
}

// Help returns the help text of the ServeCommand
func (sc *ServeCommand) Help() string {
return "No help here..."
}

// Synopsis returns the synopsis of the ServeCommand
func (sc *ServeCommand) Synopsis() string {
return "Exposes commands over HTTP"
}

func indexHandler(manifest *config.Manifest) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
statusCode := http.StatusOK
response := api.IndexResponse{}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)

js, err := json.Marshal(response)
if err == nil {
w.Write(js)
}
}
}

func executeHandler(manifest *config.Manifest) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
statusCode := http.StatusOK
response := api.ExecuteResponse{}

var body api.ExecuteRequest
var buf bytes.Buffer

decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&body)
if err != nil {
statusCode = http.StatusBadRequest
}

args := []string{}
args = append(args, manifest.Path)
args = append(args, strings.Fields(body.Args)...)

// Build
context := NewContext(API, io.InputOutput{
Stdin: nil,
Stdout: &buf,
Stderr: &buf,
})

context.commandEnabled = func(cmd config.Command) bool {
if cmd.Annotations == nil || cmd.Annotations[config.APIServeAnnotation] != "true" {
return false
}

return true
}

runtime := Create(args, context)

// Run
exitCode := runtime.Execute()

response.Result = buf.String()
response.ExitCode = exitCode

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)

js, err := json.Marshal(response)
if err == nil {
w.Write(js)
}
}
}

0 comments on commit c956c2e

Please sign in to comment.