Skip to content
Merged
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
18 changes: 18 additions & 0 deletions .cursorignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info

# Virtual environments
.venv

# Django
*.sqlite3
config.toml

# Vite local .env files
.env
.env.*
6 changes: 1 addition & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,7 @@ jobs:
--health-retries 5

env:
DJANGO_PG_DB: firetower_test
DJANGO_PG_USER: postgres
DJANGO_PG_PASS: postgres
DJANGO_PG_HOST: localhost
DJANGO_PG_PORT: 5432
CONFIG_FILE_PATH: ../config.ci.toml

steps:
- name: Checkout code
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ wheels/

# Django
*.sqlite3
config.toml

# Vite local .env files
.env
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ More docs to come eventually :')
## Starting the dev database

```sh
cp config.example.toml config.toml
# Edit config.toml to contain the values you need
docker compose -f docker-compose.db.yml up -d
uv run manage.py migrate
```
Expand Down
25 changes: 25 additions & 0 deletions config.ci.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
project_key = "INC"
django_secret_key = "django-insecure-gmj)qc*_dk&^i1=z7oy(ew7%5*fz^yowp8=4=0882_d=i3hl69"
sentry_dsn = ""

# This is the actual essential part. Values match the container set up by GHA
[postgres]
db = "firetower_test"
host = "localhost"
user = "postgres"
password = "postgres"

[jira]
domain = ""
account = ""
api_key = ""
severity_field = ""

[slack]
bot_token = ""
team_id = ""
participant_sync_throttle_seconds = 300

[auth]
iap_enabled = false
iap_audience = ""
28 changes: 28 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
project_key = "INC"
django_secret_key = "django-insecure-gmj)qc*_dk&^i1=z7oy(ew7%5*fz^yowp8=4=0882_d=i3hl69"
sentry_dsn = "https://your-sentry-dsn@o1.ingest.us.sentry.io/project-id"

[postgres]
db = "firetower"
host = "localhost"
user = "postgres"
password = "dummy_dev_password"

[jira]
domain = "https://<jira-domain>.atlassian.net"
account = ""
api_key = ""
severity_field = "customfield_11023"

[slack]
bot_token = ""
team_id = "<slack-team-id>"
participant_sync_throttle_seconds = 300

[auth]
iap_enabled = false
iap_audience = ""

[datadog]
api_key = ""
app_key = ""
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dependencies = [
"google-auth>=2.37.0",
"jira>=3.5.0",
"psycopg[binary]>=3.2.11",
"pyserde[toml]>=0.28.0",
"slack-sdk>=3.31.0",
]

Expand Down
124 changes: 124 additions & 0 deletions src/firetower/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""
Configuration file loader for Firetower.

Loads configuration values from a TOML file and validates that all required keys are present.
"""

from pathlib import Path
from typing import Any

from serde import deserialize, from_dict
from serde.toml import from_toml


@deserialize
class PostgresConfig:
db: str
host: str
user: str
password: str


@deserialize
class DatadogConfig:
api_key: str
app_key: str


@deserialize
class JIRAConfig:
domain: str
account: str
api_key: str
severity_field: str


@deserialize
class SlackConfig:
bot_token: str
team_id: str
participant_sync_throttle_seconds: int


@deserialize
class AuthConfig:
iap_enabled: bool
iap_audience: str | None


@deserialize
class ConfigFile:
"""
Load string configuration values from a TOML file.
"""

postgres: PostgresConfig
datadog: DatadogConfig | None
jira: JIRAConfig
slack: SlackConfig
auth: AuthConfig

project_key: str
django_secret_key: str
sentry_dsn: str

@classmethod
def from_file(cls, file_path: str | Path) -> "ConfigFile":
"""
Load configuration from a TOML file.

Args:
file_path: Path to the TOML configuration file.

Returns:
ConfigFile instance with loaded configuration.
"""
with open(file_path) as f:
data: ConfigFile = from_toml(ConfigFile, f.read())
return data

@classmethod
def from_dict(cls, data: dict[str, Any]) -> "ConfigFile":
"""
Load configuration from a dictionary.

Args:
data: Dictionary containing configuration key-value pairs.

Returns:
ConfigFile instance with loaded configuration.
"""
return from_dict(ConfigFile, data)


class DummyConfigFile(ConfigFile):
"""
A dummy configuration file for use when running collectstatic.
"""

def __init__(self) -> None:
self.postgres = PostgresConfig(
db="firetower",
host="localhost",
user="postgres",
password="dummy_dev_password",
)
self.jira = JIRAConfig(
domain="",
account="",
api_key="",
severity_field="",
)
self.slack = SlackConfig(
bot_token="",
team_id="",
participant_sync_throttle_seconds=0,
)
self.auth = AuthConfig(
iap_enabled=False,
iap_audience="",
)
self.datadog = None
self.project_key = ""
self.django_secret_key = ""
self.sentry_dsn = ""
87 changes: 46 additions & 41 deletions src/firetower/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,50 +11,58 @@
"""

import os
import sys
from pathlib import Path

from datadog import initialize, statsd
from datadog import initialize
from datadog.dogstatsd.base import statsd

from firetower.config import ConfigFile, DummyConfigFile


