clean up command service

This commit is contained in:
Victor Vrantchan
2016-05-17 09:46:01 -04:00
parent 4b1d989357
commit f81bc3c0c2
4 changed files with 245 additions and 342 deletions

View File

@@ -8,7 +8,7 @@ import (
"time"
"github.com/garyburd/redigo/redis"
"github.com/go-kit/kit/log"
kitlog "github.com/go-kit/kit/log"
"github.com/groob/plist"
"github.com/micromdm/mdm"
)
@@ -18,7 +18,7 @@ var (
ErrNoKey = errors.New("There is no such key in redis.")
)
// Datastore manages MDM Payloads in redis
// Datastore provides methods for saving and retrieving MDM commands
type Datastore interface {
// Saves the payload in redis
// SET CommandUUID plistData
@@ -30,68 +30,20 @@ type Datastore interface {
DeleteCommand(deviceUDID, commandUUID string) (int, error)
}
type redisDB struct {
pool *redis.Pool
}
// NewDB creates a new databases connection
func NewDB(driver, conn string, options ...func(*config) error) Datastore {
conf := &config{}
defaultLogger := log.NewLogfmtLogger(os.Stderr)
for _, option := range options {
if err := option(conf); err != nil {
defaultLogger.Log("err", err)
os.Exit(1)
}
}
//NewDB creates a Datastore
func NewDB(driver, conn string, logger kitlog.Logger) (Datastore, error) {
var ds Datastore
switch driver {
case "redis":
return redisDB{pool: redisPool(conn, conf.logger)}
ds = redisDB{pool: redisPool(conn, logger)}
return ds, nil
default:
conf.logger.Log("err", "unknown driver")
os.Exit(1)
return nil
return nil, errors.New("unknown driver")
}
}
func redisPool(conn string, logger log.Logger) *redis.Pool {
pool := &redis.Pool{
MaxIdle: 3,
IdleTimeout: 240 * time.Second,
Dial: func() (redis.Conn, error) {
c, err := redis.Dial("tcp", conn)
if err != nil {
return nil, err
}
return c, err
},
TestOnBorrow: func(c redis.Conn, t time.Time) error {
_, err := c.Do("PING")
return err
},
}
checkRedisConn(pool, logger)
return pool
}
func checkRedisConn(pool *redis.Pool, logger log.Logger) {
conn := pool.Get()
defer conn.Close()
var dbError error
maxAttempts := 20
for attempts := 1; attempts <= maxAttempts; attempts++ {
_, dbError = conn.Do("PING")
if dbError == nil {
break
}
logger.Log("msg", fmt.Sprintf("could not connect to redis: %v", dbError))
time.Sleep(time.Duration(attempts) * time.Second)
}
if dbError != nil {
logger.Log("err", dbError)
os.Exit(1)
}
type redisDB struct {
pool *redis.Pool
}
func (rds redisDB) SavePayload(payload *mdm.Payload) error {
@@ -171,3 +123,43 @@ func (rds redisDB) DeleteCommand(deviceUDID, commandUUID string) (int, error) {
}
return total, nil
}
func redisPool(conn string, logger kitlog.Logger) *redis.Pool {
pool := &redis.Pool{
MaxIdle: 3,
IdleTimeout: 240 * time.Second,
Dial: func() (redis.Conn, error) {
c, err := redis.Dial("tcp", conn)
if err != nil {
return nil, err
}
return c, err
},
TestOnBorrow: func(c redis.Conn, t time.Time) error {
_, err := c.Do("PING")
return err
},
}
checkRedisConn(pool, logger)
return pool
}
func checkRedisConn(pool *redis.Pool, logger kitlog.Logger) {
conn := pool.Get()
defer conn.Close()
var dbError error
maxAttempts := 20
for attempts := 1; attempts <= maxAttempts; attempts++ {
_, dbError = conn.Do("PING")
if dbError == nil {
break
}
logger.Log("msg", fmt.Sprintf("could not connect to redis: %v", dbError))
time.Sleep(time.Duration(attempts) * time.Second)
}
if dbError != nil {
logger.Log("err", dbError)
os.Exit(1)
}
}

100
command/endpoint.go Normal file
View File

@@ -0,0 +1,100 @@
package command
import (
"errors"
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
"github.com/micromdm/mdm"
)
var (
// ErrEmptyRequest is returned if the request body is empty
ErrEmptyRequest = errors.New("request must contain UDID of the device")
errBadRouting = errors.New("inconsistent mapping between route and handler (programmer error)")
)
// newCommandRequest represents an HTTP Request for a new MDM Command
type newCommandRequest struct {
*mdm.CommandRequest
}
// newCommandResponse is a command reponse
type newCommandResponse struct {
*mdm.Payload
Err error `json:"error,omitempty"`
}
func (r newCommandResponse) error() error { return r.Err }
func makeNewCommandEndpoint(svc Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(newCommandRequest)
if req.UDID == "" || req.RequestType == "" {
return newCommandResponse{Err: ErrEmptyRequest}, nil
}
payload, err := svc.NewCommand(req.CommandRequest)
if err != nil {
return newCommandResponse{Err: err}, nil
}
return newCommandResponse{Payload: payload}, nil
}
}
// NextCommandRequest is a request to return the next command in a device queue
type nextCommandRequest struct {
UDID string
}
// NextCommandResponse is a response for the next command
type nextCommandResponse struct {
Payload []byte `json:"command_payload"`
Total int `json:"total_payloads"`
Err error `json:"error,omitempty"`
}
func makeNextCommandEndpoint(svc Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(nextCommandRequest)
if req.UDID == "" {
return nextCommandResponse{Err: ErrEmptyRequest}, nil
}
payload, total, err := svc.NextCommand(req.UDID)
if err != nil {
return nextCommandResponse{Err: err}, nil
}
return nextCommandResponse{Payload: payload, Total: total}, nil
}
}
// deleteCommandRequest is a request to delete a command
type deleteCommandRequest struct {
// device UDID
UDID string
// command UUID
UUID string
}
// deleteCommandResponse is a response for a delete request
type deleteCommandResponse struct {
Total int `json:"remaining_payloads"`
Err error `json:"error,omitempty"`
}
func makeDeleteCommandEndpoint(svc Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(deleteCommandRequest)
if req.UDID == "" {
return deleteCommandResponse{Err: ErrEmptyRequest}, nil
}
if req.UUID == "" {
return deleteCommandResponse{Err: ErrEmptyRequest}, nil
}
total, err := svc.DeleteCommand(req.UDID, req.UUID)
if err != nil {
return deleteCommandResponse{Err: err}, nil
}
return deleteCommandResponse{Total: total}, nil
}
}

