fdk

package module
v0.22.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 20, 2024 License: MIT Imports: 22 Imported by: 3

README

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

  1. config: A type the raw json config is unmarshalled into.
  2. logger: A dedicated logger is provided to capture function logs in all environments (both locally and distributed).
    1. Using a different logger may produce logs in the runtime but won't make it into the logscale infrastructure.
  3. Request: Request payload and metadata. At the time of this writing, the Request struct consists of:
    1. Body: The input io.Reader for the payload as given in the Function Gateway body payload field or streamed in.
    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.
  4. RequestOf: The same as Request only that the Body field is json unmarshalled into the generic type (i.e. request type above)
  5. 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).
  6. 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 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

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

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:

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
}



WE STOP BREACHES

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrCfgNotFound defines the inability to find a config at the expected location.
	ErrCfgNotFound = errors.New("no config provided")
)

Functions

func FalconClientOpts added in v0.9.0

func FalconClientOpts() (out struct {
	Cloud     string
	UserAgent string
})

FalconClientOpts provides the cloud for use with the falcon client. To setup the falcon Client you can follow the following example.

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

func JSON added in v0.4.0

func JSON(v any) json.Marshaler

JSON jsonifies the input to valid json upon request marshaling.

func RegisterConfigLoader added in v0.4.0

func RegisterConfigLoader(loaderType string, cr ConfigLoader)

RegisterConfigLoader will register a config loader at the specified type. Similar to registering a database with the database/sql, you're able to provide a config for use at runtime. During Run, the config loader defined by the env var, CS_CONFIG_LOADER_TYPE, is used. If one is not provided, then the fs config loader will be used.

func RegisterRunner added in v0.4.0

func RegisterRunner(runnerType string, r Runner)

RegisterRunner registers a runner.

func Run added in v0.4.0

func Run[T Cfg](ctx context.Context, newHandlerFn func(context.Context, *slog.Logger, T) Handler)

Run is the meat and potatoes. This is the entrypoint for everything.

Types

type APIError

type APIError struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

APIError defines a error that is shared back to the caller.

func (APIError) Error added in v0.7.0

func (a APIError) Error() string

Error provides a human readable error message.

type Cfg added in v0.4.0

type Cfg interface {
	OK() error
}

Cfg marks the configuration type parameter. Any config type must have a validation method, OK, defined on it.

type ConfigLoader added in v0.4.0

type ConfigLoader interface {
	LoadConfig(ctx context.Context) ([]byte, error)
}

ConfigLoader defines the behavior for loading config.

type File added in v0.11.0

type File struct {
	ContentType string        `json:"content_type"`
	Encoding    string        `json:"encoding"`
	Filename    string        `json:"filename"`
	Contents    io.ReadCloser `json:"-"`
}

File represents a response that is a response body. The runner is in charge of getting the contents to the destination. The metadata will be received. One note, we call NormalizeFile on the File's in the Runner's that execute the handler. Testing through the Run function will illustrate this.

func CompressGzip added in v0.12.0

func CompressGzip(file File) File

CompressGzip compresses a files contents with gzip compression.

func NormalizeFile added in v0.13.0

func NormalizeFile(f File) File

NormalizeFile normalizes a file so that all fields are set with sane defaults.

func (File) MarshalJSON added in v0.11.0

func (f File) MarshalJSON() ([]byte, error)

MarshalJSON marshals the file metadata.

type Handler

type Handler interface {
	Handle(ctx context.Context, r Request) Response
}

Handler provides a handler for our incoming request.

func ErrHandler added in v0.4.0

func ErrHandler(errs ...APIError) Handler

ErrHandler creates a new handler to respond with only errors.

func HandleFnOf added in v0.4.0

func HandleFnOf[T any](fn func(ctx context.Context, r RequestOf[T]) Response) Handler

HandleFnOf provides a means to translate the incoming requests to the destination body type. This normalizes the sad path and provides the caller with a zero fuss request to work with. Reducing json boilerplate for what is essentially the same operation on different types.

func HandleWorkflow added in v0.6.0

func HandleWorkflow(fn func(ctx context.Context, r Request, wrkCtx WorkflowCtx) Response) Handler

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 HandleWorkflowOf added in v0.6.0

