add kit service for workflows

This commit is contained in:
Victor Vrantchan
2016-05-03 15:54:27 -04:00
parent 821f702f05
commit 4dfd04fd8d
11 changed files with 421 additions and 29 deletions

View File

@@ -1,9 +1,10 @@
FROM golang:alpine
ENV GO15VENDOREXPERIMENT=1
ENV GLIDE='https://github.com/Masterminds/glide/releases/download/0.10.2/glide-0.10.2-linux-amd64.tar.gz'
RUN apk --no-cache add curl git && \
curl -L https://github.com/Masterminds/glide/releases/download/0.9.1/glide-0.9.1-linux-amd64.tar.gz -o glide.tar.gz && \
curl -L "${GLIDE}" -o glide.tar.gz && \
tar xzf glide.tar.gz -C /tmp && \
mv /tmp/linux-amd64/glide /usr/bin/ && \
rm -f glide.tar.gz && \
@@ -14,7 +15,7 @@ RUN mkdir -p /go/src/github.com/micromdm/micromdm/
WORKDIR /go/src/github.com/micromdm/micromdm/
COPY . /go/src/github.com/micromdm/micromdm/
RUN glide install
RUN go build && mv micromdm /
#RUN glide install
RUN GOGC=500 go build && mv micromdm /
CMD ["/micromdm"]

View File

