Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow docker compose to be hosted at a subpath #748

Draft
wants to merge 1 commit into
base: v1.3.5-release
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions backend/config/settings/site_specific.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,8 @@ class MetagridFrontendSettings(BaseSettings):
examples=["UA-12345678-1"],
description="The Google Analytics tracking ID for tracking user interactions.",
)
HOST_SUBPATH: str = Field(
default="/",
examples=["/", "/metagrid"],
description="The subpath at which metagrid is hosted.",
)
3 changes: 3 additions & 0 deletions backend/config/settings/static.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,3 +343,6 @@ class DjangoStaticSettings(BaseSettings):
STATICFILES_STORAGE: str = (
"whitenoise.storage.CompressedManifestStaticFilesStorage"
)

# https://docs.djangoproject.com/en/5.1/ref/settings/#force-script-name
FORCE_SCRIPT_NAME: Optional[str] = None
21 changes: 17 additions & 4 deletions backend/config/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import os
import sys

from django.conf import settings
from django.core.wsgi import get_wsgi_application

app_path = os.path.abspath(
Expand All @@ -32,7 +33,19 @@
# This application object is used by any WSGI server configured to use this
# file. This includes Django's development server, if the WSGI_APPLICATION
# setting points here.
application = get_wsgi_application()
# Apply WSGI middleware here.
# from helloworld.wsgi import HelloWorldApplication
# application = HelloWorldApplication(application)
_application = get_wsgi_application()


def application(environ, start_response):
script_name = settings.FORCE_SCRIPT_NAME
if script_name:
environ["SCRIPT_NAME"] = script_name
path_info = environ["PATH_INFO"]
if path_info.startswith(script_name):
environ["PATH_INFO"] = path_info[len(script_name) :]

scheme = environ.get("HTTP_X_SCHEME", "")
if scheme:
environ["wsgi.url_scheme"] = scheme

return _application(environ, start_response)
11 changes: 5 additions & 6 deletions frontend/index.html
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base href="/" />
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="icon" href="favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site created using create-react-app" />
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="apple-touch-icon" href="logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="/manifest.json" />
<link rel="manifest" href="manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Expand All @@ -22,7 +21,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link rel="preload" as="fetch" href="./frontend-config.js" crossorigin="anonymous" />
<link rel="preload" as="fetch" href="frontend-config.js" crossorigin="anonymous" />
<link
rel="preload"
as="fetch"
Expand All @@ -35,7 +34,7 @@
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
<script type="module" src="src/index.tsx"></script>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
Expand Down
34 changes: 17 additions & 17 deletions frontend/src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,77 +56,77 @@ export const clickableRoute = (route: string): string => route.replace('/proxy/s
const apiRoutes: ApiRoutes = {
// Globus APIs
globusAuth: {
path: `${window.location.origin}/proxy/globus-auth/`,
path: 'proxy/globus-auth/',
handleErrorMsg: (HTTPCode) => mapHTTPErrorCodes('Globus', HTTPCode),
},
globusSearchEndpoints: {
path: `${window.location.origin}/proxy/globus-search-endpoints/`,
path: `proxy/globus-search-endpoints/`,
handleErrorMsg: (HTTPCode) => mapHTTPErrorCodes('Globus', HTTPCode),
},
// MetaGrid APIs
keycloakAuth: {
path: `${window.location.origin}/dj-rest-auth/keycloak`,
path: `dj-rest-auth/keycloak`,
handleErrorMsg: (HTTPCode) => mapHTTPErrorCodes('Keycloak', HTTPCode),
},
globusTransfer: {
path: `${window.location.origin}/globus/transfer`,
path: `globus/transfer`,
handleErrorMsg: (HTTPCode) => mapHTTPErrorCodes('Globus transfer', HTTPCode),
},
userInfo: {
path: `${window.location.origin}/dj-rest-auth/user/`,
path: `dj-rest-auth/user/`,
handleErrorMsg: (HTTPCode) => mapHTTPErrorCodes('user authentication', HTTPCode),
},
userCart: {
path: `${window.location.origin}/api/v1/carts/datasets/:pk/`,
path: `api/v1/carts/datasets/:pk/`,
handleErrorMsg: (HTTPCode) => mapHTTPErrorCodes('user cart', HTTPCode),
},
userSearches: {
path: `${window.location.origin}/api/v1/carts/searches/`,
path: `api/v1/carts/searches/`,
handleErrorMsg: (HTTPCode) => mapHTTPErrorCodes('user saved searches', HTTPCode),
},
userSearch: {
path: `${window.location.origin}/api/v1/carts/searches/:pk/`,
path: `api/v1/carts/searches/:pk/`,
handleErrorMsg: (HTTPCode) => mapHTTPErrorCodes('user saved searches', HTTPCode),
},
projects: {
path: `${window.location.origin}/api/v1/projects/`,
path: `api/v1/projects/`,
handleErrorMsg: (HTTPCode) => mapHTTPErrorCodes('MetaGrid projects API', HTTPCode),
},
// ESGF Search API
esgfSearch: {
path: `${window.location.origin}/proxy/search`,
path: `proxy/search`,
handleErrorMsg: (HTTPCode) => mapHTTPErrorCodes('ESGF Search API', HTTPCode),
},
// ESGF Citation API (uses dummy path 'citation_url' for testing since the
// URL is included in each Search API dataset result)
citation: {
path: `${window.location.origin}/proxy/citation`,
path: `proxy/citation`,
handleErrorMsg: (HTTPCode) => mapHTTPErrorCodes('ESGF Citation API', HTTPCode),
},
// ESGF wget API
wget: {
path: `${window.location.origin}/proxy/wget`,
path: `proxy/wget`,
handleErrorMsg: (HTTPCode) => mapHTTPErrorCodes('ESGF wget API', HTTPCode),
},
// ESGF Node Status API
nodeStatus: {
path: `${window.location.origin}/proxy/status`,
path: `proxy/status`,
handleErrorMsg: (HTTPCode) => mapHTTPErrorCodes('ESGF Node Status API', HTTPCode),
},
tempStorageGet: {
path: `${window.location.origin}/tempStorage/get`,
path: `tempStorage/get`,
handleErrorMsg: (HTTPCode) => mapHTTPErrorCodes('Temp Storage Get', HTTPCode),
},
tempStorageSet: {
path: `${window.location.origin}/tempStorage/set`,
path: `tempStorage/set`,
handleErrorMsg: (HTTPCode) => mapHTTPErrorCodes('Temp Storage Set', HTTPCode),
},
frontendConfig: {
path: `${window.location.origin}/frontend-config.js`,
path: `frontend-config.js`,
handleErrorMsg: (HTTPCode) => mapHTTPErrorCodes('Frontend Config', HTTPCode),
},
introMarkdown: {
path: `${window.location.origin}/messages/metagrid_messages.md`,
path: `messages/metagrid_messages.md`,
handleErrorMsg: (HTTPCode) => mapHTTPErrorCodes('Introduction Markdown', HTTPCode),
},
};
Expand Down
1 change: 1 addition & 0 deletions frontend/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum AppPage {
}

export type FrontendConfig = {
HOST_SUBPATH: string;
// General
AUTHENTICATION_METHOD: 'keycloak' | 'globus';
GOOGLE_ANALYTICS_TRACKING_ID: string | null;
Expand Down
1 change: 0 additions & 1 deletion frontend/src/components/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,6 @@ const App: React.FC<React.PropsWithChildren<Props>> = ({ searchQuery }) => {
<Routes>
<Route path="/" element={<Navigate to="/search" />} />
<Route path="/cart" element={<Navigate to="/cart/items" />} />
<></>
<Route
path="/search/*"
element={
Expand Down
18 changes: 9 additions & 9 deletions frontend/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ import { FrontendConfig } from './common/types';
const container = document.getElementById('root');
const root = createRoot(container!);

const appRouter: React.ReactNode = (
<BrowserRouter>
<ReactJoyrideProvider>
<App searchQuery={getSearchFromUrl()} />
</ReactJoyrideProvider>
</BrowserRouter>
);

fetch('/frontend-config.js')
fetch('./frontend-config.js')
.then((response) => response.json() as Promise<FrontendConfig>)
.then((response) => {
window.METAGRID = response;

const appRouter: React.ReactNode = (
<BrowserRouter basename={window.METAGRID.HOST_SUBPATH}>
<ReactJoyrideProvider>
<App searchQuery={getSearchFromUrl()} />
</ReactJoyrideProvider>
</BrowserRouter>
);

if (window.METAGRID.GOOGLE_ANALYTICS_TRACKING_ID != null) {
// Setup Google Analytics
ReactGA.initialize(window.METAGRID.GOOGLE_ANALYTICS_TRACKING_ID);
Expand Down
1 change: 0 additions & 1 deletion frontend/src/lib/axios/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import httpAdapter from 'axios/lib/adapters/http';

export default axios.create({
adapter: httpAdapter as AxiosAdapter,
baseURL: window.location.origin,
// When a "cross-origin" request happens, the "Origin" request header is set by the browser.
// However, in this case, the browser does not because the initial request is "same-origin".
// https://github.com/Rob--W/cors-anywhere/issues/7#issuecomment-354740387
Expand Down
1 change: 1 addition & 0 deletions frontend/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default defineConfig(() => {
strictPort: true,
watch: true,
},
base: process.env.HOST_SUBPATH || '/',
build: {
outDir: 'build',
},
Expand Down
60 changes: 36 additions & 24 deletions traefik/traefik.local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,49 +16,53 @@ http:
service: django
rule: >-
Host(`{{ env "DOMAIN_NAME" }}`) && (
PathPrefix(`/account-confirm-email`) ||
PathPrefix(`/accounts`) ||
PathPrefix(`/api`) ||
PathPrefix(`/complete`) ||
PathPrefix(`/frontend-config.js`) ||
PathPrefix(`/globus`) ||
PathPrefix(`/login`) ||
PathPrefix(`/proxy`) ||
PathPrefix(`/redoc`) ||
PathPrefix(`/swagger`) ||
PathPrefix(`/tempStorage`) )
PathPrefix(`{{ env "HOST_SUBPATH" }}/account-confirm-email`) ||
PathPrefix(`{{ env "HOST_SUBPATH" }}/accounts`) ||
PathPrefix(`{{ env "HOST_SUBPATH" }}/api`) ||
PathPrefix(`{{ env "HOST_SUBPATH" }}/complete`) ||
PathPrefix(`{{ env "HOST_SUBPATH" }}/frontend-config.js`) ||
PathPrefix(`{{ env "HOST_SUBPATH" }}/globus`) ||
PathPrefix(`{{ env "HOST_SUBPATH" }}/login`) ||
PathPrefix(`{{ env "HOST_SUBPATH" }}/proxy`) ||
PathPrefix(`{{ env "HOST_SUBPATH" }}/redoc`) ||
PathPrefix(`{{ env "HOST_SUBPATH" }}/swagger`) ||
PathPrefix(`{{ env "HOST_SUBPATH" }}/tempStorage`) )
entryPoints:
- web
middlewares:
- subpathHeader

backend-secure:
service: django
rule: >-
Host(`{{ env "DOMAIN_NAME" }}`) && (
PathPrefix(`/account-confirm-email`) ||
PathPrefix(`/accounts`) ||
PathPrefix(`/api`) ||
PathPrefix(`/complete`) ||
PathPrefix(`/frontend-config.js`) ||
PathPrefix(`/globus`) ||
PathPrefix(`/login`) ||
PathPrefix(`/proxy`) ||
PathPrefix(`/redoc`) ||
PathPrefix(`/swagger`) ||
PathPrefix(`/tempStorage`) )
PathPrefix(`{{ env "HOST_SUBPATH" }}/account-confirm-email`) ||
PathPrefix(`{{ env "HOST_SUBPATH" }}/accounts`) ||
PathPrefix(`{{ env "HOST_SUBPATH" }}/api`) ||
PathPrefix(`{{ env "HOST_SUBPATH" }}/complete`) ||
PathPrefix(`{{ env "HOST_SUBPATH" }}/frontend-config.js`) ||
PathPrefix(`{{ env "HOST_SUBPATH" }}/globus`) ||
PathPrefix(`{{ env "HOST_SUBPATH" }}/login`) ||
PathPrefix(`{{ env "HOST_SUBPATH" }}/proxy`) ||
PathPrefix(`{{ env "HOST_SUBPATH" }}/redoc`) ||
PathPrefix(`{{ env "HOST_SUBPATH" }}/swagger`) ||
PathPrefix(`{{ env "HOST_SUBPATH" }}/tempStorage`) )
entryPoints:
- web-secure
middlewares:
- subpathHeader

frontend:
service: react
rule: >-
Host(`{{ env "DOMAIN_NAME" }}`) && PathPrefix(`/`)
Host(`{{ env "DOMAIN_NAME" }}`) && PathPrefix(`{{ env "HOST_SUBPATH" }}/`)
entryPoints:
- web

frontend-secure:
service: react
rule: >-
Host(`{{ env "DOMAIN_NAME" }}`) && PathPrefix(`/`)
Host(`{{ env "DOMAIN_NAME" }}`) && PathPrefix(`{{ env "HOST_SUBPATH" }}/`)
entryPoints:
- web-secure
services:
Expand All @@ -70,6 +74,14 @@ http:
loadBalancer:
servers:
- url: http://react:8080
middlewares:
subpathHeader:
# Add a custom middleware to add the 'X-Forwarded-Prefix' header to the request
# This header is used by Django's middleware to determine the request's full URL
# and extract the subpath
headers:
customRequestHeaders:
SCRIPT_NAME: '{{ env "HOST_SUBPATH" }}'

providers:
# https://docs.traefik.io/master/providers/file/
Expand Down
Loading
Loading