Skip to content

Commit

Permalink
feat: custom router modules (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
StarpTech authored Sep 6, 2023
1 parent 076e21e commit 75825d9
Show file tree
Hide file tree
Showing 54 changed files with 13,896 additions and 488 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/cli-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:
- name: Generate code
run: pnpm buf generate --template buf.ts.gen.yaml

- name: Check if git is not dirty after generating files
run: git diff --no-ext-diff --exit-code

- name: Build
run: pnpm run --filter ./cli --filter ./connect --filter ./shared --filter ./composition build

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/composition-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ jobs:
- name: Generate code
run: pnpm buf generate --template buf.ts.gen.yaml

- name: Check if git is not dirty after generating files
run: git diff --no-ext-diff --exit-code

- name: Build
run: pnpm run --filter ./composition --filter ./connect --filter ./shared build

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/controlplane-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ jobs:
- name: Generate code
run: pnpm buf generate --template buf.ts.gen.yaml

- name: Check if git is not dirty after generating files
run: git diff --no-ext-diff --exit-code

- name: Build
run: pnpm run --filter ./controlplane --filter ./connect --filter ./shared --filter ./composition build

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ jobs:
- name: Generate code
run: pnpm buf generate --template buf.ts.gen.yaml

- name: Check if git is not dirty after generating files
run: git diff --no-ext-diff --exit-code

- name: Build
run: pnpm run --filter='!./studio' build

Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/router-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ jobs:
run: make setup-build-tools

- name: Generate code
run: buf generate --template buf.go.gen.yaml
run: make generate-go

- name: Check if git is not dirty after generating files
run: git diff --no-ext-diff --exit-code

- name: Install dependencies
working-directory: ./router
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/shared-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ jobs:
- name: Generate code
run: pnpm buf generate --template buf.ts.gen.yaml

- name: Check if git is not dirty after generating files
run: git diff --no-ext-diff --exit-code

- name: Build
run: pnpm run --filter ./connect --filter ./shared --filter ./connect --filter ./composition build

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/studio-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ jobs:
- name: Generate code
run: pnpm buf generate --template buf.ts.gen.yaml

- name: Check if git is not dirty after generating files
run: git diff --no-ext-diff --exit-code

- name: Build
run: pnpm run --filter ./studio --filter ./connect --filter ./shared --filter ./composition build

Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,5 @@ node_modules
# docker volumes
**/_docker
tsconfig.tsbuildinfo
gen
#lerna
lerna-debug.log
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ generate:
make generate-go

generate-go:
buf generate --template buf.go.gen.yaml
rm -rf router/gen && buf generate --template buf.go.gen.yaml

start-cp:
pnpm -r run --filter './controlplane' dev
Expand Down
3 changes: 3 additions & 0 deletions router/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**/.env
**/.env.local
CHANGELOG.md
2 changes: 1 addition & 1 deletion router/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1 @@
router
/router
4 changes: 2 additions & 2 deletions router/Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
dev:
go run main.go
go run cmd/router/main.go

test:
go test ./...

build:
CGO_ENABLED=0 GOOS=linux go build -trimpath -a -o router .
CGO_ENABLED=0 GOOS=linux go build -trimpath -a -o router cmd/router/main.go
22 changes: 17 additions & 5 deletions router/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,31 @@ The router is the component that understands the GraphQL Federation protocol. It

## Getting Started

test

### Prerequisites

- [Go 1.20](https://golang.org/doc/install)
- [Connect for Go](https://connect.build/docs/go/getting-started)

Use the `.env.example` file to create a `.env` file with the required environment variables.

```shell
go run main.go
make dev
```

## Code Generation

Code is committed to the repository, but if you want to regenerate the code, you can run the command in the root of the repository:

```shell
make generate-go
```

## Build your own Router

See [Router Customizability](https://cosmo-docs.wundergraph.com/router/customizability) how to build your own router.

# Architecture

We use [Connect](https://connect.build/) to communicate with the controlplane. Connect is framework build on top of [gRPC](https://grpc.io/) and simplify code-generation and reuse between `Studio` -> `Controlplane` <- `Router`.
The router is a HTTP server that accepts GraphQL requests and forwards them to the correct service.
The core aka [`the Engine`](https://github.com/wundergraph/graphql-go-tools) implements the GraphQL Federation protocol and is responsible for parsing the request, resolving the query and aggregating the responses.

We use [Connect](https://connect.build/) to communicate with the controlplane. Connect is framework build on top of [gRPC](https://grpc.io/) and simplify code-generation and reuse between `Studio` -> `Controlplane` <- `Router`.
55 changes: 55 additions & 0 deletions router/cmd/custom/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Custom Router

This entrypoint serve as an example about how to build your own custom Cosmo Router.
The main.go file is the entrypoint of the router and is responsible for starting the router.
You can see we will load the default Router and your custom module.

```go
package main

import (
routercmd "github.com/wundergraph/cosmo/router/cmd"
// Import your modules here
_ "github.com/wundergraph/cosmo/router/cmd/custom/module"
)

func main() {
routercmd.Main()
}
```

## Run the Router

Before you can run the router, you need to copy the `.env.example` to `.env` and adjust the values.

```bash
go run ./cmd/custom/main.go
```

## Build your own Router

```bash
go build -o router ./cmd/custom/main.go
```

## Build your own Image

```bash
docker build -f custom.Dockerfile -t router-custom:latest .
```

## Run tests

In order to run the tests, you need to run the example subgraph first. We use the demo subgraph for this.

```
make dc-subgraphs-demo
```

In practice, you would create a custom router config and mock the subgraph dependencies in your tests.

_All commands are run from the root of the router directory._

## Credits

The module system is inspired by [Caddy](https://github.com/caddyserver/caddy).
11 changes: 11 additions & 0 deletions router/cmd/custom/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package main

import (
routercmd "github.com/wundergraph/cosmo/router/cmd"
// Import your modules here
_ "github.com/wundergraph/cosmo/router/cmd/custom/module"
)

func main() {
routercmd.Main()
}
112 changes: 112 additions & 0 deletions router/cmd/custom/module/module.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package module

import (
"fmt"
"github.com/wundergraph/cosmo/router/pkg/app"
"github.com/wundergraph/cosmo/router/pkg/graphql"
"go.uber.org/zap"
"net/http"
)

func init() {
// Register your module here
app.RegisterModule(&MyModule{})
}

const myModuleID = "myModule"

// MyModule is a simple module that has access to the GraphQL operation and add a header to the response
// It demonstrates how to use the different handlers to customize the router.
// It also shows how to use the config file to configure and validate your module config.
// By default, the config file is located at `config.yaml` in the working directory of the router.
type MyModule struct {
// Properties that are set by the config file are automatically populated based on the `mapstructure` tag
// Create a new section under `modules.<name>` in the config file with the same name as your module.
// Don't forget in Go the first letter of a property must be uppercase to be exported

Value uint64 `mapstructure:"value"`

Logger *zap.Logger
}

func (m MyModule) Provision(ctx *app.ModuleContext) error {
// Provision your module here, validate config etc.

if m.Value == 0 {
ctx.Logger().Error("Value must be greater than 0")
return fmt.Errorf("value must be greater than 0")
}

// Assign the logger to the module for non-request related logging
m.Logger = ctx.Logger()

return nil
}

func (m MyModule) Cleanup() error {
// Shutdown your module here, close connections etc.

return nil
}

func (m MyModule) OnOriginResponse(response *http.Response, request *http.Request) (*http.Response, error) {
// Return a new response or nil if you want to pass it to the next handler
// If you want to modify the response, return a new response
// If you return an error, the request will be aborted and the response will exit with a 500 status code

c := app.GetRequestContext(request.Context())

// Set a header on the client response
c.ResponseHeader().Set("myHeader", c.GetString("myKey"))

return nil, nil
}

func (m MyModule) OnOriginRequest(request *http.Request) {
// Read the request or modify headers here before it is sent to the origin
c := app.GetRequestContext(request.Context())

// Use the request logger to log information
c.Logger().Info("Subgraph request", zap.String("host", request.Host))
}

func (m MyModule) Middleware(w http.ResponseWriter, r *http.Request, next http.Handler) {
ctx := r.Context()

c := graphql.GetOperationContext(ctx)

// Access the GraphQL operation context
fmt.Println(
c.Name,
c.Type,
c.Hash,
c.Content,
)

// Share a value between different handlers
// In OnOriginResponse we will read this value and set it as response header
appCtx := app.GetRequestContext(ctx)
appCtx.Set("myKey", "myValue")

// Call the next handler in the chain or return early by calling w.Write()
next.ServeHTTP(w, r)
}

func (m MyModule) Module() app.ModuleInfo {
return app.ModuleInfo{
// This is the ID of your module, it must be unique
ID: myModuleID,
New: func() app.Module {
return MyModule{}
},
}
}

// Interface guard
var (
_ app.RouterMiddlewareHandler = (*MyModule)(nil)
_ app.EnginePreOriginHandler = (*MyModule)(nil)
_ app.EnginePostOriginHandler = (*MyModule)(nil)
_ app.Provisioner = (*MyModule)(nil)
_ app.Cleaner = (*MyModule)(nil)
)
62 changes: 62 additions & 0 deletions router/cmd/custom/module/module_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package module

import (
"bytes"
"context"
"github.com/stretchr/testify/assert"
"github.com/wundergraph/cosmo/router/pkg/app"
"github.com/wundergraph/cosmo/router/pkg/config"
"net/http/httptest"
"os"
"testing"
)

func TestMyModule(t *testing.T) {

if os.Getenv("MODULE_TESTS") == "" {
t.Skip("Skipping testing in CI environment")
}

ctx := context.Background()
cfg := config.Config{
FederatedGraphName: "production",
Modules: map[string]interface{}{
"myModule": MyModule{
Value: 1,
},
},
}

routerConfig, err := app.SerializeConfigFromFile("./router-config.json")
assert.Nil(t, err)

rs, err := app.New(
app.WithFederatedGraphName(cfg.FederatedGraphName),
app.WithStaticRouterConfig(routerConfig),
app.WithModulesConfig(cfg.Modules),
app.WithListenerAddr("http://localhost:3002"),
)
assert.Nil(t, err)
t.Cleanup(func() {
assert.Nil(t, rs.Shutdown(ctx))
})

router, err := rs.NewTestRouter(ctx)
assert.Nil(t, err)

rr := httptest.NewRecorder()

var jsonData = []byte(`{
"query": "query MyQuery { employees { id } }",
"operationName": "MyQuery"
}`)
req := httptest.NewRequest("POST", "/graphql", bytes.NewBuffer(jsonData))
router.Server.Handler.ServeHTTP(rr, req)

assert.Equal(t, 200, rr.Code)

// This header was set by the module
assert.Equal(t, rr.Result().Header.Get("myHeader"), "myValue")

assert.JSONEq(t, rr.Body.String(), `{"data":{"employees":[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13}]}}`)
}
Loading

0 comments on commit 75825d9

Please sign in to comment.