Skip to content

Commit 58e3cd0

Browse files
authored
feat: use golang echo as reverse proxy (#623)
1 parent 43f2946 commit 58e3cd0

File tree

17 files changed

+1565
-354
lines changed

17 files changed

+1565
-354
lines changed

Dockerfile.revproxy

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
FROM golang:1.19.5-alpine3.17 as builder
2+
WORKDIR /src
3+
COPY go.mod go.sum ./
4+
COPY cmd/revproxy/ ./
5+
RUN go build -o /revproxy
6+
7+
FROM alpine:3.17
8+
USER 1000:1000
9+
COPY --from=builder /revproxy /revproxy
10+
ENTRYPOINT [ "/revproxy" ]

app/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,6 @@ def auth():
214214
if (
215215
"anon-id" not in request.cookies
216216
and request.headers.get("X-Requested-With", "") == "XMLHttpRequest"
217-
and request.headers["X-Forwarded-Uri"] == "/api/user"
218217
and "Authorization" not in headers
219218
):
220219
resp = Response(

chartpress.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ charts:
99
- .
1010
images:
1111
renku-gateway:
12-
# Context to send to docker build for use by the Dockerfile
1312
contextPath: .
14-
# Dockerfile path relative to chartpress.yaml
1513
dockerfilePath: Dockerfile
1614
valuesPath: image.auth
15+
renku-revproxy:
16+
contextPath: .
17+
dockerfilePath: Dockerfile.revproxy
18+
valuesPath: reverseProxy.image

cmd/revproxy/config.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"net/url"
7+
"os"
8+
"reflect"
9+
"strings"
10+
11+
"github.com/mitchellh/mapstructure"
12+
"github.com/spf13/viper"
13+
)
14+
15+
type renkuServicesConfig struct {
16+
Notebooks *url.URL `mapstructure:"renku_services_notebooks"`
17+
KG *url.URL `mapstructure:"renku_services_kg"`
18+
Webhook *url.URL `mapstructure:"renku_services_webhook"`
19+
Core *url.URL `mapstructure:"renku_services_core"`
20+
Auth *url.URL `mapstructure:"renku_services_auth"`
21+
}
22+
23+
type metricsConfig struct {
24+
Enabled bool `mapstructure:"metrics_enabled"`
25+
Port int `mapstructure:"metrics_port"`
26+
}
27+
28+
type revProxyConfig struct {
29+
RenkuBaseURL *url.URL `mapstructure:"renku_base_url"`
30+
AllowOrigin []string `mapstructure:"allow_origin"`
31+
ExternalGitlabURL *url.URL `mapstructure:"external_gitlab_url"`
32+
RenkuServices renkuServicesConfig `mapstructure:",squash"`
33+
Metrics metricsConfig `mapstructure:",squash"`
34+
Port int
35+
}
36+
37+
func parseStringAsURL() mapstructure.DecodeHookFuncType {
38+
return func(
39+
f reflect.Type,
40+
t reflect.Type,
41+
data interface{},
42+
) (interface{}, error) {
43+
// Check that the data is string
44+
if f.Kind() != reflect.String {
45+
return data, nil
46+
}
47+
48+
// Check that the target type is our custom type
49+
if t != reflect.TypeOf(url.URL{}) {
50+
return data, nil
51+
}
52+
53+
// Return the parsed value
54+
dataStr, ok := data.(string)
55+
if !ok {
56+
return nil, fmt.Errorf("cannot cast URL value to string")
57+
}
58+
if dataStr == "" {
59+
return nil, fmt.Errorf("empty values are not allowed for URLs")
60+
}
61+
url, err := url.Parse(dataStr)
62+
if err != nil {
63+
return nil, err
64+
}
65+
return url, nil
66+
}
67+
}
68+
69+
func getConfig() revProxyConfig {
70+
var config revProxyConfig
71+
prefix := "revproxy"
72+
viper.SetEnvPrefix(prefix)
73+
viper.AutomaticEnv()
74+
viper.AllowEmptyEnv(false)
75+
envKeysMap := &map[string]interface{}{}
76+
if err := mapstructure.Decode(config, &envKeysMap); err != nil {
77+
log.Fatal(err)
78+
}
79+
for k := range *envKeysMap {
80+
if _, ok := os.LookupEnv(strings.ToUpper(prefix) + "_" + strings.ToUpper(k)); !ok {
81+
log.Fatalf("Environment variable %s is not defined\n", strings.ToUpper(prefix)+"_"+strings.ToUpper(k))
82+
}
83+
if bindErr := viper.BindEnv(k); bindErr != nil {
84+
log.Fatal(bindErr)
85+
}
86+
}
87+
err := viper.Unmarshal(&config, viper.DecodeHook(parseStringAsURL()))
88+
if err != nil {
89+
log.Fatalf("unable to decode config into struct, %v\n", err)
90+
}
91+
return config
92+
}
93+
94+
// AddQueryParams makes a copy of the provided URL, adds the query parameters
95+
// and returns a url with the added parameters. The original URL is left unchanged.
96+
func AddQueryParams(url *url.URL, params map[string]string) *url.URL {
97+
newURL := *url
98+
query := newURL.Query()
99+
for k, v := range params {
100+
query.Add(k, v)
101+
}
102+
newURL.RawQuery = query.Encode()
103+
return &newURL
104+
}

cmd/revproxy/main.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Package main contains the definition of all routes, proxying and authentication
2+
// performed by the reverse proxy that is part of the Renku gateway.
3+
package main
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"net/http"
9+
"os"
10+
"os/signal"
11+
"time"
12+
13+
"github.com/labstack/echo/v4"
14+
"github.com/labstack/echo/v4/middleware"
15+
)
16+
17+
func setupServer(config revProxyConfig) *echo.Echo {
18+
// Intialize common reverse proxy middlewares
19+
fallbackProxy := proxyFromURL(config.RenkuBaseURL)
20+
renkuBaseProxyHost := setHost(config.RenkuBaseURL.Host)
21+
var gitlabProxy, gitlabProxyHost echo.MiddlewareFunc
22+
if config.ExternalGitlabURL != nil {
23+
gitlabProxy = proxyFromURL(config.ExternalGitlabURL)
24+
gitlabProxyHost = setHost(config.ExternalGitlabURL.Host)
25+
} else {
26+
gitlabProxy = fallbackProxy
27+
gitlabProxyHost = setHost(config.RenkuBaseURL.Host)
28+
}
29+
notebooksProxy := proxyFromURL(config.RenkuServices.Notebooks)
30+
authSvcProxy := proxyFromURL(config.RenkuServices.Auth)
31+
coreProxy := proxyFromURL(config.RenkuServices.Core)
32+
kgProxy := proxyFromURL(config.RenkuServices.KG)
33+
webhookProxy := proxyFromURL(config.RenkuServices.Webhook)
34+
logger := middleware.Logger()
35+
36+
// Initialize common authentication middleware
37+
notebooksAuth := authenticate(AddQueryParams(config.RenkuServices.Auth, map[string]string{"auth": "notebook"}), "Renku-Auth-Access-Token", "Renku-Auth-Id-Token", "Renku-Auth-Git-Credentials", "Renku-Auth-Anon-Id", "Renku-Auth-Refresh-Token")
38+
renkuAuth := authenticate(AddQueryParams(config.RenkuServices.Auth, map[string]string{"auth": "renku"}), "Authorization", "Renku-user-id", "Renku-user-fullname", "Renku-user-email")
39+
gitlabAuth := authenticate(AddQueryParams(config.RenkuServices.Auth, map[string]string{"auth": "gitlab"}), "Authorization")
40+
cliGitlabAuth := authenticate(AddQueryParams(config.RenkuServices.Auth, map[string]string{"auth": "cli-gitlab"}), "Authorization")
41+
42+
// Server instance
43+
e := echo.New()
44+
e.Pre(middleware.RemoveTrailingSlash())
45+
e.Use(middleware.Recover())
46+
47+
// Routing for Renku services
48+
e.Group("/api/auth", logger, authSvcProxy)
49+
e.Group("/api/notebooks", logger, notebooksAuth, noCookies, stripPrefix("/api"), notebooksProxy)
50+
e.Group("/api/projects/:projectID/graph", logger, gitlabAuth, noCookies, kgProjectsGraphRewrites, webhookProxy)
51+
e.Group("/api/datasets", logger, noCookies, regexRewrite("^/api(.*)", "/knowledge-graph$1"), kgProxy)
52+
e.Group("/api/kg", logger, gitlabAuth, noCookies, regexRewrite("^/api/kg(.*)", "/knowledge-graph$1"), kgProxy)
53+
e.Group("/api/renku", logger, renkuAuth, noCookies, stripPrefix("/api"), coreProxy)
54+
55+
// Routes that end up proxied to Gitlab
56+
if config.ExternalGitlabURL != nil {
57+
// Redirect "old" style bundled /gitlab pathing if an external Gitlab is used
58+
e.Group("/gitlab", logger, stripPrefix("/gitlab"), gitlabProxyHost, gitlabProxy)
59+
e.Group("/api/graphql", logger, gitlabAuth, gitlabProxyHost, gitlabProxy)
60+
e.Group("/api/direct", logger, stripPrefix("/api/direct"), gitlabProxyHost, gitlabProxy)
61+
e.Group("/api/repos", logger, cliGitlabAuth, noCookies, stripPrefix("/api/repos"), gitlabProxyHost, gitlabProxy)
62+
// If nothing is matched in any other more specific /api route then fall back to Gitlab
63+
e.Group("/api", logger, gitlabAuth, noCookies, regexRewrite("^/api(.*)", "/api/v4$1"), gitlabProxyHost, gitlabProxy)
64+
} else {
65+
e.Group("/api/graphql", logger, gitlabAuth, regexRewrite("^(.*)", "/gitlab$1"), gitlabProxyHost, gitlabProxy)
66+
e.Group("/api/direct", logger, regexRewrite("^/api/direct(.*)", "/gitlab$1"), gitlabProxyHost, gitlabProxy)
67+
e.Group("/api/repos", logger, cliGitlabAuth, noCookies, regexRewrite("^/api/repos(.*)", "/gitlab$1"), gitlabProxyHost, gitlabProxy)
68+
// If nothing is matched in any other more specific /api route then fall back to Gitlab
69+
e.Group("/api", logger, gitlabAuth, noCookies, regexRewrite("^/api(.*)", "/gitlab/api/v4$1"), gitlabProxyHost, gitlabProxy)
70+
}
71+
72+
// If nothing is matched from any of the routes above then fall back to the UI
73+
e.Group("/", logger, renkuBaseProxyHost, fallbackProxy)
74+
75+
// Reverse proxy specific endpoints
76+
rp := e.Group("/revproxy")
77+
rp.GET("/health", func(c echo.Context) error {
78+
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
79+
})
80+
81+
return e
82+
}
83+
84+
func main() {
85+
config := getConfig()
86+
e := setupServer(config)
87+
// Start API server
88+
e.Logger.Printf("Starting server with config: %+v", config)
89+
go func() {
90+
if err := e.Start(fmt.Sprintf(":%d", config.Port)); err != nil && err != http.ErrServerClosed {
91+
e.Logger.Fatal(err)
92+
}
93+
}()
94+
// Start metrics server if enabled
95+
var metricsServer *echo.Echo
96+
if (config.Metrics.Enabled) {
97+
metricsServer = getMetricsServer(e, config.Metrics.Port)
98+
go func() {
99+
if err := metricsServer.Start(fmt.Sprintf(":%d", config.Metrics.Port)); err != nil && err != http.ErrServerClosed {
100+
metricsServer.Logger.Fatal(err)
101+
}
102+
}()
103+
}
104+
quit := make(chan os.Signal, 1)
105+
signal.Notify(quit, os.Interrupt)
106+
<-quit // Wait for interrupt signal from OS
107+
// Start shutting down servers
108+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
109+
defer cancel()
110+
if err := e.Shutdown(ctx); err != nil {
111+
e.Logger.Fatal(err)
112+
}
113+
if config.Metrics.Enabled {
114+
if err := metricsServer.Shutdown(ctx); err != nil {
115+
metricsServer.Logger.Fatal(err)
116+
}
117+
}
118+
}

0 commit comments

Comments
 (0)