Skip to content

Commit

Permalink
Merge pull request #11 from CrowdStrike/feat/workflow_integrations
Browse files Browse the repository at this point in the history
feat: add helpers for function handlers that integrate with Falcon Fusion workflows
  • Loading branch information
jsteenb2 committed Nov 8, 2023
2 parents 1847746 + 3925e1a commit 331e596
Show file tree
Hide file tree
Showing 3 changed files with 252 additions and 30 deletions.
92 changes: 66 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,14 @@ func (c config) OK() error {
5. `Context`: Caller-supplied raw context.
6. `AccessToken`: Caller-supplied access token.
3. `Response`
1. The `Response` contains fields `Body` (the payload of the response), `Code` (an HTTP status code),
`Errors` (a slice of `APIError`s), and `Headers` (a map of any special HTTP headers which should be present on
the response).
1. The `Response` contains fields `Body` (the payload of the response), `Code` (an HTTP status code),
`Errors` (a slice of `APIError`s), and `Headers` (a map of any special HTTP headers which should be present on
the response).
4. `main()`: Initialization and bootstrap logic all contained with fdk.Run and handler constructor.

more examples can be found at:
* [fn with config](examples/fn_config)

* [fn with config](examples/fn_config)
* [fn without config](examples/fn_no_config)
* [more complex/complete example](examples/complex)

Expand Down Expand Up @@ -167,7 +168,44 @@ func newHandler(_ context.Context, cfg config) fdk.Handler {
// omitting rest of implementation
```

---
## Integration with Falcon Fusion workflows

When integrating with a Falcon Fusion workflow, the `Request.Context` can be decoded into
`WorkflowCtx` type. You may json unmarshal into that type. The type provides some additional
context from the workflow. This context is from the execution of the workflow, and may be
dynamic in some usecases. To simplify things further for authors, we have introduced two
handler functions to remove the boilerplate of dealing with a workflow.

```go
package somefn

import (
"context"

fdk "github.com/CrowdStrike/foundry-fn-go"
)

type reqBody struct {
Foo string `json:"foo"`
}

func New(ctx context.Context, _ fdk.SkipCfg) fdk.Handler {
m := fdk.NewMux()

// for get/delete reqs use HandleWorkflow. The path is just an examples, any payh can be used.
m.Get("/workflow", fdk.HandleWorkflow(func(ctx context.Context, r fdk.Request, workflowCtx fdk.WorkflowCtx) fdk.Response {
// ... trim impl
}))

// for handlers that expect a request body (i.e. PATCH/POST/PUT)
m.Post("/workflow", fdk.HandleWorkflowOf(func(ctx context.Context, r fdk.RequestOf[reqBody], workflowCtx fdk.WorkflowCtx) fdk.Response {
// .. trim imple
}))

return m
}

```

## Working with Request and Response Schemas

Expand All @@ -178,16 +216,16 @@ with a handler. Example:
package somefn_test

import (
"context"
"net/http"
"testing"
"context"
"net/http"
"testing"

fdk "github.com/CrowdStrike/foundry-fn-go"
"github.com/CrowdStrike/foundry-fn-go/fdktest"
fdk "github.com/CrowdStrike/foundry-fn-go"
"github.com/CrowdStrike/foundry-fn-go/fdktest"
)

func TestHandlerIntegration(t *testing.T) {
reqSchema := `{
reqSchema := `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
Expand All @@ -206,7 +244,7 @@ func TestHandlerIntegration(t *testing.T) {
]
}`

respSchema := `{
respSchema := `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
Expand All @@ -220,23 +258,25 @@ func TestHandlerIntegration(t *testing.T) {
"foo"
]
}`
handler := fdk.HandlerFn(func(ctx context.Context, r fdk.Request) fdk.Response {
return fdk.Response{Body: fdk.JSON(map[string]string{"foo": "bar"})}
})

req := fdk.Request{
URL: "/",
Method: http.MethodPost,
Body: json.RawMessage(`{"postalCode": "55755"}`),
}

err := fdktest.HandlerSchemaOK(handler, req, reqSchema, respSchema)
if err != nil {
t.Fatal("unexpected err: ", err)
}
handler := fdk.HandlerFn(func(ctx context.Context, r fdk.Request) fdk.Response {
return fdk.Response{Body: fdk.JSON(map[string]string{"foo": "bar"})}
})

req := fdk.Request{
URL: "/",
Method: http.MethodPost,
Body: json.RawMessage(`{"postalCode": "55755"}`),
}

err := fdktest.HandlerSchemaOK(handler, req, reqSchema, respSchema)
if err != nil {
t.Fatal("unexpected err: ", err)
}
}

```

---

<p align="center"><img src="https://raw.githubusercontent.com/CrowdStrike/falconpy/main/docs/asset/cs-logo-footer.png"><BR/><img width="250px" src="https://raw.githubusercontent.com/CrowdStrike/falconpy/main/docs/asset/adversary-red-eyes.png"></P>
<h3><P align="center">WE STOP BREACHES</P></h3>
153 changes: 153 additions & 0 deletions runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,155 @@ func TestRun_httprunner(t *testing.T) {
t.Run(tt.name, fn)
}
})

