Skip to content

Commit 08423f0

Browse files
committed
Added WIP golang coprocessor
1 parent 760bff1 commit 08423f0

File tree

9 files changed

+534
-0
lines changed

9 files changed

+534
-0
lines changed

golang-coprocessor/README.md

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Golang Coprocessor
2+
3+
[Style Guide](https://google.github.io/styleguide/go/best-practices.html)
4+
[Project Layout](https://github.com/golang-standards/project-layout)
5+
[Some best practices](https://go.dev/talks/2013/bestpractices.slide)
6+
7+
## Structure
8+
9+
Modeled after the ["Server project"](https://go.dev/doc/modules/layout) layout
10+
11+
## Run
12+
13+
1. Pick one of the Golang frameworks exampled under `cmd`. Customers often have a "platform" preferred one they write other services in.
14+
1. Start the coprocessor
15+
```shell
16+
go run cmd/simple-http/main.go
17+
```
18+
or
19+
```shell
20+
go run cmd/gorilla-mux/main.go
21+
```
22+
... etc.
23+
1. [Start the router](/router/README.md#running-the-router)
24+
25+
## Packaging for a customer
26+
27+
1. Copy whole directory
28+
1. Remove unnecessary `cmd`'s
29+
1. Run `go mod tidy` which will cleanup the imports for all the other frameworks
30+
31+
## Logging types
32+
33+
https://go.dev/play/p/yHGoKST0lEx
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"net/http"
7+
"os"
8+
"time"
9+
10+
coprocessor "github.com/apollographql/coprocessor/internal"
11+
"github.com/gorilla/mux"
12+
)
13+
14+
func main() {
15+
port, exists := os.LookupEnv("PORT")
16+
if !exists {
17+
port = "3007"
18+
}
19+
20+
host, exists := os.LookupEnv("HOST")
21+
if !exists {
22+
host = "localhost"
23+
}
24+
25+
router := mux.NewRouter()
26+
router.HandleFunc("/coprocessor", http.HandlerFunc(coprocessor.RequestHandler))
27+
http.Handle("/", router)
28+
29+
srv := &http.Server{
30+
Handler: router,
31+
Addr: fmt.Sprintf("%s:%s", host, port),
32+
WriteTimeout: 15 * time.Second,
33+
ReadTimeout: 15 * time.Second,
34+
IdleTimeout: 120 * time.Second,
35+
}
36+
37+
log.Printf("Starting on http://%s:%s", host, port)
38+
srv.ListenAndServe()
39+
}
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"net/http"
7+
"os"
8+
"strconv"
9+
10+
coprocessor "github.com/apollographql/coprocessor/internal"
11+
)
12+
13+
func main() {
14+
port, err := strconv.Atoi(os.Getenv("PORT"))
15+
if err != nil {
16+
port = 3007
17+
}
18+
19+
http.HandleFunc("/", coprocessor.RequestHandler)
20+
log.Printf("Starting on :%v", port)
21+
http.ListenAndServe(fmt.Sprintf(":%v", port), nil)
22+
}

golang-coprocessor/go.mod

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module github.com/apollographql/coprocessor
2+
3+
go 1.21.3
4+
5+
require (
6+
github.com/go-logr/logr v1.4.1
7+
github.com/go-logr/zerologr v1.2.3
8+
github.com/rs/zerolog v1.31.0
9+
)
10+
11+
require (
12+
github.com/gorilla/mux v1.8.1
13+
github.com/mattn/go-colorable v0.1.13 // indirect
14+
github.com/mattn/go-isatty v0.0.19 // indirect
15+
github.com/pkg/errors v0.9.1 // indirect
16+
golang.org/x/sys v0.12.0 // indirect
17+
)

golang-coprocessor/go.sum

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
2+
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
3+
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
4+
github.com/go-logr/zerologr v1.2.3 h1:up5N9vcH9Xck3jJkXzgyOxozT14R47IyDODz8LM1KSs=
5+
github.com/go-logr/zerologr v1.2.3/go.mod h1:BxwGo7y5zgSHYR1BjbnHPyF/5ZjVKfKxAZANVu6E8Ho=
6+
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
7+
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
8+
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
9+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
10+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
11+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
12+
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
13+
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
14+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
15+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
16+
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
17+
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
18+
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
19+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
20+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
21+
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
22+
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

golang-coprocessor/internal/logger.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package coprocessor
2+
3+
import (
4+
"os"
5+
"time"
6+
7+
"github.com/go-logr/logr"
8+
"github.com/go-logr/zerologr"
9+
"github.com/rs/zerolog"
10+
"github.com/rs/zerolog/pkgerrors"
11+
)
12+
13+
var logger logr.Logger = defaultLogger()
14+
15+
func defaultLogger() logr.Logger {
16+
zerolog.TimeFieldFormat = time.RFC3339Nano
17+
zerolog.MessageFieldName = "message"
18+
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
19+
w := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339Nano}
20+
zl := zerolog.New(w).With().Timestamp().Logger().Level(zerolog.InfoLevel)
21+
zerologr.VerbosityFieldName = ""
22+
return zerologr.New(&zl)
23+
}
+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package coprocessor
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
)
9+
10+
type RouterLifecycleRequest struct {
11+
Stage string `json:"stage"`
12+
Body any `json:"body"`
13+
}
14+
15+
type CommonProperties struct {
16+
Version int `json:"version"`
17+
Stage string `json:"stage"`
18+
ID string `json:"id,omitempty"`
19+
}
20+
21+
type Headers struct {
22+
Headers http.Header `json:"headers,omitempty"`
23+
}
24+
25+
// The value of "control" coming from the router is always a string value of "continue"
26+
// If you don't modify it, or respond with it, and you don't respond with an object, everything proceeds normally
27+
// The only time you are going to modify it, you're going to set it to an object with a Break property and specific status code
28+
// https://www.apollographql.com/docs/router/customizations/coprocessor/#terminating-a-client-request
29+
type BreakControl struct {
30+
Break float64 `json:"break,omitempty"`
31+
}
32+
33+
type Context struct {
34+
Entries map[string]any `json:"entries"`
35+
}
36+
37+
type Body struct {
38+
Errors []Error `json:"errors,omitempty"`
39+
Query string `json:"query,omitempty"`
40+
OperationName string `json:"operationName,omitempty"`
41+
Variables map[string]any `json:"variables,omitempty"`
42+
43+
// RouterResponse Stage
44+
Data any `json:"data,omitempty"`
45+
}
46+
47+
type Error struct {
48+
Message string `json:"message,omitempty"`
49+
Extensions *Extension `json:"extensions,omitempty"`
50+
}
51+
52+
type Extension struct {
53+
Code string `json:"code,omitempty"`
54+
55+
// Adding defaults to ErrorType and ErrorCode means even if there are no errors
56+
// JSON.Marshal sees this as non-nil and will erroneously add them to all responses
57+
ErrorType string `json:"errorType,omitempty"`
58+
ErrorCode string `json:"errorCode,omitempty"`
59+
ServiceName string `json:"serviceName,omitempty"`
60+
}
61+
62+
func RequestHandler(w http.ResponseWriter, r *http.Request) {
63+
var err error
64+
var response []byte
65+
66+
cr, err := HandleRequest(w, r)
67+
if err != nil {
68+
logger.Error(err, "error handling coprocessor request")
69+
http.Error(w, fmt.Sprintf("error: %s", err), http.StatusInternalServerError)
70+
return
71+
}
72+
73+
response, err = json.Marshal(&cr)
74+
if err != nil {
75+
logger.Error(err, "failed to marshal response")
76+
http.Error(w, fmt.Sprintf("failed to marshal response: %s", err), http.StatusInternalServerError)
77+
return
78+
}
79+
80+
_, err = w.Write(response)
81+
if err != nil {
82+
logger.Error(err, "error writing coprocessor response")
83+
http.Error(w, fmt.Sprintf("error: %s", err), http.StatusInternalServerError)
84+
return
85+
}
86+
}
87+
88+
func NewRequest(r *http.Request) (*[]byte, string, error) {
89+
var err error
90+
var cr *RouterLifecycleRequest
91+
92+
httpRequestBody, err := io.ReadAll(r.Body)
93+
94+
if err != nil {
95+
return nil, "", fmt.Errorf("error reading request body: %w", err)
96+
}
97+
98+
// If the Router is configured to send data, this should never be empty
99+
// If it isn't configured to send data, it shouldn't call the coprocessor
100+
if len(httpRequestBody) == 0 {
101+
return nil, "", fmt.Errorf("error empty http request body at /%s", r.URL.Path[1:])
102+
}
103+
104+
err = json.Unmarshal(httpRequestBody, &cr)
105+
if err != nil {
106+
fmt.Println(err)
107+
return nil, "", err
108+
}
109+
110+
return &httpRequestBody, cr.Stage, nil
111+
}

golang-coprocessor/internal/stages.go

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package coprocessor
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
)
7+
8+
// Examples can be found:
9+
// https://www.apollographql.com/docs/router/customizations/coprocessor/#coprocessor-request-format
10+
func HandleRequest(w http.ResponseWriter, r *http.Request) (interface{}, error) {
11+
httpRequestBody, stage, err := NewRequest(r)
12+
if err != nil {
13+
return nil, fmt.Errorf("error unmarshaling httpRequestBody: %w", err)
14+
}
15+
16+
switch stage {
17+
case "RouterRequest":
18+
return handleRouterRequest(httpRequestBody)
19+
case "RouterResponse":
20+
return handleRouterResponse(httpRequestBody)
21+
case "":
22+
// This shouldn't happen, everything should have a Stage
23+
return nil, fmt.Errorf("no stage for request %+v", httpRequestBody)
24+
default:
25+
return nil, fmt.Errorf("unhandled coprocessor request stage of type: %T", stage)
26+
}
27+
}

0 commit comments

Comments
 (0)