Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-pages.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVladimir Shushlin <vshushlin@gitlab.com>2020-01-31 16:56:42 +0300
committerVladimir Shushlin <vshushlin@gitlab.com>2020-01-31 16:56:42 +0300
commit57a8b1184da1d53184aad5d4f0bedea51d330b1a (patch)
tree63cb7370a7651759c86c719ae6dbb5045af81dc2
parentac272f96081c8ffaa1ca5ba3459ff1e47a6e9bb3 (diff)
parent613ca50baffdbe4bc3e28ef6ad977d943cb1e9b6 (diff)
Merge branch 'feature/gb/add-serverless-serving' into 'master'
Add a new serverless serving Closes #325 See merge request gitlab-org/gitlab-pages!216
-rw-r--r--go.mod1
-rw-r--r--go.sum2
-rw-r--r--internal/serving/serverless/certs.go26
-rw-r--r--internal/serving/serverless/cluster.go28
-rw-r--r--internal/serving/serverless/director.go22
-rw-r--r--internal/serving/serverless/errors.go26
-rw-r--r--internal/serving/serverless/function.go18
-rw-r--r--internal/serving/serverless/function_test.go17
-rw-r--r--internal/serving/serverless/serverless.go37
-rw-r--r--internal/serving/serverless/serverless_test.go181
-rw-r--r--internal/serving/serverless/transport.go46
11 files changed, 404 insertions, 0 deletions
diff --git a/go.mod b/go.mod
index 2ae4178c..9d49da99 100644
--- a/go.mod
+++ b/go.mod
@@ -20,6 +20,7 @@ require (
github.com/rs/cors v1.7.0
github.com/sirupsen/logrus v1.4.2
github.com/stretchr/testify v1.4.0
+ github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad
gitlab.com/gitlab-org/labkit v0.0.0-20190902063225-3253d7975ca7
gitlab.com/lupine/go-mimedb v0.0.0-20180307000149-e8af1d659877
diff --git a/go.sum b/go.sum
index a4d32739..5f962470 100644
--- a/go.sum
+++ b/go.sum
@@ -127,6 +127,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
+github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
+github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
github.com/uber/jaeger-client-go v2.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-lib v1.5.0/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
diff --git a/internal/serving/serverless/certs.go b/internal/serving/serverless/certs.go
new file mode 100644
index 00000000..674e8b25
--- /dev/null
+++ b/internal/serving/serverless/certs.go
@@ -0,0 +1,26 @@
+package serverless
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+)
+
+// Certs holds definition of certificates we use to perform mTLS
+// handshake with a cluster
+type Certs struct {
+ RootCerts *x509.CertPool
+ Certificate tls.Certificate
+}
+
+// NewClusterCerts creates a new cluster configuration from cert / key pair
+func NewClusterCerts(clientCert, clientKey string) (*Certs, error) {
+ cert, err := tls.X509KeyPair([]byte(clientCert), []byte(clientKey))
+ if err != nil {
+ return nil, err
+ }
+
+ caCertPool := x509.NewCertPool()
+ caCertPool.AppendCertsFromPEM([]byte(clientCert))
+
+ return &Certs{RootCerts: caCertPool, Certificate: cert}, nil
+}
diff --git a/internal/serving/serverless/cluster.go b/internal/serving/serverless/cluster.go
new file mode 100644
index 00000000..6bdd51da
--- /dev/null
+++ b/internal/serving/serverless/cluster.go
@@ -0,0 +1,28 @@
+package serverless
+
+import (
+ "crypto/tls"
+ "strings"
+)
+
+// Cluster represent a Knative cluster that we want to proxy requests to
+type Cluster struct {
+ Address string // Address is a real IP address of a cluster ingress
+ Port string // Port is a real port of HTTP TLS service
+ Name string // Name is a cluster name, used in cluster certificates
+ Certs *Certs
+}
+
+// Host returns a real cluster location based on IP address and port
+func (c Cluster) Host() string {
+ return strings.Join([]string{c.Address, c.Port}, ":")
+}
+
+// TLSConfig builds a new tls.Config and returns a pointer to it
+func (c Cluster) TLSConfig() *tls.Config {
+ return &tls.Config{
+ Certificates: []tls.Certificate{c.Certs.Certificate},
+ RootCAs: c.Certs.RootCerts,
+ ServerName: c.Name,
+ }
+}
diff --git a/internal/serving/serverless/director.go b/internal/serving/serverless/director.go
new file mode 100644
index 00000000..4478b104
--- /dev/null
+++ b/internal/serving/serverless/director.go
@@ -0,0 +1,22 @@
+package serverless
+
+import (
+ "net/http"
+
+ "github.com/tomasen/realip"
+)
+
+// NewDirectorFunc returns a director function capable of configuring a proxy
+// request
+func NewDirectorFunc(function Function) func(*http.Request) {
+ return func(request *http.Request) {
+ host := function.Host()
+
+ request.Host = host
+ request.URL.Host = host
+ request.URL.Scheme = "https"
+ request.Header.Set("User-Agent", "GitLab Pages Daemon")
+ request.Header.Set("X-Forwarded-For", realip.FromRequest(request))
+ request.Header.Set("X-Forwarded-Proto", "https")
+ }
+}
diff --git a/internal/serving/serverless/errors.go b/internal/serving/serverless/errors.go
new file mode 100644
index 00000000..d208a11d
--- /dev/null
+++ b/internal/serving/serverless/errors.go
@@ -0,0 +1,26 @@
+package serverless
+
+import (
+ "encoding/json"
+ "net/http"
+)
+
+// NewErrorHandler returns a func(http.ResponseWriter, *http.Request, error)
+// responsible for handling proxy errors
+func NewErrorHandler() func(http.ResponseWriter, *http.Request, error) {
+ return func(w http.ResponseWriter, r *http.Request, err error) {
+ w.WriteHeader(http.StatusInternalServerError)
+
+ message := "cluster error: " + err.Error()
+ msgmap := map[string]string{"error": message}
+
+ json, err := json.Marshal(msgmap)
+ if err != nil {
+ w.Write([]byte(message))
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.Write(json)
+ }
+}
diff --git a/internal/serving/serverless/function.go b/internal/serving/serverless/function.go
new file mode 100644
index 00000000..20d4ec2c
--- /dev/null
+++ b/internal/serving/serverless/function.go
@@ -0,0 +1,18 @@
+package serverless
+
+import "strings"
+
+// Function represents a Knative service that is going to be invoked by the
+// proxied request
+type Function struct {
+ Name string // Name is a function name, it includes a "service name" component too
+ Namespace string // Namespace is a kubernetes namespace this function has been deployed to
+ BaseDomain string // BaseDomain is a cluster base domain, used to route requests to apropriate service
+}
+
+// Host returns a function address that we are going to expose in the `Host:`
+// header to make it possible to route a proxied request to appropriate service
+// in a Knative cluster
+func (f Function) Host() string {
+ return strings.Join([]string{f.Name, f.Namespace, f.BaseDomain}, ".")
+}
diff --git a/internal/serving/serverless/function_test.go b/internal/serving/serverless/function_test.go
new file mode 100644
index 00000000..65d84eb7
--- /dev/null
+++ b/internal/serving/serverless/function_test.go
@@ -0,0 +1,17 @@
+package serverless
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestFunctionHost(t *testing.T) {
+ function := Function{
+ Name: "my-func",
+ Namespace: "my-namespace-123",
+ BaseDomain: "knative.example.com",
+ }
+
+ require.Equal(t, "my-func.my-namespace-123.knative.example.com", function.Host())
+}
diff --git a/internal/serving/serverless/serverless.go b/internal/serving/serverless/serverless.go
new file mode 100644
index 00000000..a8d090da
--- /dev/null
+++ b/internal/serving/serverless/serverless.go
@@ -0,0 +1,37 @@
+package serverless
+
+import (
+ "net/http/httputil"
+
+ "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/serving"
+)
+
+// Serverless is a servering used to proxy requests between a client and
+// Knative cluster.
+type Serverless struct {
+ proxy *httputil.ReverseProxy
+}
+
+// New returns a new serving instance
+func New(function Function, cluster Cluster) serving.Serving {
+ proxy := httputil.ReverseProxy{
+ Director: NewDirectorFunc(function),
+ Transport: NewTransport(cluster),
+ ErrorHandler: NewErrorHandler(),
+ }
+
+ return &Serverless{proxy: &proxy}
+}
+
+// ServeFileHTTP handle an incoming request and proxies it to Knative cluster
+func (s *Serverless) ServeFileHTTP(h serving.Handler) bool {
+ s.proxy.ServeHTTP(h.Writer, h.Request)
+
+ return true
+}
+
+// ServeNotFoundHTTP responds with 404
+func (s *Serverless) ServeNotFoundHTTP(h serving.Handler) {
+ httperrors.Serve404(h.Writer)
+}
diff --git a/internal/serving/serverless/serverless_test.go b/internal/serving/serverless/serverless_test.go
new file mode 100644
index 00000000..c330cbda
--- /dev/null
+++ b/internal/serving/serverless/serverless_test.go
@@ -0,0 +1,181 @@
+package serverless
+
+import (
+ "crypto/tls"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "gitlab.com/gitlab-org/gitlab-pages/internal/fixture"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/serving"
+)
+
+func withTestCluster(t *testing.T, cert, key string, block func(*http.ServeMux, *url.URL, *Certs)) {
+ mux := http.NewServeMux()
+ cluster := httptest.NewUnstartedServer(mux)
+
+ certs, err := NewClusterCerts(fixture.Certificate, fixture.Key)
+ require.NoError(t, err)
+
+ cluster.TLS = &tls.Config{
+ Certificates: []tls.Certificate{certs.Certificate},
+ RootCAs: certs.RootCerts,
+ }
+
+ cluster.StartTLS()
+ defer cluster.Close()
+
+ address, err := url.Parse(cluster.URL)
+ require.NoError(t, err)
+
+ block(mux, address, certs)
+}
+
+func TestServeFileHTTP(t *testing.T) {
+ t.Run("when proxying simple request to a cluster", func(t *testing.T) {
+ withTestCluster(t, fixture.Certificate, fixture.Key, func(mux *http.ServeMux, server *url.URL, certs *Certs) {
+ serverless := New(
+ Function{
+ Name: "my-func",
+ Namespace: "my-namespace-123",
+ BaseDomain: "knative.example.com",
+ },
+ Cluster{
+ Name: "knative.gitlab-example.com",
+ Address: server.Hostname(),
+ Port: server.Port(),
+ Certs: certs,
+ },
+ )
+
+ writer := httptest.NewRecorder()
+ request := httptest.NewRequest("GET", "http://example.gitlab.com/", nil)
+ handler := serving.Handler{Writer: writer, Request: request}
+ request.Header.Set("X-Real-IP", "127.0.0.105")
+
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ require.Equal(t, "my-func.my-namespace-123.knative.example.com", r.Host)
+ require.Equal(t, "GitLab Pages Daemon", r.Header.Get("User-Agent"))
+ require.Equal(t, "https", r.Header.Get("X-Forwarded-Proto"))
+ require.Contains(t, r.Header.Get("X-Forwarded-For"), "127.0.0.105")
+ })
+
+ served := serverless.ServeFileHTTP(handler)
+ result := writer.Result()
+
+ require.True(t, served)
+ require.Equal(t, http.StatusOK, result.StatusCode)
+ })
+ })
+
+ t.Run("when proxying request with invalid hostname", func(t *testing.T) {
+ withTestCluster(t, fixture.Certificate, fixture.Key, func(mux *http.ServeMux, server *url.URL, certs *Certs) {
+ serverless := New(
+ Function{
+ Name: "my-func",
+ Namespace: "my-namespace-123",
+ BaseDomain: "knative.example.com",
+ },
+ Cluster{
+ Name: "knative.invalid-gitlab-example.com",
+ Address: server.Hostname(),
+ Port: server.Port(),
+ Certs: certs,
+ },
+ )
+
+ writer := httptest.NewRecorder()
+ request := httptest.NewRequest("GET", "http://example.gitlab.com/", nil)
+ handler := serving.Handler{Writer: writer, Request: request}
+
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusBadRequest)
+ })
+
+ served := serverless.ServeFileHTTP(handler)
+ result := writer.Result()
+ body, err := ioutil.ReadAll(writer.Body)
+ require.NoError(t, err)
+
+ require.True(t, served)
+ require.Equal(t, http.StatusInternalServerError, result.StatusCode)
+ require.Contains(t, string(body), "cluster error: x509: certificate")
+ })
+ })
+
+ t.Run("when a cluster responds with an error", func(t *testing.T) {
+ withTestCluster(t, fixture.Certificate, fixture.Key, func(mux *http.ServeMux, server *url.URL, certs *Certs) {
+ serverless := New(
+ Function{
+ Name: "my-func",
+ Namespace: "my-namespace-123",
+ BaseDomain: "knative.example.com",
+ },
+ Cluster{
+ Name: "knative.gitlab-example.com",
+ Address: server.Hostname(),
+ Port: server.Port(),
+ Certs: certs,
+ },
+ )
+
+ writer := httptest.NewRecorder()
+ request := httptest.NewRequest("GET", "http://example.gitlab.com/", nil)
+ handler := serving.Handler{Writer: writer, Request: request}
+
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusServiceUnavailable)
+ w.Write([]byte("sorry, service unavailable"))
+ })
+
+ served := serverless.ServeFileHTTP(handler)
+ result := writer.Result()
+ body, err := ioutil.ReadAll(writer.Body)
+ require.NoError(t, err)
+
+ require.True(t, served)
+ require.Equal(t, http.StatusServiceUnavailable, result.StatusCode)
+ require.Contains(t, string(body), "sorry, service unavailable")
+ })
+ })
+
+ t.Run("when a cluster responds correctly", func(t *testing.T) {
+ withTestCluster(t, fixture.Certificate, fixture.Key, func(mux *http.ServeMux, server *url.URL, certs *Certs) {
+ serverless := New(
+ Function{
+ Name: "my-func",
+ Namespace: "my-namespace-123",
+ BaseDomain: "knative.example.com",
+ },
+ Cluster{
+ Name: "knative.gitlab-example.com",
+ Address: server.Hostname(),
+ Port: server.Port(),
+ Certs: certs,
+ },
+ )
+
+ writer := httptest.NewRecorder()
+ request := httptest.NewRequest("GET", "http://example.gitlab.com/", nil)
+ handler := serving.Handler{Writer: writer, Request: request}
+
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("OK"))
+ })
+
+ served := serverless.ServeFileHTTP(handler)
+ result := writer.Result()
+ body, err := ioutil.ReadAll(writer.Body)
+ require.NoError(t, err)
+
+ require.True(t, served)
+ require.Equal(t, http.StatusOK, result.StatusCode)
+ require.Contains(t, string(body), "OK")
+ })
+ })
+}
diff --git a/internal/serving/serverless/transport.go b/internal/serving/serverless/transport.go
new file mode 100644
index 00000000..5a0f5165
--- /dev/null
+++ b/internal/serving/serverless/transport.go
@@ -0,0 +1,46 @@
+package serverless
+
+import (
+ "context"
+ "net"
+ "net/http"
+ "time"
+)
+
+// Transport is a struct that handle the proxy connection round trip to Knative
+// cluster
+type Transport struct {
+ cluster Cluster
+ transport *http.Transport
+}
+
+// NewTransport fabricates as new transport type
+func NewTransport(cluster Cluster) *Transport {
+ dialer := net.Dialer{
+ Timeout: 4 * time.Minute,
+ KeepAlive: 6 * time.Minute,
+ }
+
+ dialContext := func(ctx context.Context, network, address string) (net.Conn, error) {
+ address = cluster.Host()
+
+ return dialer.DialContext(ctx, network, address)
+ }
+
+ return &Transport{
+ cluster: cluster,
+ transport: &http.Transport{
+ DialContext: dialContext,
+ TLSHandshakeTimeout: 5 * time.Second,
+ TLSClientConfig: cluster.TLSConfig(),
+ },
+ }
+}
+
+// RoundTrip performs a connection to a Knative cluster and returns a response
+func (t *Transport) RoundTrip(request *http.Request) (*http.Response, error) {
+ response, err := t.transport.RoundTrip(request)
+
+ // TODO add prometheus metrics for round trip timing
+ return response, err
+}