diff options
author | Janis Altherr <jaltherr@gitlab.com> | 2023-06-08 09:54:32 +0300 |
---|---|---|
committer | Jaime Martinez <jmartinez@gitlab.com> | 2023-06-08 09:54:32 +0300 |
commit | 3cbfc605d0ede7e0a79d912fac01d610089f7eef (patch) | |
tree | ab4c8aea756d110ec1e676ee6088327809803b2e | |
parent | 5a6388a4479dcfbd522a55b525127f9096e79cdc (diff) |
Use pages root directory from API
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 Binary files differnew file mode 100644 index 00000000..f6c566bb --- /dev/null +++ b/shared/pages/group.customroot/customroot/public.zip 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 Binary files differnew file mode 100644 index 00000000..9348c010 --- /dev/null +++ b/shared/pages/group.customroot/nosubdir/public.zip 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 } |