Deployment re-jiggling

This commit is contained in:
Andrew Godwin 2022-11-18 17:24:43 -07:00
parent 81de10b70c
commit 8019311490
14 changed files with 162 additions and 84 deletions

View File

@ -2,3 +2,6 @@
image:
docker build -t takahe -f docker/Dockerfile .
docs:
cd docs/ && make html

View File

@ -168,6 +168,8 @@ class Config(models.Model):
identity_max_per_user: int = 5
identity_max_age: int = 24 * 60 * 60
restricted_usernames: str = "admin\nadmins\nadministrator\nadministrators\nsystem\nroot\nannounce\nannouncement\nannouncements"
class UserOptions(pydantic.BaseModel):
pass

View File

@ -1,29 +1,19 @@
# Build stage
FROM python:3.11.0-slim-buster
FROM python:3.11.0-buster as builder
RUN mkdir -p /takahe
RUN python -m venv /takahe/.venv
RUN apt-get update && apt-get -y install libpq-dev python3-dev
WORKDIR /takahe
RUN apt-get update && apt-get -y install libpq-dev python3-dev build-essential
COPY requirements.txt requirements.txt
RUN . /takahe/.venv/bin/activate \
&& pip install --upgrade pip \
&& pip install --upgrade -r requirements.txt
RUN pip3 install --upgrade pip \
&& pip3 install --upgrade -r requirements.txt
# Final image stage
FROM python:3.11.0-slim-buster
RUN apt-get update && apt-get install -y libpq5
COPY --from=builder /takahe /takahe
COPY . /takahe
WORKDIR /takahe
# We use development here to skip settings checks
RUN DJANGO_SETTINGS_MODULE=takahe.settings.development python3 manage.py collectstatic
EXPOSE 8000
CMD ["/takahe/docker/start.sh"]
CMD ["sh", "/takahe/docker/start.sh"]

View File

@ -1,7 +1,5 @@
#!/bin/sh
. /takahe/.venv/bin/activate
python3 manage.py migrate
python manage.py migrate
exec gunicorn takahe.asgi:application -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000
exec gunicorn takahe.wsgi:application -w 8 -b 0.0.0.0:8000

View File

@ -29,6 +29,9 @@ be provided from the first boot.
* ``PGHOST``, ``PGPORT``, ``PGUSER``, ``PGDATABASE``, and ``PGPASSWORD`` are the
standard PostgreSQL environment variables for configuring your database.
* ``TAKAHE_SECRET_KEY`` must be a fixed, random value (it's used for internal
cryptography). Don't change this unless you want to invalidate all sessions.
* ``TAKAHE_MEDIA_BACKEND`` must be one of ``local``, ``s3`` or ``gcs``.
* If it is set to ``local``, you must also provide ``TAKAHE_MEDIA_ROOT``,
@ -36,7 +39,8 @@ be provided from the first boot.
fully-qualified URL prefix that serves that directory.
* If it is set to ``gcs``, you must also provide ``TAKAHE_MEDIA_BUCKET``,
the name of the bucket to store files in.
the name of the bucket to store files in. The bucket must be publically
readable and have "uniform access control" enabled.
* If it is set to ``s3``, you must also provide ``TAKAHE_MEDIA_BUCKET``,
the name of the bucket to store files in.
@ -60,6 +64,36 @@ be provided from the first boot.
be automatically promoted to administrator when it signs up. You only need
this for initial setup, and can unset it after that if you like.
* ``TAKAHE_STATOR_TOKEN`` should be a random string that you are using to
protect the stator (task runner) endpoint. You'll use this value later.
* If your installation is behind a HTTPS endpoint that is proxying it, set
``TAKAHE_SECURE_HEADER`` to the header name used to signify that HTTPS is
being used (usually ``X-Forwarded-Proto``)
* If you want to receive emails about internal site errors, set
``TAKAHE_ERROR_EMAILS`` to a comma-separated list of email addresses that
should get them.
Setting Up Task Runners
-----------------------
Takahe is designed to not require a continuously-running background worker;
instead, you can trigger the "Stator Runner" (our internal task system) either
via a periodic admin command or via a periodic hit to a URL (which is useful
if you are on "serverless" hosting that does not allow background tasks).
To use the URL method, configure something to hit
``/.stator/runner/?token=ABCDEF`` every 60 seconds. You can do this less often
if you don't mind delays in content and profiles being fetched, or more often
if you are under increased load. The value of the token should be the same
as what you set for ``TAKAHE_STATOR_TOKEN``.
Alternatively, you can set up ``python manage.py runstator`` to run in the
Docker image with the same time interval. We still recommend setting
``TAKAHE_STATOR_TOKEN`` in this case so nobody else can trigger it from a URL.
Making An Admin Account
-----------------------
@ -74,3 +108,13 @@ admin account.
If your email settings have a problem and you don't get the email, don't worry;
fix them and then follow the "reset my password" flow on the login screen, and
you'll get another password reset email that you can use.
Adding A Domain
---------------
When you login you'll be greeted with the "make an identity" screen, but you
won't be able to as you will have no domains yet.
You should navigate directly to ``/admin/domains/`` and make one, and then
you will be able to create an identity.

