mirror of
https://github.com/micromdm/micromdm/
synced 2026-08-12 21:35:40 +08:00
add kit service for workflows
This commit is contained in:
79
workflow/service.go
Normal file
79
workflow/service.go
Normal 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
109
workflow/transport.go
Normal 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
114
workflow/transport_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user