func HandleWorkflowOf[T any](fn func(ctx context.Context, r RequestOf[T], wrkCtx WorkflowCtx) Response) Handler

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 HandlerFnOfOK added in v0.19.0

func HandlerFnOfOK[T interface{ OK() []APIError }](fn func(ctx context.Context, r RequestOf[T]) Response) Handler

HandlerFnOfOK provides a means to translate the incoming requests to the destination body type and execute validation on that type. This normalizes the sad path for both the unmarshalling of the request body and the validation of that request type using its OK() method.

type HandlerFn added in v0.4.0

type HandlerFn func(ctx context.Context, r Request) Response

HandlerFn wraps a function to return a handler. Similar to the http.HandlerFunc.

func (HandlerFn) Handle added in v0.4.0

func (h HandlerFn) Handle(ctx context.Context, r Request) Response

Handle is the request/response lifecycle handler.

type Mux added in v0.4.0

type Mux struct {
	// contains filtered or unexported fields
}

Mux defines a handler that will dispatch to a matching route/method combination. Much like the std lib http.ServeMux, but with slightly more opinionated route setting. We only support the DELETE, GET, POST, and PUT.

func NewMux added in v0.4.0

func NewMux() *Mux

NewMux creates a new Mux that is ready for assignment.

func (*Mux) Delete added in v0.4.0

func (m *Mux) Delete(route string, h Handler)

Delete creates a DELETE route.

func (*Mux) Get added in v0.4.0

func (m *Mux) Get(route string, h Handler)

Get creates a GET route.

func (*Mux) Handle added in v0.4.0

func (m *Mux) Handle(ctx context.Context, r Request) Response

Handle enacts the handler to process the request/response lifecycle. The mux fulfills the Handler interface and can dispatch to any number of sub routes.

func (*Mux) Post added in v0.4.0

func (m *Mux) Post(route string, h Handler)

Post creates a POST route.

func (*Mux) Put added in v0.4.0

func (m *Mux) Put(route string, h Handler)

Put creates a PUT route.

type Request

type Request RequestOf[io.Reader]

Request defines a request structure that is given to the runner. The Body is set to io.Reader, to enable decoration/middleware.

type RequestOf added in v0.4.0

type RequestOf[T any] struct {
	FnID      string
	FnVersion int

	Body T

	Context json.RawMessage
	Params  struct {
		Header http.Header
		Query  url.Values
	}
	URL         string
	Method      string
	AccessToken string
	TraceID     string
}

RequestOf provides a generic body we can target our unmarshaling into.

type Response

type Response struct {
	Body   json.Marshaler
	Code   int
	Errors []APIError
	Header http.Header
}

Response is the domain type for the response.

func ErrResp added in v0.10.0

func ErrResp(errs ...APIError) Response

ErrResp creates a sad path errors only response.

Note: the highest status code from the errors will be used for the response status if no status code is set on the response.

func (Response) StatusCode added in v0.4.0

func (r Response) StatusCode() int

StatusCode returns the response status code. When a Response.Code is not set and errors exist, then the highest code on the errors is returned.

type Runner added in v0.4.0

type Runner func(ctx context.Context, newHandlerFn func(context.Context, *slog.Logger) Handler)

Runner defines the runtime that executes the request/response handler lifecycle.

type SkipCfg added in v0.4.0

type SkipCfg struct{}

SkipCfg indicates the config is not needed and will skip the config loading procedure.

func (SkipCfg) OK added in v0.4.0

func (n SkipCfg) OK() error

OK is a noop validation.

type WorkflowCtx added in v0.6.0

type WorkflowCtx struct {
	ActivityExecID    string `json:"activity_execution_id"`
	AppID             string `json:"app_id"`
	CID               string `json:"cid"`
	OwnerCID          string `json:"owner_cid"`
	DefinitionID      string `json:"definition_id,omitempty"`
	DefinitionVersion int    `json:"definition_version,omitempty"`
	ExecutionID       string `json:"execution_id,omitempty"`
	Activity          struct {
		ID     string `json:"id"`
		Name   string `json:"name"`
		NodeID string `json:"node_id"`
	} `json:"activity"`
	Trigger struct {
		ID   string `json:"id,omitempty"`
		Name string `json:"name,omitempty"`
	} `json:"trigger"`
}

WorkflowCtx is the Request.Context field when integrating a function with Falcon Fusion workflow.

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL