diff options
Diffstat (limited to 'internal/source')
-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 |
9 files changed, 251 insertions, 48 deletions
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) }) } |