-
Notifications
You must be signed in to change notification settings - Fork 138
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: Golang function developer's guide (#297)
* docs: Golang function developer's guide Signed-off-by: Zbynek Roubalik <zroubali@redhat.com>
- Loading branch information
Showing
1 changed file
with
251 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |