Skip to content

CrowdStrike/foundry-fn-go

Repository files navigation

CrowdStrike Falcon

Foundry Function as a Service Go SDK

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.

Installation ⚙️

Via go get

The SDK can be installed or updated via go get:

go get github.com/CrowdStrike/foundry-fn-go

From source

The SDK can be built from source via standard build:

go mod tidy
go build .

Quickstart 💫

Code

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...)
}
  1. config: A type the raw json config is unmarshalled into.
  2. Request: Request payload and metadata. At the time of this writing, the Request struct consists of:
    1. Body: The raw request payload as given in the Function Gateway body payload field.
    2. Params: Contains request headers and query parameters.
    3. URL: The request path relative to the function as a string.
    4. Method: The request HTTP method or verb.
    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 APIErrors), 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:

Testing locally

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"
}'

Convenience Functionality 🧰

gofalcon

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

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.

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

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)
	}
}

A note on os.Exit

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
}


WE STOP BREACHES