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"
"errors"
"log/slog"
"net/http"
fdk "github.com/CrowdStrike/foundry-fn-go"
)
func main() {
fdk.Run(context.Background(), newHandler)
}
type request struct {
Name string `json:"name"`
Val string `json:"val"`
}
// 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, logger *slog.Logger, cfg config) fdk.Handler {
mux := fdk.NewMux()
mux.Get("/name", fdk.HandlerFn(func(_ context.Context, r fdk.Request) fdk.Response {
return fdk.Response{
Body: fdk.JSON(map[string]string{"name": r.Params.Query.Get("name")}),
Code: 200,
}
}))
mux.Post("/echo", fdk.HandlerFnOfOK(func(_ context.Context, r fdk.RequestOf[request]) fdk.Response {
if r.Body.Name == "kaboom" {
logger.Error("encountered the kaboom")
}
return fdk.Response{
Body: fdk.JSON(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.logger
: A dedicated logger is provided to capture function logs in all environments (both locally and distributed).- Using a different logger may produce logs in the runtime but won't make it into the logscale infrastructure.
Request
: Request payload and metadata. At the time of this writing, theRequest
struct consists of:Body
: The input io.Reader for the payload as given in the Function Gatewaybody
payload field or streamed in.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.
RequestOf
: The same as Request only that the Body field is json unmarshalled into the generic type (i.e.request
type above)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 integrates with gofalcon in a few simple lines.
package main
import (
"context"
"log/slog"
fdk "github.com/CrowdStrike/foundry-fn-go"
"github.com/CrowdStrike/gofalcon/falcon"
"github.com/CrowdStrike/gofalcon/falcon/client"
)
func newHandler(_ context.Context, _ *slog.Logger, cfg config) fdk.Handler {
mux := fdk.NewMux()
mux.Post("/echo", fdk.HandlerFn(func(ctx context.Context, r fdk.Request) fdk.Response {
client, err := newFalconClient(ctx, r.AccessToken)
if err != nil {
if err == falcon.ErrFalconNoToken {
// not a processable request
return fdk.Response{ /* snip */ }
}
// some other error - see gofalcon documentation
}
// trim rest
}))
return mux
}
func newFalconClient(ctx context.Context, token string) (*client.CrowdStrikeAPISpecification, error) {
opts := fdk.FalconClientOpts()
return falcon.NewClient(&falcon.ApiConfig{
AccessToken: token,
Cloud: falcon.Cloud(opts.Cloud),
Context: ctx,
UserAgentOverride: out.UserAgent,
})
}
// 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"
"log/slog"
fdk "github.com/CrowdStrike/foundry-fn-go"
)
type reqBody struct {
Foo string `json:"foo"`
}
func New(ctx context.Context, _ *slog.Logger, _ 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
:
package main
import (
"context"
"log/slog"
"net/http"
fdk "github.com/CrowdStrike/foundry-fn-go"
)
func newHandler(_ context.Context, logger *slog.Logger, _ fdk.SkipCfg) fdk.Handler {
foo, err := newFoo()
if err != nil {
// leave yourself/author the nitty-gritty details and return to the end user/caller
// a valid error that doesn't expose all the implementation details
logger.Error("failed to create foo", "err", err.Error())
return fdk.ErrHandler(fdk.APIError{Code: http.StatusInternalServerError, Message: "unexpected error starting function"})
}
mux := fdk.NewMux()
// ...trim rest of setup
return mux
}