t.Run("when executing with workflow integration", func(t *testing.T) {
type (
inputs struct {
body []byte
context []byte
method string
path string
}

wantFn func(t *testing.T, resp *http.Response, status int, workflowCtx fdk.WorkflowCtx, errs []fdk.APIError)
)

tests := []struct {
name string
inputs inputs
newHandlerFn func(ctx context.Context, cfg fdk.SkipCfg) fdk.Handler
want wantFn
}{
{
name: "with workflow integration and GET request should pass",
inputs: inputs{
context: []byte(`{"app_id":"aPp1","cid":"ciD1"}`),
method: "GET",
path: "/workflow",
},
newHandlerFn: func(ctx context.Context, cfg fdk.SkipCfg) fdk.Handler {
m := fdk.NewMux()
m.Get("/workflow", fdk.HandleWorkflow(func(ctx context.Context, r fdk.Request, workflowCtx fdk.WorkflowCtx) fdk.Response {
return fdk.Response{
Code: 202,
Body: fdk.JSON(workflowCtx),
}
}))
return m
},
want: func(t *testing.T, resp *http.Response, code int, workflowCtx fdk.WorkflowCtx, errs []fdk.APIError) {
equalVals(t, 202, resp.StatusCode)
equalVals(t, 202, code)

if len(errs) > 0 {
t.Errorf("received unexpected errors\n\t\tgot:\t%+v", errs)
}

want := fdk.WorkflowCtx{AppID: "aPp1", CID: "ciD1"}
if want != workflowCtx {
t.Errorf("workflow contexts to not match:\n\t\twant: %#v\n\t\tgot: %#v", want, workflowCtx)
}
},
},
{
name: "with workflow integration and POST request should pass",
inputs: inputs{
body: []byte(`{"dodgers":"stink"}`),
context: []byte(`{"app_id":"aPp1","cid":"ciD1"}`),
method: "POST",
path: "/workflow",
},
newHandlerFn: func(ctx context.Context, cfg fdk.SkipCfg) fdk.Handler {
m := fdk.NewMux()
m.Post("/workflow", fdk.HandleWorkflowOf(func(ctx context.Context, r fdk.RequestOf[reqBodyDodgers], workflowCtx fdk.WorkflowCtx) fdk.Response {
workflowCtx.CID += "-" + r.Body.Dodgers
return fdk.Response{
Code: 202,
Body: fdk.JSON(workflowCtx),
}
}))
return m
},
want: func(t *testing.T, resp *http.Response, code int, workflowCtx fdk.WorkflowCtx, errs []fdk.APIError) {
equalVals(t, 202, resp.StatusCode)
equalVals(t, 202, code)

if len(errs) > 0 {
t.Errorf("received unexpected errors\n\t\tgot:\t%+v", errs)
}

want := fdk.WorkflowCtx{AppID: "aPp1", CID: "ciD1-stink"}
if want != workflowCtx {
t.Errorf("workflow contexts to not match:\n\t\twant: %#v\n\t\tgot: %#v", want, workflowCtx)
}
},
},
}

for _, tt := range tests {
fn := func(t *testing.T) {
port := newIP(t)
t.Setenv("PORT", port)

readyChan := make(chan struct{})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

done := make(chan struct{})
go func() {
defer close(done)
fdk.Run(ctx, func(ctx context.Context, cfg fdk.SkipCfg) fdk.Handler {
h := tt.newHandlerFn(ctx, cfg)
close(readyChan)
return h
})
}()

select {
case <-readyChan:
case <-time.After(50 * time.Millisecond):
}

body := struct {
Body json.RawMessage `json:"body"`
Context json.RawMessage `json:"context"`
Method string `json:"method"`
URL string `json:"url"`
}{
Body: tt.inputs.body,
URL: tt.inputs.path,
Method: tt.inputs.method,
Context: tt.inputs.context,
}

b, err := json.Marshal(body)
mustNoErr(t, err)

req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
"http://localhost:"+port,
bytes.NewBuffer(b),
)
mustNoErr(t, err)

resp, err := http.DefaultClient.Do(req)
mustNoErr(t, err)
cancel()
defer func() { _ = resp.Body.Close() }()

var got struct {
Code int `json:"code"`
Errs []fdk.APIError `json:"errors"`
WorkflowCtx fdk.WorkflowCtx `json:"body"`
}
decodeBody(t, resp.Body, &got)

tt.want(t, resp, got.Code, got.WorkflowCtx, got.Errs)
}
t.Run(tt.name, fn)
}
})
}

