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:
authorJanis Altherr <jaltherr@gitlab.com>2023-06-08 09:54:32 +0300
committerJaime Martinez <jmartinez@gitlab.com>2023-06-08 09:54:32 +0300
commit3cbfc605d0ede7e0a79d912fac01d610089f7eef (patch)
treeab4c8aea756d110ec1e676ee6088327809803b2e
parent5a6388a4479dcfbd522a55b525127f9096e79cdc (diff)
Use pages root directory from API
-rw-r--r--Makefile.util.mk2
-rw-r--r--internal/domain/domain_test.go23
-rw-r--r--internal/feature/feature.go6
-rw-r--r--internal/redirects/validations_test.go2
-rw-r--r--internal/serving/disk/local/serving_test.go21
-rw-r--r--internal/serving/disk/projectroot/root.go47
-rw-r--r--internal/serving/disk/projectroot/root_test.go89
-rw-r--r--internal/serving/disk/reader.go12
-rw-r--r--internal/serving/lookup_path.go1
-rw-r--r--internal/source/gitlab/api/lookup_path.go1
-rw-r--r--internal/source/gitlab/factory.go1
-rw-r--r--internal/vfs/zip/archive.go7
-rw-r--r--internal/vfs/zip/archive_test.go136
-rw-r--r--internal/vfs/zip/vfs_test.go8
-rw-r--r--shared/pages/group.customroot/customroot/foo/index.html6
-rw-r--r--shared/pages/group.customroot/customroot/public.zipbin0 -> 384 bytes
-rw-r--r--shared/pages/group.customroot/nosubdir/index.html1
-rw-r--r--shared/pages/group.customroot/nosubdir/public.zipbin0 -> 185 bytes
-rw-r--r--test/acceptance/custom_root_folder_test.go61
-rw-r--r--test/gitlabstub/api_responses.go39
20 files changed, 375 insertions, 88 deletions
diff --git a/Makefile.util.mk b/Makefile.util.mk
index b0a282cf..25c7320d 100644
--- a/Makefile.util.mk
+++ b/Makefile.util.mk
@@ -70,4 +70,4 @@ endif
zip:
cd $(PWD)/shared/pages/$(PROJECT_SUBDIR)/ && \
- zip -r public.zip public/
+ zip -r ../public.zip ./
diff --git a/internal/domain/domain_test.go b/internal/domain/domain_test.go
index 3496a6da..5698aead 100644
--- a/internal/domain/domain_test.go
+++ b/internal/domain/domain_test.go
@@ -36,9 +36,10 @@ func TestIsHTTPSOnly(t *testing.T) {
domain: domain.New("custom-domain", "", "",
mockResolver(t,
&serving.LookupPath{
- Path: "group/project/public",
- SHA256: "foo",
- IsHTTPSOnly: true,
+ Path: "group/project/public",
+ SHA256: "foo",
+ IsHTTPSOnly: true,
+ RootDirectory: "public",
},
"",
nil)),
@@ -50,9 +51,10 @@ func TestIsHTTPSOnly(t *testing.T) {
domain: domain.New("custom-domain", "", "",
mockResolver(t,
&serving.LookupPath{
- Path: "group/project/public",
- SHA256: "foo",
- IsHTTPSOnly: false,
+ Path: "group/project/public",
+ SHA256: "foo",
+ IsHTTPSOnly: false,
+ RootDirectory: "public",
},
"",
nil)),
@@ -142,8 +144,9 @@ func TestServeNamespaceNotFound(t *testing.T) {
path: "/unknown",
resolver: mockResolver(t,
&serving.LookupPath{
- Path: "group.404/group.404.gitlab-example.com/public",
+ Path: "group.404/group.404.gitlab-example.com",
IsNamespaceProject: true,
+ RootDirectory: "public",
},
"/unknown",
nil,
@@ -156,9 +159,10 @@ func TestServeNamespaceNotFound(t *testing.T) {
path: "/private_project/unknown",
resolver: mockResolver(t,
&serving.LookupPath{
- Path: "group.404/group.404.gitlab-example.com/public",
+ Path: "group.404/group.404.gitlab-example.com",
IsNamespaceProject: true,
HasAccessControl: false,
+ RootDirectory: "public",
},
"/",
nil,
@@ -171,9 +175,10 @@ func TestServeNamespaceNotFound(t *testing.T) {
path: "/unknown",
resolver: mockResolver(t,
&serving.LookupPath{
- Path: "group.404/group.404.gitlab-example.com/public",
+ Path: "group.404/group.404.gitlab-example.com",
IsNamespaceProject: true,
HasAccessControl: true,
+ RootDirectory: "public",
},
"/",
nil,
diff --git a/internal/feature/feature.go b/internal/feature/feature.go
index d5c340ff..c9df43aa 100644
--- a/internal/feature/feature.go
+++ b/internal/feature/feature.go
@@ -19,6 +19,12 @@ var HandleReadErrors = Feature{
EnvVariable: "FF_HANDLE_READ_ERRORS",
}
+// ConfigurableRoot enables a project's root directory to be customized using
+// the GitLab API
+var ConfigurableRoot = Feature{
+ EnvVariable: "FF_CONFIGURABLE_ROOT_DIR",
+}
+
// Enabled reads the environment variable responsible for the feature flag
// if FF is disabled by default, the environment variable needs to be "true" to explicitly enable it
// if FF is enabled by default, variable needs to be "false" to explicitly disable it
diff --git a/internal/redirects/validations_test.go b/internal/redirects/validations_test.go
index 8cb11e11..2b8ac1b1 100644
--- a/internal/redirects/validations_test.go
+++ b/internal/redirects/validations_test.go
@@ -117,7 +117,7 @@ func TestRedirectsValidateRule(t *testing.T) {
expectedErr: errNoStartingForwardSlashInURLPath,
},
"no_parameters": {
- rule: "/ /something 302 foo=bar",
+ rule: "/ /something 302 foo=bar",
expectedErr: errNoParams,
},
"invalid_status": {
diff --git a/internal/serving/disk/local/serving_test.go b/internal/serving/disk/local/serving_test.go
index dbb14138..11a328c3 100644
--- a/internal/serving/disk/local/serving_test.go
+++ b/internal/serving/disk/local/serving_test.go
@@ -20,34 +20,40 @@ func TestDisk_ServeFileHTTP(t *testing.T) {
path string
expectedStatus int
expectedBody string
+ rootDirectory string
}{
"accessing /index.html": {
- vfsPath: "group/serving/public",
+ vfsPath: "group/serving",
path: "/index.html",
+ rootDirectory: "public",
expectedStatus: http.StatusOK,
expectedBody: "HTML Document",
},
"accessing /": {
- vfsPath: "group/serving/public",
+ vfsPath: "group/serving",
path: "/",
+ rootDirectory: "public",
expectedStatus: http.StatusOK,
expectedBody: "HTML Document",
},
"accessing without /": {
- vfsPath: "group/serving/public",
+ vfsPath: "group/serving",
path: "",
+ rootDirectory: "public",
expectedStatus: http.StatusFound,
expectedBody: `<a href="//group.gitlab-example.com/serving/">Found</a>.`,
},
"accessing vfs path that is missing": {
- vfsPath: "group/serving/public-missing",
- path: "/index.html",
+ vfsPath: "group/serving/public-missing",
+ path: "/index.html",
+ rootDirectory: "public",
// we expect the status to not be set
expectedStatus: 0,
},
"accessing vfs path that is forbidden (like file)": {
vfsPath: "group/serving/public/index.html",
path: "/index.html",
+ rootDirectory: "public",
expectedStatus: http.StatusInternalServerError,
},
}
@@ -64,8 +70,9 @@ func TestDisk_ServeFileHTTP(t *testing.T) {
Writer: w,
Request: r,
LookupPath: &serving.LookupPath{
- Prefix: "/serving/",
- Path: test.vfsPath,
+ Prefix: "/serving/",
+ Path: test.vfsPath,
+ RootDirectory: test.rootDirectory,
},
SubPath: test.path,
}
diff --git a/internal/serving/disk/projectroot/root.go b/internal/serving/disk/projectroot/root.go
new file mode 100644
index 00000000..64f830c0
--- /dev/null
+++ b/internal/serving/disk/projectroot/root.go
@@ -0,0 +1,47 @@
+package projectroot
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+
+ "gitlab.com/gitlab-org/gitlab-pages/internal/feature"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/vfs"
+)
+
+// projectRoot implements the more low-level vfs.Root interface and can be used in its
+// stead. The difference is, it always resolves the files inside the project's
+// rootDirectory by prepending that dir to any open request.
+type projectRoot struct {
+ rootDirectory string
+ vfsRoot vfs.Root
+}
+
+func New(rootDirectory string, vfsRoot vfs.Root) vfs.Root {
+ if !feature.ConfigurableRoot.Enabled() || rootDirectory == "" {
+ // In case the GitLab API is not up-to-date this string may be empty.
+ // In that case default to the legacy behavior
+ rootDirectory = "public"
+ }
+
+ return &projectRoot{
+ rootDirectory: rootDirectory,
+ vfsRoot: vfsRoot,
+ }
+}
+
+func (r *projectRoot) Open(ctx context.Context, name string) (vfs.File, error) {
+ return r.vfsRoot.Open(ctx, r.getPath(name))
+}
+
+func (r *projectRoot) Lstat(ctx context.Context, name string) (os.FileInfo, error) {
+ return r.vfsRoot.Lstat(ctx, r.getPath(name))
+}
+
+func (r *projectRoot) Readlink(ctx context.Context, name string) (string, error) {
+ return r.vfsRoot.Readlink(ctx, r.getPath(name))
+}
+
+func (r *projectRoot) getPath(name string) string {
+ return filepath.Join(r.rootDirectory, name)
+}
diff --git a/internal/serving/disk/projectroot/root_test.go b/internal/serving/disk/projectroot/root_test.go
new file mode 100644
index 00000000..b8549d10
--- /dev/null
+++ b/internal/serving/disk/projectroot/root_test.go
@@ -0,0 +1,89 @@
+package projectroot
+
+import (
+ "context"
+ "os"
+ "strconv"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "gitlab.com/gitlab-org/gitlab-pages/internal/feature"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/vfs"
+)
+
+// In case this moch struct ever grows in complexity,
+// consider using mockgen instead:
+// e.g. internal/domain/mock/resolver_mock.go
+type mockRoot struct {
+ lstatCalledWith string
+ readlinkCalledWith string
+ openCalledWith string
+}
+
+func (m *mockRoot) Lstat(ctx context.Context, name string) (os.FileInfo, error) {
+ m.lstatCalledWith = name
+ return nil, nil
+}
+
+func (m *mockRoot) Readlink(ctx context.Context, name string) (string, error) {
+ m.readlinkCalledWith = name
+ return "", nil
+}
+
+func (m *mockRoot) Open(ctx context.Context, name string) (vfs.File, error) {
+ m.openCalledWith = name
+ return nil, nil
+}
+
+func TestProjectRoot(t *testing.T) {
+ tests := map[string]struct {
+ path string
+ rootDir string
+ expectedPath string
+ featureEnabled bool
+ }{
+ "when the root dir is provided": {
+ path: "some/path",
+ rootDir: "my_root_dir",
+ expectedPath: "my_root_dir/some/path",
+ featureEnabled: true,
+ },
+ "when the root dir is not provided": {
+ path: "some/path",
+ rootDir: "",
+ expectedPath: "public/some/path",
+ featureEnabled: true,
+ },
+ "when the feature is disabled": {
+ path: "some/path",
+ rootDir: "my_root_dir",
+ expectedPath: "public/some/path",
+ featureEnabled: false,
+ },
+ "when the feature is disabled and no root path is provided": {
+ path: "some/path",
+ rootDir: "",
+ expectedPath: "public/some/path",
+ featureEnabled: false,
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ t.Setenv(feature.ConfigurableRoot.EnvVariable,
+ strconv.FormatBool(test.featureEnabled))
+ originalRoot := &mockRoot{}
+ wrappedRoot := New(test.rootDir, originalRoot)
+
+ wrappedRoot.Lstat(context.Background(), test.path)
+ require.Equal(t, test.expectedPath, originalRoot.lstatCalledWith)
+
+ wrappedRoot.Readlink(context.Background(), test.path)
+ require.Equal(t, test.expectedPath, originalRoot.readlinkCalledWith)
+
+ wrappedRoot.Open(context.Background(), test.path)
+ require.Equal(t, test.expectedPath, originalRoot.openCalledWith)
+ })
+ }
+}
diff --git a/internal/serving/disk/reader.go b/internal/serving/disk/reader.go
index f250a0d6..7a1af61b 100644
--- a/internal/serving/disk/reader.go
+++ b/internal/serving/disk/reader.go
@@ -18,6 +18,7 @@ import (
"gitlab.com/gitlab-org/gitlab-pages/internal/logging"
"gitlab.com/gitlab-org/gitlab-pages/internal/redirects"
"gitlab.com/gitlab-org/gitlab-pages/internal/serving"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/serving/disk/projectroot"
"gitlab.com/gitlab-org/gitlab-pages/internal/serving/disk/symlink"
"gitlab.com/gitlab-org/gitlab-pages/internal/vfs"
vfsServing "gitlab.com/gitlab-org/gitlab-pages/internal/vfs/serving"
@@ -284,11 +285,18 @@ func (reader *Reader) serveCustomFile(ctx context.Context, w http.ResponseWriter
return nil
}
-// root tries to resolve the vfs.Root and handles errors for it.
+// root tries to resolve the vfs.Root, wrap it in a projectroot.Root and handles
+// errors for it.
// It returns whether we served the response or not.
func (reader *Reader) root(h serving.Handler) (vfs.Root, bool) {
- root, err := reader.vfs.Root(h.Request.Context(), h.LookupPath.Path, h.LookupPath.SHA256)
+ vfsRoot, err := reader.vfs.Root(h.Request.Context(), h.LookupPath.Path,
+ h.LookupPath.SHA256)
+
if err == nil {
+ // The project root directory changes based on the response obtained
+ // from the API. It currently depends on feature.ConfigurableRoot
+ // being enabled.
+ root := projectroot.New(h.LookupPath.RootDirectory, vfsRoot)
return root, false
}
diff --git a/internal/serving/lookup_path.go b/internal/serving/lookup_path.go
index 6a283b6e..2a4ed218 100644
--- a/internal/serving/lookup_path.go
+++ b/internal/serving/lookup_path.go
@@ -11,4 +11,5 @@ type LookupPath struct {
HasAccessControl bool
ProjectID uint64
UniqueHost string
+ RootDirectory string
}
diff --git a/internal/source/gitlab/api/lookup_path.go b/internal/source/gitlab/api/lookup_path.go
index a61d2f7c..8cdd0205 100644
--- a/internal/source/gitlab/api/lookup_path.go
+++ b/internal/source/gitlab/api/lookup_path.go
@@ -8,6 +8,7 @@ type LookupPath struct {
Prefix string `json:"prefix,omitempty"`
Source Source `json:"source,omitempty"`
UniqueHost string `json:"unique_host,omitempty"`
+ RootDirectory string `json:"root_directory,omitempty"`
}
// Source describes GitLab Page serving variant
diff --git a/internal/source/gitlab/factory.go b/internal/source/gitlab/factory.go
index 1c5d15f5..86013de2 100644
--- a/internal/source/gitlab/factory.go
+++ b/internal/source/gitlab/factory.go
@@ -32,6 +32,7 @@ func fabricateLookupPath(size int, lookup api.LookupPath) *serving.LookupPath {
HasAccessControl: lookup.AccessControl,
ProjectID: uint64(lookup.ProjectID),
UniqueHost: lookup.UniqueHost,
+ RootDirectory: lookup.RootDirectory,
}
}
diff --git a/internal/vfs/zip/archive.go b/internal/vfs/zip/archive.go
index 1708f238..373e9818 100644
--- a/internal/vfs/zip/archive.go
+++ b/internal/vfs/zip/archive.go
@@ -16,6 +16,7 @@ import (
"gitlab.com/gitlab-org/labkit/log"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/feature"
"gitlab.com/gitlab-org/gitlab-pages/internal/httprange"
"gitlab.com/gitlab-org/gitlab-pages/internal/vfs"
"gitlab.com/gitlab-org/gitlab-pages/metrics"
@@ -144,7 +145,7 @@ func (a *zipArchive) readArchive(url string) {
// TODO: Improve preprocessing of zip archives https://gitlab.com/gitlab-org/gitlab-pages/-/issues/432
for _, file := range archive.File {
- if !strings.HasPrefix(file.Name, dirPrefix) {
+ if !feature.ConfigurableRoot.Enabled() && !strings.HasPrefix(file.Name, dirPrefix) {
continue
}
@@ -193,13 +194,13 @@ func (a *zipArchive) addPathDirectory(pathname string) {
}
func (a *zipArchive) findFile(name string) *zip.File {
- name = path.Clean(dirPrefix + name)
+ name = path.Clean(name)
return a.files[name]
}
func (a *zipArchive) findDirectory(name string) *zip.FileHeader {
- name = path.Clean(dirPrefix + name)
+ name = path.Clean(name)
return a.directories[name+"/"]
}
diff --git a/internal/vfs/zip/archive_test.go b/internal/vfs/zip/archive_test.go
index 731f1608..6a0bfb00 100644
--- a/internal/vfs/zip/archive_test.go
+++ b/internal/vfs/zip/archive_test.go
@@ -11,9 +11,9 @@ import (
"net/http"
"net/http/httptest"
"os"
+ "path"
"path/filepath"
"strconv"
- "strings"
"sync/atomic"
"testing"
"time"
@@ -46,35 +46,42 @@ func testOpen(t *testing.T, zip *zipArchive) {
file string
expectedContent string
expectedErr error
+ rootDirectory string
}{
"file_exists": {
file: "index.html",
+ rootDirectory: "public",
expectedContent: "zip.gitlab.io/project/index.html\n",
expectedErr: nil,
},
"file_exists_in_subdir": {
file: "subdir/hello.html",
+ rootDirectory: "public",
expectedContent: "zip.gitlab.io/project/subdir/hello.html\n",
expectedErr: nil,
},
"file_exists_symlink": {
file: "symlink.html",
+ rootDirectory: "public",
expectedContent: "subdir/linked.html",
expectedErr: errNotFile,
},
"is_dir": {
- file: "subdir",
- expectedErr: errNotFile,
+ file: "subdir",
+ rootDirectory: "public",
+ expectedErr: errNotFile,
},
"file_does_not_exist": {
- file: "unknown.html",
- expectedErr: fs.ErrNotExist,
+ file: "unknown.html",
+ rootDirectory: "public",
+ expectedErr: fs.ErrNotExist,
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
- f, err := zip.Open(context.Background(), tt.file)
+ filePath := path.Clean(tt.rootDirectory + "/" + tt.file)
+ f, err := zip.Open(context.Background(), filePath)
if tt.expectedErr != nil {
require.ErrorIs(t, err, tt.expectedErr)
return
@@ -113,7 +120,7 @@ func TestOpenCached(t *testing.T) {
name: "open file first time",
vfsPath: testServerURL + "/public.zip",
sha256: "d6b318b399cfe9a1c8483e49847ee49a2676d8cfd6df57ec64d971ad03640a75",
- filePath: "index.html",
+ filePath: "public/index.html",
// we expect five requests to:
// read resource and zip metadata
// read file: data offset and content
@@ -124,7 +131,7 @@ func TestOpenCached(t *testing.T) {
name: "open file second time",
vfsPath: testServerURL + "/public.zip",
sha256: "d6b318b399cfe9a1c8483e49847ee49a2676d8cfd6df57ec64d971ad03640a75",
- filePath: "index.html",
+ filePath: "public/index.html",
// we expect one request to read file with cached data offset
expectedRequests: 1,
expectedArchiveStatus: archiveOpened,
@@ -133,7 +140,7 @@ func TestOpenCached(t *testing.T) {
name: "when the URL changes",
vfsPath: testServerURL + "/public.zip?new-secret",
sha256: "d6b318b399cfe9a1c8483e49847ee49a2676d8cfd6df57ec64d971ad03640a75",
- filePath: "index.html",
+ filePath: "public/index.html",
expectedRequests: 1,
expectedArchiveStatus: archiveOpened,
},
@@ -141,7 +148,7 @@ func TestOpenCached(t *testing.T) {
name: "when opening cached file and content changes",
vfsPath: testServerURL + "/public.zip?changed-content=1",
sha256: "d6b318b399cfe9a1c8483e49847ee49a2676d8cfd6df57ec64d971ad03640a75",
- filePath: "index.html",
+ filePath: "public/index.html",
expectedRequests: 1,
// we receive an error on `read` as `open` offset is already cached
expectedReadErr: vfs.NewReadError(httprange.ErrRangeRequestsNotSupported),
@@ -151,7 +158,7 @@ func TestOpenCached(t *testing.T) {
name: "after content change archive is reloaded",
vfsPath: testServerURL + "/public.zip?new-secret",
sha256: "d6b318b399cfe9a1c8483e49847ee49a2676d8cfd6df57ec64d971ad03640a75",
- filePath: "index.html",
+ filePath: "public/index.html",
expectedRequests: 5,
expectedArchiveStatus: archiveOpened,
},
@@ -159,7 +166,7 @@ func TestOpenCached(t *testing.T) {
name: "when opening non-cached file and content changes",
vfsPath: testServerURL + "/public.zip?changed-content=1",
sha256: "d6b318b399cfe9a1c8483e49847ee49a2676d8cfd6df57ec64d971ad03640a75",
- filePath: "subdir/hello.html",
+ filePath: "public/subdir/hello.html",
expectedRequests: 1,
// we receive an error on `read` as `open` offset is already cached
expectedOpenErr: vfs.NewReadError(httprange.ErrRangeRequestsNotSupported),
@@ -209,54 +216,64 @@ func TestLstat(t *testing.T) {
func testLstat(t *testing.T, zip *zipArchive) {
tests := map[string]struct {
- file string
- isDir bool
- isSymlink bool
- expectedName string
- expectedErr error
+ file string
+ isDir bool
+ isSymlink bool
+ expectedName string
+ expectedErr error
+ rootDirectory string
}{
"file_exists": {
- file: "index.html",
- expectedName: "index.html",
+ file: "index.html",
+ rootDirectory: "public",
+ expectedName: "index.html",
},
"file_exists_in_subdir": {
- file: "subdir/hello.html",
- expectedName: "hello.html",
+ file: "subdir/hello.html",
+ rootDirectory: "public",
+ expectedName: "hello.html",
},
"file_exists_symlink": {
- file: "symlink.html",
- isSymlink: true,
- expectedName: "symlink.html",
+ file: "symlink.html",
+ rootDirectory: "public",
+ isSymlink: true,
+ expectedName: "symlink.html",
},
"has_root": {
- file: "",
- isDir: true,
- expectedName: "public",
+ file: "",
+ rootDirectory: "public",
+ isDir: true,
+ expectedName: "public",
},
"has_root_dot": {
- file: ".",
- isDir: true,
- expectedName: "public",
+ file: ".",
+ rootDirectory: "public",
+ isDir: true,
+ expectedName: "public",
},
"has_root_slash": {
- file: "/",
- isDir: true,
- expectedName: "public",
+ file: "/",
+ rootDirectory: "public",
+ isDir: true,
+ expectedName: "public",
},
"is_dir": {
- file: "subdir",
- isDir: true,
- expectedName: "subdir",
+ file: "subdir",
+ rootDirectory: "public",
+ isDir: true,
+ expectedName: "subdir",
},
"file_does_not_exist": {
- file: "unknown.html",
- expectedErr: fs.ErrNotExist,
+ file: "unknown.html",
+ rootDirectory: "public",
+ expectedErr: fs.ErrNotExist,
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
- fi, err := zip.Lstat(context.Background(), tt.file)
+ filePath := path.Clean(tt.rootDirectory + "/" + tt.file)
+ fi, err := zip.Lstat(context.Background(), filePath)
if tt.expectedErr != nil {
require.ErrorIs(t, err, tt.expectedErr)
return
@@ -291,33 +308,40 @@ func TestReadLink(t *testing.T) {
func testReadLink(t *testing.T, zip *zipArchive) {
tests := map[string]struct {
- file string
- expectedErr error
+ file string
+ rootDirectory string
+ expectedErr error
}{
"symlink_success": {
- file: "symlink.html",
+ file: "symlink.html",
+ rootDirectory: "public",
},
"file": {
- file: "index.html",
- expectedErr: errNotSymlink,
+ file: "index.html",
+ rootDirectory: "public",
+ expectedErr: errNotSymlink,
},
"dir": {
- file: "subdir",
- expectedErr: errNotSymlink,
+ file: "subdir",
+ rootDirectory: "public",
+ expectedErr: errNotSymlink,
},
"symlink_too_big": {
- file: "bad_symlink.html",
- expectedErr: errSymlinkSize,
+ file: "bad_symlink.html",
+ rootDirectory: "public",
+ expectedErr: errSymlinkSize,
},
"file_does_not_exist": {
- file: "unknown.html",
- expectedErr: fs.ErrNotExist,
+ file: "unknown.html",
+ rootDirectory: "public",
+ expectedErr: fs.ErrNotExist,
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
- link, err := zip.Readlink(context.Background(), tt.file)
+ filePath := path.Clean(tt.rootDirectory + "/" + tt.file)
+ link, err := zip.Readlink(context.Background(), filePath)
if tt.expectedErr != nil {
require.ErrorIs(t, err, tt.expectedErr)
return
@@ -336,14 +360,14 @@ func TestReadlinkCached(t *testing.T) {
t.Run("readlink first time", func(t *testing.T) {
requestsStart := atomic.LoadInt64(&requests)
- _, err := zip.Readlink(context.Background(), "symlink.html")
+ _, err := zip.Readlink(context.Background(), "public/symlink.html")
require.NoError(t, err)
require.Equal(t, int64(2), atomic.LoadInt64(&requests)-requestsStart, "we expect two requests to read symlink: data offset and link")
})
t.Run("readlink second time", func(t *testing.T) {
requestsStart := atomic.LoadInt64(&requests)
- _, err := zip.Readlink(context.Background(), "symlink.html")
+ _, err := zip.Readlink(context.Background(), "public/symlink.html")
require.NoError(t, err)
require.Equal(t, int64(0), atomic.LoadInt64(&requests)-requestsStart, "we expect no additional requests to read cached symlink")
})
@@ -363,7 +387,7 @@ func TestArchiveCanBeReadAfterOpenCtxCanceled(t *testing.T) {
<-zip.done
- file, err := zip.Open(context.Background(), "index.html")
+ file, err := zip.Open(context.Background(), "public/index.html")
require.NoError(t, err)
data, err := io.ReadAll(file)
require.NoError(t, err)
@@ -453,7 +477,7 @@ func TestMinimalRangeRequests(t *testing.T) {
continue
}
- f, err := zip.Open(context.Background(), strings.TrimPrefix(zf.Name, "public/"))
+ f, err := zip.Open(context.Background(), zf.Name)
require.NoError(t, err)
io.Copy(io.Discard, f)
@@ -564,7 +588,7 @@ func benchmarkArchiveRead(b *testing.B, size int64) {
err := z.openArchive(context.Background(), ts.URL+"/public.zip")
require.NoError(b, err)
- f, err := z.Open(context.Background(), "file.txt")
+ f, err := z.Open(context.Background(), "public/file.txt")
require.NoError(b, err)
_, err = io.Copy(io.Discard, f)
diff --git a/internal/vfs/zip/vfs_test.go b/internal/vfs/zip/vfs_test.go
index af1d263b..d34ba066 100644
--- a/internal/vfs/zip/vfs_test.go
+++ b/internal/vfs/zip/vfs_test.go
@@ -55,18 +55,18 @@ func TestVFSRoot(t *testing.T) {
require.NoError(t, err)
require.IsType(t, &zipArchive{}, root)
- f, err := root.Open(context.Background(), "index.html")
+ f, err := root.Open(context.Background(), "public/index.html")
require.NoError(t, err)
content, err := io.ReadAll(f)
require.NoError(t, err)
require.Equal(t, "zip.gitlab.io/project/index.html\n", string(content))
- fi, err := root.Lstat(context.Background(), "index.html")
+ fi, err := root.Lstat(context.Background(), "public/index.html")
require.NoError(t, err)
require.Equal(t, "index.html", fi.Name())
- link, err := root.Readlink(context.Background(), "symlink.html")
+ link, err := root.Readlink(context.Background(), "public/symlink.html")
require.NoError(t, err)
require.Equal(t, "subdir/linked.html", link)
})
@@ -249,7 +249,7 @@ func TestVFSReconfigureTransport(t *testing.T) {
root, err := vfs.Root(context.Background(), fileURL, key)
require.NoError(t, err)
- fi, err := root.Lstat(context.Background(), "index.html")
+ fi, err := root.Lstat(context.Background(), "public/index.html")
require.NoError(t, err)
require.Equal(t, "index.html", fi.Name())
}
diff --git a/shared/pages/group.customroot/customroot/foo/index.html b/shared/pages/group.customroot/customroot/foo/index.html
new file mode 100644
index 00000000..b3a2c1c3
--- /dev/null
+++ b/shared/pages/group.customroot/customroot/foo/index.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html lang="en">
+ <body>
+ <p>HTML Document</p>
+ </body>
+</html>
diff --git a/shared/pages/group.customroot/customroot/public.zip b/shared/pages/group.customroot/customroot/public.zip
new file mode 100644
index 00000000..f6c566bb
--- /dev/null
+++ b/shared/pages/group.customroot/customroot/public.zip
Binary files differ
diff --git a/shared/pages/group.customroot/nosubdir/index.html b/shared/pages/group.customroot/nosubdir/index.html
new file mode 100644
index 00000000..4ea0a6ae
--- /dev/null
+++ b/shared/pages/group.customroot/nosubdir/index.html
@@ -0,0 +1 @@
+project-subdir
diff --git a/shared/pages/group.customroot/nosubdir/public.zip b/shared/pages/group.customroot/nosubdir/public.zip
new file mode 100644
index 00000000..9348c010
--- /dev/null
+++ b/shared/pages/group.customroot/nosubdir/public.zip
Binary files differ
diff --git a/test/acceptance/custom_root_folder_test.go b/test/acceptance/custom_root_folder_test.go
new file mode 100644
index 00000000..24ebe2d1
--- /dev/null
+++ b/test/acceptance/custom_root_folder_test.go
@@ -0,0 +1,61 @@
+package acceptance_test
+
+import (
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "gitlab.com/gitlab-org/gitlab-pages/internal/feature"
+ "gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers"
+)
+
+func TestCustomRoot(t *testing.T) {
+ t.Setenv(feature.ConfigurableRoot.EnvVariable, "true")
+
+ RunPagesProcess(t)
+
+ tests := []struct {
+ name string
+ requestDomain string
+ requestPath string
+ redirectURL string
+ httpStatus int
+ }{
+ {
+ name: "custom root",
+ requestDomain: "custom-root.gitlab-example.com",
+ httpStatus: http.StatusOK,
+ },
+ {
+ name: "custom root legacy",
+ requestDomain: "custom-root-legacy.gitlab-example.com",
+ httpStatus: http.StatusOK,
+ },
+ {
+ name: "custom root explicitly public",
+ requestDomain: "custom-root-explicit-public.gitlab-example.com",
+ httpStatus: http.StatusOK,
+ },
+ {
+ name: "project does not have pages files inside a subdirectory",
+ requestDomain: "custom-root-no-subdir.gitlab-example.com",
+ httpStatus: http.StatusNotFound,
+ },
+ {
+ name: "used a root dir value that does not exist inside the archive",
+ requestDomain: "custom-root-wrong-dir.gitlab-example.com",
+ httpStatus: http.StatusNotFound,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ rsp, err := GetRedirectPage(t, httpsListener, test.requestDomain, test.requestPath)
+ require.NoError(t, err)
+ testhelpers.Close(t, rsp.Body)
+
+ require.Equal(t, test.httpStatus, rsp.StatusCode)
+ })
+ }
+}
diff --git a/test/gitlabstub/api_responses.go b/test/gitlabstub/api_responses.go
index 02b396cb..87233969 100644
--- a/test/gitlabstub/api_responses.go
+++ b/test/gitlabstub/api_responses.go
@@ -47,6 +47,7 @@ type Response struct {
httpsOnly bool
pathOnDisk string // base directory is gitlab-pages/shared/pages
uniqueHost string
+ rootDirectory string
}
func (responses Responses) virtualDomain(wd string) api.VirtualDomain {
@@ -78,6 +79,7 @@ func (response Response) lookupPath(prefix, wd string) api.LookupPath {
AccessControl: response.accessControl,
HTTPSOnly: response.httpsOnly,
UniqueHost: response.uniqueHost,
+ RootDirectory: response.rootDirectory,
Source: api.Source{
Type: "zip",
Path: sourcePath,
@@ -315,9 +317,36 @@ var apiResponses = APIResponses{
pathOnDisk: "group/project",
},
},
- // NOTE: before adding more domains here, you can:
- // use an existing project or generate a new zip archive.
- // To generate a new zip archive run the following command, where PROJECT_SUBDIR
- // is a project folder within `gitlab-pages/shared/pages`
- // `make zip PROJECT_SUBDIR=group/serving`
+ "custom-root.gitlab-example.com": {
+ "/": {
+ pathOnDisk: "group.customroot/customroot",
+ rootDirectory: "foo",
+ },
+ },
+ "custom-root-legacy.gitlab-example.com": {
+ "/": {
+ pathOnDisk: "group/project",
+ },
+ },
+ "custom-root-explicit-public.gitlab-example.com": {
+ "/": {
+ pathOnDisk: "group/project",
+ rootDirectory: "public",
+ },
+ },
+ "custom-root-no-subdir.gitlab-example.com": {
+ "/": {
+ pathOnDisk: "group.customroot/nosubdir",
+ rootDirectory: "/",
+ },
+ },
+ "custom-root-wrong-dir.gitlab-example.com": {
+ "/": {
+ pathOnDisk: "group/project",
+ rootDirectory: "foo",
+ },
+ },
+ // NOTE: before adding more domains here, generate the zip archive by running (per project)
+ // make zip PROJECT_SUBDIR=group/project2/public
+ // make zip PROJECT_SUBDIR=group/customroot/foo
}