Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
**/agents.md
agents.md
htmlcov/
federation-shared.env
7 changes: 7 additions & 0 deletions federation-shared.example.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Shared by gateway + federation sync (repo root; gateway/scripts/generate-secrets.sh).
# Compose: env_file → ../../federation-shared.env (gateway) or ../federation-shared.env (federation).
# FEDERATION_SYNC_DRF_TOKEN: bootstrap Token for get-federation-sync-api-key.
# FEDERATION_SYNC_SERVER_API_KEY: export Api-Key (sync service writes after mint).

FEDERATION_SYNC_DRF_TOKEN=
FEDERATION_SYNC_SERVER_API_KEY=
18 changes: 18 additions & 0 deletions gateway/.envs/example/django.env
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ HOSTNAME=localhost:8000
API_VERSION=v1
API_KEY=

# FEDERATION
# ------------------------------------------------------------------------------
# Load federation-shared.env from repo root on gateway + sync compose stacks (generate-secrets).
# prepare_gateway runs init_federation_sync_token (DB Token from FEDERATION_SYNC_DRF_TOKEN).
# Sync startup mints FEDERATION_SYNC_SERVER_API_KEY via get-federation-sync-api-key.
# Set FEDERATION_SITE_NAME (e.g. crc) when enabling federation; use SDS_SITE_FQDN for the public host (RFC [site].fqdn).
# Master switch for export APIs and Redis federation events.
# FEDERATION_ENABLED=true
# RFC [site].name (short peer id); set SDS_SITE_FQDN separately for [site].fqdn.
# FEDERATION_SITE_NAME=crc
# Redis pub/sub channel federation-sync subscribes to.
# FEDERATION_EVENTS_CHANNEL=federation:events
# Health probe target (federation-sync service).
# FEDERATION_SYNC_HEALTH_URL=http://federation-sync:8000/sync/health
# FEDERATION_SYNC_USER_EMAIL=federation-sync@internal.local
# Comma-separated CIDRs allowed to call export (default: private Docker ranges).
# FEDERATION_EXPORT_ALLOWED_CIDRS=

Comment thread
klpoland marked this conversation as resolved.
Comment thread
klpoland marked this conversation as resolved.
# AUTH0
# ------------------------------------------------------------------------------
# Set these from your Auth0 application settings
Expand Down
1 change: 1 addition & 0 deletions gateway/compose.local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ services:
- ./.envs/local/storage.env
- ./.envs/local/postgres.env
- ./.envs/local/opensearch.env
- ../federation-shared.env
environment:
- SKIP_POSTGRES_WAIT=1
# remember /entrypoint runs first
Expand Down
2 changes: 2 additions & 0 deletions gateway/config/api_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from sds_gateway.api_methods.views.auth_endpoints import ValidateAuthViewSet
from sds_gateway.api_methods.views.capture_endpoints import CaptureViewSet
from sds_gateway.api_methods.views.dataset_endpoints import DatasetViewSet
from sds_gateway.api_methods.views.federation_endpoints import FederationViewSet
from sds_gateway.api_methods.views.file_endpoints import FileViewSet
from sds_gateway.api_methods.views.file_endpoints import check_contents_exist
from sds_gateway.users.api.views import UserViewSet
Expand All @@ -17,6 +18,7 @@
router.register(r"assets/files", FileViewSet, basename="files")
router.register(r"assets/captures", CaptureViewSet, basename="captures")
router.register(r"assets/datasets", DatasetViewSet, basename="datasets")
router.register(r"federation", FederationViewSet, basename="federation")