@@ -17,7 +17,7 @@ import (
var (
// ErrEmptyRequest is returned if the request body is empty
ErrEmptyRequest = errors.New("Request must contain UDID of the device")
ErrEmptyRequest = errors.New("request must contain UDID of the device")
errBadRouting = errors.New("inconsistent mapping between route and handler (programmer error)")
)

11
compose-pg.yml Normal file
View File

@@ -0,0 +1,11 @@
postgres:
image: postgres
restart: always
environment:
- POSTGRES_USER=micromdm
- POSTGRES_PASSWORD=micromdm
- POSTGRES_DB=micromdm
- SSLMODE=disable
ports:
- "127.0.0.1:5432:5432"

5
glide.lock generated
View File

@@ -1,5 +1,5 @@
hash: 8fe304e9385e4fbe963eba356a9784b3217ae75b65f50178322a7e1e41950e8d
updated: 2016-04-24T10:56:48.110433681-04:00
updated: 2016-05-03T12:33:50.516339682-04:00
imports:
- name: github.com/beorn7/perks
version: 3ac7bf7a47d159a033b107610db8a1b6575507a4
@@ -20,6 +20,7 @@ imports:
- metrics
- metrics/prometheus
- transport/http
- log/levels
- name: github.com/go-logfmt/logfmt
version: 08ab82a63ef462ac643ec79e659f023891f588f5
- name: github.com/go-stack/stack
@@ -50,6 +51,8 @@ imports:
- pbutil
- name: github.com/micromdm/mdm
version: d64bbc40594da804b956062912d62453cad70459
- name: github.com/pkg/errors
version: 42fa80f2ac6ed17a977ce826074bd3009593fa9d
- name: github.com/prometheus/client_golang
version: 90c15b5efa0dc32a7d259234e02ac9a99e6d3b82
subpackages:

15
main.go
View File

@@ -13,6 +13,7 @@ import (
"github.com/micromdm/micromdm/command"
"github.com/micromdm/micromdm/connect"
"github.com/micromdm/micromdm/device"
"github.com/micromdm/micromdm/workflow"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/context"
)
@@ -117,6 +118,17 @@ func main() {
)
connectHandler := connect.ServiceHandler(ctx, connectSvc)
workflowDB := workflow.NewDB(
"postgres",
*flPGconn,
workflow.Logger(logger),
workflow.Debug(),
)
workflowSvc := workflow.NewService(workflow.DB(workflowDB))
workflowHandler := workflow.ServiceHandler(ctx, workflowSvc)
// router
r := mux.NewRouter()
r.Methods("PUT", "POST").Path("/mdm/checkin").Handler(checkinHandler)
@@ -126,6 +138,9 @@ func main() {
r.Methods("GET").Path("/mdm/commands/{udid}/next").Handler(commandHandler)
r.Methods("DELETE").Path("/mdm/commands/{udid}/{uuid}").Handler(commandHandler)
r.Handle("/mdm/worflows", workflowHandler)
r.Methods("POST").Path("/mdm/workflows").Handler(workflowHandler)
http.Handle("/", r)
http.Handle("/metrics", stdprometheus.Handler())

View File

@@ -3,8 +3,15 @@ package profile
import "time"
// Profile is a configuration profile
// See https://developer.apple.com/library/ios/featuredarticles/iPhoneConfigurationProfileRef/Introduction/Introduction.html#//apple_ref/doc/uid/TP40010206-CH1-SW4
type Profile struct {
UUID string `plist:"-" json:"-" db:"profile_uuid"`
PayloadIdentifier string `json:"payload_identifier" db:"identifier"`
Data []byte `json:"data" db:"data"`
}
// XMLProfile is a configuration profile
// See https://developer.apple.com/library/ios/featuredarticles/iPhoneConfigurationProfileRef/Introduction/Introduction.html#//apple_ref/doc/uid/TP40010206-CH1-SW4
type XMLProfile struct {
UUID string `plist:"-" json:"-" db:"profile_uuid"`
PayloadContent []PayloadDictionary
PayloadDescription string `plist:",omitempty" json:",omitempty"`

79
workflow/service.go Normal file
View File

@@ -0,0 +1,79 @@
package workflow
import (
"net/http"
"os"
httptransport "github.com/go-kit/kit/transport/http"
"golang.org/x/net/context"
"github.com/go-kit/kit/log"
"github.com/gorilla/mux"
)
// Service manages workflows
type Service interface {
CreateWorkflow(string) (*Workflow, error)
ListWorkflows() ([]Workflow, error)
}
type workflowService struct {
info log.Logger
debug log.Logger
db Datastore
}
func (svc workflowService) CreateWorkflow(name string) (*Workflow, error) {
svc.debug.Log("action", "CreateWorkflow", "name", name)
return svc.db.CreateWorkflow(name)
}
func (svc workflowService) ListWorkflows() ([]Workflow, error) {
svc.debug.Log("Listing Workflows")
return nil, nil
}
// NewService creates a new MDM Command Service
func NewService(options ...func(*config) error) Service {
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 Service
svc = workflowService{
info: infoLogger(conf),
debug: debugLogger(conf),
db: conf.db,
}
return svc
}
// 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 Service) http.Handler {
commonOptions := []httptransport.ServerOption{
httptransport.ServerErrorEncoder(encodeError),
}
newWorkflowEndpoint := makeNewWorkflowEndpoint(svc)
newWorkflowHandler := httptransport.NewServer(
ctx,
newWorkflowEndpoint,
decodeNewWorkflowRequest,
encodeResponse,
commonOptions...,
)
r := mux.NewRouter()
r.Methods("POST").Path("/mdm/workflows").Handler(newWorkflowHandler)
return r
}

109
workflow/transport.go Normal file
View File

@@ -0,0 +1,109 @@
package workflow
import (
"encoding/json"
"errors"
"log"
"net/http"
"github.com/go-kit/kit/endpoint"
"golang.org/x/net/context"
)
var (
// ErrEmptyRequest is returned if the request body is empty
ErrEmptyRequest = errors.New("request must contain a name")
errBadRouting = errors.New("inconsistent mapping between route and handler (programmer error)")
)
// NewWorkflowRequest in an HTTP request for a new workflow
type NewWorkflowRequest struct {
Name string `json:"name"`
}
func decodeNewWorkflowRequest(r *http.Request) (interface{}, error) {
var request NewWorkflowRequest
err := json.NewDecoder(r.Body).Decode(&request)
return request, err
}
// NewWorkflowResponse is a command reponse
type NewWorkflowResponse struct {
*Workflow
Err error `json:"error,omitempty"`
}
// for API statuses other than 200OK
type statuser interface {
status() int
}
func (r NewWorkflowResponse) status() int { return 201 }
func (r NewWorkflowResponse) 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
}
// 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 {
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())
return nil
}
// for success responses
if e, ok := response.(statuser); ok {
w.WriteHeader(e.status())
}
jsn, err := json.MarshalIndent(response, "", " ")
if err != nil {
return err
}
w.Write(jsn)
return nil
}
func encodeError(w http.ResponseWriter, err error) {
w.WriteHeader(codeFrom(err))
response := map[string]interface{}{
"error": err.Error(),
}
jsn, err := json.MarshalIndent(response, "", " ")
if err != nil {
log.Println(err)
return
}
w.Write(jsn)
}
func codeFrom(err error) int {
switch err {
default:
return http.StatusInternalServerError
}
}
// ENDPOINTS
func makeNewWorkflowEndpoint(svc Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(NewWorkflowRequest)
if req.Name == "" {
return NewWorkflowResponse{Err: ErrEmptyRequest}, nil
}
workflow, err := svc.CreateWorkflow(req.Name)
if err != nil {
return NewWorkflowResponse{Err: err}, nil
}
return NewWorkflowResponse{Workflow: workflow}, nil
}
}

