Skip to content

Commit

Permalink
docs: Golang function developer's guide (#297)
Browse files Browse the repository at this point in the history
* docs: Golang function developer's guide

Signed-off-by: Zbynek Roubalik <zroubali@redhat.com>
  • Loading branch information
zroubalik committed Apr 14, 2021
1 parent 4f60504 commit 2309dd3
Showing 1 changed file with 251 additions and 0 deletions.
251 changes: 251 additions & 0 deletions docs/guides/golang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
# Golang Function Developer's Guide

When creating a Go (Golang) function using the `func` CLI, the project directory
looks like a typical Go project. Both HTTP and Event functions have the same
template structure.

```
❯ func create -l go fn
Project path: /home/developer/projects/fn
Function name: fn
Runtime: go
Trigger: http
❯ tree
fn
├── README.md
├── func.yaml
├── go.mod
├── go.sum
├── handle.go
└── handle_test.go
```

Aside from the `func.yaml` file, this looks like the beginning of just about
any Go project. For now, we will ignore the `func.yaml` file, and just
say that it is a configuration file that is used when building your project.
If you're really interested, check out the [reference doc](config-reference.doc).
To learn more about the CLI and the details for each supported command, see
the [CLI Commands document](commands.md#cli-commands).

## Running the function locally

To run a function, you'll first need to build it. This step creates an OCI
container image that can be run locally on your computer, or on a Kubernetes
cluster.

```
❯ func build
```

After the function has been built, it can be run locally.

```
❯ func run
```

Functions can be invoked with a simple HTTP request.
You can test to see if the function is working by using your browser to visit
http://localhost:8080. You can also access liveness and readiness
endpoints at http://localhost:8080/health/liveness and
http://localhost:8080/health/readiness. These two endpoints are used
by Kubernetes to determine the health of your function. If everything
is good, both of these will return `{"ok":true}`.

## Deploying the function to a cluster

To deploy your function to a Kubenetes cluster, use the `deploy` command.

```
❯ func deploy
```

You can get the URL for your deployed function with the `describe` command.

```
❯ func describe
```

## Testing a function locally


Go functions can be tested locally on your computer. In the project there is
a `handle_test.go` file which contains simple test which can be extended as needed.
Yo can run this test locally as you would do with any Go project.

```
❯ go test
```

## Function reference

Boson Go functions have very few restrictions. You can add any required dependencies
in `go.mod` and you may include additional local Go files. The only real requirement are
that your project is defined in a `function` module and exports the function `Handle()`
(supported contracts of this function will be discussed more deeply later).
In this section, we will look in a little more detail at how Boson functions are invoked,
and what APIs are available to you as a developer.

### Invocation parameters

When using the `func` CLI to create a function project, you may choose to generate a project
that responds to a `CloudEvent` or simple HTTP. `CloudEvents` in Knative are transported over
HTTP as a `POST` request, so in many ways, the two types of functions are very much the same.
They each will listen and respond to incoming HTTP events.

#### Function triggered by HTTP request

When an incoming request is received, your function will be invoked with a standard
Golang [Context](https://golang.org/pkg/context/) as the first parameter followed by
two parameters: Golang's [http.ResponseWriter](https://golang.org/pkg/net/http/#ResponseWriter)
and [http.Request](https://golang.org/pkg/net/http/#Request).

Then you can use standard Golang techniques to access the request (eg. read the body)
and set a proper HTTP response of your function, as you can see on the following example:

```go
func Handle(ctx context.Context, res http.ResponseWriter, req *http.Request) {

// Read body
body, err := ioutil.ReadAll(req.Body)
defer req.Body.Close()
if err != nil {
http.Error(res, err.Error(), 500)
return
}

// Process body & function logic
// ...
}
```

#### Function triggered by CloudEvent

If the incoming request is a `CloudEvent`, the event is provided via
[CloudEvents Golang SDK](https://cloudevents.github.io/sdk-go/) and its `Event` type
as a parameter. There's possibility to leverage Golang's
[Context](https://golang.org/pkg/context/) as the optional parameter in the function contract,
as you can see in the list of supported function signatures:

```go
Handle()
Handle() error
Handle(context.Context)
Handle(context.Context) error
Handle(cloudevents.Event)
Handle(cloudevents.Event) error
Handle(context.Context, cloudevents.Event)
Handle(context.Context, cloudevents.Event) error
Handle(cloudevents.Event) *cloudevents.Event
Handle(cloudevents.Event) (*cloudevents.Event, error)
Handle(context.Context, cloudevents.Event) *cloudevents.Event
Handle(context.Context, cloudevents.Event) (*cloudevents.Event, error)
```

For example, a `CloudEvent` is received which contains a JSON string such as this in its data property,

```json
{
"customerId": "0123456",
"productId": "6543210"
}
```

to access this data, we need to define `Purchase` structure, which maps properties in `CloudEvents`
data and retrive it from the incoming event:

```go
type Purchase struct {
CustomerId string `json:"customerId"`
ProductId string `json:"productId"`
}

func Handle(ctx context.Context, event cloudevents.Event) err error {

purchase := &Purchase{}
if err = cloudevents.DataAs(purchase); err != nil {
fmt.Fprintf(os.Stderr, "failed to parse incoming CloudEvent %s\n", err)
return
}

// ...
}
```

Or we can use Golang's `encoding/json` package to access the `CloudEvent` directly as
a JSON in form of bytes array:

```golang
func Handle(ctx context.Context, event cloudevents.Event) {

bytes, err := json.Marshal(event)

// ...
}
```

### Return Values
As mentioned above, HTTP triggered functions can set the response directly via
Golang's [http.ResponseWriter](https://golang.org/pkg/net/http/#ResponseWriter).

```go
func Handle(ctx context.Context, res http.ResponseWriter, req *http.Request) {

// Set response
res.Header().Add("Content-Type", "text/plain")
res.Header().Add("Content-Length", "3")
res.WriteHeader(200)

_, err := fmt.Fprintf(res, "OK\n")
if err != nil {
fmt.Fprintf(os.Stderr, "error or response write: %v", err)
}
}
```

Functions triggered by `CloudEvent` may return nothing, `error` and/or `CloudEvent` in order
to push events into the Knative eventing system. In this case, the developer is required
to set a unique `ID`, proper `Source` and a `Type` of the CloudEvent. The data can be populated
from a defined structure or from a `map`.

```go
func Handle(ctx context.Context, event cloudevents.Event) (resp *cloudevents.Event, err error) {

// ...

response := cloudevents.NewEvent()
response.SetID("example-uuid-32943bac6fea")
response.SetSource("purchase/getter")
response.SetType("purchase")

// Set the data from Purchase type
response.SetData(cloudevents.ApplicationJSON, Purchase{
CustomerId: custId,
ProductId: prodId,
})

// OR set the data directly from map
response.SetData(cloudevents.ApplicationJSON, map[string]string{"customerId": custId, "productId": prodId})

// Validate the response
resp = &response
if err = resp.Validate(); err != nil {
fmt.Printf("invalid event created. %v", err)
}

return
}
```

## Dependencies
Developers are not restricted to the dependencies provided in the template
`go.mod` file. Additional dependencies can be added as they would be in any
other Golang project.

### Example
```console
go get gopkg.in/yaml.v2@v2.4.0
```

When the project is built for deployment, these dependencies will be included
in the resulting runtime container image.

0 comments on commit 2309dd3

Please sign in to comment.