if settings.VISUALIZATIONS_ENABLED:
router.register(r"visualizations", VisualizationViewSet, basename="visualizations")
Expand Down
94 changes: 93 additions & 1 deletion gateway/config/settings/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Base settings to build other settings files upon."""
# ruff: noqa: ERA001

import ipaddress
import random
import string
from pathlib import Path
Expand All @@ -9,6 +10,9 @@

from celery.schedules import crontab
from environs import env
from sds_gateway.api_methods.federation.redis_channel import (
resolve_federation_events_channel,
)

from config.settings.logs import ColoredFormatter
from config.settings.utils import guess_admin_console_env
Expand All @@ -24,6 +28,41 @@ def __get_random_token(length: int) -> str:
)


FEDERATION_EXPORT_ALLOWED_CIDRS_DEFAULT: list[str] = [
"127.0.0.1/32",
"::1/128",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
]


def _parse_cidrs(
raw: list[str],
) -> list[ipaddress.IPv4Network | ipaddress.IPv6Network]:
networks: list[ipaddress.IPv4Network | ipaddress.IPv6Network] = []
for item in raw:
text = str(item).strip()
if not text:
continue
networks.append(ipaddress.ip_network(text, strict=False))
return networks


def _federation_export_allowed_cidrs_from_env() -> list[
ipaddress.IPv4Network | ipaddress.IPv6Network
]:
raw = env.list(
"FEDERATION_EXPORT_ALLOWED_CIDRS",
default=FEDERATION_EXPORT_ALLOWED_CIDRS_DEFAULT,
)
tokens = [str(item).strip() for item in raw]
tokens = [token for token in tokens if token]
if not tokens:
tokens = list(FEDERATION_EXPORT_ALLOWED_CIDRS_DEFAULT)
return _parse_cidrs(tokens)


env.read_env()

SITE_DOMAIN: str = env.str("SITE_DOMAIN", default="localhost:8000")
Expand Down Expand Up @@ -610,7 +649,10 @@ def _strip_endpoint_scheme(endpoint_url: str) -> str:
"rest_framework.authentication.SessionAuthentication",
"sds_gateway.api_methods.authentication.APIKeyAuthentication",
),
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
"DEFAULT_PERMISSION_CLASSES": (
"rest_framework.permissions.IsAuthenticated",
"sds_gateway.api_methods.permissions.DisallowFederationSyncKey",
),
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_THROTTLE_RATES": {
"vis_stream": VIS_STREAM_THROTTLE_RATE,
Expand Down Expand Up @@ -709,6 +751,56 @@ def _strip_endpoint_scheme(endpoint_url: str) -> str:
SDS_PROGRAMMATIC_SITE_NAME: str = env.str("SDS_PROGRAMMATIC_SITE_NAME", default="sds")
SDS_SITE_FQDN: str = env.str("SDS_SITE_FQDN", default="localhost")

# Federation peer short name (RFC [site].name, e.g. crc, haystack);
# not SDS_PROGRAMMATIC_SITE_NAME.
FEDERATION_SITE_NAME: str = env.str("FEDERATION_SITE_NAME", default="").strip()
# Master switch: when False, federation export and Redis events are inactive.
FEDERATION_ENABLED: bool = env.bool("FEDERATION_ENABLED", default=False)
_federation_events_channel_override: str = env.str(
"FEDERATION_EVENTS_CHANNEL",
default="",
).strip()
FEDERATION_EVENTS_CHANNEL: str = resolve_federation_events_channel(
site_name=FEDERATION_SITE_NAME,
channel_override=_federation_events_channel_override,
)
FEDERATION_SYNC_USER_EMAIL: str = env.str(
"FEDERATION_SYNC_USER_EMAIL",
default="federation-sync@internal.local",
)
# From federation-shared.env (compose env_file);
# used by init_federation_sync_token only.
FEDERATION_SYNC_DRF_TOKEN: str = env.str("FEDERATION_SYNC_DRF_TOKEN", default="")
FEDERATION_SYNC_HEALTH_URL: str = env.str(
"FEDERATION_SYNC_HEALTH_URL",
default="http://federation-sync:8000/sync/health",
)
FEDERATION_SYNC_HEALTH_PROBE_TIMEOUT: float = env.float(
"FEDERATION_SYNC_HEALTH_PROBE_TIMEOUT",
default=2.0,
)
FEDERATION_SKIP_SYNC_HEALTH_PROBE: bool = env.bool(
"FEDERATION_SKIP_SYNC_HEALTH_PROBE",
default=False,
)
FEDERATION_SKIP_SYNC_API_KEY_CHECK: bool = env.bool(
"FEDERATION_SKIP_SYNC_API_KEY_CHECK",
default=False,
)
FEDERATION_SKIP_REDIS_PROBE: bool = env.bool(
"FEDERATION_SKIP_REDIS_PROBE",
default=False,
)
# Set at startup / periodic recheck by federation.availability.
FEDERATION_OPERATIONAL: bool = False
FEDERATION_OPERATIONAL_REASON: str = ""
# Tests may set via override_settings without running probes.
FEDERATION_OPERATIONAL_OVERRIDE: bool | None = None
# Export API: internal Docker/private networks (sync → django on sds-network).
FEDERATION_EXPORT_ALLOWED_CIDRS: list[ipaddress.IPv4Network | ipaddress.IPv6Network] = (
_federation_export_allowed_cidrs_from_env()
)

Comment thread
klpoland marked this conversation as resolved.
# ADMIN_CONSOLE_ENV is used to visually distinguish between different environments
# (production, staging, local) in the admin console and error emails. It does not affect
# any functionality and it is meant to prevent changes in production meant for testing
Expand Down
4 changes: 4 additions & 0 deletions gateway/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@

# https://deptry.com/usage/#per-rule-ignores
[tool.deptry.per_rule_ignores]
DEP001 = [
# optional monorepo sibling; contract tests add federation/ to sys.path
"sds_federation",
]
DEP002 = [
# packages that are installed but not imported
"argon2-cffi", # used by django for argon2 password hashing
Expand Down
33 changes: 20 additions & 13 deletions gateway/scripts/fallow-cross-file-dupes.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,31 @@
set -euo pipefail
cd "$(dirname "$0")/.."

# Use first available: vpx > bunx > npx
if command -v pnpx &>/dev/null; then
RUNNER=pnpx
elif command -v vpx &>/dev/null; then
RUNNER=vpx
elif command -v bunx &>/dev/null; then
RUNNER=bunx
elif command -v npx &>/dev/null; then
RUNNER=npx
LOCAL_FALLOW="node_modules/.bin/fallow"
if [[ -x "${LOCAL_FALLOW}" ]]; then
"${LOCAL_FALLOW}" dupes --format json -q | jq -e '
[ (.clone_groups // .dupes.clone_groups // [])[]
| select((.instances | map(.file) | unique | length) > 1)
] | length == 0
' >/dev/null
else
echo "Error: neither vpx, bunx, nor npx found in PATH" >&2
exit 1
fi
# Use first available: vpx > bunx > npx (avoid pnpx; global pnpm may need newer Node)
if command -v vpx &>/dev/null; then
RUNNER=(vpx)
elif command -v bunx &>/dev/null; then
RUNNER=(bunx)
elif command -v npx &>/dev/null; then
RUNNER=(npx)
else
echo "Error: neither local fallow, vpx, bunx, nor npx found" >&2
exit 1
fi

"${RUNNER}" fallow dupes --format json -q | jq -e '
"${RUNNER[@]}" fallow dupes --format json -q | jq -e '
[ (.clone_groups // .dupes.clone_groups // [])[]
| select((.instances | map(.file) | unique | length) > 1)
] | length == 0
' >/dev/null
fi

echo "No cross-file clone groups detected."
54 changes: 50 additions & 4 deletions gateway/scripts/generate-secrets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ set -Eeuo pipefail

SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
GATEWAY_ROOT=$(cd "${SCRIPT_DIR}/.." && pwd)
SFS_ROOT=$(cd "${GATEWAY_ROOT}/../seaweedfs" && pwd)
SDS_ROOT=$(cd "${GATEWAY_ROOT}/.." && pwd)
FEDERATION_ROOT=$(cd "${SDS_ROOT}/federation" && pwd)
SFS_ROOT=$(cd "${SDS_ROOT}/seaweedfs" && pwd)
EXAMPLE_DIR="${GATEWAY_ROOT}/.envs/example"
FEDERATION_SHARED_TEMPLATE="${SDS_ROOT}/federation-shared.example.env"
FEDERATION_SHARED_OUTPUT="${SDS_ROOT}/federation-shared.env"

# PRIMARY (RustFS or SeaweedFS)
PRIMARY_ACCESS_KEY_ID=""
Expand All @@ -16,6 +20,20 @@ PRIMARY_S3_ENDPOINT_URL=""
SECONDARY_ACCESS_KEY_ID=""
SECONDARY_SECRET_ACCESS_KEY=""

FEDERATION_SYNC_DRF_TOKEN=""

function ensure_federation_sync_drf_token() {
local env_type="$1"
if [[ -n "${FEDERATION_SYNC_DRF_TOKEN}" ]]; then
return 0
fi
if [[ "${env_type}" == "ci" ]]; then
FEDERATION_SYNC_DRF_TOKEN="ci-federation-sync-drf-01234567890123456"
else
FEDERATION_SYNC_DRF_TOKEN=$(generate_secret 40)
fi
}

function usage() {
cat <<EOF
Usage: ${0} [OPTIONS] <local|production|ci>
Expand All @@ -35,7 +53,8 @@ EXAMPLES:
${0} production # Generate production env files

NOTES:
- Generated files are placed in .envs/<env>/ directory
- Generated gateway files are placed in gateway/.envs/<env>/
- federation-shared.env is written to the repo root (from federation-shared.example.env)
- Example templates are read from .envs/example/
- Secrets are randomly generated using OpenSSL
- CI environment uses insecure but deterministic values for ephemeral usage
Expand Down Expand Up @@ -120,7 +139,7 @@ function process_env_file() {
return 0
fi

echo " ✓ Generating ${output}"
echo " ✓ Generating ${output} (from ${template})"

local content
content=$(cat "${template}")
Expand Down Expand Up @@ -167,6 +186,11 @@ function process_env_file() {
# set WEB_CONCURRENCY based on CPU cores (applies to all environments)
content="${content//WEB_CONCURRENCY=4/WEB_CONCURRENCY=${web_concurrency}}"

if [[ "${filename}" == "federation-shared.example.env" ]]; then
ensure_federation_sync_drf_token "${env_type}"
content="${content//FEDERATION_SYNC_DRF_TOKEN=/FEDERATION_SYNC_DRF_TOKEN=${FEDERATION_SYNC_DRF_TOKEN}}"
fi

if [[ "${filename}" == "storage.env" ]]; then
# PRIMARY vars
content="${content//PRIMARY_ACCESS_KEY_ID=admin/PRIMARY_ACCESS_KEY_ID=${PRIMARY_ACCESS_KEY_ID}}"
Expand Down Expand Up @@ -232,7 +256,11 @@ function process_env_file() {
# content="${content//RUSTFS_SECRET_ACCESS_KEY=<SAME AS SECONDARY_ROOT_PASSWORD>/RUSTFS_SECRET_ACCESS_KEY=${SECONDARY_SECRET_ACCESS_KEY}}"
fi

# write to output
# write to output (never overwrite *.example.env templates)
if [[ "${output}" == *".example.env" ]]; then
echo "ERROR: refused to write secrets to example template: ${output}" >&2
exit 1
fi
mkdir -p "$(dirname "${output}")"
echo "${content}" >"${output}"
chmod 600 "${output}"
Expand All @@ -243,12 +271,16 @@ function set_permissions() {
env_dirs=(
"${GATEWAY_ROOT}/.envs"
"${SFS_ROOT}/.envs"
"${FEDERATION_ROOT}/.envs"
)
for dir in "${env_dirs[@]}"; do
if [ -d "${dir}" ]; then
find "${dir}" -type f -name "*.env" -exec chmod --changes 600 {} \;
fi
done
if [[ -f "${FEDERATION_SHARED_OUTPUT}" ]]; then
chmod --changes 600 "${FEDERATION_SHARED_OUTPUT}"
fi
}

function main() {
Expand Down Expand Up @@ -326,10 +358,24 @@ function main() {
process_env_file "${template}" "${output}" "${env_type}" "${force}"
done

if [[ -f "${FEDERATION_SHARED_TEMPLATE}" ]]; then
# Read federation-shared.example.env → write repo-root federation-shared.env only
process_env_file \
"${FEDERATION_SHARED_TEMPLATE}" \
"${FEDERATION_SHARED_OUTPUT}" \
"${env_type}" \
"${force}"
else
echo " ⏭ ${FEDERATION_SHARED_TEMPLATE} not found (skipping federation-shared.env)"
fi

set_permissions

echo ""
echo "✅ Secrets generated successfully in ${target_dir_gwy}/"
if [[ -f "${FEDERATION_SHARED_OUTPUT}" ]]; then
echo " Federation shared env: ${FEDERATION_SHARED_OUTPUT}"
fi
echo ""
echo "Next steps:"
if [[ "${env_type}" == "ci" ]]; then
Expand Down
Loading
Loading