View File

@ -12,3 +12,4 @@ bleach~=5.0.1
pydantic~=1.10.2
django-htmx~=1.13.0
django-storages[google,boto3]~=1.13.1
whitenoise~=6.2.0

View File

@ -646,6 +646,10 @@ h1.identity small {
margin: -10px 0 0 0;
}
.bio {
margin: 0 0 20px 0;
}
.system-note {
background: var(--color-bg-menu);
color: var(--color-text-dull);

View File

@ -19,9 +19,9 @@ class StatorRunner:
def __init__(
self,
models: List[Type[StatorModel]],
concurrency: int = 30,
concurrency_per_model: int = 5,
run_period: int = 30,
concurrency: int = 50,
concurrency_per_model: int = 10,
run_period: int = 60,
wait_period: int = 30,
):
self.models = models

View File

@ -1,8 +1,9 @@
from django.http import HttpResponse
from django.conf import settings
from django.http import HttpResponse, HttpResponseForbidden
from django.views import View
from stator.models import StatorModel
from stator.runner import StatorRunner
from users.models import Follow
class RequestRunner(View):
@ -12,6 +13,11 @@ class RequestRunner(View):
"""
async def get(self, request):
runner = StatorRunner([Follow])
# Check the token, if supplied
if settings.STATOR_TOKEN:
if request.GET.get("token") != settings.STATOR_TOKEN:
return HttpResponseForbidden()
# Run on all models
runner = StatorRunner(StatorModel.subclasses)
handled = await runner.run()
return HttpResponse(f"Handled {handled}")

View File

@ -1,5 +1,4 @@
import os
import sys
from pathlib import Path
from typing import Optional
@ -23,6 +22,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
@ -109,49 +109,10 @@ STATICFILES_DIRS = [
BASE_DIR / "static",
]
STATIC_ROOT = BASE_DIR / "static-collected"
ALLOWED_HOSTS = ["*"]
### User-configurable options, pulled from the environment ###
AUTO_ADMIN_EMAIL: Optional[str] = None
MAIN_DOMAIN = os.environ["TAKAHE_MAIN_DOMAIN"]
if "/" in MAIN_DOMAIN:
print("TAKAHE_MAIN_DOMAIN should be just the domain name - no https:// or path")
sys.exit(1)
if os.environ.get("TAKAHE_EMAIL_CONSOLE_ONLY"):
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
EMAIL_FROM = "test@example.com"
else:
EMAIL_FROM = os.environ["TAKAHE_EMAIL_FROM"]
if "TAKAHE_EMAIL_SENDGRID_KEY" in os.environ:
EMAIL_HOST = "smtp.sendgrid.net"
EMAIL_PORT = 587
EMAIL_HOST_USER: Optional[str] = "apikey"
EMAIL_HOST_PASSWORD: Optional[str] = os.environ["TAKAHE_EMAIL_SENDGRID_KEY"]
EMAIL_USE_TLS = True
else:
EMAIL_HOST = os.environ["TAKAHE_EMAIL_HOST"]
EMAIL_PORT = int(os.environ["TAKAHE_EMAIL_PORT"])
EMAIL_HOST_USER = os.environ.get("TAKAHE_EMAIL_USER")
EMAIL_HOST_PASSWORD = os.environ.get("TAKAHE_EMAIL_PASSWORD")
EMAIL_USE_SSL = EMAIL_PORT == 465
EMAIL_USE_TLS = EMAIL_PORT == 587
AUTO_ADMIN_EMAIL = os.environ.get("TAKAHE_AUTO_ADMIN_EMAIL")
# Set up media storage
MEDIA_BACKEND = os.environ.get("TAKAHE_MEDIA_BACKEND", None)
if MEDIA_BACKEND == "local":
# Note that this MUST be a fully qualified URL in production
MEDIA_URL = os.environ.get("TAKAHE_MEDIA_URL", "/media/")
MEDIA_ROOT = os.environ.get("TAKAHE_MEDIA_ROOT", BASE_DIR / "media")
elif MEDIA_BACKEND == "gcs":
DEFAULT_FILE_STORAGE = "storages.backends.gcloud.GoogleCloudStorage"
GS_BUCKET_NAME = os.environ["TAKAHE_MEDIA_BUCKET"]
elif MEDIA_BACKEND == "s3":
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
AWS_STORAGE_BUCKET_NAME = os.environ["TAKAHE_MEDIA_BUCKET"]
else:
print("Unknown TAKAHE_MEDIA_BACKEND value")
sys.exit(1)
STATOR_TOKEN: Optional[str] = None

View File

@ -18,3 +18,9 @@ CSRF_TRUSTED_ORIGINS = [
]
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
SERVER_EMAIL = "test@example.com"
MAIN_DOMAIN = os.environ.get("TAKAHE_MAIN_DOMAIN", "https://example.com")
MEDIA_URL = os.environ.get("TAKAHE_MEDIA_URL", "/media/")
MEDIA_ROOT = os.environ.get("TAKAHE_MEDIA_ROOT", BASE_DIR / "media")

View File

@ -1,16 +1,79 @@
import os
import sys
from typing import Optional
from .base import * # noqa
# Load secret key from environment
# Ensure debug features are off
DEBUG = bool(os.environ.get("TAKAHE__SECURITY_HAZARD__DEBUG", False))
# TODO: Allow better setting of allowed_hosts, if we need to
ALLOWED_HOSTS = ["*"]
### User-configurable options, pulled from the environment ###
# Secret key
try:
SECRET_KEY = os.environ["TAKAHE_SECRET_KEY"]
except KeyError:
print("You must specify the TAKAHE_SECRET_KEY environment variable!")
os._exit(1)
sys.exit(1)
# Ensure debug features are off
DEBUG = False
# SSL proxy header
if "TAKAHE_SECURE_HEADER" in os.environ:
SECURE_PROXY_SSL_HEADER = (
"HTTP_" + os.environ["TAKAHE_SECURE_HEADER"].replace("-", "_").upper(),
"https",
)
# TODO: Allow better setting of allowed_hosts, if we need to
ALLOWED_HOSTS = ["*"]
# Fallback domain for links
MAIN_DOMAIN = os.environ["TAKAHE_MAIN_DOMAIN"]
if "/" in MAIN_DOMAIN:
print("TAKAHE_MAIN_DOMAIN should be just the domain name - no https:// or path")
sys.exit(1)
# Email config
if os.environ.get("TAKAHE_EMAIL_CONSOLE_ONLY"):
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
SERVER_EMAIL = "test@example.com"
else:
SERVER_EMAIL = os.environ["TAKAHE_EMAIL_FROM"]
if "TAKAHE_EMAIL_SENDGRID_KEY" in os.environ:
EMAIL_HOST = "smtp.sendgrid.net"
EMAIL_PORT = 587
EMAIL_HOST_USER: Optional[str] = "apikey"
EMAIL_HOST_PASSWORD: Optional[str] = os.environ["TAKAHE_EMAIL_SENDGRID_KEY"]
EMAIL_USE_TLS = True
else:
EMAIL_HOST = os.environ["TAKAHE_EMAIL_HOST"]
EMAIL_PORT = int(os.environ["TAKAHE_EMAIL_PORT"])
EMAIL_HOST_USER = os.environ.get("TAKAHE_EMAIL_USER")
EMAIL_HOST_PASSWORD = os.environ.get("TAKAHE_EMAIL_PASSWORD")
EMAIL_USE_SSL = EMAIL_PORT == 465
EMAIL_USE_TLS = EMAIL_PORT == 587
AUTO_ADMIN_EMAIL = os.environ.get("TAKAHE_AUTO_ADMIN_EMAIL")
# Media storage
MEDIA_BACKEND = os.environ.get("TAKAHE_MEDIA_BACKEND", None)
if MEDIA_BACKEND == "local":
# Note that this MUST be a fully qualified URL in production
MEDIA_URL = os.environ.get("TAKAHE_MEDIA_URL", "/media/")
MEDIA_ROOT = os.environ.get("TAKAHE_MEDIA_ROOT", BASE_DIR / "media")
elif MEDIA_BACKEND == "gcs":
DEFAULT_FILE_STORAGE = "storages.backends.gcloud.GoogleCloudStorage"
GS_BUCKET_NAME = os.environ["TAKAHE_MEDIA_BUCKET"]
GS_QUERYSTRING_AUTH = False
elif MEDIA_BACKEND == "s3":
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
AWS_STORAGE_BUCKET_NAME = os.environ["TAKAHE_MEDIA_BUCKET"]
else:
print("Unknown TAKAHE_MEDIA_BACKEND value")
sys.exit(1)
# Stator secret token
STATOR_TOKEN = os.environ.get("TAKAHE_STATOR_TOKEN")
# Error email recipients
if "TAKAHE_ERROR_EMAILS" in os.environ:
ADMINS = [("Admin", e) for e in os.environ["TAKAHE_ERROR_EMAILS"].split(",")]

View File

@ -38,7 +38,7 @@
</h1>
{% if identity.summary %}
<div class="summary">
<div class="bio">
{{ identity.safe_summary }}
</div>
{% endif %}
@ -59,6 +59,6 @@
{% for post in posts %}
{% include "activities/_post.html" %}
{% empty %}
No posts yet.
<span class="empty">No posts yet.</a>
{% endfor %}
{% endblock %}

View File

@ -34,7 +34,7 @@ class PasswordResetStates(StateGraph):
"settings": settings,
},
),
from_email=settings.EMAIL_FROM,
from_email=settings.SERVER_EMAIL,
recipient_list=[reset.user.email],
)
else:
@ -48,7 +48,7 @@ class PasswordResetStates(StateGraph):
"settings": settings,
},
),
from_email=settings.EMAIL_FROM,
from_email=settings.SERVER_EMAIL,
recipient_list=[reset.user.email],
)
return cls.sent