type config struct {
Expand Down Expand Up @@ -498,6 +647,10 @@ type (
Method string `json:"method"`
AccessToken string `json:"access_token"`
}

reqBodyDodgers struct {
Dodgers string `json:"dodgers"`
}
)

func newSimpleHandler(cfg config) fdk.Handler {
Expand Down
37 changes: 33 additions & 4 deletions sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,22 +60,51 @@ func HandleFnOf[T any](fn func(context.Context, RequestOf[T]) Response) Handler
})
}

// WorkflowCtx is the Request.Context field when integrating a function with Falcon Fusion workflow.
type WorkflowCtx struct {
AppID string `json:"app_id"`
CID string `json:"cid"`
}

// HandleWorkflow provides a means to create a handler with workflow integration. This function
// does not have an opinion on the request body but does expect a workflow integration. Typically,
// this is useful for DELETE/GET handlers.
func HandleWorkflow(fn func(context.Context, Request, WorkflowCtx) Response) Handler {
return HandlerFn(func(ctx context.Context, r Request) Response {
var w WorkflowCtx
if err := json.Unmarshal(r.Context, &w); err != nil {
return Response{Errors: []APIError{{Code: http.StatusBadRequest, Message: "failed to unmarshal workflow context: " + err.Error()}}}
}

return fn(ctx, r, w)
})
}

// HandleWorkflowOf provides a means to create a handler with Workflow integration. This
// function is useful when you expect a request body and have workflow integrations. Typically, this
// is with PATCH/POST/PUT handlers.
func HandleWorkflowOf[T any](fn func(context.Context, RequestOf[T], WorkflowCtx) Response) Handler {
return HandleWorkflow(func(ctx context.Context, r Request, workflowCtx WorkflowCtx) Response {
next := HandleFnOf(func(ctx context.Context, r RequestOf[T]) Response {
return fn(ctx, r, workflowCtx)
})
return next.Handle(ctx, r)
})
}

type (
// Request defines a request structure that is given to the runner. The Body is set to
// json.RawMessage, to enable decoration/middleware.
Request RequestOf[json.RawMessage]

// RequestOf provides a generic body we can target our unmarshaling into.
RequestOf[T any] struct {
Body T
// TODO(berg): can we axe Context? have workflow put details in the body/headers/params instead?
Body T
Context json.RawMessage
Params struct {
Header http.Header
Query url.Values
}
// TODO(berg): explore changing this field to Path, as URL is misleading. It's never
// an fqdn, only the path of the url.
URL string
Method string
AccessToken string
Expand Down

0 comments on commit 331e596

Please sign in to comment.