View File

@@ -1,103 +1,26 @@
package command
import (
"net/http"
"os"
import "github.com/micromdm/mdm"
"golang.org/x/net/context"
httptransport "github.com/go-kit/kit/transport/http"
"github.com/go-kit/kit/log"
"github.com/gorilla/mux"
"github.com/micromdm/mdm"
)
// MDMCommandService allows creating and deleting MDM Command Payloads
type MDMCommandService interface {
// Service defines methods for managing MDM commands
type Service interface {
NewCommand(*mdm.CommandRequest) (*mdm.Payload, error)
NextCommand(udid string) ([]byte, int, error)
DeleteCommand(deviceUDID, commandUUID string) (int, error)
}
type mdmCommandService struct {
// a redis datastore
// NewService returns a new command service
func NewService(ds Datastore) Service {
return &service{
db: ds,
}
}
type service struct {
db Datastore
}
type config struct {
logger log.Logger
db Datastore
}
// NewCommandService creates a new MDM Command Service
func NewCommandService(options ...func(*config) error) MDMCommandService {
conf := &config{}
defaultLogger := log.NewLogfmtLogger(os.Stderr)
for _, option := range options {
if err := option(conf); err != nil {
defaultLogger.Log("err", err)
os.Exit(1)
}
}
var svc MDMCommandService
svc = mdmCommandService{db: conf.db}
return svc
}
// Logger adds a logger to the service
func Logger(logger log.Logger) func(*config) error {
return func(c *config) error {
c.logger = logger
return nil
}
}
// DB adds a db connection to the service
func DB(db Datastore) func(*config) error {
return func(c *config) error {
c.db = db
return nil
}
}
// ServiceHandler returns an http handler for the command service
func ServiceHandler(ctx context.Context, svc MDMCommandService) http.Handler {
commonOptions := []httptransport.ServerOption{
httptransport.ServerErrorEncoder(encodeError),
}
newCommandEndpoint := makeNewCommandEndpoint(svc)
newCommandHandler := httptransport.NewServer(
ctx,
newCommandEndpoint,
decodeNewCommandRequest,
encodeResponse,
commonOptions...,
)
nextCommandEndpoint := makeNextCommandEndpoint(svc)
nextCommandHandler := httptransport.NewServer(
ctx,
nextCommandEndpoint,
decodeNextCommandRequest,
encodeResponse,
commonOptions...,
)
deleteCommandEndpoint := makeDeleteCommandEndpoint(svc)
deleteCommandHandler := httptransport.NewServer(
ctx,
deleteCommandEndpoint,
decodeDeleteCommandRequest,
encodeResponse,
commonOptions...,
)
r := mux.NewRouter()
r.Methods("POST").Path("/mdm/commands").Handler(newCommandHandler)
r.Methods("GET").Path("/mdm/commands/{udid}/next").Handler(nextCommandHandler)
r.Methods("DELETE").Path("/mdm/commands/{udid}/{uuid}").Handler(deleteCommandHandler)
return r
}
func (svc mdmCommandService) NewCommand(request *mdm.CommandRequest) (*mdm.Payload, error) {
func (svc service) NewCommand(request *mdm.CommandRequest) (*mdm.Payload, error) {
// create a payload
payload, err := mdm.NewPayload(request)
if err != nil {
@@ -118,11 +41,11 @@ func (svc mdmCommandService) NewCommand(request *mdm.CommandRequest) (*mdm.Paylo
}
// NextCommand returns an MDM Payload from a list of queued payloads
func (svc mdmCommandService) NextCommand(udid string) ([]byte, int, error) {
func (svc service) NextCommand(udid string) ([]byte, int, error) {
return svc.db.NextCommand(udid)
}
// DeleteCommand returns an MDM Payload from a list of queued payloads
func (svc mdmCommandService) DeleteCommand(deviceUDID, commandUUID string) (int, error) {
func (svc service) DeleteCommand(deviceUDID, commandUUID string) (int, error) {
return svc.db.DeleteCommand(deviceUDID, commandUUID)
}

View File

@@ -1,133 +1,71 @@
package command
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"github.com/go-kit/kit/endpoint"
kitlog "github.com/go-kit/kit/log"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/gorilla/mux"
"github.com/micromdm/mdm"
"golang.org/x/net/context"
)
var (
// ErrEmptyRequest is returned if the request body is empty
ErrEmptyRequest = errors.New("request must contain UDID of the device")
errBadRouting = errors.New("inconsistent mapping between route and handler (programmer error)")
)
// ServiceHandler returns an HTTP Handler for the command service
func ServiceHandler(ctx context.Context, svc Service, logger kitlog.Logger) http.Handler {
opts := []kithttp.ServerOption{
kithttp.ServerErrorLogger(logger),
kithttp.ServerErrorEncoder(encodeError),
}
// NewCommandRequest represents an HTTP Request for a new MDM Command
type NewCommandRequest struct {
*mdm.CommandRequest
newCommandHandler := kithttp.NewServer(
ctx,
makeNewCommandEndpoint(svc),
decodeNewCommandRequest,
encodeResponse,
opts...,
)
nextCommandHandler := kithttp.NewServer(
ctx,
makeNextCommandEndpoint(svc),
decodeNextCommandRequest,
encodeResponse,
opts...,
)
deleteCommandHandler := kithttp.NewServer(
ctx,
makeDeleteCommandEndpoint(svc),
decodeDeleteCommandRequest,
encodeResponse,
opts...,
)
r := mux.NewRouter()
r.Handle("/mdm/commands", newCommandHandler).Methods("POST")
r.Handle("/mdm/commands/{udid}/next", nextCommandHandler).Methods("GET")
r.Handle("/mdm/commands/{udid}/{uuid}", deleteCommandHandler).Methods("DELETE")
return r
}
func decodeNewCommandRequest(r *http.Request) (interface{}, error) {
var request NewCommandRequest
func decodeNewCommandRequest(_ context.Context, r *http.Request) (interface{}, error) {
var request newCommandRequest
err := json.NewDecoder(r.Body).Decode(&request.CommandRequest)
return request, err
}
// NewCommandResponse is a command reponse
type NewCommandResponse struct {
*mdm.Payload
Err error `json:"error,omitempty"`
}
func (r NewCommandResponse) error() error { return r.Err }
// errorer is implemented by all concrete response types. It allows us to
// change the HTTP response code without needing to trigger an endpoint
// (transport-level) error. For more information, read the big comment in
// endpoint.go.
type errorer interface {
error() error
}
// NextCommandRequest is a request to return the next command in a device queue
type NextCommandRequest struct {
UDID string
}
func decodeNextCommandRequest(r *http.Request) (interface{}, error) {
func decodeNextCommandRequest(_ context.Context, r *http.Request) (interface{}, error) {
vars := mux.Vars(r)
udid, ok := vars["udid"]
if !ok {
return nil, errBadRouting
}
var request NextCommandRequest
var request nextCommandRequest
request.UDID = udid
return request, nil
}
// EncodeNextCommandRequest encodes a request for the NextCommand endpoint
func EncodeNextCommandRequest(r *http.Request, request interface{}) error {
req := request.(NextCommandRequest)
path := r.URL.Path
r.URL.Path = fmt.Sprintf("%v/%v/next", path, req.UDID)
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(request); err != nil {
return err
}
r.Body = ioutil.NopCloser(&buf)
return nil
}
// NextCommandResponse is a response for the next command
type NextCommandResponse struct {
Payload []byte `json:"command_payload"`
Total int `json:"total_payloads"`
Err error `json:"error,omitempty"`
}
// DecodeNextCommandResponse decodes the response from the provided HTTP response,
// simply by JSON decoding from the response body. It's designed to be used in
// transport/http.Client.
// first decode into map[string]interface{} and check for error in the response
func DecodeNextCommandResponse(resp *http.Response) (interface{}, error) {
var r map[string]interface{}
var response NextCommandResponse
err := json.NewDecoder(resp.Body).Decode(&r)
if rs, ok := r["error"]; ok {
response.Err = errors.New(rs.(string))
}
if rs, ok := r["total_payloads"]; ok {
response.Total = int(rs.(float64))
}
if rs, ok := r["command_payload"]; ok {
response.Payload = []byte(rs.(string))
}
return response, err
}
func (r NextCommandResponse) error() error { return r.Err }
// DeleteCommandRequest is a request to delete a command
type DeleteCommandRequest struct {
// device UDID
UDID string
// command UUID
UUID string
}
// EncodeDeleteCommandRequest encodes a request for the NextCommand endpoint
func EncodeDeleteCommandRequest(r *http.Request, request interface{}) error {
req := request.(DeleteCommandRequest)
path := r.URL.Path
r.URL.Path = fmt.Sprintf("%v/%v/%v", path, req.UDID, req.UUID)
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(request); err != nil {
return err
}
r.Body = ioutil.NopCloser(&buf)
return nil
}
func decodeDeleteCommandRequest(r *http.Request) (interface{}, error) {
func decodeDeleteCommandRequest(_ context.Context, r *http.Request) (interface{}, error) {
vars := mux.Vars(r)
udid, ok := vars["udid"]
if !ok {
@@ -137,115 +75,65 @@ func decodeDeleteCommandRequest(r *http.Request) (interface{}, error) {
if !ok {
return nil, errBadRouting
}
var request DeleteCommandRequest
var request deleteCommandRequest
request.UDID = udid
request.UUID = uuid
return request, nil
}
// DeleteCommandResponse is a response for a delete request
type DeleteCommandResponse struct {
Total int `json:"remaining_payloads"`
Err error `json:"error,omitempty"`
type errorer interface {
error() error
}
// DecodeDeleteCommandResponse decodes the response from the provided HTTP response,
// simply by JSON decoding from the response body. It's designed to be used in
// transport/http.Client.
// first decode into map[string]interface{} and check for error in the response
func DecodeDeleteCommandResponse(resp *http.Response) (interface{}, error) {
var r map[string]interface{}
var response DeleteCommandResponse
err := json.NewDecoder(resp.Body).Decode(&r)
if rs, ok := r["error"]; ok {
response.Err = errors.New(rs.(string))
}
if rs, ok := r["remaining_payloads"]; ok {
response.Total = int(rs.(float64))
}
return response, err
type statuser interface {
status() int
}
func makeNewCommandEndpoint(svc MDMCommandService) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(NewCommandRequest)
if req.UDID == "" || req.RequestType == "" {
return NewCommandResponse{Err: ErrEmptyRequest}, nil
}
payload, err := svc.NewCommand(req.CommandRequest)
if err != nil {
return NewCommandResponse{Err: err}, nil
}
return NewCommandResponse{Payload: payload}, nil
}
type listEncoder interface {
encodeList(w http.ResponseWriter) error
}
func makeNextCommandEndpoint(svc MDMCommandService) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(NextCommandRequest)
if req.UDID == "" {
return NextCommandResponse{Err: ErrEmptyRequest}, nil
}
payload, total, err := svc.NextCommand(req.UDID)
if err != nil {
return NextCommandResponse{Err: err}, nil
}
return NextCommandResponse{Payload: payload, Total: total}, nil
}
}
func makeDeleteCommandEndpoint(svc MDMCommandService) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(DeleteCommandRequest)
if req.UDID == "" {
return DeleteCommandResponse{Err: ErrEmptyRequest}, nil
}
if req.UUID == "" {
return DeleteCommandResponse{Err: ErrEmptyRequest}, nil
}
total, err := svc.DeleteCommand(req.UDID, req.UUID)
if err != nil {
return DeleteCommandResponse{Err: err}, nil
}
return DeleteCommandResponse{Total: total}, nil
}
}
// encodeResponse is the common method to encode all response types to the
// client. I chose to do it this way because I didn't know if something more
// specific was necessary. It's certainly possible to specialize on a
// per-response (per-method) basis.
func encodeResponse(w http.ResponseWriter, response interface{}) error {
func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
if e, ok := response.(errorer); ok && e.error() != nil {
// Not a Go kit transport error, but a business-logic error.
// Provide those as HTTP errors.
encodeError(w, e.error())
encodeError(ctx, e.error(), w)
return nil
}
jsn, err := json.MarshalIndent(response, "", " ")
if err != nil {
return err
w.Header().Set("Content-Type", "application/json; charset=utf-8")
// for success responses
if e, ok := response.(statuser); ok {
w.WriteHeader(e.status())
if e.status() == http.StatusNoContent {
return nil
}
}
w.Write(jsn)
return nil
// check if this is a collection
if e, ok := response.(listEncoder); ok {
return e.encodeList(w)
}
return json.NewEncoder(w).Encode(response)
}
func encodeError(w http.ResponseWriter, err error) {
w.WriteHeader(codeFrom(err))
response := map[string]interface{}{
"error": err.Error(),
// encode errors from business-logic
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
// unwrap if the error is wrapped by kit http in it's own error type
if httperr, ok := err.(kithttp.Error); ok {
err = httperr.Err
}
jsn, err := json.MarshalIndent(response, "", " ")
if err != nil {
log.Println(err)
return
}
w.Write(jsn)
}
func codeFrom(err error) int {
switch err {
// case ErrNotFound:
// w.WriteHeader(http.StatusNotFound)
// case errEmptyRequest, errBadUUID:
// w.WriteHeader(http.StatusBadRequest)
// case workflow.ErrExists:
// w.WriteHeader(http.StatusConflict)
default:
return http.StatusInternalServerError
w.WriteHeader(http.StatusInternalServerError)
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(map[string]interface{}{
"error": err.Error(),
})
}