114
workflow/transport_test.go Normal file
View File

@@ -0,0 +1,114 @@
package workflow
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"github.com/go-kit/kit/log"
"golang.org/x/net/context"
)
var (
client = NewTestClient()
jsonMedia = "application/json; charset=utf-8"
)
func newTestServer() *httptest.Server {
ctx := context.Background()
logger := log.NewLogfmtLogger(os.Stderr)
//
workflowDB := NewDB(
"postgres",
testConn,
Logger(logger),
Debug(),
)
workflowSvc := NewService(DB(workflowDB), Logger(logger), Debug())
workflowHandler := ServiceHandler(ctx, workflowSvc)
server := httptest.NewServer(workflowHandler)
return server
}
type TestClient struct {
client *http.Client
server *httptest.Server
// Base URL for API requests.
BaseURL *url.URL
}
func NewTestClient() *TestClient {
client := &TestClient{client: http.DefaultClient}
client.server = newTestServer()
client.BaseURL, _ = url.Parse(client.server.URL)
client.BaseURL.Path = "mdm/"
return client
}
// create testclient request
func (c *TestClient) NewRequest(endpoint, resource, mediaType, method string) (*http.Request, error) {
var urlStr string
if resource != "" {
urlStr = endpoint + "/" + resource
} else {
urlStr = endpoint
}
rel, err := url.Parse(urlStr)
if err != nil {
return nil, err
}
u := c.BaseURL.ResolveReference(rel)
req, err := http.NewRequest(method, u.String(), nil)
if err != nil {
return nil, err
}
return req, nil
}
// run the request
func (c *TestClient) Do(req *http.Request, into interface{}) (*http.Response, error) {
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
return resp, nil
}
func (c *TestClient) teardown() {
c.server.Close()
}
// a face io.ReadCloser for constructing request Body
type nopCloser struct {
io.Reader
}
func (nopCloser) Close() error { return nil }
// HTTP Test Code
var createWorkflowRequest = []byte(`{"name" :"http_test_workflow"}`)
func TestHTTPCreateWorkflow(t *testing.T) {
req, err := client.NewRequest("workflows", "", jsonMedia, "POST")
if err != nil {
t.Fatal(err)
}
body := createWorkflowRequest
req.Body = &nopCloser{bytes.NewBuffer(body)}
resp, err := client.Do(req, nil)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusCreated {
t.Error("Expected", http.StatusCreated, "got", resp.StatusCode)
io.Copy(os.Stdout, resp.Body)
}
}

View File

