diff --git a/devenv/frontend-service/local-cloud-shim/README.md b/devenv/frontend-service/local-cloud-shim/README.md new file mode 100644 index 00000000000..e1d9fe0b697 --- /dev/null +++ b/devenv/frontend-service/local-cloud-shim/README.md @@ -0,0 +1,39 @@ +# Local Cloud Shim + +This is a local development shim, that forwards MT-Front-end requests to MT-tilt back-end setup. + +This is still very much work in progress, so things will change a lot. + +``` + ┌──────────────────┐ + │ │ + ┌─────────▶ MT Front-end │ + │ │ │ +┌──────────────────┐ ┌──────────────────┐ │ └──────────────────┘ +│ │ │ │ │ +│ User request │────────▶ Nginx ├─────┼─┐ ┌──────────────────────┐ +│ │ │ │ │ │ │ │ +└──────────────────┘ └──────────────────┘ │ └───────▶ Downstream MT-tilt │ + │ │ │ + │ └──────────────────────┘ + │ + │ ┌──────────────────────┐ + │ │ │ + └─────────▶ Mocked responses │ + │ │ + └──────────────────────┘ +``` + +## Proxying + +Currently, the proxying is done by an Nginx handling the requests in the following way: + +1. Most requests are forwarded to the MT-Front-end +2. All the requests that match this pattern `apis|logout|swagger|openapi/v3|login` are forwarded to the MT-tilt back-end +3. Some requests that are breaking the front-end are mocked. For now `/bootdata` and `/api/user/orgs` are mocked. The mock data are stored under the `./mock-data` folder. + +## Dev setup + +The Tiltfile and the docker compose file, are simplified to avoid spinning up all services, as most are not needed for the shim setup to work. + +The rest of the downstream services are handled by `mt-tilt` setup in Enterprise repo. diff --git a/devenv/frontend-service/local-cloud-shim/Tiltfile b/devenv/frontend-service/local-cloud-shim/Tiltfile new file mode 100644 index 00000000000..1a7f2c2db4b --- /dev/null +++ b/devenv/frontend-service/local-cloud-shim/Tiltfile @@ -0,0 +1,49 @@ +# --- Frontend Build (One-time) +local_resource( + 'frontend-assets', + cmd='yarn install && yarn dev', + dir='../../..', + deps=[ + '../../../public/app', + '../../../packages', + '../../../scripts/webpack', + '../../../tsconfig.json', + '../../../package.json', + '../../../yarn.lock', + ], + labels=["local"] +) + +# --- Backend Build +local_resource( + 'backend-build', + cmd='mkdir -p build && bash ./build-grafana.sh', + dir='..', + deps=[ + '../../../pkg', + '../../../apps', + '../../../kinds', + '../../../kindsv2', + '../../../local', + '../../../scripts', + '../../../conf', + '../../../go.sum', + '../../../go.mod', + ], + allow_parallel=True, + labels=["local"] +) + +# --- Docker Compose +docker_compose('./docker-compose.yaml') + +dc_resource("proxy", + resource_deps=["frontend-service"], + labels=["services"] +) + +dc_resource("frontend-service", + resource_deps=["frontend-assets", "backend-build"], + labels=["services"], +) + diff --git a/devenv/frontend-service/local-cloud-shim/docker-compose.yaml b/devenv/frontend-service/local-cloud-shim/docker-compose.yaml new file mode 100644 index 00000000000..df0cfa94413 --- /dev/null +++ b/devenv/frontend-service/local-cloud-shim/docker-compose.yaml @@ -0,0 +1,47 @@ +name: grafana-fs-dev-shim + +services: + proxy: + image: grafana-proxy + build: + context: ../ + dockerfile: proxy.dockerfile + volumes: + - ../../../public/build:/cdn/public/build + - ../../../public/app/plugins:/cdn/public/app/plugins + - ../../../public/fonts:/cdn/public/fonts + - ./nginx.conf:/etc/nginx/conf.d/default.conf + - ./mock-data:/etc/nginx/mock-data:ro + ports: + - "12345:80" # Gateway + - "3010:81" # CDN + depends_on: + - frontend-service + labels: + - "alloy.logs=true" + + frontend-service: + image: grafana-fs-dev + build: + context: ../../.. + dockerfile: devenv/frontend-service/grafana-fs-dev.dockerfile + entrypoint: ["bin/grafana", "server", "target"] + volumes: + - ../configs/frontend-service.local.ini:/grafana/conf/custom.ini + # Port 3000 is only accessible via the proxy, not directly exposed + labels: + - "alloy.logs=true" + environment: + OTEL_BSP_SCHEDULE_DELAY: 500 + GF_DATABASE_SKIP_MIGRATIONS: true + GF_DATABASE_ENSURE_DEFAULT_ORG_AND_USER: false + GF_DEFAULT_APP_MODE: development + GF_DEFAULT_TARGET: frontend-server + GF_SECURITY_CONTENT_SECURITY_POLICY: false + GF_FEATURE_TOGGLES_ENABLE: enableNativeHTTPHistogram + GF_SERVER_CDN_URL: http://localhost:3010 + GF_SERVER_ROUTER_LOGGING: true + GF_LOG_LEVEL: info + OTEL_SERVICE_NAME: frontend-service + GF_TRACING_OPENTELEMETRY_OTLP_ADDRESS: "alloy:4317" + GF_TRACING_OPENTELEMETRY_OTLP_PROPAGATION: jaeger,w3c diff --git a/devenv/frontend-service/local-cloud-shim/mock-data/bootdata.json b/devenv/frontend-service/local-cloud-shim/mock-data/bootdata.json new file mode 100644 index 00000000000..9fe61ac82e8 --- /dev/null +++ b/devenv/frontend-service/local-cloud-shim/mock-data/bootdata.json @@ -0,0 +1,138 @@ +{ + "user": { + "isSignedIn": true, + "id": 0, + "uid": "", + "login": "", + "email": "", + "name": "", + "theme": "dark", + "lightTheme": false, + "orgCount": 1, + "orgId": 1, + "orgName": "Main Org.", + "orgRole": "Viewer", + "isGrafanaAdmin": false, + "gravatarUrl": "", + "timezone": "browser", + "weekStart": "browser", + "locale": "en-US", + "regionalFormat": "", + "language": "en-US", + "helpFlags1": 0, + "hasEditPermissionInFolders": false, + "authenticatedBy": "", + "permissions": {}, + "analytics": { + "identifier": "" + } + }, + "settings": { + "defaultDatasource": "-- Grafana --", + "datasources": {}, + "minRefreshInterval": "5s", + "panels": {}, + "apps": {}, + "appUrl": "http://localhost:12345/", + "appSubUrl": "", + "allowOrgCreate": false, + "authProxyEnabled": false, + "ldapEnabled": false, + "jwtHeaderName": "", + "jwtUrlLogin": false, + "liveEnabled": true, + "autoAssignOrg": true, + "verifyEmailEnabled": false, + "sigV4AuthEnabled": false, + "azureAuthEnabled": false, + "rbacEnabled": true, + "exploreEnabled": true, + "helpEnabled": true, + "profileEnabled": true, + "newsFeedEnabled": true, + "queryHistoryEnabled": true, + "googleAnalyticsId": "", + "googleAnalytics4Id": "", + "rudderstackWriteKey": "", + "rudderstackDataPlaneUrl": "", + "feedbackLinksEnabled": true, + "disableLoginForm": false, + "disableUserSignUp": true, + "loginHint": "", + "passwordHint": "", + "viewersCanEdit": false, + "disableSanitizeHtml": false, + "auth": { + "disableLogin": false + }, + "buildInfo": { + "hideVersion": false, + "version": "12.4.0-dev", + "commit": "dev", + "commitShort": "dev", + "buildstamp": 0, + "edition": "Open Source", + "latestVersion": "", + "hasUpdate": false, + "env": "development" + }, + "licenseInfo": { + "expiry": 0, + "stateInfo": "", + "licenseUrl": "", + "edition": "Open Source" + }, + "featureToggles": {}, + "anonymousEnabled": false, + "rendererAvailable": false, + "http2Enabled": false, + "pluginCatalogURL": "https://grafana.com/grafana/plugins/", + "pluginAdminEnabled": true, + "pluginAdminExternalManageEnabled": false, + "pluginCatalogHiddenPlugins": [], + "expressionsEnabled": true, + "awsAllowedAuthProviders": ["keys"], + "awsAssumeRoleEnabled": false, + "azure": { + "cloud": "AzureCloud" + }, + "caching": { + "enabled": false + }, + "unifiedAlertingEnabled": true, + "unifiedAlerting": { + "minInterval": "10s" + }, + "oauth": {}, + "samlEnabled": false, + "samlName": "", + "dateFormats": { + "fullDate": "YYYY-MM-DD HH:mm:ss", + "useBrowserLocale": false, + "interval": { + "second": "HH:mm:ss", + "minute": "HH:mm", + "hour": "MM/DD HH:mm", + "day": "MM/DD", + "month": "YYYY-MM", + "year": "YYYY" + }, + "defaultTimezone": "browser", + "defaultWeekStart": "browser" + }, + "licensing": { + "usageBilling": { + "enabled": false + } + } + }, + "navTree": [ + { + "id": "home", + "text": "Home", + "icon": "home-alt", + "url": "/", + "sortWeight": -4000 + } + ] +} diff --git a/devenv/frontend-service/local-cloud-shim/mock-data/user-orgs.json b/devenv/frontend-service/local-cloud-shim/mock-data/user-orgs.json new file mode 100644 index 00000000000..f1b23a54c25 --- /dev/null +++ b/devenv/frontend-service/local-cloud-shim/mock-data/user-orgs.json @@ -0,0 +1,7 @@ +[ + { + "orgId": 1, + "name": "Main Org.", + "role": "Admin" + } +] diff --git a/devenv/frontend-service/local-cloud-shim/nginx.conf b/devenv/frontend-service/local-cloud-shim/nginx.conf new file mode 100644 index 00000000000..d66d54ef4a7 --- /dev/null +++ b/devenv/frontend-service/local-cloud-shim/nginx.conf @@ -0,0 +1,162 @@ +otel_exporter { + endpoint alloy:4317; +} + +otel_service_name "proxy"; + +### +# Instance +### +upstream backend { + server host.docker.internal:3001; +} + +upstream frontend { + server frontend-service:3000; +} + +map "$request_method:$cookie_fs_unavailable" $reject_login { + default 0; + "POST:1" 1; +} + +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 80; + server_name _; + + otel_trace on; + otel_trace_context propagate; + otel_span_name "$request_method Request"; + + location ~ ^/-/down/?$ { + add_header Set-Cookie "fs_unavailable=true; Max-Age=60; Path=/; HttpOnly" always; + return 302 $scheme://$http_host/; + } + + location ~ ^/-/down/(?\d+)/?$ { + add_header Set-Cookie "fs_unavailable=true; Max-Age=$age; Path=/; HttpOnly" always; + return 302 $scheme://$http_host/; + } + + location ~ ^/-/up/?$ { + return 302 $scheme://$http_host/-/down/0; + } + + # Mock bootdata endpoints (both /bootdata and /api/frontend/settings) + location = /bootdata { + default_type application/json; + add_header Access-Control-Allow-Origin "*" always; + alias /etc/nginx/mock-data/bootdata.json; + } + + # Mock user organizations endpoint + location = /api/user/orgs { + default_type application/json; + add_header Access-Control-Allow-Origin "*" always; + alias /etc/nginx/mock-data/user-orgs.json; + } + + + # Special‐case POST /login to backend, GET to frontend + location = /login { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + if ($reject_login) { + add_header Content-Type application/json always; + return 503 '{"code":"Loading", "message": "Soon!"}'; + } + + if ($request_method = POST) { + proxy_pass http://backend; + break; + } + + if ($request_method = GET) { + proxy_pass http://frontend; + break; + } + + return 405; + } + + # API calls go to the backend + # location ~ ^/(api|apis|avatar|bootdata|render|logout|public/plugins|public/openapi3|public/api-merged|goto|swagger|openapi/v3) { + # For test only allow logout, apis, and login + location ~ ^/(apis|logout|swagger|openapi/v3|login) { + # Add debug headers to the response + add_header Nginx-Trace-Id $otel_trace_id always; + add_header Nginx-Route "backend" always; + + if ($cookie_fs_unavailable) { + add_header Content-Type application/json always; + return 503 '{"code":"Loading", "message": "Soon!"}'; + } + + proxy_pass http://backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Everything else to the frontend + location / { + # Add debug headers to the response + add_header Nginx-Trace-Id $otel_trace_id always; + add_header Nginx-Route "frontend" always; + + proxy_pass http://frontend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + + +### +# CDN +### +map $request $loggable { + default 1; + "~^\x16\x03" 0; +} + +server { + listen 81; + server_name localhost; + root /cdn; + + # Enable directory listing + autoindex on; + autoindex_exact_size off; + autoindex_format html; + autoindex_localtime on; + + add_header Cache-Control "no-cache, no-store, must-revalidate" always; + add_header Pragma "no-cache" always; + add_header Expires "0" always; + add_header Access-Control-Allow-Origin "*" always; + add_header Access-Control-Allow-Headers "X-Grafana-Device-Id" always; + + # Suppress access log for malformed HTTPS requests (those starting with a TLS handshake) + access_log /var/log/nginx/access.log main if=$loggable; + + # This serves paths like /grafana/12.1.0-88106/public/build/foo + location ~ ^/grafana[^/]*/[^/]+/(.*)$ { + alias /cdn/$1; + } +} +