From f81bc3c0c299aca6782b1dac14ae768bccd20882 Mon Sep 17 00:00:00 2001 From: Victor Vrantchan Date: Tue, 17 May 2016 09:46:01 -0400 Subject: [PATCH] clean up command service --- command/datastore.go | 108 ++++++++--------- command/endpoint.go | 100 ++++++++++++++++ command/service.go | 105 +++-------------- command/transport.go | 274 +++++++++++++------------------------------ 4 files changed, 245 insertions(+), 342 deletions(-) create mode 100644 command/endpoint.go diff --git a/command/datastore.go b/command/datastore.go index 121252b6..6faac8b9 100644 --- a/command/datastore.go +++ b/command/datastore.go @@ -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) + } +} diff --git a/command/endpoint.go b/command/endpoint.go new file mode 100644 index 00000000..7af1959b --- /dev/null +++ b/command/endpoint.go @@ -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 + } +} diff --git a/command/service.go b/command/service.go index d9d11455..31aeed7a 100644 --- a/command/service.go +++ b/command/service.go @@ -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) } diff --git a/command/transport.go b/command/transport.go index e87ae2bf..17b456b7 100644 --- a/command/transport.go +++ b/command/transport.go @@ -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(), + }) }