Petrolyst server infrastructure: Traefik, Trino and Superset
Traefik as the server-wide edge (TLS via Let's Encrypt, dashboard on traefik.petrolyst.com behind oauth2-proxy and Keycloak). The fuel-monitoring stack: Superset on atoma.petrolyst.com with Keycloak login and Petrolyst branding, Trino reading the TimescaleDB fuel-db as catalog "fuel". Customization lives in petrolyst-insights/, deployment in the other folders. Deploys run server-side (deploy/deploy.sh) from a Gitea workflow or manually; releases are immutable, Superset metadata is backed up before migrations, and deploy/rollback.sh restores it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
24327f9462
commit
d3800ac9dd
34 files changed
+2117
No files matched your search
@@ -0,0 +1,252 @@
|
||||
"""Superset configuration for Petrolyst (ATOMA fuel monitoring).
|
||||
|
||||
Customization only, and no secrets: those are read from /run/secrets/<name>,
|
||||
which stacks/fuel-monitoring/compose.yaml mounts from /opt/petrolyst/secrets/.
|
||||
Baked into the image at /app/pythonpath/, which is on PYTHONPATH in
|
||||
apache/superset:6.1.0, so Superset loads it without SUPERSET_CONFIG_PATH.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
from copy import deepcopy
|
||||
|
||||
from flask_appbuilder.security.manager import AUTH_OAUTH
|
||||
from sqlalchemy.engine.url import URL
|
||||
|
||||
from petrolyst_security import PetrolystSecurityManager
|
||||
from superset import config as superset_defaults
|
||||
|
||||
|
||||
def _read_secret(name: str, env_var: str | None = None) -> str:
|
||||
"""Prefer /run/secrets/<name>; fall back to an env var for local dev.
|
||||
|
||||
Uses rstrip('\r\n') (not strip()) so passwords or keys that legitimately
|
||||
contain leading/trailing spaces are not silently mutated. Raises on empty
|
||||
values so boot fails loudly instead of accepting a blank secret.
|
||||
"""
|
||||
path = pathlib.Path(f"/run/secrets/{name}")
|
||||
if path.is_file():
|
||||
value = path.read_text().rstrip("\r\n")
|
||||
else:
|
||||
value = os.environ[env_var or name.upper()]
|
||||
if not value:
|
||||
raise RuntimeError(f"secret {name!r} must not be empty")
|
||||
return value
|
||||
|
||||
|
||||
SECRET_KEY = _read_secret("superset_secret_key", "SUPERSET_SECRET_KEY")
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = URL.create(
|
||||
"postgresql+psycopg2",
|
||||
username="superset",
|
||||
password=_read_secret("superset_db_password", "SUPERSET_DB_PASSWORD"),
|
||||
host=os.environ.get("SUPERSET_DB_HOST", "superset-db"),
|
||||
port=5432,
|
||||
database="superset",
|
||||
).render_as_string(hide_password=False)
|
||||
|
||||
# --- Sign-in through Keycloak (client "petrolyst") ----------------------------
|
||||
# Users and roles are managed in Keycloak; there is no local admin account.
|
||||
# Break-glass access: `superset fab ...` inside the container on the server.
|
||||
KEYCLOAK_REALM_URL = os.environ.get(
|
||||
"KEYCLOAK_REALM_URL", "https://iam.thedigitalbridge.io/realms/thedigitalbridge"
|
||||
).rstrip("/")
|
||||
|
||||
AUTH_TYPE = AUTH_OAUTH
|
||||
CUSTOM_SECURITY_MANAGER = PetrolystSecurityManager
|
||||
OAUTH_PROVIDERS = [
|
||||
{
|
||||
"name": "keycloak",
|
||||
"icon": "fa-key",
|
||||
"token_key": "access_token",
|
||||
"remote_app": {
|
||||
"client_id": os.environ.get("KEYCLOAK_CLIENT_ID", "petrolyst"),
|
||||
"client_secret": _read_secret(
|
||||
"keycloak_petrolyst_client_secret", "KEYCLOAK_CLIENT_SECRET"
|
||||
),
|
||||
"server_metadata_url": f"{KEYCLOAK_REALM_URL}/.well-known/openid-configuration",
|
||||
"api_base_url": f"{KEYCLOAK_REALM_URL}/protocol/openid-connect/",
|
||||
"client_kwargs": {
|
||||
"scope": "openid email profile",
|
||||
"code_challenge_method": "S256",
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
# Keys are Keycloak client roles of "petrolyst" (claim petrolyst_roles).
|
||||
AUTH_ROLES_MAPPING = {
|
||||
"superset-admin": ["Admin"],
|
||||
"superset-editor": ["Alpha"],
|
||||
"superset-viewer": ["Gamma"],
|
||||
}
|
||||
AUTH_ROLES_SYNC_AT_LOGIN = True
|
||||
AUTH_USER_REGISTRATION = True
|
||||
# Public has no permissions: a Keycloak user without a Superset role signs in
|
||||
# and sees nothing.
|
||||
AUTH_USER_REGISTRATION_ROLE = "Public"
|
||||
|
||||
# Viewers (Gamma) see a dashboard once it lists their role.
|
||||
FEATURE_FLAGS = {"DASHBOARD_RBAC": True}
|
||||
|
||||
# --- Behind Traefik -----------------------------------------------------------
|
||||
ENABLE_PROXY_FIX = True
|
||||
PREFERRED_URL_SCHEME = "https"
|
||||
SESSION_COOKIE_SECURE = True
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
SESSION_COOKIE_SAMESITE = "Lax"
|
||||
TALISMAN_CONFIG = deepcopy(superset_defaults.TALISMAN_CONFIG)
|
||||
TALISMAN_CONFIG["session_cookie_secure"] = True
|
||||
|
||||
# --- Redis: caches and rate limits (no Celery, so no results backend) ---------
|
||||
REDIS_URL = os.environ.get("REDIS_URL", "redis://redis:6379")
|
||||
|
||||
CACHE_CONFIG = {
|
||||
"CACHE_TYPE": "RedisCache",
|
||||
"CACHE_DEFAULT_TIMEOUT": 300,
|
||||
"CACHE_KEY_PREFIX": "superset_cache_",
|
||||
"CACHE_REDIS_URL": f"{REDIS_URL}/0",
|
||||
}
|
||||
DATA_CACHE_CONFIG = {
|
||||
"CACHE_TYPE": "RedisCache",
|
||||
"CACHE_DEFAULT_TIMEOUT": 3600,
|
||||
"CACHE_KEY_PREFIX": "superset_data_",
|
||||
"CACHE_REDIS_URL": f"{REDIS_URL}/0",
|
||||
}
|
||||
FILTER_STATE_CACHE_CONFIG = {
|
||||
"CACHE_TYPE": "RedisCache",
|
||||
"CACHE_DEFAULT_TIMEOUT": 86400,
|
||||
"CACHE_KEY_PREFIX": "superset_filter_",
|
||||
"CACHE_REDIS_URL": f"{REDIS_URL}/0",
|
||||
}
|
||||
EXPLORE_FORM_DATA_CACHE_CONFIG = {
|
||||
"CACHE_TYPE": "RedisCache",
|
||||
"CACHE_DEFAULT_TIMEOUT": 86400,
|
||||
"CACHE_KEY_PREFIX": "superset_explore_",
|
||||
"CACHE_REDIS_URL": f"{REDIS_URL}/0",
|
||||
}
|
||||
RATELIMIT_STORAGE_URI = f"{REDIS_URL}/1"
|
||||
|
||||
# --- Petrolyst look and feel --------------------------------------------------
|
||||
# Values from petrolyst-ui/design/brand/07_UI_Tokens/petrolyst_tokens.css.
|
||||
# Assets live in assets/branding/ and are served from /static/assets/branding/.
|
||||
BRANDING = "/static/assets/branding"
|
||||
|
||||
APP_NAME = "Petrolyst"
|
||||
APP_ICON = f"{BRANDING}/petrolyst_logo_standard_fullcolor_no_tagline.svg"
|
||||
FAVICONS = [
|
||||
{"href": f"{BRANDING}/favicon.svg", "type": "image/svg+xml"},
|
||||
{"href": f"{BRANDING}/favicon.ico", "type": "image/x-icon"},
|
||||
{"href": f"{BRANDING}/petrolyst_icon_180.png", "rel": "apple-touch-icon"},
|
||||
]
|
||||
|
||||
THEME_DEFAULT = deepcopy(superset_defaults.THEME_DEFAULT)
|
||||
THEME_DEFAULT["token"].update(
|
||||
{
|
||||
"brandAppName": APP_NAME,
|
||||
"brandLogoAlt": "Petrolyst",
|
||||
"brandLogoUrl": APP_ICON,
|
||||
"brandLogoHref": "/",
|
||||
# The standard logo is stacked (mark over wordmark); 40px keeps the Y
|
||||
# legible and, with the margin, the 60px navbar height.
|
||||
"brandLogoHeight": "40px",
|
||||
"brandLogoMargin": "10px 0",
|
||||
# Action teal, not the raw brand teal #009688: the brand guide keeps
|
||||
# raw teal off small text and controls.
|
||||
"colorPrimary": "#00695C",
|
||||
"colorLink": "#00695C",
|
||||
"colorSuccess": "#0F7B6C",
|
||||
"colorWarning": "#B45309",
|
||||
"colorError": "#B3261E",
|
||||
"colorInfo": "#2563EB",
|
||||
"colorTextBase": "#0B1D33",
|
||||
"colorBgLayout": "#F8FAFC",
|
||||
"colorBorder": "#E2E8F0",
|
||||
"borderRadius": 8,
|
||||
"fontFamily": "Inter, system-ui, sans-serif",
|
||||
}
|
||||
)
|
||||
|
||||
THEME_DARK = deepcopy(THEME_DEFAULT)
|
||||
THEME_DARK["algorithm"] = "dark"
|
||||
THEME_DARK["token"].update(
|
||||
{
|
||||
"brandLogoUrl": f"{BRANDING}/petrolyst_logo_standard_white_no_tagline.svg",
|
||||
"colorPrimary": "#35C5B6",
|
||||
"colorLink": "#35C5B6",
|
||||
"colorSuccess": "#35C5B6",
|
||||
"colorWarning": "#F59E0B",
|
||||
"colorError": "#FF6B6B",
|
||||
"colorInfo": "#60A5FA",
|
||||
"colorTextBase": "#F8FAFC",
|
||||
"colorBgBase": "#071425",
|
||||
"colorBgLayout": "#071425",
|
||||
"colorBgContainer": "#12263D",
|
||||
"colorBorder": "#526D82",
|
||||
"colorEditorSelection": superset_defaults.THEME_DARK["token"][
|
||||
"colorEditorSelection"
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
# Chart colours. Validated with the dataviz skill's validator on both chart
|
||||
# surfaces (#FFFFFF light, #12263D dark): all slots in both lightness bands and
|
||||
# >= 3:1; worst adjacent CVD dE 13.7, normal-vision dE 26.2; the first three
|
||||
# slots also pass all-pairs (scatter, maps). Brand teal leads; brand cyan
|
||||
# #00B6E6 is snapped to #0097C4 because it is too light for the dark surface
|
||||
# and below 3:1 on white. The other hues come from the validated reference set.
|
||||
# Status meaning (warning, critical) never rides on these colours.
|
||||
EXTRA_CATEGORICAL_COLOR_SCHEMES = [
|
||||
{
|
||||
"id": "petrolyst",
|
||||
"description": "Petrolyst categorical palette, brand teal first",
|
||||
"label": "Petrolyst",
|
||||
"isDefault": True,
|
||||
"colors": [
|
||||
"#009688", # brand teal
|
||||
"#d95926", # orange
|
||||
"#9085e9", # violet
|
||||
"#c98500", # amber
|
||||
"#0097C4", # brand cyan, snapped
|
||||
"#e34948", # red
|
||||
"#3987e5", # blue
|
||||
"#d55181", # magenta
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
# One hue, light to dark; the lightest step means "near zero" and may recede.
|
||||
EXTRA_SEQUENTIAL_COLOR_SCHEMES = [
|
||||
{
|
||||
"id": "petrolyst_teal",
|
||||
"description": "Petrolyst teal, light to dark",
|
||||
"label": "Petrolyst Teal",
|
||||
"isDiverging": False,
|
||||
"isDefault": True,
|
||||
"colors": [
|
||||
"#E6F4F2",
|
||||
"#B3DED8",
|
||||
"#7FC8BE",
|
||||
"#4CB2A4",
|
||||
"#009688",
|
||||
"#00695C",
|
||||
"#004D44",
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "petrolyst_navy",
|
||||
"description": "Petrolyst navy, light to dark",
|
||||
"label": "Petrolyst Navy",
|
||||
"isDiverging": False,
|
||||
"isDefault": False,
|
||||
"colors": [
|
||||
"#E8EEF5",
|
||||
"#C3D1E2",
|
||||
"#8FA7C4",
|
||||
"#5C7DA6",
|
||||
"#2F5585",
|
||||
"#183A63",
|
||||
"#0B1D33",
|
||||
],
|
||||
},
|
||||
]
|
||||
Reference in new issue
Block a user