diff options
author | Alessio Caiazza <acaiazza@gitlab.com> | 2018-11-28 14:40:55 +0300 |
---|---|---|
committer | Alessio Caiazza <acaiazza@gitlab.com> | 2018-11-29 11:28:55 +0300 |
commit | 1218644d7ad7f0c8ee244759fdbb3ed0e8418646 (patch) | |
tree | ae53601b5a12edd95df6e4d41cf01d5e30e72b56 | |
parent | a00d3b3b461e748ce7342f49f23bebc5538d3a7b (diff) |
subgroup support
-rw-r--r-- | acceptance_test.go | 5 | ||||
-rw-r--r-- | internal/domain/domain.go | 25 | ||||
-rw-r--r-- | internal/domain/group.go | 38 | ||||
-rw-r--r-- | internal/domain/group_test.go | 97 | ||||
-rw-r--r-- | internal/domain/map.go | 59 | ||||
-rw-r--r-- | internal/domain/map_test.go | 8 | ||||
-rw-r--r-- | shared/pages/group/subgroup/project/public/index.html | 1 | ||||
-rw-r--r-- | shared/pages/group/subgroup/project/public/subdir/index.html | 1 |
8 files changed, 204 insertions, 30 deletions
diff --git a/acceptance_test.go b/acceptance_test.go index 4100fde1..cf3c7d09 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -128,6 +128,11 @@ func TestKnownHostReturns200(t *testing.T) { host: "CapitalGroup.gitlab-example.com", path: "CapitalProject/", }, + { + name: "subgroup", + host: "group.gitlab-example.com", + path: "subgroup/project/", + }, } for _, test := range tests { diff --git a/internal/domain/domain.go b/internal/domain/domain.go index 5555ab91..3638c372 100644 --- a/internal/domain/domain.go +++ b/internal/domain/domain.go @@ -21,6 +21,13 @@ import ( "gitlab.com/gitlab-org/gitlab-pages/internal/httputil" ) +const ( + // MaxProjectDepth is set to the maximum nested project depth in gitlab (21) plus 3. + // One for the project, one for the first empty element of the split (URL.Path starts with /), + // and one for the real file path + MaxProjectDepth int = 24 +) + type locationDirectoryError struct { FullPath string RelativePath string @@ -33,16 +40,6 @@ type project struct { ID uint64 } -type projects map[string]*project -type subgroups map[string]*group - -type group struct { - name string - - // group domains: - projects projects -} - // D is a domain that gitlab-pages can serve. type D struct { group @@ -123,11 +120,11 @@ func getHost(r *http.Request) string { func (d *D) getProjectWithSubpath(r *http.Request) (*project, string, string) { // Check for a project specified in the URL: http://group.gitlab.io/projectA // If present, these projects shadow the group domain. - split := strings.SplitN(r.URL.Path, "/", 3) + split := strings.SplitN(r.URL.Path, "/", MaxProjectDepth) if len(split) >= 2 { - projectName := strings.ToLower(split[1]) - if project := d.projects[projectName]; project != nil { - return project, split[1], strings.Join(split[2:], "/") + project, projectPath, urlPath := d.digProjectWithSubpath("", split[1:]) + if project != nil { + return project, projectPath, urlPath } } diff --git a/internal/domain/group.go b/internal/domain/group.go new file mode 100644 index 00000000..83b8d255 --- /dev/null +++ b/internal/domain/group.go @@ -0,0 +1,38 @@ +package domain + +import ( + "path" + "strings" +) + +type projects map[string]*project +type subgroups map[string]*group + +type group struct { + name string + + // nested groups + subgroups subgroups + + // group domains: + projects projects +} + +func (g *group) digProjectWithSubpath(parentPath string, keys []string) (*project, string, string) { + if len(keys) >= 1 { + head := keys[0] + tail := keys[1:] + currentPath := path.Join(parentPath, head) + search := strings.ToLower(head) + + if project := g.projects[search]; project != nil { + return project, currentPath, path.Join(tail...) + } + + if subgroup := g.subgroups[search]; subgroup != nil { + return subgroup.digProjectWithSubpath(currentPath, tail) + } + } + + return nil, "", "" +} diff --git a/internal/domain/group_test.go b/internal/domain/group_test.go new file mode 100644 index 00000000..2e41ef53 --- /dev/null +++ b/internal/domain/group_test.go @@ -0,0 +1,97 @@ +package domain + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGroupDig(t *testing.T) { + matchingProject := &project{ID: 1} + + tests := []struct { + name string + g group + path string + expectedProject *project + expectedProjectPath string + expectedPath string + }{ + { + name: "empty group", + path: "projectb/demo/features.html", + g: group{}, + }, + { + name: "group with project", + path: "projectb/demo/features.html", + g: group{ + projects: projects{"projectb": matchingProject}, + }, + expectedProject: matchingProject, + expectedProjectPath: "projectb", + expectedPath: "demo/features.html", + }, + { + name: "group with project and no path in URL", + path: "projectb", + g: group{ + projects: projects{"projectb": matchingProject}, + }, + expectedProject: matchingProject, + expectedProjectPath: "projectb", + }, + { + name: "group with subgroup and project", + path: "projectb/demo/features.html", + g: group{ + projects: projects{"projectb": matchingProject}, + subgroups: subgroups{ + "sub1": &group{ + projects: projects{"another": &project{}}, + }, + }, + }, + expectedProject: matchingProject, + expectedProjectPath: "projectb", + expectedPath: "demo/features.html", + }, + { + name: "group with project inside a subgroup", + path: "sub1/projectb/demo/features.html", + g: group{ + subgroups: subgroups{ + "sub1": &group{ + projects: projects{"projectb": matchingProject}, + }, + }, + projects: projects{"another": &project{}}, + }, + expectedProject: matchingProject, + expectedProjectPath: "sub1/projectb", + expectedPath: "demo/features.html", + }, + { + name: "group with matching subgroup but no project", + path: "sub1/projectb/demo/features.html", + g: group{ + subgroups: subgroups{ + "sub1": &group{ + projects: projects{"another": &project{}}, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + project, projectPath, urlPath := test.g.digProjectWithSubpath("", strings.Split(test.path, "/")) + + assert.Equal(t, test.expectedProject, project) + assert.Equal(t, test.expectedProjectPath, projectPath) + assert.Equal(t, test.expectedPath, urlPath) + }) + } +} diff --git a/internal/domain/map.go b/internal/domain/map.go index 3d9a4073..26657a1c 100644 --- a/internal/domain/map.go +++ b/internal/domain/map.go @@ -36,7 +36,7 @@ func (dm Map) updateDomainMap(domainName string, domain *D) { func (dm Map) addDomain(rootDomain, groupName, projectName string, config *domainConfig) { newDomain := &D{ - group: group { name: groupName }, + group: group{name: groupName}, projectName: projectName, config: config, } @@ -46,21 +46,41 @@ func (dm Map) addDomain(rootDomain, groupName, projectName string, config *domai dm.updateDomainMap(domainName, newDomain) } -func (dm Map) updateGroupDomain(rootDomain, groupName, projectName string, httpsOnly bool, accessControl bool, id uint64) { +func (dm Map) updateGroupDomain(rootDomain, groupName, projectPath string, httpsOnly bool, accessControl bool, id uint64) { domainName := strings.ToLower(groupName + "." + rootDomain) groupDomain := dm[domainName] if groupDomain == nil { groupDomain = &D{ - group: group{ - name: groupName, - projects: make(projects), + group: group{ + name: groupName, + projects: make(projects), + subgroups: make(subgroups), }, } } - groupDomain.projects[strings.ToLower(projectName)] = &project{ - NamespaceProject: domainName == strings.ToLower(projectName), + split := strings.SplitN(strings.ToLower(projectPath), "/", MaxProjectDepth) + projectName := split[len(split)-1] + g := &groupDomain.group + + for i := 0; i < len(split)-1; i++ { + subgroupName := split[i] + subgroup := g.subgroups[subgroupName] + if subgroup == nil { + subgroup = &group{ + name: subgroupName, + projects: make(projects), + subgroups: make(subgroups), + } + g.subgroups[subgroupName] = subgroup + } + + g = subgroup + } + + g.projects[projectName] = &project{ + NamespaceProject: domainName == projectName, HTTPSOnly: httpsOnly, AccessControl: accessControl, ID: id, @@ -88,7 +108,7 @@ func (dm Map) readProjectConfig(rootDomain string, group, projectName string, co } } -func readProject(group, projectName string, fanIn chan<- jobResult) { +func readProject(group, parent, projectName string, fanIn chan<- jobResult) { if strings.HasPrefix(projectName, ".") { return } @@ -98,25 +118,32 @@ func readProject(group, projectName string, fanIn chan<- jobResult) { return } - if _, err := os.Lstat(filepath.Join(group, projectName, "public")); err != nil { + projectPath := filepath.Join(parent, projectName) + if _, err := os.Lstat(filepath.Join(group, projectPath, "public")); err != nil { + // maybe it's a subgroup + buf := make([]byte, 2*os.Getpagesize()) + readProjects(group, projectPath, buf, fanIn) + return } // We read the config.json file _before_ fanning in, because it does disk // IO and it does not need access to the domains map. config := &domainsConfig{} - if err := config.Read(group, projectName); err != nil { + if err := config.Read(group, projectPath); err != nil { config = nil } - fanIn <- jobResult{group: group, project: projectName, config: config} + fanIn <- jobResult{group: group, project: projectPath, config: config} } -func readProjects(group string, buf []byte, fanIn chan<- jobResult) { - fis, err := godirwalk.ReadDirents(group, buf) +func readProjects(group, parent string, buf []byte, fanIn chan<- jobResult) { + subgroup := filepath.Join(group, parent) + fis, err := godirwalk.ReadDirents(subgroup, buf) if err != nil { log.WithError(err).WithFields(log.Fields{ - "group": group, + "group": group, + "parent": parent, }).Print("readdir failed") return } @@ -127,7 +154,7 @@ func readProjects(group string, buf []byte, fanIn chan<- jobResult) { continue } - readProject(group, project.Name(), fanIn) + readProject(group, parent, project.Name(), fanIn) } } @@ -151,7 +178,7 @@ func (dm Map) ReadGroups(rootDomain string, fis godirwalk.Dirents) { for group := range fanOutGroups { started := time.Now() - readProjects(group, buf, fanIn) + readProjects(group, "", buf, fanIn) log.WithFields(log.Fields{ "group": group, diff --git a/internal/domain/map_test.go b/internal/domain/map_test.go index a2b2b575..2306edef 100644 --- a/internal/domain/map_test.go +++ b/internal/domain/map_test.go @@ -70,6 +70,14 @@ func TestReadProjects(t *testing.T) { exp2 := &domainConfig{Domain: "other.domain.com", Certificate: "test", Key: "key"} assert.Equal(t, exp2, dm["other.domain.com"].config) + + // check subgroups + domain, ok := dm["group.test.io"] + require.True(t, ok, "missing group.test.io domain") + subgroup, ok := domain.subgroups["subgroup"] + require.True(t, ok, "missing group.test.io subgroup") + _, ok = subgroup.projects["project"] + require.True(t, ok, "missing project for subgrup in group.test.io domain") } // This write must be atomic, otherwise we cannot predict the state of the diff --git a/shared/pages/group/subgroup/project/public/index.html b/shared/pages/group/subgroup/project/public/index.html new file mode 100644 index 00000000..91ec223d --- /dev/null +++ b/shared/pages/group/subgroup/project/public/index.html @@ -0,0 +1 @@ +A subgroup project diff --git a/shared/pages/group/subgroup/project/public/subdir/index.html b/shared/pages/group/subgroup/project/public/subdir/index.html new file mode 100644 index 00000000..59bf0492 --- /dev/null +++ b/shared/pages/group/subgroup/project/public/subdir/index.html @@ -0,0 +1 @@ +A subgroup project-subsubdir |