@@ -3,15 +3,15 @@ package workflow
import (
"database/sql"
"errors"
"fmt"
"os"
"time"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/levels"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq" // postgres driver
"github.com/micromdm/micromdm/profile"
"github.com/pkg/errors"
"golang.org/x/net/context"
)
@@ -39,11 +39,16 @@ type application struct {
ManifestURL string
}
type configProfile struct {
UUID string `plist:"-" json:"-" db:"profile_uuid"`
PayloadIdentifier string `json:"payload_identifier" db:"identifier"`
}
// Workflow is a device workflow
type Workflow struct {
UUID string `db:"workflow_uuid"`
Name string `db:"name"`
Profiles []profile.Profile
UUID string `json:"uuid" db:"workflow_uuid"`
Name string `json:"name" db:"name"`
Profiles []configProfile `json:"profiles"`
// Applications []application
// IncludedWorkflows []Workflow
}
@@ -57,6 +62,8 @@ type Datastore interface {
}
type pgDatastore struct {
info log.Logger
debug log.Logger
*sqlx.DB
}
@@ -65,11 +72,14 @@ func (db pgDatastore) CreateWorkflow(name string) (*Workflow, error) {
workflow := &Workflow{Name: name}
err := db.QueryRow(createWorkflowStmt, name).Scan(&workflow.UUID)
if err == sql.ErrNoRows {
db.debug.Log("err", "exists", "workflow", name)
return nil, ErrNoRowsModified
}
if err != nil {
return nil, err
db.info.Log("err", err, "workflow", name)
return nil, errors.Wrap(err, "create workflow failed")
}
db.debug.Log("msg", "created", "workflow", name, "uuid", workflow.UUID)
return workflow, nil
}
@@ -94,28 +104,33 @@ func (db pgDatastore) GetWorkflows() ([]Workflow, error) {
// AddProfile adds a profile to a workflow
func (db pgDatastore) AddProfile(wfUUID, pfUUID string) error {
db.debug.Log("action", "AddProfile", "workflow", wfUUID, "profile", pfUUID)
result, err := db.Exec(
addProfileStmt,
wfUUID,
pfUUID,
)
if err != nil {
db.debug.Log("action", "AddProfile", "workflow", wfUUID, "profile", pfUUID, "err", err)
return err
}
if _, err := result.RowsAffected(); err != nil {
return err
}
db.debug.Log("action", "AddProfile", "workflow", wfUUID, "profile", pfUUID, "status", "success")
return nil
}
// RemoveProfile removes a profile wrom a workflow
func (db pgDatastore) RemoveProfile(wfUUID, pfUUID string) error {
db.debug.Log("action", "RemoveProfile", "workflow", wfUUID, "profile", pfUUID)
result, err := db.Exec(
removeProfileStmt,
wfUUID,
pfUUID,
)
if err != nil {
db.debug.Log("err", err)
return err
}
if _, err := result.RowsAffected(); err != nil {
@@ -124,8 +139,8 @@ func (db pgDatastore) RemoveProfile(wfUUID, pfUUID string) error {
return nil
}
func (db pgDatastore) getProfilesForWorkflow(workflowUUID string) ([]profile.Profile, error) {
var profiles []profile.Profile
func (db pgDatastore) getProfilesForWorkflow(workflowUUID string) ([]configProfile, error) {
var profiles []configProfile
err := db.Select(&profiles, getProfilesForWorkflowStmt, workflowUUID)
if err != nil {
return nil, err
@@ -141,14 +156,26 @@ func Logger(logger log.Logger) func(*config) error {
}
}
// Debug adds a debug logger to the database config
func Debug() func(*config) error {
return func(c *config) error {
c.debug = true
return nil
}
}
type config struct {
db Datastore
context context.Context
logger log.Logger
debug bool
}
// NewDB creates a new databases connection
func NewDB(driver, conn string, options ...func(*config) error) Datastore {
conf := &config{}
conf := &config{
logger: log.NewNopLogger(),
}
defaultLogger := log.NewLogfmtLogger(os.Stderr)
for _, option := range options {
if err := option(conf); err != nil {
@@ -156,9 +183,6 @@ func NewDB(driver, conn string, options ...func(*config) error) Datastore {
os.Exit(1)
}
}
if conf.logger == nil {
conf.logger = log.NewNopLogger()
}
switch driver {
case "postgres":
db, err := sqlx.Open(driver, conn)
@@ -183,7 +207,11 @@ func NewDB(driver, conn string, options ...func(*config) error) Datastore {
migrate(db)
// TODO: configurable with default
db.SetMaxOpenConns(5)
store := pgDatastore{db}
store := pgDatastore{
info: infoLogger(conf),
debug: debugLogger(conf),
DB: db,
}
return store
default:
conf.logger.Log("err", "unknown driver")
@@ -192,9 +220,28 @@ func NewDB(driver, conn string, options ...func(*config) error) Datastore {
}
}
func infoLogger(conf *config) log.Logger {
return levels.New(conf.logger).Info()
}
func debugLogger(conf *config) log.Logger {
if conf.debug {
logger := levels.New(conf.logger).Debug()
ctx := log.NewContext(logger).With("caller", log.DefaultCaller)
return ctx
}
return log.NewNopLogger()
}
func migrate(db *sqlx.DB) {
schema := `
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE IF NOT EXISTS profiles (
profile_uuid uuid PRIMARY KEY
DEFAULT uuid_generate_v4(),
identifier text UNIQUE NOT NULL
);
CREATE TABLE IF NOT EXISTS workflows (
workflow_uuid uuid PRIMARY KEY
DEFAULT uuid_generate_v4(),

View File

@@ -1,10 +1,12 @@
package workflow
import (
"log"
"os"
"testing"
stdLog "log"
"github.com/go-kit/kit/log"
"github.com/jmoiron/sqlx"
"github.com/micromdm/micromdm/profile"
)
@@ -22,10 +24,10 @@ func newDB(driver string) *sqlx.DB {
func TestMain(m *testing.M) {
db := newDB("postgres")
setup(db)
// setup(db)
retCode := m.Run()
teardown(db)
client.teardown()
// call with result of m.Run()
os.Exit(retCode)
}
@@ -42,7 +44,7 @@ func setup(db *sqlx.DB) {
}
func teardown(db *sqlx.DB) {
log.Println("workflow: dropping test tables")
stdLog.Println("workflow: dropping test tables")
drop := `
DROP TABLE IF EXISTS workflow_workflow;
DROP TABLE IF EXISTS workflow_profile;
@@ -64,7 +66,8 @@ func TestNewDBConnection(t *testing.T) {
}
func TestCreateWorkflow(t *testing.T) {
store := NewDB("postgres", testConn)
store := NewDB("postgres", testConn,
Logger(log.NewLogfmtLogger(os.Stderr)), Debug())
db := newDB("postgres")
db.MustExec(`INSERT INTO profiles (identifier) VALUES ($1);`, "com.micromdm.test")
db.MustExec(`INSERT INTO profiles (identifier) VALUES ($1);`, "com.micromdm.test2")
@@ -79,6 +82,7 @@ func TestCreateWorkflow(t *testing.T) {
t.Fatal(err)
}
// duplicate test
wf, err = store.CreateWorkflow("test_workflowX")
if err == nil {
t.Fatalf("create workflow should fail on duplicate workflow, got no errors")
@@ -108,7 +112,8 @@ func TestCreateWorkflow(t *testing.T) {
}
func TestGetWorkflows(t *testing.T) {
store := NewDB("postgres", testConn)
store := NewDB("postgres", testConn,
Logger(log.NewLogfmtLogger(os.Stderr)), Debug())
workflows, err := store.GetWorkflows()
if err != nil {
t.Fatal(err)
@@ -117,13 +122,14 @@ func TestGetWorkflows(t *testing.T) {
t.Fatal("should have at least one workflow")
}
if len(workflows[1].Profiles) == 0 {
t.Fatal("should have at least one profile")
}
// if len(workflows[1].Profiles) == 0 {
// t.Fatal("should have at least one profile")
// }
}
func TestRemoveProfile(t *testing.T) {
store := NewDB("postgres", testConn)
store := NewDB("postgres", testConn,
Logger(log.NewLogfmtLogger(os.Stderr)), Debug())
db := newDB("postgres")
var pf profile.Profile
err := db.Get(&pf, "SELECT * FROM profiles WHERE identifier=$1", "com.micromdm.test")