diff options
author | Alessio Caiazza <acaiazza@gitlab.com> | 2020-02-28 18:51:48 +0300 |
---|---|---|
committer | Alessio Caiazza <acaiazza@gitlab.com> | 2020-02-28 18:51:48 +0300 |
commit | 487aaebc4184020c1f7695c55b631fe7989f437d (patch) | |
tree | b2499ae291b9283e0691ce43b0a0ab1091c4acbb | |
parent | 70879969bde8aaf97d5ed97d15b904747a44bfb0 (diff) | |
parent | 2f086a69431d0095ec2e46c69d3bcea187a67085 (diff) |
Merge branch 'feature/gb/serverless-serving-enable' into 'master'
Enable serverless serving
See merge request gitlab-org/gitlab-pages!232
-rw-r--r-- | internal/domain/domain.go | 50 | ||||
-rw-r--r-- | internal/domain/domain_test.go | 9 | ||||
-rw-r--r-- | internal/domain/resolver.go | 8 | ||||
-rw-r--r-- | internal/serving/disk/serving.go | 22 | ||||
-rw-r--r-- | internal/serving/handler.go | 3 | ||||
-rw-r--r-- | internal/serving/request.go | 35 | ||||
-rw-r--r-- | internal/serving/serverless/director.go | 8 | ||||
-rw-r--r-- | internal/serving/serverless/function.go | 18 | ||||
-rw-r--r-- | internal/serving/serverless/function_test.go | 17 | ||||
-rw-r--r-- | internal/serving/serverless/serverless.go | 31 | ||||
-rw-r--r-- | internal/serving/serverless/serverless_test.go | 24 | ||||
-rw-r--r-- | internal/source/disk/custom.go | 9 | ||||
-rw-r--r-- | internal/source/disk/group.go | 13 | ||||
-rw-r--r-- | internal/source/domains.go | 27 | ||||
-rw-r--r-- | internal/source/domains_test.go | 31 | ||||
-rw-r--r-- | internal/source/gitlab/api/lookup_path.go | 27 | ||||
-rw-r--r-- | internal/source/gitlab/factory.go | 51 | ||||
-rw-r--r-- | internal/source/gitlab/factory_test.go | 64 | ||||
-rw-r--r-- | internal/source/gitlab/gitlab.go | 31 | ||||
-rw-r--r-- | internal/source/gitlab/gitlab_test.go | 46 |
20 files changed, 367 insertions, 157 deletions
diff --git a/internal/domain/domain.go b/internal/domain/domain.go index 28eb3196..17a0e1d3 100644 --- a/internal/domain/domain.go +++ b/internal/domain/domain.go @@ -19,8 +19,6 @@ type Domain struct { Resolver Resolver - serving serving.Serving - certificate *tls.Certificate certificateError error certificateOnce sync.Once @@ -39,42 +37,28 @@ func (d *Domain) isUnconfigured() bool { return d.Resolver == nil } -func (d *Domain) resolve(r *http.Request) (*serving.LookupPath, string) { - lookupPath, subpath, _ := d.Resolver.Resolve(r) +func (d *Domain) resolve(r *http.Request) *serving.Request { + request, _ := d.Resolver.Resolve(r) - // Current implementation does not return errors in any case - if lookupPath == nil { - return nil, "" + // TODO improve code around default serving, when `disk` serving gets removed + // https://gitlab.com/gitlab-org/gitlab-pages/issues/353 + if request == nil { + return &serving.Request{Serving: disk.New()} } - return lookupPath, subpath + return request } -// GetLookupPath returns a project details based on the request +// GetLookupPath returns a project details based on the request. It returns nil +// if project does not exist. func (d *Domain) GetLookupPath(r *http.Request) *serving.LookupPath { - lookupPath, _ := d.resolve(r) - - return lookupPath -} + request := d.resolve(r) -// Serving returns domain serving driver -func (d *Domain) Serving() serving.Serving { - if d.serving == nil { - d.serving = disk.New() + if request == nil { + return nil } - return d.serving -} - -func (d *Domain) toHandler(w http.ResponseWriter, r *http.Request) serving.Handler { - project, subpath := d.resolve(r) - - return serving.Handler{ - Writer: w, - Request: r, - LookupPath: project, - SubPath: subpath, - } + return request.LookupPath } // IsHTTPSOnly figures out if the request should be handled with HTTPS @@ -168,7 +152,9 @@ func (d *Domain) ServeFileHTTP(w http.ResponseWriter, r *http.Request) bool { return true } - return d.Serving().ServeFileHTTP(d.toHandler(w, r)) + request := d.resolve(r) + + return request.ServeFileHTTP(w, r) } // ServeNotFoundHTTP serves the not found pages from the projects. @@ -178,5 +164,7 @@ func (d *Domain) ServeNotFoundHTTP(w http.ResponseWriter, r *http.Request) { return } - d.Serving().ServeNotFoundHTTP(d.toHandler(w, r)) + request := d.resolve(r) + + request.ServeNotFoundHTTP(w, r) } diff --git a/internal/domain/domain_test.go b/internal/domain/domain_test.go index a43d3c9a..aa95eb95 100644 --- a/internal/domain/domain_test.go +++ b/internal/domain/domain_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitlab-pages/internal/serving" + "gitlab.com/gitlab-org/gitlab-pages/internal/serving/disk" "gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers" ) @@ -21,8 +22,12 @@ type stubbedResolver struct { err error } -func (resolver *stubbedResolver) Resolve(*http.Request) (*serving.LookupPath, string, error) { - return resolver.project, resolver.subpath, resolver.err +func (resolver *stubbedResolver) Resolve(*http.Request) (*serving.Request, error) { + return &serving.Request{ + Serving: disk.New(), + LookupPath: resolver.project, + SubPath: resolver.subpath, + }, resolver.err } func serveFileOrNotFound(domain *Domain) http.HandlerFunc { diff --git a/internal/domain/resolver.go b/internal/domain/resolver.go index 6bc10f8c..4b93baed 100644 --- a/internal/domain/resolver.go +++ b/internal/domain/resolver.go @@ -6,9 +6,9 @@ import ( "gitlab.com/gitlab-org/gitlab-pages/internal/serving" ) -// Resolver represents an interface responsible for resolving a project -// per-request +// Resolver represents an interface responsible for resolving a pages serving +// request for each HTTP request type Resolver interface { - // Resolve returns a project with a file path and an error if it occurred - Resolve(*http.Request) (*serving.LookupPath, string, error) + // Resolve returns a serving request and an error if it occurred + Resolve(*http.Request) (*serving.Request, error) } diff --git a/internal/serving/disk/serving.go b/internal/serving/disk/serving.go index db184d3c..682791fe 100644 --- a/internal/serving/disk/serving.go +++ b/internal/serving/disk/serving.go @@ -5,15 +5,17 @@ import ( "gitlab.com/gitlab-org/gitlab-pages/internal/serving" ) -// Serving describes a disk access serving -type Serving struct { - Reader +var disk *Disk = &Disk{} + +// Disk describes a disk access serving +type Disk struct { + reader Reader } // ServeFileHTTP serves a file from disk and returns true. It returns false // when a file could not been found. -func (s *Serving) ServeFileHTTP(h serving.Handler) bool { - if s.tryFile(h) == nil { +func (s *Disk) ServeFileHTTP(h serving.Handler) bool { + if s.reader.tryFile(h) == nil { return true } @@ -21,8 +23,8 @@ func (s *Serving) ServeFileHTTP(h serving.Handler) bool { } // ServeNotFoundHTTP tries to read a custom 404 page -func (s *Serving) ServeNotFoundHTTP(h serving.Handler) { - if s.tryNotFound(h) == nil { +func (s *Disk) ServeNotFoundHTTP(h serving.Handler) { + if s.reader.tryNotFound(h) == nil { return } @@ -33,5 +35,9 @@ func (s *Serving) ServeNotFoundHTTP(h serving.Handler) { // New returns a serving instance that is capable of reading files // from the disk func New() serving.Serving { - return &Serving{} + if disk == nil { + disk = &Disk{} + } + + return disk } diff --git a/internal/serving/handler.go b/internal/serving/handler.go index 99c4ca2f..a0d66ecb 100644 --- a/internal/serving/handler.go +++ b/internal/serving/handler.go @@ -8,6 +8,5 @@ type Handler struct { Writer http.ResponseWriter Request *http.Request LookupPath *LookupPath - // Parsed representation of Request.URI that is part of LookupPath.Prefix - SubPath string + SubPath string } diff --git a/internal/serving/request.go b/internal/serving/request.go new file mode 100644 index 00000000..694d0df4 --- /dev/null +++ b/internal/serving/request.go @@ -0,0 +1,35 @@ +package serving + +import "net/http" + +// Request is a type that aggregates a serving itself, project lookup path and +// a request subpath based on an incoming request to serve page. +type Request struct { + Serving Serving // Serving chosen to serve this request + LookupPath *LookupPath // LookupPath contains pages project details + SubPath string // Subpath is a URL path subcomponent for this request +} + +// ServeFileHTTP forwards serving request handler to the serving itself +func (s *Request) ServeFileHTTP(w http.ResponseWriter, r *http.Request) bool { + handler := Handler{ + Writer: w, + Request: r, + LookupPath: s.LookupPath, + SubPath: s.SubPath, + } + + return s.Serving.ServeFileHTTP(handler) +} + +// ServeNotFoundHTTP forwards serving request handler to the serving itself +func (s *Request) ServeNotFoundHTTP(w http.ResponseWriter, r *http.Request) { + handler := Handler{ + Writer: w, + Request: r, + LookupPath: s.LookupPath, + SubPath: s.SubPath, + } + + s.Serving.ServeNotFoundHTTP(handler) +} diff --git a/internal/serving/serverless/director.go b/internal/serving/serverless/director.go index 4478b104..3f1bc99a 100644 --- a/internal/serving/serverless/director.go +++ b/internal/serving/serverless/director.go @@ -8,12 +8,10 @@ import ( // NewDirectorFunc returns a director function capable of configuring a proxy // request -func NewDirectorFunc(function Function) func(*http.Request) { +func NewDirectorFunc(service string) func(*http.Request) { return func(request *http.Request) { - host := function.Host() - - request.Host = host - request.URL.Host = host + request.Host = service + request.URL.Host = service request.URL.Scheme = "https" request.Header.Set("User-Agent", "GitLab Pages Daemon") request.Header.Set("X-Forwarded-For", realip.FromRequest(request)) diff --git a/internal/serving/serverless/function.go b/internal/serving/serverless/function.go deleted file mode 100644 index 20d4ec2c..00000000 --- a/internal/serving/serverless/function.go +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 65d84eb7..00000000 --- a/internal/serving/serverless/function_test.go +++ /dev/null @@ -1,17 +0,0 @@ -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 index a73affeb..e1881362 100644 --- a/internal/serving/serverless/serverless.go +++ b/internal/serving/serverless/serverless.go @@ -1,10 +1,12 @@ package serverless import ( + "errors" "net/http/httputil" "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors" "gitlab.com/gitlab-org/gitlab-pages/internal/serving" + "gitlab.com/gitlab-org/gitlab-pages/internal/source/gitlab/api" "gitlab.com/gitlab-org/gitlab-pages/metrics" ) @@ -14,10 +16,35 @@ type Serverless struct { proxy *httputil.ReverseProxy } +// NewFromAPISource returns a serverless serving instance built from GitLab API +// response +func NewFromAPISource(config api.Serverless) (serving.Serving, error) { + if len(config.Service) == 0 { + return nil, errors.New("incomplete serverless serving config") + } + + certs, err := NewClusterCerts( + config.Cluster.CertificateCert, + config.Cluster.CertificateKey, + ) + if err != nil { + return nil, err + } + + cluster := Cluster{ + Name: config.Cluster.Hostname, + Address: config.Cluster.Address, + Port: config.Cluster.Port, + Certs: certs, + } + + return New(config.Service, cluster), nil +} + // New returns a new serving instance -func New(function Function, cluster Cluster) serving.Serving { +func New(service string, cluster Cluster) serving.Serving { proxy := httputil.ReverseProxy{ - Director: NewDirectorFunc(function), + Director: NewDirectorFunc(service), Transport: NewTransport(cluster), ErrorHandler: NewErrorHandler(), } diff --git a/internal/serving/serverless/serverless_test.go b/internal/serving/serverless/serverless_test.go index c330cbda..ebd14343 100644 --- a/internal/serving/serverless/serverless_test.go +++ b/internal/serving/serverless/serverless_test.go @@ -39,11 +39,7 @@ 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", - }, + "my-func.my-namespace-123.knative.example.com", Cluster{ Name: "knative.gitlab-example.com", Address: server.Hostname(), @@ -75,11 +71,7 @@ func TestServeFileHTTP(t *testing.T) { 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", - }, + "my-func.my-namespace-123.knative.example.com", Cluster{ Name: "knative.invalid-gitlab-example.com", Address: server.Hostname(), @@ -110,11 +102,7 @@ func TestServeFileHTTP(t *testing.T) { 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", - }, + "my-func.my-namespace-123.knative.example.com", Cluster{ Name: "knative.gitlab-example.com", Address: server.Hostname(), @@ -146,11 +134,7 @@ func TestServeFileHTTP(t *testing.T) { 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", - }, + "my-func.my-namespace-123.knative.example.com", Cluster{ Name: "knative.gitlab-example.com", Address: server.Hostname(), diff --git a/internal/source/disk/custom.go b/internal/source/disk/custom.go index cc4f3f4c..2668ed81 100644 --- a/internal/source/disk/custom.go +++ b/internal/source/disk/custom.go @@ -4,6 +4,7 @@ import ( "net/http" "gitlab.com/gitlab-org/gitlab-pages/internal/serving" + "gitlab.com/gitlab-org/gitlab-pages/internal/serving/disk" ) type customProjectResolver struct { @@ -12,7 +13,7 @@ type customProjectResolver struct { path string } -func (p *customProjectResolver) Resolve(r *http.Request) (*serving.LookupPath, string, error) { +func (p *customProjectResolver) Resolve(r *http.Request) (*serving.Request, error) { lookupPath := &serving.LookupPath{ Prefix: "/", Path: p.path, @@ -22,5 +23,9 @@ func (p *customProjectResolver) Resolve(r *http.Request) (*serving.LookupPath, s ProjectID: p.config.ID, } - return lookupPath, r.URL.Path, nil + return &serving.Request{ + Serving: disk.New(), + LookupPath: lookupPath, + SubPath: r.URL.Path, + }, nil } diff --git a/internal/source/disk/group.go b/internal/source/disk/group.go index 9f466bc4..e0365bbd 100644 --- a/internal/source/disk/group.go +++ b/internal/source/disk/group.go @@ -8,6 +8,7 @@ import ( "gitlab.com/gitlab-org/gitlab-pages/internal/host" "gitlab.com/gitlab-org/gitlab-pages/internal/serving" + "gitlab.com/gitlab-org/gitlab-pages/internal/serving/disk" ) const ( @@ -77,11 +78,13 @@ func (g *Group) getProjectConfigWithSubpath(r *http.Request) (*projectConfig, st // Resolve tries to find project and its config recursively for a given request // to a group domain -func (g *Group) Resolve(r *http.Request) (*serving.LookupPath, string, error) { +func (g *Group) Resolve(r *http.Request) (*serving.Request, error) { projectConfig, prefix, projectPath, subPath := g.getProjectConfigWithSubpath(r) if projectConfig == nil { - return nil, "", nil // it is not an error when project does not exist + // it is not an error when project does not exist, in that case + // serving.Request.LookupPath is nil. + return &serving.Request{Serving: disk.New()}, nil } lookupPath := &serving.LookupPath{ @@ -93,5 +96,9 @@ func (g *Group) Resolve(r *http.Request) (*serving.LookupPath, string, error) { ProjectID: projectConfig.ID, } - return lookupPath, subPath, nil + return &serving.Request{ + Serving: disk.New(), + LookupPath: lookupPath, + SubPath: subPath, + }, nil } diff --git a/internal/source/domains.go b/internal/source/domains.go index 79357766..11794b91 100644 --- a/internal/source/domains.go +++ b/internal/source/domains.go @@ -2,6 +2,7 @@ package source import ( "errors" + "regexp" "time" log "github.com/sirupsen/logrus" @@ -13,7 +14,14 @@ import ( "gitlab.com/gitlab-org/gitlab-pages/internal/source/gitlab" ) -var gitlabSourceConfig gitlabsourceconfig.GitlabSourceConfig +var ( + gitlabSourceConfig gitlabsourceconfig.GitlabSourceConfig + + // serverlessDomainRegex is a regular expression we use to check if a domain + // is a serverless domain, to short circut gitlab source rollout. It can be + // removed after the rollout is done + serverlessDomainRegex = regexp.MustCompile(`^[^.]+-[[:xdigit:]]{2}a1[[:xdigit:]]{10}f2[[:xdigit:]]{2}[[:xdigit:]]+-?.*`) +) func init() { // Start watching the config file for domains that will use the new `gitlab` source, @@ -78,6 +86,13 @@ func (d *Domains) source(domain string) Source { return d.disk } + // This check is only needed until we enable `d.gitlab` source in all + // environments (including on-premises installations) followed by removal of + // `d.disk` source. This can be safely removed afterwards. + if IsServerlessDomain(domain) { + return d.gitlab + } + for _, name := range gitlabSourceConfig.Domains.Enabled { if domain == name { return d.gitlab @@ -98,3 +113,13 @@ func (d *Domains) source(domain string) Source { return d.disk } + +// IsServerlessDomain checks if a domain requested is a serverless domain we +// need to handle differently. +// +// Domain is a serverless domain when it matches `serverlessDomainRegex`. The +// regular expression is also defined on the gitlab-rails side, see +// https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/serverless/domain.rb#L7 +func IsServerlessDomain(domain string) bool { + return serverlessDomainRegex.MatchString(domain) +} diff --git a/internal/source/domains_test.go b/internal/source/domains_test.go index d639b02d..9afde412 100644 --- a/internal/source/domains_test.go +++ b/internal/source/domains_test.go @@ -109,6 +109,37 @@ func TestGetDomain(t *testing.T) { require.Nil(t, domain) require.NoError(t, err) }) + + t.Run("when requesting a serverless domain", func(t *testing.T) { + testDomain := "func-aba1aabbccddeef2abaabbcc.serverless.gitlab.io" + + newSource := NewMockSource() + newSource.On("GetDomain", testDomain). + Return(&domain.Domain{Name: testDomain}, nil). + Once() + defer newSource.AssertExpectations(t) + + domains := &Domains{ + disk: disk.New(), + gitlab: newSource, + } + + domains.GetDomain(testDomain) + }) +} + +func TestIsServerlessDomain(t *testing.T) { + t.Run("when a domain is serverless domain", func(t *testing.T) { + require.True(t, IsServerlessDomain("some-function-aba1aabbccddeef2abaabbcc.serverless.gitlab.io")) + }) + + t.Run("when a domain is serverless domain with environment", func(t *testing.T) { + require.True(t, IsServerlessDomain("some-function-aba1aabbccddeef2abaabbcc-testing.serverless.gitlab.io")) + }) + + t.Run("when a domain is not a serverless domain", func(t *testing.T) { + require.False(t, IsServerlessDomain("somedomain.gitlab.io")) + }) } func TestGetDomainWithIncrementalrolloutOfGitLabSource(t *testing.T) { diff --git a/internal/source/gitlab/api/lookup_path.go b/internal/source/gitlab/api/lookup_path.go index b0407638..77b264ff 100644 --- a/internal/source/gitlab/api/lookup_path.go +++ b/internal/source/gitlab/api/lookup_path.go @@ -6,8 +6,27 @@ type LookupPath struct { AccessControl bool `json:"access_control,omitempty"` HTTPSOnly bool `json:"https_only,omitempty"` Prefix string `json:"prefix,omitempty"` - Source struct { - Type string `json:"type,omitempty"` - Path string `json:"path,omitempty"` - } + Source Source `json:"source,omitempty"` +} + +// Source describes GitLab Page serving variant +type Source struct { + Type string `json:"type,omitempty"` + Path string `json:"path,omitempty"` + Serverless Serverless `json:"serverless,omitempty"` +} + +// Serverless describes serverless serving configuration +type Serverless struct { + Service string `json:"service,omitempty"` + Cluster Cluster `json:"cluster,omitempty"` +} + +// Cluster describes serverless cluster configuration +type Cluster struct { + Address string `json:"address,omitempty"` + Port string `json:"port,omitempty"` + Hostname string `json:"hostname,omitempty"` + CertificateCert string `json:"cert,omitempty"` + CertificateKey string `json:"key,omitempty"` } diff --git a/internal/source/gitlab/factory.go b/internal/source/gitlab/factory.go new file mode 100644 index 00000000..d526994f --- /dev/null +++ b/internal/source/gitlab/factory.go @@ -0,0 +1,51 @@ +package gitlab + +import ( + "strings" + + log "github.com/sirupsen/logrus" + + "gitlab.com/gitlab-org/gitlab-pages/internal/serving" + "gitlab.com/gitlab-org/gitlab-pages/internal/serving/disk" + "gitlab.com/gitlab-org/gitlab-pages/internal/serving/serverless" + "gitlab.com/gitlab-org/gitlab-pages/internal/source/gitlab/api" +) + +// fabricateLookupPath fabricates a serving LookupPath based on the API LookupPath +// `size` argument is DEPRECATED, see +// https://gitlab.com/gitlab-org/gitlab-pages/issues/272 +func fabricateLookupPath(size int, lookup api.LookupPath) *serving.LookupPath { + return &serving.LookupPath{ + Prefix: lookup.Prefix, + Path: strings.TrimPrefix(lookup.Source.Path, "/"), + IsNamespaceProject: (lookup.Prefix == "/" && size > 1), + IsHTTPSOnly: lookup.HTTPSOnly, + HasAccessControl: lookup.AccessControl, + ProjectID: uint64(lookup.ProjectID), + } +} + +// fabricateServing fabricates serving based on the GitLab API response +func fabricateServing(lookup api.LookupPath) serving.Serving { + source := lookup.Source + + switch source.Type { + case "file": + return disk.New() + case "serverless": + serving, err := serverless.NewFromAPISource(source.Serverless) + if err != nil { + log.WithError(err).Errorf("could not fabricate serving for project %d", lookup.ProjectID) + + break + } + + return serving + } + + return defaultServing() +} + +func defaultServing() serving.Serving { + return disk.New() +} diff --git a/internal/source/gitlab/factory_test.go b/internal/source/gitlab/factory_test.go new file mode 100644 index 00000000..2f3e1994 --- /dev/null +++ b/internal/source/gitlab/factory_test.go @@ -0,0 +1,64 @@ +package gitlab + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-pages/internal/fixture" + "gitlab.com/gitlab-org/gitlab-pages/internal/serving/disk" + "gitlab.com/gitlab-org/gitlab-pages/internal/serving/serverless" + "gitlab.com/gitlab-org/gitlab-pages/internal/source/gitlab/api" +) + +func TestFabricateLookupPath(t *testing.T) { + t.Run("when lookup path is not a namespace project", func(t *testing.T) { + lookup := api.LookupPath{Prefix: "/something"} + + path := fabricateLookupPath(1, lookup) + + require.Equal(t, path.Prefix, "/something") + require.False(t, path.IsNamespaceProject) + }) + + t.Run("when lookup path is a namespace project", func(t *testing.T) { + lookup := api.LookupPath{Prefix: "/"} + + path := fabricateLookupPath(2, lookup) + + require.Equal(t, path.Prefix, "/") + require.True(t, path.IsNamespaceProject) + }) +} + +func TestFabricateServing(t *testing.T) { + t.Run("when lookup path requires disk serving", func(t *testing.T) { + lookup := api.LookupPath{ + Prefix: "/", + Source: api.Source{Type: "file"}, + } + + require.IsType(t, &disk.Disk{}, fabricateServing(lookup)) + }) + + t.Run("when lookup path requires serverless serving", func(t *testing.T) { + lookup := api.LookupPath{ + Prefix: "/", + Source: api.Source{ + Type: "serverless", + Serverless: api.Serverless{ + Service: "my-func.knative.example.com", + Cluster: api.Cluster{ + Address: "127.0.0.10", + Port: "443", + Hostname: "my-cluster.example.com", + CertificateCert: fixture.Certificate, + CertificateKey: fixture.Key, + }, + }, + }, + } + + require.IsType(t, &serverless.Serverless{}, fabricateServing(lookup)) + }) +} diff --git a/internal/source/gitlab/gitlab.go b/internal/source/gitlab/gitlab.go index cce70733..6260200a 100644 --- a/internal/source/gitlab/gitlab.go +++ b/internal/source/gitlab/gitlab.go @@ -45,6 +45,8 @@ func (g *Gitlab) GetDomain(name string) (*domain.Domain, error) { return nil, nil } + // TODO introduce a second-level cache for domains, invalidate using etags + // from first-level cache domain := domain.Domain{ Name: name, CertificateCert: lookup.Domain.Certificate, @@ -55,40 +57,39 @@ func (g *Gitlab) GetDomain(name string) (*domain.Domain, error) { return &domain, nil } -// Resolve is supposed to get the serving lookup path based on the request from -// the GitLab source -func (g *Gitlab) Resolve(r *http.Request) (*serving.LookupPath, string, error) { +// Resolve is supposed to return the serving request containing lookup path, +// subpath for a given lookup and the serving itself created based on a request +// from GitLab pages domains source +func (g *Gitlab) Resolve(r *http.Request) (*serving.Request, error) { host := request.GetHostWithoutPort(r) response := g.client.Resolve(r.Context(), host) if response.Error != nil { - return nil, "", response.Error + return &serving.Request{Serving: defaultServing()}, response.Error } urlPath := path.Clean(r.URL.Path) + size := len(response.Domain.LookupPaths) for _, lookup := range response.Domain.LookupPaths { isSubPath := strings.HasPrefix(urlPath, lookup.Prefix) isRootPath := urlPath == path.Clean(lookup.Prefix) if isSubPath || isRootPath { - lookupPath := &serving.LookupPath{ - Prefix: lookup.Prefix, - Path: strings.TrimPrefix(lookup.Source.Path, "/"), - IsNamespaceProject: (lookup.Prefix == "/" && len(response.Domain.LookupPaths) > 1), - IsHTTPSOnly: lookup.HTTPSOnly, - HasAccessControl: lookup.AccessControl, - ProjectID: uint64(lookup.ProjectID), - } - subPath := "" if isSubPath { subPath = strings.TrimPrefix(urlPath, lookup.Prefix) } - return lookupPath, subPath, nil + return &serving.Request{ + Serving: fabricateServing(lookup), + LookupPath: fabricateLookupPath(size, lookup), + SubPath: subPath}, nil } } - return nil, "", errors.New("could not match lookup path") + // TODO improve code around default serving, when `disk` serving gets removed + // https://gitlab.com/gitlab-org/gitlab-pages/issues/353 + return &serving.Request{Serving: defaultServing()}, + errors.New("could not match lookup path") } diff --git a/internal/source/gitlab/gitlab_test.go b/internal/source/gitlab/gitlab_test.go index 0e855f10..e6f194ee 100644 --- a/internal/source/gitlab/gitlab_test.go +++ b/internal/source/gitlab/gitlab_test.go @@ -39,62 +39,62 @@ func TestResolve(t *testing.T) { target := "https://test.gitlab.io:443/my/pages/project/" request := httptest.NewRequest("GET", target, nil) - lookup, subpath, err := source.Resolve(request) + response, err := source.Resolve(request) require.NoError(t, err) - require.Equal(t, "/my/pages/project/", lookup.Prefix) - require.Equal(t, "some/path/to/project/", lookup.Path) - require.Equal(t, "", subpath) - require.False(t, lookup.IsNamespaceProject) + require.Equal(t, "/my/pages/project/", response.LookupPath.Prefix) + require.Equal(t, "some/path/to/project/", response.LookupPath.Path) + require.Equal(t, "", response.SubPath) + require.False(t, response.LookupPath.IsNamespaceProject) }) t.Run("when requesting a nested group project with full path", func(t *testing.T) { target := "https://test.gitlab.io:443/my/pages/project/path/index.html" request := httptest.NewRequest("GET", target, nil) - lookup, subpath, err := source.Resolve(request) + response, err := source.Resolve(request) require.NoError(t, err) - require.Equal(t, "/my/pages/project/", lookup.Prefix) - require.Equal(t, "some/path/to/project/", lookup.Path) - require.Equal(t, "path/index.html", subpath) - require.False(t, lookup.IsNamespaceProject) + require.Equal(t, "/my/pages/project/", response.LookupPath.Prefix) + require.Equal(t, "some/path/to/project/", response.LookupPath.Path) + require.Equal(t, "path/index.html", response.SubPath) + require.False(t, response.LookupPath.IsNamespaceProject) }) t.Run("when requesting the group root project with root path", func(t *testing.T) { target := "https://test.gitlab.io:443/" request := httptest.NewRequest("GET", target, nil) - lookup, subpath, err := source.Resolve(request) + response, err := source.Resolve(request) require.NoError(t, err) - require.Equal(t, "/", lookup.Prefix) - require.Equal(t, "some/path/to/project-3/", lookup.Path) - require.Equal(t, "", subpath) - require.True(t, lookup.IsNamespaceProject) + require.Equal(t, "/", response.LookupPath.Prefix) + require.Equal(t, "some/path/to/project-3/", response.LookupPath.Path) + require.Equal(t, "", response.SubPath) + require.True(t, response.LookupPath.IsNamespaceProject) }) t.Run("when requesting the group root project with full path", func(t *testing.T) { target := "https://test.gitlab.io:443/path/to/index.html" request := httptest.NewRequest("GET", target, nil) - lookup, subpath, err := source.Resolve(request) + response, err := source.Resolve(request) require.NoError(t, err) - require.Equal(t, "/", lookup.Prefix) - require.Equal(t, "path/to/index.html", subpath) - require.Equal(t, "some/path/to/project-3/", lookup.Path) - require.True(t, lookup.IsNamespaceProject) + require.Equal(t, "/", response.LookupPath.Prefix) + require.Equal(t, "path/to/index.html", response.SubPath) + require.Equal(t, "some/path/to/project-3/", response.LookupPath.Path) + require.True(t, response.LookupPath.IsNamespaceProject) }) t.Run("when request path has not been sanitized", func(t *testing.T) { target := "https://test.gitlab.io:443/something/../something/../my/pages/project/index.html" request := httptest.NewRequest("GET", target, nil) - lookup, subpath, err := source.Resolve(request) + response, err := source.Resolve(request) require.NoError(t, err) - require.Equal(t, "/my/pages/project/", lookup.Prefix) - require.Equal(t, "index.html", subpath) + require.Equal(t, "/my/pages/project/", response.LookupPath.Prefix) + require.Equal(t, "index.html", response.SubPath) }) } |