docs: Golang function developer's guide (#297)

* docs: Golang function developer's guide

Signed-off-by: Zbynek Roubalik <zroubali@redhat.com>
This commit is contained in:
Zbynek Roubalik 2021-04-14 20:39:15 +02:00 committed by GitHub
parent 4f60504708
commit 2309dd3a53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 251 additions and 0 deletions

251
docs/guides/golang.md Normal file
View File

@ -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.