foundry-fn-go
is a community-driven, open source project designed to enable the authoring of functions.
While not a formal CrowdStrike product, foundry-fn-go
is maintained by CrowdStrike and supported in partnership
with the open source developer community.
The SDK can be installed or updated via go get
:
go get github.com/CrowdStrike/foundry-fn-go
The SDK can be built from source via standard build
:
go mod tidy
go build .
Add the SDK to your project by following the installation instructions above, then create
your main.go
:
package main
import (
"context"
"encoding/json"
"errors"
"net/http"
fdk "github.com/CrowdStrike/foundry-fn-go"
)
func main() {
fdk.Run(context.Background(), newHandler)
}
// newHandler here is showing how a config is integrated. It is using generics,
// so we can unmarshal the config into a concrete type and then validate it. The
// OK method is run to validate the contents of the config.
func newHandler(_ context.Context, cfg config) fdk.Handler {
mux := fdk.NewMux()
mux.Post("/echo", fdk.HandlerFn(func(ctx context.Context, r fdk.Request) fdk.Response {
return fdk.Response{
Body: r.Body,
Code: 201,
Header: http.Header{"X-Cs-Method": []string{r.Method}},
}
}))
return mux
}
type config struct {
Int int `json:"integer"`
Str string `json:"string"`
}
func (c config) OK() error {
var errs []error
if c.Int < 1 {
errs = append(errs, errors.New("integer must be greater than 0"))
}
if c.Str == "" {
errs = append(errs, errors.New("non empty string must be provided"))
}
return errors.Join(errs...)
}
config
: A type the raw json config is unmarshalled into.Request
: Request payload and metadata. At the time of this writing, theRequest
struct consists of:Body
: The raw request payload as given in the Function Gatewaybody
payload field.Params
: Contains request headers and query parameters.URL
: The request path relative to the function as a string.Method
: The request HTTP method or verb.Context
: Caller-supplied raw context.AccessToken
: Caller-supplied access token.
Response
- The
Response
contains fieldsBody
(the payload of the response),Code
(an HTTP status code),Errors
(a slice ofAPIError
s), andHeaders
(a map of any special HTTP headers which should be present on the response).
- The
main()
: Initialization and bootstrap logic all contained with fdk.Run and handler constructor.
more examples can be found at:
The SDK provides an out-of-the-box runtime for executing the function. A basic HTTP server will be listening on port 8081.
# build the project which uses the sdk
cd my-project && go mod tidy && go build -o run_me .
# run the executable. config should be in json format here.
CS_FN_CONFIG_PATH=$PATH_TO_CONFIG_JSON ./run_me
Requests can now be made against the executable.
curl -X POST http://localhost:8081/ \
-H "Content-Type: application/json" \
--data '{
"body": {
"foo": "bar"
},
"method": "POST",
"url": "/greetings"
}'
Foundry Function Go ships with gofalcon pre-integrated and a convenience constructor. While it is not strictly necessary to use convenience function, it is recommended.
Important: Create a new instance of the gofalcon
client on each request.
package main
import (
"context"
"net/http"
/* omitting other imports */
fdk "github.com/crowdstrike/foundry-fn-go"
)
func newHandler(_ context.Context, cfg config) fdk.Handler {
mux := fdk.NewMux()
mux.Post("/echo", fdk.HandlerFn(func(ctx context.Context, r fdk.Request) fdk.Response {
client, err := fdk.FalconClient(ctx, request)
if err != nil {
if err == fdk.ErrFalconNoToken {
// not a processable request
return fdk.Response{ /* snip */ }
}
// some other error - see gofalcon documentation
}
// trim rest
}))
return mux
}
// omitting rest of implementation
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.
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
}
Within the fdktest pkg, we maintain test funcs for validating a schema and its integration with a handler. Example:
package somefn_test
import (
"context"
"net/http"
"testing"
fdk "github.com/CrowdStrike/foundry-fn-go"
"github.com/CrowdStrike/foundry-fn-go/fdktest"
)
func TestHandlerIntegration(t *testing.T) {
reqSchema := `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"postalCode": {
"type": "string",
"description": "The person's first name.",
"pattern": "\\d{5}"
},
"optional": {
"type": "string",
"description": "The person's last name."
}
},
"required": [
"postalCode"
]
}`
respSchema := `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"foo": {
"type": "string",
"description": "The person's first name.",
"enum": ["bar"]
}
},
"required": [
"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)
}
}
Please refrain from using os.Exit
. When an error is encountered, we want to return a message
to the caller. Otherwise, it'll os.Exit
and all stakeholders will have no idea what to make
of it. Instead, use something like the following in fdk.Run
:
// sdk.go
package fdk
import (
"context"
)
// Run is the meat and potatoes. This is the entrypoint for everything.
func Run[T Cfg](ctx context.Context, newHandlerFn func(_ context.Context, cfg T) Handler) {
// ... trim
cfg, loadErr := readCfg[T](ctx)
if loadErr != nil {
if loadErr.err != nil {
// these being specific to the author's eyes only
logger.Error("failed to load config", "err", loadErr.err)
}
// here we return a useful error to the caller of the function
run(ctx, logger, ErrHandler(loadErr.apiErr))
return
}
h := newHandlerFn(ctx, cfg)
run(ctx, logger, h)
return
}