Compare commits
No commits in common. "ddf250b61a95ee9111efbec132470fca58986383" and "e55ea946195a21051e8a1d24b2c9893cf24d260b" have entirely different histories.
ddf250b61a
...
e55ea94619
@ -129,10 +129,6 @@ def new():
|
||||
# The form says so before it is filled in; offering a ticked checkbox
|
||||
# and reporting the problem on submit wastes the work of filling it.
|
||||
can_create_accounts=bool(current_app.config.get("ADMIN_TOKEN")),
|
||||
# Same courtesy for mail: without a server configured the invitation
|
||||
# cannot leave the box, and the admin has to pass the link on by
|
||||
# hand. Worth knowing before filling the form rather than after.
|
||||
can_send_mail=mail.configured(),
|
||||
gitea_url=current_app.config["GITEA_URL"].rstrip("/"),
|
||||
)
|
||||
|
||||
@ -179,25 +175,19 @@ def new():
|
||||
return redirect(url_for("members.new"))
|
||||
|
||||
invite_link = None
|
||||
mail_problem = None
|
||||
if create_account:
|
||||
link = auth.invite_url(invites.issue(cursor.lastrowid, "invite"))
|
||||
try:
|
||||
mail.send_invite(email, display_name or login, link)
|
||||
except mail.MailNotConfigured:
|
||||
# Nothing is broken — this server has simply never been given a mail
|
||||
# server. Told apart from a failure on purpose: an admin sent looking
|
||||
# for an SMTP error that does not exist is an afternoon wasted.
|
||||
invite_link, mail_problem = link, "unconfigured"
|
||||
except mail.MailFailed:
|
||||
except (mail.MailFailed, mail.MailNotConfigured):
|
||||
# The account exists and the member cannot reach it. Showing the
|
||||
# admin the link is the difference between a delayed invitation and
|
||||
# a person who simply never gets in.
|
||||
invite_link, mail_problem = link, "failed"
|
||||
invite_link = link
|
||||
|
||||
return render_template("member_created.html", login=login,
|
||||
email=email, created=create_account,
|
||||
invite_link=invite_link, mail_problem=mail_problem,
|
||||
invite_link=invite_link,
|
||||
role_label=ROLE_LABELS[role])
|
||||
|
||||
|
||||
|
||||
@ -12,28 +12,14 @@
|
||||
{# The account exists but the invitation never left the building. Showing the
|
||||
link is the difference between a delayed invitation and a person who
|
||||
simply never gets in. #}
|
||||
{% if mail_problem == 'unconfigured' %}
|
||||
<p class="flash">
|
||||
Este servidor todavía no envía correo. Pásale este enlace por un canal
|
||||
privado — sirve una sola vez y caduca en 7 días.
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="flash flash--error">
|
||||
No se pudo enviar el correo: el servidor de correo rechazó el envío o no
|
||||
respondió. Pásale este enlace por un canal privado — sirve una sola vez y
|
||||
caduca en 7 días.
|
||||
No se pudo enviar el correo. Pásale este enlace por un canal privado —
|
||||
sirve una sola vez y caduca en 7 días.
|
||||
</p>
|
||||
{% endif %}
|
||||
<p class="password mono">{{ invite_link }}</p>
|
||||
<p class="muted small">
|
||||
{% if mail_problem == 'unconfigured' %}
|
||||
Añade los valores <span class="mono">MAIL_*</span> en
|
||||
<span class="mono">/srv/board/.env</span> para que la próxima invitación
|
||||
salga sola.
|
||||
{% else %}
|
||||
Revisa la configuración de correo del servidor para que la próxima
|
||||
invitación salga sola.
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% else %}
|
||||
|
||||
@ -37,16 +37,6 @@
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if can_create_accounts and not can_send_mail %}
|
||||
{# Not an error: the account is still created and the invitation still
|
||||
issued. But it leaves by hand, and finding that out after filling the
|
||||
form is how an invitation ends up believed sent and never delivered. #}
|
||||
<p class="muted small">
|
||||
Este servidor todavía no envía correo, así que la invitación no se
|
||||
enviará sola: al terminar te mostraremos el enlace para que se lo pases.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if can_make_admin %}
|
||||
<label for="role">Rol</label>
|
||||
<select id="role" name="role">
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
"""Guards on the gap between what the app reads and what the server hands it.
|
||||
|
||||
This is the one class of bug the rest of the suite cannot see. Every test here
|
||||
runs against a Flask config built in-process, so a setting can be documented in
|
||||
`.env.example`, read in `app.py`, and still reach the container as nothing at
|
||||
all — because Compose does not pass `.env` to a container, it only substitutes
|
||||
into the compose file. The symptom is a feature reporting itself unconfigured
|
||||
with the settings sitting right there on disk, which is a miserable thing to
|
||||
debug from the outside.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
INFRA = Path(__file__).resolve().parents[3] / "infra" / "board"
|
||||
ENV_EXAMPLE = INFRA / ".env.example"
|
||||
COMPOSE = INFRA / "docker-compose.yml"
|
||||
|
||||
SETTING = re.compile(r"^([A-Z][A-Z0-9_]*)=", re.MULTILINE)
|
||||
|
||||
|
||||
def documented() -> set[str]:
|
||||
return set(SETTING.findall(ENV_EXAMPLE.read_text(encoding="utf-8")))
|
||||
|
||||
|
||||
def test_every_documented_setting_reaches_the_container():
|
||||
compose = COMPOSE.read_text(encoding="utf-8")
|
||||
missing = sorted(
|
||||
name for name in documented()
|
||||
if f"${{{name}}}" not in compose and f"${{{name}:-" not in compose
|
||||
)
|
||||
assert not missing, (
|
||||
"these are in .env.example but never substituted into "
|
||||
f"docker-compose.yml, so the container never sees them: {missing}"
|
||||
)
|
||||
|
||||
|
||||
def test_the_mail_settings_are_optional_at_the_compose_level():
|
||||
"""`${MAIL_HOST}` without a default makes Compose warn on every command for
|
||||
a server that has deliberately not configured mail. Running without it is a
|
||||
supported state, so it must be a quiet one."""
|
||||
compose = COMPOSE.read_text(encoding="utf-8")
|
||||
for name in sorted(n for n in documented() if n.startswith("MAIL_")):
|
||||
assert f"${{{name}:-" in compose, f"{name} has no default in docker-compose.yml"
|
||||
@ -262,35 +262,3 @@ def test_linking_an_existing_account_sends_nothing(
|
||||
"role": "user", "create_account": "",
|
||||
})
|
||||
assert outbox == []
|
||||
|
||||
|
||||
# --- when the server cannot send at all -----------------------------------
|
||||
|
||||
def test_an_unconfigured_server_does_not_blame_the_mail_server(
|
||||
app, client, post, owner_id, sign_in, monkeypatch):
|
||||
"""No MAIL_HOST is not a failure, and saying "no se pudo enviar" sends the
|
||||
admin hunting for an SMTP error that was never produced."""
|
||||
monkeypatch.setattr(gitea, "admin_create_user",
|
||||
lambda login, email, name, password: None)
|
||||
app.config["MAIL_HOST"] = ""
|
||||
sign_in(owner_id)
|
||||
|
||||
page = post("/comunidad/miembros/nuevo", {
|
||||
"login": "maria", "display_name": "María", "email": "m@example.com",
|
||||
"role": "user", "create_account": "on",
|
||||
}).get_data(as_text=True)
|
||||
|
||||
assert "todavía no envía correo" in page
|
||||
assert "No se pudo enviar el correo" not in page
|
||||
assert "/comunidad/invitacion/" in page
|
||||
|
||||
|
||||
def test_the_form_warns_before_it_is_filled_in(app, client, owner_id, sign_in):
|
||||
app.config["MAIL_HOST"] = ""
|
||||
sign_in(owner_id)
|
||||
assert "no envía correo" in client.get(
|
||||
"/comunidad/miembros/nuevo").get_data(as_text=True)
|
||||
|
||||
app.config["MAIL_HOST"] = "smtp.example.com"
|
||||
assert "no envía correo" not in client.get(
|
||||
"/comunidad/miembros/nuevo").get_data(as_text=True)
|
||||
|
||||
@ -193,16 +193,9 @@ your laptop):
|
||||
cd ~/vienalatina
|
||||
git remote add gitea https://git.vienalatina.com/pablo/vienalatina.git
|
||||
git push gitea main
|
||||
|
||||
# Name the GitHub remote too, while you are here. The clone in step 5 called it
|
||||
# `origin`, but `main` came to track `gitea/main` — so a bare `git pull` asks
|
||||
# Gitea, and new work arrives on GitHub. Having both named saves you from typing
|
||||
# the URL every time you deploy.
|
||||
git remote add github https://github.com/pablovolenski/vienalatina.git
|
||||
git remote -v
|
||||
```
|
||||
|
||||
(Gitea will ask for your Gitea username/password.)
|
||||
(It will ask for your Gitea username/password.)
|
||||
|
||||
In Woodpecker (**https://ci.vienalatina.com**):
|
||||
|
||||
@ -379,51 +372,6 @@ It cannot be used to take ownership later: that is deliberate, because otherwise
|
||||
editing a file on disk would be a quieter route to the top than asking for it.
|
||||
Ownership moves only through *Transferir titularidad* inside the app.
|
||||
|
||||
#### Updating it later — a pull is not a deploy
|
||||
|
||||
The app's code is **inside the image**: `docker/board/Dockerfile` ends with
|
||||
`COPY apps /srv/apps`. So pulling new commits into `~/vienalatina` changes
|
||||
nothing that is running, and neither does `docker compose up -d
|
||||
--force-recreate` — same image tag, same layers, same old code. Everything
|
||||
reports success and the server behaves exactly as it did before, which is the
|
||||
most expensive kind of nothing.
|
||||
|
||||
**And `git pull` on its own will not fetch it.** This is worth knowing before
|
||||
it costs you an afternoon: `main` on the server tracks `gitea/main`, while new
|
||||
work is pushed to a branch on **GitHub**. So a bare `git pull` asks Gitea,
|
||||
finds Gitea level with your local `main`, and answers *"Already up to date."* —
|
||||
which is true about the wrong remote, and reads exactly like there is nothing
|
||||
to do.
|
||||
|
||||
An update is a named pull, a push to Gitea so the build pipeline sees it, and a
|
||||
rebuild:
|
||||
|
||||
```sh
|
||||
cd ~/vienalatina
|
||||
git pull --no-edit github <the-branch-name> # e.g. claude/relaxed-faraday-h4zd09
|
||||
git push gitea main
|
||||
sudo bash scripts/deploy-board.sh
|
||||
```
|
||||
|
||||
`--no-edit` accepts the default merge message. Without it git opens an editor,
|
||||
which is a strange place to find yourself mid-deploy.
|
||||
|
||||
If `github` is not a remote yet, add it once — see the end of step 8:
|
||||
|
||||
```sh
|
||||
git remote add github https://github.com/pablovolenski/vienalatina.git
|
||||
```
|
||||
|
||||
That rebuilds the image, copies the compose file across, restarts, and prints
|
||||
the log. It never touches `/srv/board/.env` — that file holds the secrets and
|
||||
lives only on the server — but it does compare it against `.env.example` and
|
||||
name any setting that has appeared in the repository and is missing from yours.
|
||||
New settings are always added by hand.
|
||||
|
||||
The database schema is applied at start-up with `CREATE TABLE IF NOT EXISTS`,
|
||||
so a release that adds a table needs no migration step: the table appears when
|
||||
the new code does.
|
||||
|
||||
### 11.4 Route it through Caddy
|
||||
|
||||
Add to the `vienalatina.com` block in `/etc/caddy/Caddyfile` (already present in
|
||||
@ -674,20 +622,9 @@ perfectly in a mail client fails here, and the error it produces is a timeout
|
||||
rather than anything that names the cause.
|
||||
|
||||
```sh
|
||||
cd ~/vienalatina && sudo bash scripts/deploy-board.sh
|
||||
cd /srv/board && sudo docker compose up -d --force-recreate
|
||||
```
|
||||
|
||||
Not `docker compose up -d --force-recreate` on its own: if the code that reads
|
||||
these settings arrived in the same pull, that restart runs the old image and
|
||||
mail stays unconfigured with the settings sitting right there in `.env`. The
|
||||
script rebuilds first.
|
||||
|
||||
The variables also have to be listed in `/srv/board/docker-compose.yml`, which
|
||||
they now are. Compose does not hand `.env` to a container — it substitutes into
|
||||
the compose file — so a setting added to `.env` and not to the compose file is
|
||||
read by nobody. `apps/board/tests/test_deployment.py` fails the build if the
|
||||
two ever drift apart again.
|
||||
|
||||
Test it by adding a member with an address you can read. If the mail cannot be
|
||||
sent, the screen says so and shows you the invitation link to pass on by hand —
|
||||
the account is created either way, so a mail problem delays somebody rather
|
||||
|
||||
@ -35,23 +35,6 @@ services:
|
||||
- GITEA_ADMIN_TOKEN=${GITEA_ADMIN_TOKEN:-}
|
||||
|
||||
- BOARD_DB=/data/board.db
|
||||
|
||||
# Outgoing mail, for invitations and password resets. Empty MAIL_HOST is
|
||||
# a supported state: the members area still works, but nobody can be
|
||||
# invited or recover a password, and the screens say so.
|
||||
#
|
||||
# Every one of these has to be listed here. Compose does not hand the
|
||||
# contents of .env to the container by itself — it only substitutes into
|
||||
# this file — so a variable added to .env and not added here is read by
|
||||
# nobody, and the app reports mail as unconfigured with the settings
|
||||
# sitting right there on disk.
|
||||
- MAIL_HOST=${MAIL_HOST:-}
|
||||
- MAIL_PORT=${MAIL_PORT:-587}
|
||||
- MAIL_SECURITY=${MAIL_SECURITY:-starttls}
|
||||
- MAIL_USER=${MAIL_USER:-}
|
||||
- MAIL_PASSWORD=${MAIL_PASSWORD:-}
|
||||
- "MAIL_FROM=${MAIL_FROM:-Viena Latina <hola@vienalatina.com>}"
|
||||
|
||||
volumes:
|
||||
- ./data:/data
|
||||
ports:
|
||||
|
||||
@ -1,67 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Put the current checkout of the members area onto the server.
|
||||
#
|
||||
# The app's code is baked into the image by docker/board/Dockerfile (`COPY apps
|
||||
# /srv/apps`), so pulling new commits changes nothing on its own — the container
|
||||
# keeps running the code that was in the image when it was built. `docker
|
||||
# compose up -d --force-recreate` does not help either: same tag, same layers,
|
||||
# same old code. That is a quiet failure, because everything reports success and
|
||||
# the site behaves exactly as it did before.
|
||||
#
|
||||
# This script is the whole update, in the order that matters:
|
||||
#
|
||||
# git pull # done by you, first
|
||||
# sudo bash scripts/deploy-board.sh
|
||||
#
|
||||
# It never touches /srv/board/.env. That file holds the secrets and exists only
|
||||
# on the server; the repository has .env.example instead, and the two drifting
|
||||
# apart is expected — new settings appear in the example and have to be copied
|
||||
# across by hand.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TARGET="${BOARD_DIR:-/srv/board}"
|
||||
IMAGE="vienalatina/board:1"
|
||||
|
||||
cd "$REPO"
|
||||
|
||||
echo "==> Building $IMAGE from $(git rev-parse --short HEAD)"
|
||||
docker build -t "$IMAGE" -f docker/board/Dockerfile .
|
||||
|
||||
echo "==> Syncing compose files into $TARGET (never .env)"
|
||||
mkdir -p "$TARGET"
|
||||
cp "$REPO/infra/board/docker-compose.yml" "$TARGET/docker-compose.yml"
|
||||
cp "$REPO/infra/board/.env.example" "$TARGET/.env.example"
|
||||
|
||||
if [ ! -f "$TARGET/.env" ]; then
|
||||
echo "No $TARGET/.env — copy .env.example to .env and fill it in first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Settings that appear in the example and not in the live file. Nothing is
|
||||
# copied automatically: some of them are secrets, and a blank line silently
|
||||
# added to .env is worse than a line missing loudly.
|
||||
missing="$(comm -23 \
|
||||
<(grep -oE '^[A-Z][A-Z0-9_]*=' "$TARGET/.env.example" | sort -u) \
|
||||
<(grep -oE '^[A-Z][A-Z0-9_]*=' "$TARGET/.env" | sort -u) || true)"
|
||||
if [ -n "$missing" ]; then
|
||||
echo
|
||||
echo "!! These settings exist in .env.example but not in your .env:"
|
||||
echo "$missing" | sed 's/^/ /'
|
||||
echo " Add them to $TARGET/.env and run this again if the feature needs them."
|
||||
echo
|
||||
fi
|
||||
|
||||
echo "==> Restarting"
|
||||
cd "$TARGET"
|
||||
docker compose up -d --force-recreate
|
||||
|
||||
# The schema is applied at start-up with CREATE TABLE IF NOT EXISTS, so a new
|
||||
# table arrives with the new code. If the container is not up a few seconds
|
||||
# later it died during that, and the log says why.
|
||||
sleep 3
|
||||
docker compose ps
|
||||
echo
|
||||
echo "Recent log:"
|
||||
docker compose logs --tail 20 board
|
||||
Loading…
Reference in New Issue
Block a user