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:
authorAlessio Caiazza <acaiazza@gitlab.com>2019-11-29 18:37:23 +0300
committerAlessio Caiazza <acaiazza@gitlab.com>2019-11-29 18:37:23 +0300
commitb0f9d9c6daab4fec25b9504c5cf0ff639dd1eba3 (patch)
tree95a047b4865736a4f23aeb0d732e2b4cc460574f
parent29b0a2d8ebc10ef9fa070d5634ab5d18938c935e (diff)
parent16e6e7e947fcc9235bdea9c72d0cbcc2dbd21bd0 (diff)
Merge branch 'feature/gitlab-source-enum-domains' into 'master'
Add transitional GitLab domain source (no cache) Closes #254 See merge request gitlab-org/gitlab-pages!201
-rw-r--r--acceptance_test.go35
-rw-r--r--app.go28
-rw-r--r--app_config.go10
-rw-r--r--go.mod4
-rw-r--r--go.sum8
-rw-r--r--helpers_test.go23
-rw-r--r--internal/auth/auth.go16
-rw-r--r--internal/auth/auth_test.go8
-rw-r--r--internal/domain/domain.go6
-rw-r--r--internal/serving/disk/reader.go3
-rw-r--r--internal/serving/lookup_path.go6
-rw-r--r--internal/source/config.go7
-rw-r--r--internal/source/disk/custom.go3
-rw-r--r--internal/source/disk/disk.go57
-rw-r--r--internal/source/disk/group.go5
-rw-r--r--internal/source/domains.go86
-rw-r--r--internal/source/domains_test.go63
-rw-r--r--internal/source/gitlab/api/lookup_path.go (renamed from internal/source/gitlab/response.go)12
-rw-r--r--internal/source/gitlab/api/virtual_domain.go10
-rw-r--r--internal/source/gitlab/cache/cache.go10
-rw-r--r--internal/source/gitlab/client.go144
-rw-r--r--internal/source/gitlab/client/client.go153
-rw-r--r--internal/source/gitlab/client/client_stub.go27
-rw-r--r--internal/source/gitlab/client/client_test.go (renamed from internal/source/gitlab/client_test.go)17
-rw-r--r--internal/source/gitlab/client/config.go8
-rw-r--r--internal/source/gitlab/client/testdata/test.gitlab.io.json36
-rw-r--r--internal/source/gitlab/gitlab.go73
-rw-r--r--internal/source/gitlab/gitlab_test.go75
-rw-r--r--internal/source/source.go8
-rw-r--r--internal/source/source_mock.go24
-rw-r--r--shared/lookups/new-source-test.gitlab.io.json16
-rw-r--r--shared/pages/group/new-source-test.gitlab.io/public/index.html1
32 files changed, 746 insertions, 236 deletions
diff --git a/acceptance_test.go b/acceptance_test.go
index 44dbc90d..6f2e5fb4 100644
--- a/acceptance_test.go
+++ b/acceptance_test.go
@@ -16,6 +16,7 @@ import (
"time"
"github.com/namsral/flag"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -432,6 +433,20 @@ func TestPageNotAvailableIfNotLoaded(t *testing.T) {
require.Equal(t, http.StatusServiceUnavailable, rsp.StatusCode)
}
+func TestPageNotAvailableInDomainSource(t *testing.T) {
+ skipUnlessEnabled(t)
+
+ brokenDomain := "GITLAB_NEW_SOURCE_BROKEN_DOMAIN=pages-broken-poc.gitlab.io"
+ teardown := RunPagesProcessWithEnvs(t, false, *pagesBinary, listeners, "", []string{brokenDomain}, "-pages-root=shared/invalid-pages")
+ defer teardown()
+ waitForRoundtrips(t, listeners, 5*time.Second)
+
+ rsp, err := GetPageFromListener(t, httpListener, "pages-broken-poc.gitlab.io", "index.html")
+ require.NoError(t, err)
+ defer rsp.Body.Close()
+ require.Equal(t, http.StatusBadGateway, rsp.StatusCode)
+}
+
func TestObscureMIMEType(t *testing.T) {
skipUnlessEnabled(t)
teardown := RunPagesProcessWithoutWait(t, *pagesBinary, listeners, "")
@@ -1513,3 +1528,23 @@ func TestTLSVersions(t *testing.T) {
})
}
}
+
+func TestGitlabDomainsSource(t *testing.T) {
+ skipUnlessEnabled(t)
+
+ source := NewGitlabDomainsSourceStub(t)
+ defer source.Close()
+
+ newSourceDomains := "GITLAB_NEW_SOURCE_DOMAINS=new-source-test.gitlab.io,other-test.gitlab.io"
+ teardown := RunPagesProcessWithEnvs(t, true, *pagesBinary, listeners, "", []string{newSourceDomains}, "-gitlab-server", source.URL)
+ defer teardown()
+
+ response, err := GetPageFromListener(t, httpListener, "new-source-test.gitlab.io", "/my/pages/project/")
+ require.NoError(t, err)
+
+ defer response.Body.Close()
+ body, _ := ioutil.ReadAll(response.Body)
+
+ assert.Equal(t, http.StatusOK, response.StatusCode)
+ assert.Equal(t, "New Pages GitLab Source TEST OK\n", string(body))
+}
diff --git a/app.go b/app.go
index 09bf039e..eeefaeaf 100644
--- a/app.go
+++ b/app.go
@@ -53,7 +53,7 @@ type theApp struct {
}
func (a *theApp) isReady() bool {
- return a.domains.Ready()
+ return a.domains.IsReady()
}
func (a *theApp) ServeTLS(ch *tls.ClientHelloInfo) (*tls.Certificate, error) {
@@ -61,7 +61,7 @@ func (a *theApp) ServeTLS(ch *tls.ClientHelloInfo) (*tls.Certificate, error) {
return nil, nil
}
- if domain := a.domain(ch.ServerName); domain != nil {
+ if domain, _ := a.domain(ch.ServerName); domain != nil {
tls, _ := domain.EnsureCertificate()
return tls, nil
}
@@ -86,16 +86,18 @@ func (a *theApp) redirectToHTTPS(w http.ResponseWriter, r *http.Request, statusC
http.Redirect(w, r, u.String(), statusCode)
}
-func (a *theApp) getHostAndDomain(r *http.Request) (host string, domain *domain.Domain) {
+func (a *theApp) getHostAndDomain(r *http.Request) (string, *domain.Domain, error) {
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
host = r.Host
}
- return host, a.domain(host)
+ domain, err := a.domain(host)
+
+ return host, domain, err
}
-func (a *theApp) domain(host string) *domain.Domain {
+func (a *theApp) domain(host string) (*domain.Domain, error) {
return a.domains.GetDomain(host)
}
@@ -163,7 +165,15 @@ func (a *theApp) tryAuxiliaryHandlers(w http.ResponseWriter, r *http.Request, ht
// downstream middlewares to use
func (a *theApp) routingMiddleware(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- host, domain := a.getHostAndDomain(r)
+ // if we could not retrieve a domain from domains source we break the
+ // middleware chain and simply respond with 502 after logging this
+ host, domain, err := a.getHostAndDomain(r)
+ if err != nil {
+ log.WithError(err).Error("could not fetch domain information from a source")
+
+ httperrors.Serve502(w)
+ return
+ }
r = request.WithHostAndDomain(r, host, domain)
@@ -289,7 +299,7 @@ func (a *theApp) proxyInitialMiddleware(handler http.Handler) http.Handler {
}
func (a *theApp) buildHandlerPipeline() (http.Handler, error) {
- // Handlers should be applied in reverse order
+ // Handlers should be applied in a reverse order
handler := a.serveFileOrNotFoundHandler()
if !a.DisableCrossOriginRequests {
handler = corsHandler.Handler(handler)
@@ -348,7 +358,7 @@ func (a *theApp) Run() {
a.listenMetricsFD(&wg, a.ListenMetrics)
}
- a.domains.Watch(a.Domain)
+ a.domains.Read(a.Domain)
wg.Wait()
}
@@ -403,7 +413,7 @@ func (a *theApp) listenMetricsFD(wg *sync.WaitGroup, fd uintptr) {
}
func runApp(config appConfig) {
- a := theApp{appConfig: config, domains: source.NewDomains()}
+ a := theApp{appConfig: config, domains: source.NewDomains(config)}
err := logging.ConfigureLogging(a.LogFormat, a.LogVerbose)
if err != nil {
diff --git a/app_config.go b/app_config.go
index f9be6545..639ece85 100644
--- a/app_config.go
+++ b/app_config.go
@@ -35,3 +35,13 @@ type appConfig struct {
SentryEnvironment string
CustomHeaders []string
}
+
+// GitlabServerURL returns URL to a GitLab instance.
+func (config appConfig) GitlabServerURL() string {
+ return config.GitLabServer
+}
+
+// GitlabClientSecret returns GitLab server access token.
+func (config appConfig) GitlabAPISecret() []byte {
+ return config.GitLabAPISecretKey
+}
diff --git a/go.mod b/go.mod
index f6f5d046..f76c0c92 100644
--- a/go.mod
+++ b/go.mod
@@ -23,9 +23,9 @@ require (
gitlab.com/gitlab-org/labkit v0.0.0-20190902063225-3253d7975ca7
gitlab.com/lupine/go-mimedb v0.0.0-20180307000149-e8af1d659877
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7
- golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac
+ golang.org/x/lint v0.0.0-20190930215403-16217165b5de
golang.org/x/net v0.0.0-20190909003024-a7b16738d86b
golang.org/x/sys v0.0.0-20190910064555-bbd175535a8b
- golang.org/x/tools v0.0.0-20190917032747-2dc213d980bc
+ golang.org/x/tools v0.0.0-20191010201905-e5ffc44a6fee
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
)
diff --git a/go.sum b/go.sum
index ad380534..14a9bf73 100644
--- a/go.sum
+++ b/go.sum
@@ -141,8 +141,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 h1:0hQKqeLdqlt5iIwVOBErRisrHJAN57yOiPRQItI20fU=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac h1:8R1esu+8QioDxo4E4mX6bFztO+dMTM49DNAaWfO5OeY=
-golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -177,8 +177,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190917032747-2dc213d980bc h1:AzQrNvr65FlhSjBpg0eVCY43QLsuOqtzWGtjcBqT6J8=
-golang.org/x/tools v0.0.0-20190917032747-2dc213d980bc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191010201905-e5ffc44a6fee h1:Cgj5oVkw7Gktu56MAiU0r1u0jyuT6jmtOzcAJwLj89c=
+golang.org/x/tools v0.0.0-20191010201905-e5ffc44a6fee/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
diff --git a/helpers_test.go b/helpers_test.go
index ad7c65f1..b13fb18f 100644
--- a/helpers_test.go
+++ b/helpers_test.go
@@ -5,9 +5,11 @@ import (
"crypto/tls"
"crypto/x509"
"fmt"
+ "io"
"io/ioutil"
"net"
"net/http"
+ "net/http/httptest"
"os"
"os/exec"
"strings"
@@ -145,6 +147,10 @@ func RunPagesProcessWithSSLCertFile(t *testing.T, pagesPath string, listeners []
return runPagesProcess(t, true, pagesPath, listeners, promPort, []string{"SSL_CERT_FILE=" + sslCertFile}, extraArgs...)
}
+func RunPagesProcessWithEnvs(t *testing.T, wait bool, pagesPath string, listeners []ListenSpec, promPort string, envs []string, extraArgs ...string) (teardown func()) {
+ return runPagesProcess(t, wait, pagesPath, listeners, promPort, envs, extraArgs...)
+}
+
func RunPagesProcessWithAuth(t *testing.T, pagesPath string, listeners []ListenSpec, promPort string) (teardown func()) {
return runPagesProcess(t, true, pagesPath, listeners, promPort, nil, "-auth-client-id=1",
"-auth-client-secret=1",
@@ -375,3 +381,20 @@ func waitForRoundtrips(t *testing.T, listeners []ListenSpec, timeout time.Durati
require.Equal(t, len(listeners), nListening, "all listeners must be accepting TCP connections")
}
+
+func NewGitlabDomainsSourceStub(t *testing.T) *httptest.Server {
+ handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ domain := r.URL.Query().Get("host")
+
+ fixture, err := os.Open("shared/lookups/" + domain + ".json")
+ defer fixture.Close()
+ require.NoError(t, err)
+
+ _, err = io.Copy(w, fixture)
+ require.NoError(t, err)
+
+ t.Logf("GitLab domain %s source stub served lookup", domain)
+ })
+
+ return httptest.NewServer(handler)
+}
diff --git a/internal/auth/auth.go b/internal/auth/auth.go
index 2e8473b4..f30c7407 100644
--- a/internal/auth/auth.go
+++ b/internal/auth/auth.go
@@ -107,8 +107,7 @@ func (a *Auth) checkSession(w http.ResponseWriter, r *http.Request) (*sessions.S
}
// TryAuthenticate tries to authenticate user and fetch access token if request is a callback to auth
-func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request, domains *source.Domains) bool {
-
+func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request, domains source.Source) bool {
if a == nil {
return false
}
@@ -199,17 +198,20 @@ func (a *Auth) checkAuthenticationResponse(session *sessions.Session, w http.Res
http.Redirect(w, r, redirectURI, 302)
}
-func (a *Auth) domainAllowed(domain string, domains *source.Domains) bool {
- domainConfigured := (domain == a.pagesDomain) || strings.HasSuffix("."+domain, a.pagesDomain)
+func (a *Auth) domainAllowed(name string, domains source.Source) bool {
+ isConfigured := (name == a.pagesDomain) || strings.HasSuffix("."+name, a.pagesDomain)
- if domainConfigured {
+ if isConfigured {
return true
}
- return domains.HasDomain(domain)
+ domain, err := domains.GetDomain(name)
+
+ // domain exists and there is no error
+ return (domain != nil && err == nil)
}
-func (a *Auth) handleProxyingAuth(session *sessions.Session, w http.ResponseWriter, r *http.Request, domains *source.Domains) bool {
+func (a *Auth) handleProxyingAuth(session *sessions.Session, w http.ResponseWriter, r *http.Request, domains source.Source) bool {
// If request is for authenticating via custom domain
if shouldProxyAuth(r) {
domain := r.URL.Query().Get("domain")
diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go
index c082cfdf..92e1e8c7 100644
--- a/internal/auth/auth_test.go
+++ b/internal/auth/auth_test.go
@@ -56,7 +56,7 @@ func TestTryAuthenticate(t *testing.T) {
require.NoError(t, err)
r := request.WithHTTPSFlag(&http.Request{URL: reqURL}, true)
- require.Equal(t, false, auth.TryAuthenticate(result, r, source.NewDomains()))
+ require.Equal(t, false, auth.TryAuthenticate(result, r, source.NewMockSource()))
}
func TestTryAuthenticateWithError(t *testing.T) {
@@ -67,7 +67,7 @@ func TestTryAuthenticateWithError(t *testing.T) {
require.NoError(t, err)
r := request.WithHTTPSFlag(&http.Request{URL: reqURL}, true)
- require.Equal(t, true, auth.TryAuthenticate(result, r, source.NewDomains()))
+ require.Equal(t, true, auth.TryAuthenticate(result, r, source.NewMockSource()))
require.Equal(t, 401, result.Code)
}
@@ -84,7 +84,7 @@ func TestTryAuthenticateWithCodeButInvalidState(t *testing.T) {
session.Values["state"] = "state"
session.Save(r, result)
- require.Equal(t, true, auth.TryAuthenticate(result, r, source.NewDomains()))
+ require.Equal(t, true, auth.TryAuthenticate(result, r, source.NewMockSource()))
require.Equal(t, 401, result.Code)
}
@@ -124,7 +124,7 @@ func testTryAuthenticateWithCodeAndState(t *testing.T, https bool) {
})
result := httptest.NewRecorder()
- require.Equal(t, true, auth.TryAuthenticate(result, r, source.NewDomains()))
+ require.Equal(t, true, auth.TryAuthenticate(result, r, source.NewMockSource()))
require.Equal(t, 302, result.Code)
require.Equal(t, "https://pages.gitlab-example.com/project/", result.Header().Get("Location"))
require.Equal(t, 600, result.Result().Cookies()[0].MaxAge)
diff --git a/internal/domain/domain.go b/internal/domain/domain.go
index f7eba5ca..28eb3196 100644
--- a/internal/domain/domain.go
+++ b/internal/domain/domain.go
@@ -162,10 +162,8 @@ func (d *Domain) EnsureCertificate() (*tls.Certificate, error) {
// ServeFileHTTP returns true if something was served, false if not.
func (d *Domain) ServeFileHTTP(w http.ResponseWriter, r *http.Request) bool {
if d.isUnconfigured() || !d.HasLookupPath(r) {
- // TODO: this seems to be wrong:
- // as we should rather return false,
- // and fallback to `ServeNotFoundHTTP`
- // to handle this case
+ // TODO: this seems to be wrong: as we should rather return false, and
+ // fallback to `ServeNotFoundHTTP` to handle this case
httperrors.Serve404(w)
return true
}
diff --git a/internal/serving/disk/reader.go b/internal/serving/disk/reader.go
index b52b5cff..ce4f1d8b 100644
--- a/internal/serving/disk/reader.go
+++ b/internal/serving/disk/reader.go
@@ -28,6 +28,9 @@ func (reader *Reader) tryFile(h serving.Handler) error {
if endsWithSlash(urlPath) {
fullPath, err = reader.resolvePath(h.LookupPath.Path, h.SubPath, "index.html")
} else {
+ // TODO why are we doing that? In tests it redirects to HTTPS. This seems wrong,
+ // issue about this: https://gitlab.com/gitlab-org/gitlab-pages/issues/273
+
// Concat Host with URL.Path
redirectPath := "//" + host + "/"
redirectPath += strings.TrimPrefix(urlPath, "/")
diff --git a/internal/serving/lookup_path.go b/internal/serving/lookup_path.go
index ba6e8f7a..4360358b 100644
--- a/internal/serving/lookup_path.go
+++ b/internal/serving/lookup_path.go
@@ -2,9 +2,9 @@ package serving
// LookupPath holds a domain project configuration needed to handle a request
type LookupPath struct {
- Location string
- Path string
- IsNamespaceProject bool
+ Prefix string // Project prefix, for example, /my/project in group.gitlab.io/my/project/index.html
+ Path string // Path is an internal and serving-specific location of a document
+ IsNamespaceProject bool // IsNamespaceProject is DEPRECATED, see https://gitlab.com/gitlab-org/gitlab-pages/issues/272
IsHTTPSOnly bool
HasAccessControl bool
ProjectID uint64
diff --git a/internal/source/config.go b/internal/source/config.go
new file mode 100644
index 00000000..9cf87bc6
--- /dev/null
+++ b/internal/source/config.go
@@ -0,0 +1,7 @@
+package source
+
+import "gitlab.com/gitlab-org/gitlab-pages/internal/source/gitlab/client"
+
+// Config represents an interface that is configuration provider for client
+// capable of comunicating with GitLab
+type Config client.Config
diff --git a/internal/source/disk/custom.go b/internal/source/disk/custom.go
index 8a080f20..cc4f3f4c 100644
--- a/internal/source/disk/custom.go
+++ b/internal/source/disk/custom.go
@@ -12,10 +12,9 @@ type customProjectResolver struct {
path string
}
-// TODO tests
func (p *customProjectResolver) Resolve(r *http.Request) (*serving.LookupPath, string, error) {
lookupPath := &serving.LookupPath{
- Location: "/",
+ Prefix: "/",
Path: p.path,
IsNamespaceProject: false,
IsHTTPSOnly: p.config.HTTPSOnly,
diff --git a/internal/source/disk/disk.go b/internal/source/disk/disk.go
new file mode 100644
index 00000000..b79d222d
--- /dev/null
+++ b/internal/source/disk/disk.go
@@ -0,0 +1,57 @@
+package disk
+
+import (
+ "strings"
+ "sync"
+ "time"
+
+ "gitlab.com/gitlab-org/gitlab-pages/internal/domain"
+)
+
+// Disk struct represents a map of all domains supported by pages that are
+// stored on a disk with corresponding `config.json`.
+type Disk struct {
+ dm Map
+ lock *sync.RWMutex
+}
+
+// New is a factory method for the Disk source. It is initializing a mutex. It
+// should not initialize `dm` as we later check the readiness by comparing it
+// with a nil value.
+func New() *Disk {
+ return &Disk{
+ lock: &sync.RWMutex{},
+ }
+}
+
+// GetDomain returns a domain from the domains map if it exists
+func (d *Disk) GetDomain(host string) (*domain.Domain, error) {
+ host = strings.ToLower(host)
+
+ d.lock.RLock()
+ defer d.lock.RUnlock()
+
+ domain, _ := d.dm[host]
+
+ return domain, nil
+}
+
+// IsReady checks if the domains source is ready for work. The disk source is
+// ready after traversing entire filesystem and reading all domains'
+// configuration files.
+func (d *Disk) IsReady() bool {
+ return d.dm != nil
+}
+
+// Read starts the domain source, in this case it is reading domains from
+// groups on disk concurrently.
+func (d *Disk) Read(rootDomain string) {
+ go Watch(rootDomain, d.updateDomains, time.Second)
+}
+
+func (d *Disk) updateDomains(dm Map) {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+
+ d.dm = dm
+}
diff --git a/internal/source/disk/group.go b/internal/source/disk/group.go
index 59aedc73..7094e7a2 100644
--- a/internal/source/disk/group.go
+++ b/internal/source/disk/group.go
@@ -68,7 +68,6 @@ func (g *Group) getProjectConfigWithSubpath(r *http.Request) (*projectConfig, st
// return the group project if it exists.
if host := host.FromRequest(r); host != "" {
if groupProject := g.projects[host]; groupProject != nil {
- // TODOHERE: the location here should be "/", so we return ""
return groupProject, "/", host, strings.Join(split[1:], "/")
}
}
@@ -79,14 +78,14 @@ 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) {
- projectConfig, location, projectPath, subPath := g.getProjectConfigWithSubpath(r)
+ projectConfig, prefix, projectPath, subPath := g.getProjectConfigWithSubpath(r)
if projectConfig == nil {
return nil, "", nil // it is not an error when project does not exist
}
lookupPath := &serving.LookupPath{
- Location: location,
+ Prefix: prefix,
Path: filepath.Join(g.name, projectPath, "public"),
IsNamespaceProject: projectConfig.NamespaceProject,
IsHTTPSOnly: projectConfig.HTTPSOnly,
diff --git a/internal/source/domains.go b/internal/source/domains.go
index cd2d89c5..f47884ef 100644
--- a/internal/source/domains.go
+++ b/internal/source/domains.go
@@ -1,65 +1,79 @@
package source
import (
+ "errors"
+ "os"
"strings"
- "sync"
- "time"
"gitlab.com/gitlab-org/gitlab-pages/internal/domain"
"gitlab.com/gitlab-org/gitlab-pages/internal/source/disk"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/source/gitlab"
)
+var newSourceDomains []string
+var brokenSourceDomain string
+
+func init() {
+ testDomains := os.Getenv("GITLAB_NEW_SOURCE_DOMAINS")
+ if testDomains != "" {
+ newSourceDomains = strings.Split(testDomains, ",")
+ }
+
+ brokenDomain := os.Getenv("GITLAB_NEW_SOURCE_BROKEN_DOMAIN")
+ if brokenDomain != "" {
+ brokenSourceDomain = brokenDomain
+ }
+}
+
// Domains struct represents a map of all domains supported by pages. It is
-// currently reading them from disk.
+// currently using two sources during the transition to the new GitLab domains
+// source.
type Domains struct {
- dm disk.Map
- lock *sync.RWMutex
+ gitlab Source
+ disk *disk.Disk // legacy disk source
}
// NewDomains is a factory method for domains initializing a mutex. It should
// not initialize `dm` as we later check the readiness by comparing it with a
// nil value.
-func NewDomains() *Domains {
+func NewDomains(config Config) *Domains {
return &Domains{
- lock: &sync.RWMutex{},
+ disk: disk.New(),
+ gitlab: gitlab.New(config),
}
}
-// GetDomain returns a domain from the domains map
-func (d *Domains) GetDomain(host string) *domain.Domain {
- host = strings.ToLower(host)
- d.lock.RLock()
- defer d.lock.RUnlock()
- domain, _ := d.dm[host]
-
- return domain
-}
-
-// HasDomain checks for presence of a domain in the domains map
-func (d *Domains) HasDomain(host string) bool {
- d.lock.RLock()
- defer d.lock.RUnlock()
-
- host = strings.ToLower(host)
- _, isPresent := d.dm[host]
+// GetDomain retrieves a domain information from a source. We are using two
+// sources here because it allows us to switch behavior and the domain source
+// for some subset of domains, to test / PoC the new GitLab Domains Source that
+// we plan to use to replace the disk source.
+func (d *Domains) GetDomain(name string) (*domain.Domain, error) {
+ if name == brokenSourceDomain {
+ return nil, errors.New("broken test domain used")
+ }
- return isPresent
+ return d.source(name).GetDomain(name)
}
-// Ready checks if the domains source is ready for work
-func (d *Domains) Ready() bool {
- return d.dm != nil
+// Read starts the disk domain source. It is DEPRECATED, because we want to
+// remove it entirely when disk source gets removed.
+func (d *Domains) Read(rootDomain string) {
+ d.disk.Read(rootDomain)
}
-// Watch starts the domain source, in this case it is reading domains from
-// groups on disk concurrently
-func (d *Domains) Watch(rootDomain string) {
- go disk.Watch(rootDomain, d.updateDomains, time.Second)
+// IsReady checks if the disk domain source managed to traverse entire pages
+// filesystem and is ready for use. It is DEPRECATED, because we want to remove
+// it entirely when disk source gets removed.
+func (d *Domains) IsReady() bool {
+ return d.disk.IsReady()
}
-func (d *Domains) updateDomains(dm disk.Map) {
- d.lock.Lock()
- defer d.lock.Unlock()
+func (d *Domains) source(domain string) Source {
+ for _, name := range newSourceDomains {
+ if domain == name {
+ return d.gitlab
+ }
+ }
- d.dm = dm
+ return d.disk
}
diff --git a/internal/source/domains_test.go b/internal/source/domains_test.go
new file mode 100644
index 00000000..6854c359
--- /dev/null
+++ b/internal/source/domains_test.go
@@ -0,0 +1,63 @@
+package source
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "gitlab.com/gitlab-org/gitlab-pages/internal/domain"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/source/disk"
+)
+
+func TestHasDomain(t *testing.T) {
+ newSourceDomains = []string{"new-source-test.gitlab.io"}
+ brokenSourceDomain = "pages-broken-poc.gitlab.io"
+
+ t.Run("when requesting a test domain", func(t *testing.T) {
+ testDomain := newSourceDomains[0]
+
+ 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)
+ })
+
+ t.Run("when requesting a non-test domain", func(t *testing.T) {
+ newSource := NewMockSource()
+ defer newSource.AssertExpectations(t)
+
+ domains := &Domains{
+ disk: disk.New(),
+ gitlab: newSource,
+ }
+
+ domain, err := domains.GetDomain("domain.test.io")
+
+ require.NoError(t, err)
+ assert.Nil(t, domain)
+ })
+
+ t.Run("when requesting a broken test domain", func(t *testing.T) {
+ newSource := NewMockSource()
+ defer newSource.AssertExpectations(t)
+
+ domains := &Domains{
+ disk: disk.New(),
+ gitlab: newSource,
+ }
+
+ domain, err := domains.GetDomain("pages-broken-poc.gitlab.io")
+
+ assert.Nil(t, domain)
+ assert.EqualError(t, err, "broken test domain used")
+ })
+}
diff --git a/internal/source/gitlab/response.go b/internal/source/gitlab/api/lookup_path.go
index 20597362..b0407638 100644
--- a/internal/source/gitlab/response.go
+++ b/internal/source/gitlab/api/lookup_path.go
@@ -1,6 +1,6 @@
-package gitlab
+package api
-// LookupPath represents a lookup path for a GitLab Pages virtual domain
+// LookupPath represents a lookup path for a virtual domain
type LookupPath struct {
ProjectID int `json:"project_id,omitempty"`
AccessControl bool `json:"access_control,omitempty"`
@@ -11,11 +11,3 @@ type LookupPath struct {
Path string `json:"path,omitempty"`
}
}
-
-// VirtualDomain represents a GitLab Pages virtual domain
-type VirtualDomain struct {
- Certificate string `json:"certificate,omitempty"`
- Key string `json:"key,omitempty"`
-
- LookupPaths []LookupPath `json:"lookup_paths"`
-}
diff --git a/internal/source/gitlab/api/virtual_domain.go b/internal/source/gitlab/api/virtual_domain.go
new file mode 100644
index 00000000..200c06de
--- /dev/null
+++ b/internal/source/gitlab/api/virtual_domain.go
@@ -0,0 +1,10 @@
+package api
+
+// VirtualDomain represents a GitLab Pages virtual domain that is being sent
+// from GitLab API
+type VirtualDomain struct {
+ Certificate string `json:"certificate,omitempty"`
+ Key string `json:"key,omitempty"`
+
+ LookupPaths []LookupPath `json:"lookup_paths"`
+}
diff --git a/internal/source/gitlab/cache/cache.go b/internal/source/gitlab/cache/cache.go
new file mode 100644
index 00000000..95c4c57b
--- /dev/null
+++ b/internal/source/gitlab/cache/cache.go
@@ -0,0 +1,10 @@
+package cache
+
+// Cache is a short and long caching mechanism for GitLab source
+type Cache struct {
+}
+
+// New creates a new instance of Cache and sets default expiration
+func New() *Cache {
+ return &Cache{}
+}
diff --git a/internal/source/gitlab/client.go b/internal/source/gitlab/client.go
index 9dde7b43..5b6cd07c 100644
--- a/internal/source/gitlab/client.go
+++ b/internal/source/gitlab/client.go
@@ -1,142 +1,10 @@
package gitlab
-import (
- "encoding/json"
- "errors"
- "net/http"
- "net/url"
- "time"
+import "gitlab.com/gitlab-org/gitlab-pages/internal/source/gitlab/api"
- jwt "github.com/dgrijalva/jwt-go"
-
- "gitlab.com/gitlab-org/gitlab-pages/internal/httptransport"
-)
-
-// Client is a HTTP client to access Pages internal API
-type Client struct {
- secretKey []byte
- baseURL *url.URL
- httpClient *http.Client
-}
-
-var (
- errUnknown = errors.New("Unknown")
- errNoContent = errors.New("No Content")
- errUnauthorized = errors.New("Unauthorized")
- errNotFound = errors.New("Not Found")
-)
-
-// NewClient initializes and returns new Client
-// baseUrl is appConfig.GitLabServer
-// secretKey is appConfig.GitLabAPISecretKey (not yet implemented)
-func NewClient(baseURL string, secretKey []byte) (*Client, error) {
- url, err := url.Parse(baseURL)
- if err != nil {
- return nil, err
- }
-
- return &Client{
- secretKey: secretKey,
- baseURL: url,
- httpClient: &http.Client{
- Timeout: 5 * time.Second,
- Transport: httptransport.Transport,
- },
- }, nil
-}
-
-// GetVirtualDomain returns VirtualDomain configuration for the given host
-func (gc *Client) GetVirtualDomain(host string) (*VirtualDomain, error) {
- params := map[string]string{"host": host}
-
- resp, err := gc.get("/api/v4/internal/pages", params)
- if resp != nil {
- defer resp.Body.Close()
- }
-
- if err != nil {
- return nil, err
- }
-
- var domain VirtualDomain
- err = json.NewDecoder(resp.Body).Decode(&domain)
- if err != nil {
- return nil, err
- }
-
- return &domain, nil
-}
-
-func (gc *Client) get(path string, params map[string]string) (*http.Response, error) {
- endpoint, err := gc.endpoint(path, params)
- if err != nil {
- return nil, err
- }
-
- req, err := gc.request("GET", endpoint)
- if err != nil {
- return nil, err
- }
-
- resp, err := gc.httpClient.Do(req)
- if err != nil {
- return nil, err
- }
-
- switch {
- case resp.StatusCode == http.StatusOK:
- return resp, nil
- case resp.StatusCode == http.StatusNoContent:
- return resp, errNoContent
- case resp.StatusCode == http.StatusUnauthorized:
- return resp, errUnauthorized
- case resp.StatusCode == http.StatusNotFound:
- return resp, errNotFound
- default:
- return resp, errUnknown
- }
-}
-
-func (gc *Client) endpoint(path string, params map[string]string) (*url.URL, error) {
- endpoint, err := gc.baseURL.Parse(path)
- if err != nil {
- return nil, err
- }
-
- values := url.Values{}
- for key, value := range params {
- values.Add(key, value)
- }
- endpoint.RawQuery = values.Encode()
-
- return endpoint, nil
-}
-
-func (gc *Client) request(method string, endpoint *url.URL) (*http.Request, error) {
- req, err := http.NewRequest("GET", endpoint.String(), nil)
- if err != nil {
- return nil, err
- }
-
- token, err := gc.token()
- if err != nil {
- return nil, err
- }
- req.Header.Set("Gitlab-Pages-Api-Request", token)
-
- return req, nil
-}
-
-func (gc *Client) token() (string, error) {
- claims := jwt.StandardClaims{
- Issuer: "gitlab-pages",
- ExpiresAt: time.Now().Add(5 * time.Second).Unix(),
- }
-
- token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(gc.secretKey)
- if err != nil {
- return "", err
- }
-
- return token, nil
+// Client interace represents a client capable of fetching a virtual domain
+// from an external API
+type Client interface {
+ // GetVirtualDomain retrieves a virtual domain from an external API
+ GetVirtualDomain(host string) (*api.VirtualDomain, error)
}
diff --git a/internal/source/gitlab/client/client.go b/internal/source/gitlab/client/client.go
new file mode 100644
index 00000000..6c9327dc
--- /dev/null
+++ b/internal/source/gitlab/client/client.go
@@ -0,0 +1,153 @@
+package client
+
+import (
+ "encoding/json"
+ "errors"
+ "net/http"
+ "net/url"
+ "time"
+
+ jwt "github.com/dgrijalva/jwt-go"
+
+ "gitlab.com/gitlab-org/labkit/log"
+
+ "gitlab.com/gitlab-org/gitlab-pages/internal/httptransport"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/source/gitlab/api"
+)
+
+// Client is a HTTP client to access Pages internal API
+type Client struct {
+ secretKey []byte
+ baseURL *url.URL
+ httpClient *http.Client
+}
+
+var (
+ errUnknown = errors.New("Unknown")
+ errNoContent = errors.New("No Content")
+ errUnauthorized = errors.New("Unauthorized")
+ errNotFound = errors.New("Not Found")
+)
+
+// TODO make these values configurable https://gitlab.com/gitlab-org/gitlab-pages/issues/274
+var tokenTimeout = 30 * time.Second
+var connectionTimeout = 10 * time.Second
+
+// NewClient initializes and returns new Client baseUrl is
+// appConfig.GitLabServer secretKey is appConfig.GitLabAPISecretKey
+func NewClient(baseURL string, secretKey []byte) *Client {
+ url, err := url.Parse(baseURL)
+ if err != nil {
+ log.WithError(err).Fatal("could not parse GitLab server URL")
+ }
+
+ return &Client{
+ secretKey: secretKey,
+ baseURL: url,
+ httpClient: &http.Client{
+ Timeout: connectionTimeout,
+ Transport: httptransport.Transport,
+ },
+ }
+}
+
+// NewFromConfig creates a new client from Config struct
+func NewFromConfig(config Config) *Client {
+ return NewClient(config.GitlabServerURL(), config.GitlabAPISecret())
+}
+
+// GetVirtualDomain returns VirtualDomain configuration for the given host. It
+// returns an error if non-nil `*api.VirtualDomain` can not be retuned.
+func (gc *Client) GetVirtualDomain(host string) (*api.VirtualDomain, error) {
+ params := url.Values{}
+ params.Set("host", host)
+
+ resp, err := gc.get("/api/v4/internal/pages", params)
+ if resp != nil {
+ defer resp.Body.Close()
+ } else {
+ return nil, errors.New("empty response returned")
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ var domain api.VirtualDomain
+ err = json.NewDecoder(resp.Body).Decode(&domain)
+ if err != nil {
+ return nil, err
+ }
+
+ return &domain, nil
+}
+
+func (gc *Client) get(path string, params url.Values) (*http.Response, error) {
+ endpoint, err := gc.endpoint(path, params)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := gc.request("GET", endpoint)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := gc.httpClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ switch {
+ case resp.StatusCode == http.StatusOK:
+ return resp, nil
+ case resp.StatusCode == http.StatusNoContent:
+ return resp, errNoContent
+ case resp.StatusCode == http.StatusUnauthorized:
+ return resp, errUnauthorized
+ case resp.StatusCode == http.StatusNotFound:
+ return resp, errNotFound
+ default:
+ return resp, errUnknown
+ }
+}
+
+func (gc *Client) endpoint(path string, params url.Values) (*url.URL, error) {
+ endpoint, err := gc.baseURL.Parse(path)
+ if err != nil {
+ return nil, err
+ }
+
+ endpoint.RawQuery = params.Encode()
+
+ return endpoint, nil
+}
+
+func (gc *Client) request(method string, endpoint *url.URL) (*http.Request, error) {
+ req, err := http.NewRequest("GET", endpoint.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ token, err := gc.token()
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Gitlab-Pages-Api-Request", token)
+
+ return req, nil
+}
+
+func (gc *Client) token() (string, error) {
+ claims := jwt.StandardClaims{
+ Issuer: "gitlab-pages",
+ ExpiresAt: time.Now().Add(tokenTimeout).Unix(),
+ }
+
+ token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(gc.secretKey)
+ if err != nil {
+ return "", err
+ }
+
+ return token, nil
+}
diff --git a/internal/source/gitlab/client/client_stub.go b/internal/source/gitlab/client/client_stub.go
new file mode 100644
index 00000000..6dc0af85
--- /dev/null
+++ b/internal/source/gitlab/client/client_stub.go
@@ -0,0 +1,27 @@
+package client
+
+import (
+ "encoding/json"
+ "os"
+
+ "gitlab.com/gitlab-org/gitlab-pages/internal/source/gitlab/api"
+)
+
+// StubClient is a stubbed client used for testing
+type StubClient struct {
+ File string
+}
+
+// GetVirtualDomain reads a test fixture and unmarshalls it
+func (c StubClient) GetVirtualDomain(host string) (*api.VirtualDomain, error) {
+ f, err := os.Open(c.File)
+ defer f.Close()
+ if err != nil {
+ return nil, err
+ }
+
+ var domain api.VirtualDomain
+ err = json.NewDecoder(f).Decode(&domain)
+
+ return &domain, err
+}
diff --git a/internal/source/gitlab/client_test.go b/internal/source/gitlab/client/client_test.go
index 1d63a590..d689b687 100644
--- a/internal/source/gitlab/client_test.go
+++ b/internal/source/gitlab/client/client_test.go
@@ -1,4 +1,4 @@
-package gitlab
+package client
import (
"encoding/base64"
@@ -16,17 +16,6 @@ var (
encodedSecret = "e41rcFh7XBA7sNABWVCe2AZvxMsy6QDtJ8S9Ql1UiN8=" // 32 bytes, base64 encoded
)
-func TestNewValidBaseURL(t *testing.T) {
- _, err := NewClient("https://gitlab.com", secretKey())
- require.NoError(t, err)
-}
-
-func TestNewInvalidBaseURL(t *testing.T) {
- client, err := NewClient("%", secretKey())
- require.Error(t, err)
- require.Nil(t, client)
-}
-
func TestGetVirtualDomainForErrorResponses(t *testing.T) {
tests := map[int]string{
http.StatusNoContent: "No Content",
@@ -46,7 +35,7 @@ func TestGetVirtualDomainForErrorResponses(t *testing.T) {
server := httptest.NewServer(mux)
defer server.Close()
- client, _ := NewClient(server.URL, secretKey())
+ client := NewClient(server.URL, secretKey())
actual, err := client.GetVirtualDomain("group.gitlab.io")
@@ -89,7 +78,7 @@ func TestGetVirtualDomainAuthenticatedRequest(t *testing.T) {
server := httptest.NewServer(mux)
defer server.Close()
- client, _ := NewClient(server.URL, secretKey())
+ client := NewClient(server.URL, secretKey())
actual, err := client.GetVirtualDomain("group.gitlab.io")
require.NoError(t, err)
diff --git a/internal/source/gitlab/client/config.go b/internal/source/gitlab/client/config.go
new file mode 100644
index 00000000..49c13a60
--- /dev/null
+++ b/internal/source/gitlab/client/config.go
@@ -0,0 +1,8 @@
+package client
+
+// Config represents an interface that is configuration provider for client
+// capable of comunicating with GitLab
+type Config interface {
+ GitlabServerURL() string
+ GitlabAPISecret() []byte
+}
diff --git a/internal/source/gitlab/client/testdata/test.gitlab.io.json b/internal/source/gitlab/client/testdata/test.gitlab.io.json
new file mode 100644
index 00000000..923c7344
--- /dev/null
+++ b/internal/source/gitlab/client/testdata/test.gitlab.io.json
@@ -0,0 +1,36 @@
+{
+ "certificate": "some--cert",
+ "key": "some--key",
+ "lookup_paths": [
+ {
+ "access_control": false,
+ "https_only": true,
+ "prefix": "/my/pages/project",
+ "project_id": 123,
+ "source": {
+ "path": "/some/path/to/project/",
+ "type": "file"
+ }
+ },
+ {
+ "access_control": false,
+ "https_only": true,
+ "prefix": "/my/second-project",
+ "project_id": 124,
+ "source": {
+ "path": "/some/path/to/project-2/",
+ "type": "file"
+ }
+ },
+ {
+ "access_control": false,
+ "https_only": true,
+ "prefix": "/",
+ "project_id": 125,
+ "source": {
+ "path": "/some/path/to/project-3/",
+ "type": "file"
+ }
+ }
+ ]
+}
diff --git a/internal/source/gitlab/gitlab.go b/internal/source/gitlab/gitlab.go
new file mode 100644
index 00000000..7cb88e42
--- /dev/null
+++ b/internal/source/gitlab/gitlab.go
@@ -0,0 +1,73 @@
+package gitlab
+
+import (
+ "errors"
+ "net/http"
+ "path"
+ "strings"
+
+ "gitlab.com/gitlab-org/gitlab-pages/internal/domain"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/serving"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/source/gitlab/cache"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/source/gitlab/client"
+)
+
+// Gitlab source represent a new domains configuration source. We fetch all the
+// information about domains from GitLab instance.
+type Gitlab struct {
+ client Client
+ cache *cache.Cache // WIP
+}
+
+// New returns a new instance of gitlab domain source.
+func New(config client.Config) *Gitlab {
+ return &Gitlab{client: client.NewFromConfig(config), cache: cache.New()}
+}
+
+// GetDomain return a representation of a domain that we have fetched from
+// GitLab
+func (g *Gitlab) GetDomain(name string) (*domain.Domain, error) {
+ response, err := g.client.GetVirtualDomain(name)
+ if err != nil {
+ return nil, err
+ }
+
+ domain := domain.Domain{
+ Name: name,
+ CertificateCert: response.Certificate,
+ CertificateKey: response.Key,
+ Resolver: g,
+ }
+
+ 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) {
+ response, err := g.client.GetVirtualDomain(r.Host)
+ if err != nil {
+ return nil, "", err
+ }
+
+ for _, lookup := range response.LookupPaths {
+ urlPath := path.Clean(r.URL.Path)
+
+ if strings.HasPrefix(urlPath, lookup.Prefix) {
+ lookupPath := &serving.LookupPath{
+ Prefix: lookup.Prefix,
+ Path: strings.TrimPrefix(lookup.Source.Path, "/"),
+ IsNamespaceProject: (lookup.Prefix == "/" && len(response.LookupPaths) > 1),
+ IsHTTPSOnly: lookup.HTTPSOnly,
+ HasAccessControl: lookup.AccessControl,
+ ProjectID: uint64(lookup.ProjectID),
+ }
+
+ requestPath := strings.TrimPrefix(urlPath, lookup.Prefix)
+
+ return lookupPath, strings.TrimPrefix(requestPath, "/"), nil
+ }
+ }
+
+ return nil, "", errors.New("could not match lookup path")
+}
diff --git a/internal/source/gitlab/gitlab_test.go b/internal/source/gitlab/gitlab_test.go
new file mode 100644
index 00000000..02751eea
--- /dev/null
+++ b/internal/source/gitlab/gitlab_test.go
@@ -0,0 +1,75 @@
+package gitlab
+
+import (
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "gitlab.com/gitlab-org/gitlab-pages/internal/source/gitlab/cache"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/source/gitlab/client"
+)
+
+func TestGetDomain(t *testing.T) {
+ t.Run("when the response if correct", func(t *testing.T) {
+ client := client.StubClient{File: "client/testdata/test.gitlab.io.json"}
+ source := Gitlab{client: client, cache: cache.New()}
+
+ domain, err := source.GetDomain("test.gitlab.io")
+ require.NoError(t, err)
+
+ assert.Equal(t, "test.gitlab.io", domain.Name)
+ })
+
+ t.Run("when the response is not valid", func(t *testing.T) {
+ client := client.StubClient{File: "/dev/null"}
+ source := Gitlab{client: client, cache: cache.New()}
+
+ domain, err := source.GetDomain("test.gitlab.io")
+
+ assert.NotNil(t, err)
+ assert.Nil(t, domain)
+ })
+}
+
+func TestResolve(t *testing.T) {
+ client := client.StubClient{File: "client/testdata/test.gitlab.io.json"}
+ source := Gitlab{client: client, cache: cache.New()}
+
+ t.Run("when requesting a nested group project", 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)
+ require.NoError(t, err)
+
+ assert.Equal(t, "/my/pages/project", lookup.Prefix)
+ assert.Equal(t, "path/index.html", subpath)
+ assert.False(t, lookup.IsNamespaceProject)
+ })
+
+ t.Run("when request a nested group project", 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)
+ require.NoError(t, err)
+
+ assert.Equal(t, "/", lookup.Prefix)
+ assert.Equal(t, "path/to/index.html", subpath)
+ assert.Equal(t, "some/path/to/project-3/", lookup.Path)
+ assert.True(t, lookup.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)
+ require.NoError(t, err)
+
+ assert.Equal(t, "/my/pages/project", lookup.Prefix)
+ assert.Equal(t, "index.html", subpath)
+ })
+}
diff --git a/internal/source/source.go b/internal/source/source.go
new file mode 100644
index 00000000..4b43b8f4
--- /dev/null
+++ b/internal/source/source.go
@@ -0,0 +1,8 @@
+package source
+
+import "gitlab.com/gitlab-org/gitlab-pages/internal/domain"
+
+// Source represents an abstract interface of a domains configuration source.
+type Source interface {
+ GetDomain(string) (*domain.Domain, error)
+}
diff --git a/internal/source/source_mock.go b/internal/source/source_mock.go
new file mode 100644
index 00000000..ee24d804
--- /dev/null
+++ b/internal/source/source_mock.go
@@ -0,0 +1,24 @@
+package source
+
+import (
+ "github.com/stretchr/testify/mock"
+
+ "gitlab.com/gitlab-org/gitlab-pages/internal/domain"
+)
+
+// MockSource can be used for testing
+type MockSource struct {
+ mock.Mock
+}
+
+// GetDomain is a mocked function
+func (m *MockSource) GetDomain(name string) (*domain.Domain, error) {
+ args := m.Called(name)
+
+ return args.Get(0).(*domain.Domain), args.Error(1)
+}
+
+// NewMockSource returns a new Source mock for testing
+func NewMockSource() *MockSource {
+ return &MockSource{}
+}
diff --git a/shared/lookups/new-source-test.gitlab.io.json b/shared/lookups/new-source-test.gitlab.io.json
new file mode 100644
index 00000000..0332b6c2
--- /dev/null
+++ b/shared/lookups/new-source-test.gitlab.io.json
@@ -0,0 +1,16 @@
+{
+ "certificate": "",
+ "key": "",
+ "lookup_paths": [
+ {
+ "access_control": false,
+ "https_only": false,
+ "prefix": "/my/pages/project",
+ "project_id": 123,
+ "source": {
+ "path": "/group/new-source-test.gitlab.io/public",
+ "type": "file"
+ }
+ }
+ ]
+}
diff --git a/shared/pages/group/new-source-test.gitlab.io/public/index.html b/shared/pages/group/new-source-test.gitlab.io/public/index.html
new file mode 100644
index 00000000..00e11d66
--- /dev/null
+++ b/shared/pages/group/new-source-test.gitlab.io/public/index.html
@@ -0,0 +1 @@
+New Pages GitLab Source TEST OK