def env_is_dev() -> bool:
return os.environ.get("DJANGO_ENV", "dev") == "dev"


def dev_default(key: str, default: str = "") -> str:
value = os.environ.get(key)
if value is not None and value != "":
return value
if env_is_dev():
return default
raise Exception(
f"ERROR: Environment variable {key} must be set when not in the dev environment!"
)
def cmd_needs_dummy_config() -> bool:
cmds = ["collectstatic", "mypy"]
for arg in sys.argv:
for cmd in cmds:
if cmd in arg:
return True
return False


# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent

# Load configuration from TOML file
# Set CONFIG_FILE_PATH environment variable to override default location
# Hack: There are a few things that load settings.py where we don't expect to have a working config.toml.
CONFIG_FILE_PATH = os.environ.get("CONFIG_FILE_PATH", BASE_DIR.parent / "config.toml")
config: ConfigFile = (
DummyConfigFile()
if cmd_needs_dummy_config()
else ConfigFile.from_file(CONFIG_FILE_PATH)
)


if not env_is_dev():
import sentry_sdk

sentry_sdk.init(
dsn="https://ef9a24c7ef0f1a8ba7e8f821d6ab1dd9@o1.ingest.us.sentry.io/4510076289548288",
dsn=config.sentry_dsn,
send_default_pii=False,
environment=os.environ.get("DJANGO_ENV", "dev"),
environment=os.environ.get("DJANGO_ENV", "unknown"),
)

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent

# Global project settings
PROJECT_KEY = os.environ.get("PROJECT_KEY", "INC")
PROJECT_KEY = config.project_key

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = dev_default(
"DJANGO_SECRET_KEY",
"django-insecure-gmj)qc*_dk&^i1=z7oy(ew7%5*fz^yowp8=4=0882_d=i3hl69",
)

SECRET_KEY = config.django_secret_key

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env_is_dev()
Expand Down Expand Up @@ -138,10 +146,10 @@ def dev_default(key: str, default: str = "") -> str:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": dev_default("DJANGO_PG_DB", "firetower"),
"HOST": dev_default("DJANGO_PG_HOST", "localhost"),
"USER": dev_default("DJANGO_PG_USER", "postgres"),
"PASSWORD": dev_default("DJANGO_PG_PASS", "dummy_dev_password"),
"NAME": config.postgres.db,
"HOST": config.postgres.host,
"USER": config.postgres.user,
"PASSWORD": config.postgres.password,
},
}

Expand Down Expand Up @@ -189,23 +197,20 @@ def dev_default(key: str, default: str = "") -> str:
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

# Jira Integration Configuration
# Defaults to test environment setup (matching opsbot test config)
JIRA = {
"DOMAIN": os.environ.get("JIRA_DOMAIN", "https://getsentry.atlassian.net"),
"ACCOUNT": dev_default("JIRA_ACCOUNT"),
"API_KEY": dev_default("JIRA_API_KEY"),
"SEVERITY_FIELD": dev_default("JIRA_SEVERITY_FIELD", "customfield_11023"),
"DOMAIN": config.jira.domain,
"ACCOUNT": config.jira.account,
"API_KEY": config.jira.api_key,
"SEVERITY_FIELD": config.jira.severity_field,
}

# Slack Integration Configuration
SLACK = {
"BOT_TOKEN": dev_default("SLACK_BOT_TOKEN"),
"TEAM_ID": os.environ.get("SLACK_TEAM_ID", "sentry"),
"BOT_TOKEN": config.slack.bot_token,
"TEAM_ID": config.slack.team_id,
}

PARTICIPANT_SYNC_THROTTLE_SECONDS = int(
os.environ.get("PARTICIPANT_SYNC_THROTTLE_SECONDS", "300")
)
PARTICIPANT_SYNC_THROTTLE_SECONDS = int(config.slack.participant_sync_throttle_seconds)

# Django REST Framework Configuration
REST_FRAMEWORK = {
Expand All @@ -229,14 +234,14 @@ def dev_default(key: str, default: str = "") -> str:
}

# Google IAP Authentication Configuration
IAP_ENABLED = not env_is_dev()
IAP_AUDIENCE = dev_default("IAP_AUDIENCE")
IAP_ENABLED = config.auth.iap_enabled
IAP_AUDIENCE = config.auth.iap_audience

# Validate IAP settings in production
if IAP_ENABLED and not IAP_AUDIENCE:
raise Exception(
"IAP_AUDIENCE must be set when IAP is enabled in production. "
"Set the IAP_AUDIENCE environment variable."
"Set the iap_audience value in the configuration file."
)

# django-k8s proreadiness probes
Expand All @@ -248,8 +253,8 @@ def dev_default(key: str, default: str = "") -> str:
initialize(
statsd_host=os.environ.get("DATADOG_STATSD_HOST", "localhost"),
statsd_port=int(os.environ.get("DATADOG_STATSD_PORT", "8125")),
api_key=os.environ.get("DD_API_KEY"),
app_key=os.environ.get("DD_APP_KEY"),
api_key=config.datadog.api_key if config.datadog else None,
app_key=config.datadog.app_key if config.datadog else None,
statsd_namespace="firetower",
)
statsd.constant_tags = [
Expand Down
Loading
Loading