Skip to content

Commit

Permalink
feat: init declarative functions go (#92)
Browse files Browse the repository at this point in the history
Declarative functions interface for the Go Functions Framework.

## Example usage

### main.go

Sample main package used for local development only.

```go
package main

import (
	"os"

	"github.com/GoogleCloudPlatform/functions-framework-go/funcframework"
)

func main() {
	port := "8080"
	if envPort := os.Getenv("PORT"); envPort != "" {
		port = envPort
	}
	if err := funcframework.Start(port); err != nil {
		log.Fatalf("Failed to start functions framework: %v", err)
	}
}
```

### function.go

Sample declarative function, registered in `init`.

```go
package function

import (
	"net/http"
	"os"

	"github.com/GoogleCloudPlatform/functions-framework-go/funcframework"
)

func HelloWorld(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello, World!"))
}

func init() {
	// Register your function named "hello"
	funcframework.HTTPFunction("hello", HelloWorld)
}
```

## Features

- Exposes new functions `HTTP` and `CloudEvent` that allows you to declaratively register a function with a given name.
- Creates a function registry (similar to python and Node). I expect the registry to be more useful as we add more features to declarative functions.
- Adds sample declarative functions for conformance tests.
- Adds sample runnable functions in `testdata`.
- Adds basic docs in the `README.md`.

### Example tests

- `go run testdata/declarative/http/main.go`
  -  `2021/10/28 15:38:50 Listening to function "http" at http://localhost:8080/ Serving function...`
- `go run testdata/declarative/cloudevent/main.go`
  -  `2021/10/28 15:40:54 Listening to function "cloudevent" at http://localhost:8080/ Serving function...`

### Example conformance functions

```
FUNCTION_TARGET=declarativeHTTP go run testdata/conformance/cmd/declarative/main.go
FUNCTION_TARGET=declarativeCE go run testdata/conformance/cmd/declarative/main.go
```


I'm not an expert in Go, so feel free to suggest any changes!
  • Loading branch information
grant committed Nov 2, 2021
1 parent 1592ba0 commit ae1bf32
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 11 deletions.
26 changes: 20 additions & 6 deletions .github/workflows/conformance.yml
Expand Up @@ -16,18 +16,14 @@ jobs:
steps:
- name: Check out code
uses: actions/checkout@v2

- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v2
with:
go-version: '${{ matrix.go-version }}'

- name: Pre-fetch go dependencies and build
run: 'go build ./...'

- name: Pre-install conformance test client
run: 'go get github.com/GoogleCloudPlatform/functions-framework-conformance/client@v1.2.1 && go install github.com/GoogleCloudPlatform/functions-framework-conformance/client'

- name: Run HTTP conformance tests
uses: GoogleCloudPlatform/functions-framework-conformance/action@v1.2.1
with:
Expand All @@ -36,7 +32,6 @@ jobs:
useBuildpacks: false
cmd: "'go run testdata/conformance/cmd/http/main.go'"
startDelay: 5

- name: Run event conformance tests
uses: GoogleCloudPlatform/functions-framework-conformance/action@v1.2.1
with:
Expand All @@ -46,7 +41,6 @@ jobs:
useBuildpacks: false
cmd: "'go run testdata/conformance/cmd/legacyevent/main.go'"
startDelay: 5

- name: Run CloudEvent conformance tests
uses: GoogleCloudPlatform/functions-framework-conformance/action@v1.2.1
with:
Expand All @@ -56,3 +50,23 @@ jobs:
useBuildpacks: false
cmd: "'go run testdata/conformance/cmd/cloudevent/main.go'"
startDelay: 5
- name: Run HTTP conformance tests using declarative API
uses: GoogleCloudPlatform/functions-framework-conformance/action@v1.2.1
env:
FUNCTION_TARGET: 'declarativeHTTP'
with:
version: 'v1.2.1'
functionType: 'http'
useBuildpacks: false
cmd: "'go run testdata/conformance/cmd/declarative/main.go'"
startDelay: 5
- name: Run CloudEvent conformance tests using declarative API
uses: GoogleCloudPlatform/functions-framework-conformance/action@v1.2.1
env:
FUNCTION_TARGET: 'declarativeCloudEvent'
with:
version: 'v1.2.1'
functionType: 'cloudevent'
useBuildpacks: false
cmd: "'go run testdata/conformance/cmd/declarative/main.go'"
startDelay: 5
31 changes: 31 additions & 0 deletions README.md
Expand Up @@ -216,6 +216,37 @@ These functions are registered with the handler via `funcframework.RegisterCloud

To learn more about CloudEvents, see the [Go SDK for CloudEvents](https://github.com/cloudevents/sdk-go).

### Declarative Functions

The Functions Framework also provides a way to declaratively define `HTTP` and `CloudEvent` functions:

```golang
package function

import (
"net/http"

funcframework "github.com/GoogleCloudPlatform/functions-framework-go/funcframework"
)

func init() {
funcframework.HTTP("hello", HelloWorld)
funcframework.CloudEvent("ce", CloudEvent)
}

func HelloWorld(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
}

func CloudEvent(ctx context.Context, e cloudevents.Event) error {
// Do something with event.Context and event.Data (via event.DataAs(foo)).
return nil
}
```

Upon starting, the framework will listen to HTTP requests at `/` and invoke your registered function
specified by the `FUNCTION_TARGET` environment variable (i.e. `FUNCTION_TARGET=hello`).

[ff_go_unit_img]: https://github.com/GoogleCloudPlatform/functions-framework-go/workflows/Go%20Unit%20CI/badge.svg
[ff_go_unit_link]: https://github.com/GoogleCloudPlatform/functions-framework-go/actions?query=workflow%3A"Go+Unit+CI"
[ff_go_lint_img]: https://github.com/GoogleCloudPlatform/functions-framework-go/workflows/Go%20Lint%20CI/badge.svg
Expand Down
42 changes: 40 additions & 2 deletions funcframework/framework.go
Expand Up @@ -21,12 +21,14 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"reflect"
"runtime/debug"
"strings"

"github.com/GoogleCloudPlatform/functions-framework-go/internal/registry"
cloudevents "github.com/cloudevents/sdk-go/v2"
)

Expand Down Expand Up @@ -58,7 +60,6 @@ func RegisterHTTPFunction(path string, fn interface{}) {
defer recoverPanic("Registration panic")

fnHTTP, ok := fn.(func(http.ResponseWriter, *http.Request))

if !ok {
panic("expected function to have signature func(http.ResponseWriter, *http.Request)")
}
Expand Down Expand Up @@ -96,11 +97,43 @@ func RegisterCloudEventFunctionContext(ctx context.Context, path string, fn func
return registerCloudEventFunction(ctx, path, fn, handler)
}

// Declaratively registers a HTTP function.
func HTTP(name string, fn func(http.ResponseWriter, *http.Request)) {
if err := registry.RegisterHTTP(name, fn); err != nil {
log.Fatalf("failure to register function: %s", err)
}
}

// Declaratively registers a CloudEvent function.
func CloudEvent(name string, fn func(context.Context, cloudevents.Event) error) {
if err := registry.RegisterCloudEvent(name, fn); err != nil {
log.Fatalf("failure to register function: %s", err)
}
}

// Start serves an HTTP server with registered function(s).
func Start(port string) error {
// If FUNCTION_TARGET, try to start with that registered function
// If not set, assume non-declarative functions.
target := os.Getenv("FUNCTION_TARGET")

// Check if we have a function resource set, and if so, log progress.
if os.Getenv("K_SERVICE") == "" {
fmt.Println("Serving function...")
fmt.Printf("Serving function: %s\n", target)
}

// Check if there's a registered function, and use if possible
if fn, ok := registry.GetRegisteredFunction(target); ok {
ctx := context.Background()
if fn.HTTPFn != nil {
if err := registerHTTPFunction("/", fn.HTTPFn, handler); err != nil {
return fmt.Errorf("unexpected error in registerHTTPFunction: %v", err)
}
} else if fn.CloudEventFn != nil {
if err := registerCloudEventFunction(ctx, "/", fn.CloudEventFn, handler); err != nil {
return fmt.Errorf("unexpected error in registerCloudEventFunction: %v", err)
}
}
}

return http.ListenAndServe(":"+port, handler)
Expand Down Expand Up @@ -232,3 +265,8 @@ func writeHTTPErrorResponse(w http.ResponseWriter, statusCode int, status, msg s
w.WriteHeader(statusCode)
fmt.Fprint(w, msg)
}

func overrideHandlerWithRegisteredFunctions(h *http.ServeMux) {
// override http handler for tests
handler = h
}
26 changes: 26 additions & 0 deletions funcframework/framework_test.go
Expand Up @@ -22,8 +22,10 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"testing"

"github.com/GoogleCloudPlatform/functions-framework-go/internal/registry"
cloudevents "github.com/cloudevents/sdk-go/v2"
"github.com/google/go-cmp/cmp"
)
Expand Down Expand Up @@ -441,3 +443,27 @@ func TestCloudEventFunction(t *testing.T) {
}
}
}

func TestDeclarativeFunction(t *testing.T) {
funcName := "httpfunc"
os.Setenv("FUNCTION_TARGET", funcName)

h := http.NewServeMux()
overrideHandlerWithRegisteredFunctions(h)

// register functions
HTTP(funcName, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World!")
})

if _, ok := registry.GetRegisteredFunction(funcName); !ok {
t.Fatalf("could not get registered function: %s", funcName)
}

srv := httptest.NewServer(h)
defer srv.Close()

if _, err := http.Get(srv.URL); err != nil {
t.Fatalf("could not make HTTP GET request to function: %s", err)
}
}
52 changes: 52 additions & 0 deletions internal/registry/registry.go
@@ -0,0 +1,52 @@
package registry

import (
"context"
"fmt"
"net/http"

cloudevents "github.com/cloudevents/sdk-go/v2"
)

// A declaratively registered function
type RegisteredFunction struct {
Name string // The name of the function
CloudEventFn func(context.Context, cloudevents.Event) error // Optional: The user's CloudEvent function
HTTPFn func(http.ResponseWriter, *http.Request) // Optional: The user's HTTP function
}

var (
function_registry = map[string]RegisteredFunction{}
)

// Registers a HTTP function with a given name
func RegisterHTTP(name string, fn func(http.ResponseWriter, *http.Request)) error {
if _, ok := function_registry[name]; ok {
return fmt.Errorf("function name already registered: %s", name)
}
function_registry[name] = RegisteredFunction{
Name: name,
CloudEventFn: nil,
HTTPFn: fn,
}
return nil
}

// Registers a CloudEvent function with a given name
func RegisterCloudEvent(name string, fn func(context.Context, cloudevents.Event) error) error {
if _, ok := function_registry[name]; ok {
return fmt.Errorf("function name already registered: %s", name)
}
function_registry[name] = RegisteredFunction{
Name: name,
CloudEventFn: fn,
HTTPFn: nil,
}
return nil
}

// Gets a registered function by name
func GetRegisteredFunction(name string) (RegisteredFunction, bool) {
fn, ok := function_registry[name]
return fn, ok
}
83 changes: 83 additions & 0 deletions internal/registry/registry_test.go
@@ -0,0 +1,83 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package registry

import (
"context"
"fmt"
"net/http"
"testing"

cloudevents "github.com/cloudevents/sdk-go/v2"
)

func TestRegisterHTTP(t *testing.T) {
RegisterHTTP("httpfn", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World!")
})

fn, ok := GetRegisteredFunction("httpfn")
if !ok {
t.Fatalf("Expected function to be registered")
}
if fn.Name != "httpfn" {
t.Errorf("Expected function name to be 'httpfn', got %s", fn.Name)
}
}

func TestRegisterCE(t *testing.T) {
RegisterCloudEvent("cefn", func(context.Context, cloudevents.Event) error {
return nil
})

fn, ok := GetRegisteredFunction("cefn")
if !ok {
t.Fatalf("Expected function to be registered")
}
if fn.Name != "cefn" {
t.Errorf("Expected function name to be 'cefn', got %s", fn.Name)
}
}

func TestRegisterMultipleFunctions(t *testing.T) {
if err := RegisterHTTP("multifn1", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World!")
}); err != nil {
t.Error("Expected \"multifn1\" function to be registered")
}
if err := RegisterHTTP("multifn2", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World 2!")
}); err != nil {
t.Error("Expected \"multifn2\" function to be registered")
}
if err := RegisterCloudEvent("multifn3", func(context.Context, cloudevents.Event) error {
return nil
}); err != nil {
t.Error("Expected \"multifn3\" function to be registered")
}
}

func TestRegisterMultipleFunctionsError(t *testing.T) {
if err := RegisterHTTP("samename", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World!")
}); err != nil {
t.Error("Expected no error registering function")
}

if err := RegisterHTTP("samename", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World 2!")
}); err == nil {
t.Error("Expected error registering function with same name")
}
}
2 changes: 1 addition & 1 deletion testdata/conformance/cmd/cloudevent/main.go
Expand Up @@ -27,7 +27,7 @@ import (
func main() {
ctx := context.Background()
if err := funcframework.RegisterCloudEventFunctionContext(ctx, "/", function.CloudEvent); err != nil {
log.Fatalf("Failed to register function:: %v", err)
log.Fatalf("Failed to register function: %v", err)
}

port := "8080"
Expand Down
37 changes: 37 additions & 0 deletions testdata/conformance/cmd/declarative/main.go
@@ -0,0 +1,37 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Binary that serves the HTTP conformance test function.
package main

import (
"log"
"os"

"github.com/GoogleCloudPlatform/functions-framework-go/funcframework"
_ "github.com/GoogleCloudPlatform/functions-framework-go/testdata/conformance/function"
)

// Main function used for testing only:
// FUNCTION_TARGET=declarativeHTTP go run testdata/conformance/cmd/declarative/main.go
// FUNCTION_TARGET=declarativeCloudEvent go run testdata/conformance/cmd/declarative/main.go
func main() {
port := "8080"
if envPort := os.Getenv("PORT"); envPort != "" {
port = envPort
}
if err := funcframework.Start(port); err != nil {
log.Fatalf("Failed to start functions framework: %v", err)
}
}

0 comments on commit ae1bf32

Please sign in to comment.