Websted og forum bag SEOSofts reverse proxy på appserveren
Some checks are pending
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Blocked by required conditions
deploy hub / deploy (push) Successful in 24s
CI / Build og unit tests (push) Successful in 56s
deploy community / deploy (push) Successful in 1s
CI / MSI-installer (push) Successful in 33s
Some checks are pending
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Blocked by required conditions
deploy hub / deploy (push) Successful in 24s
CI / Build og unit tests (push) Successful in 56s
deploy community / deploy (push) Successful in 1s
CI / MSI-installer (push) Successful in 33s
- deploy-community.yml: bygger og starter webstedet ved push (som hubben), bundet til DMZ-adressen. Springes over, indtil COMMUNITY_PORT er sat. Feedet læses skrivebeskyttet fra hubbens feed-mappe; download-links går til hub.sqlinsight.seosoft.dk/updates/ (ny indstilling DownloadUrl). - deploy-forum.yml (manuel): kører discourse_docker-launcheren i en helper-container med /var/discourse og docker.sock fra værten. app.yml er en skabelon, som render.py udfylder fra secrets. - Discourse uden egen TLS: lytter på DMZ-adressen, tvinger https-links og tager klient-IP fra X-Forwarded-For fra proxyen (ellers rammer rate limiting alle brugere samlet). - Caddy-opsætningen er fjernet; docs/community.md beskriver secrets og proxy-opsætning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
This commit is contained in:
parent
be35e50335
commit
69fa641f26
13 changed files with 452 additions and 124 deletions
141
.forgejo/workflows/deploy-community.yml
Normal file
141
.forgejo/workflows/deploy-community.yml
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
# =============================================================================
|
||||||
|
# Deploy SQLInsight Pro-webstedet (sqlinsight.seosoft.dk) til appserveren ved push til main.
|
||||||
|
#
|
||||||
|
# Samme mønster som deploy-hub.yml: runneren er host-mode, så .env og compose-filen skrives til INSTALL_DIR
|
||||||
|
# via en kortlivet helper-container, og 'docker compose up' køres i docker:cli.
|
||||||
|
#
|
||||||
|
# Påkrævede secrets: HOST_BIND COMMUNITY_PORT COMMUNITY_CONTACT
|
||||||
|
# Valgfri: TRUSTED_KEY_ID + TRUSTED_KEY (offentlig nøgle, som feedet er signeret med, hvis den ikke er indbygget)
|
||||||
|
# =============================================================================
|
||||||
|
name: deploy community
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- "src/**"
|
||||||
|
- "docs/**"
|
||||||
|
- "rules/**"
|
||||||
|
- "deploy/community/**"
|
||||||
|
- "Directory.*.props"
|
||||||
|
- "global.json"
|
||||||
|
- ".forgejo/workflows/deploy-community.yml"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-sqlinsight-community
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
INSTALL_DIR: /opt/sqlinsight-community
|
||||||
|
IMAGE: sqlinsight-community
|
||||||
|
CONTAINER: sqlinsight-community
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: [self-hosted, appserver]
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Configured?
|
||||||
|
id: check
|
||||||
|
env:
|
||||||
|
COMMUNITY_PORT: ${{ secrets.COMMUNITY_PORT }}
|
||||||
|
run: |
|
||||||
|
# Indtil secrets er sat, springes deploy over i stedet for at fejle hvert push.
|
||||||
|
if [ -n "${COMMUNITY_PORT:-}" ]; then
|
||||||
|
echo "configured=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "::warning::COMMUNITY_PORT er ikke sat; webstedet deployes ikke (se docs/community.md)"
|
||||||
|
echo "configured=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
if: steps.check.outputs.configured == 'true'
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build image
|
||||||
|
if: steps.check.outputs.configured == 'true'
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
docker build -f deploy/community/Dockerfile -t "${IMAGE}:latest" -t "${IMAGE}:${{ github.sha }}" .
|
||||||
|
|
||||||
|
- name: Write .env + docker-compose.yml to INSTALL_DIR
|
||||||
|
if: steps.check.outputs.configured == 'true'
|
||||||
|
env:
|
||||||
|
HOST_BIND: ${{ secrets.HOST_BIND }}
|
||||||
|
COMMUNITY_PORT: ${{ secrets.COMMUNITY_PORT }}
|
||||||
|
COMMUNITY_CONTACT: ${{ secrets.COMMUNITY_CONTACT }}
|
||||||
|
TRUSTED_KEY_ID: ${{ secrets.TRUSTED_KEY_ID }}
|
||||||
|
TRUSTED_KEY: ${{ secrets.TRUSTED_KEY }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
required="HOST_BIND COMMUNITY_PORT COMMUNITY_CONTACT"
|
||||||
|
missing=0
|
||||||
|
for v in $required; do
|
||||||
|
if [ -z "$(printenv "$v" || true)" ]; then
|
||||||
|
echo "ERROR: required secret $v is not set in repo settings"
|
||||||
|
missing=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[ "$missing" -eq 0 ] || exit 1
|
||||||
|
|
||||||
|
for v in $required TRUSTED_KEY_ID TRUSTED_KEY; do
|
||||||
|
case "$(printenv "$v" || true)" in
|
||||||
|
*"'"*|*";"*) echo "ERROR: secret $v indeholder ' eller ; – ikke understøttet"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
tmp="$(mktemp -d)"
|
||||||
|
{
|
||||||
|
printf "HOST_BIND='%s'\n" "$HOST_BIND"
|
||||||
|
printf "COMMUNITY_PORT='%s'\n" "$COMMUNITY_PORT"
|
||||||
|
printf "SQLInsight__Community__ContactEmail='%s'\n" "$COMMUNITY_CONTACT"
|
||||||
|
if [ -n "${TRUSTED_KEY_ID:-}" ] && [ -n "${TRUSTED_KEY:-}" ]; then
|
||||||
|
printf "SQLInsight__Community__TrustedKeys__%s='%s'\n" "$TRUSTED_KEY_ID" "$TRUSTED_KEY"
|
||||||
|
fi
|
||||||
|
} > "$tmp/.env"
|
||||||
|
cp deploy/community/docker-compose.prod.yml "$tmp/docker-compose.yml"
|
||||||
|
|
||||||
|
tar -C "$tmp" -cf - .env docker-compose.yml | docker run --rm -i \
|
||||||
|
-v "${INSTALL_DIR}:/out" alpine:3 \
|
||||||
|
sh -c 'tar -C /out -xf - && chown root:root /out/.env /out/docker-compose.yml && chmod 600 /out/.env && chmod 644 /out/docker-compose.yml'
|
||||||
|
rm -rf "$tmp"
|
||||||
|
|
||||||
|
- name: Deploy (compose up)
|
||||||
|
if: steps.check.outputs.configured == 'true'
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
docker run --rm \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
-v "${INSTALL_DIR}:${INSTALL_DIR}" \
|
||||||
|
-w "${INSTALL_DIR}" \
|
||||||
|
docker:cli \
|
||||||
|
docker compose --project-name sqlinsight-community up -d --force-recreate --remove-orphans
|
||||||
|
|
||||||
|
- name: Wait for /health
|
||||||
|
if: steps.check.outputs.configured == 'true'
|
||||||
|
env:
|
||||||
|
HOST_BIND: ${{ secrets.HOST_BIND }}
|
||||||
|
COMMUNITY_PORT: ${{ secrets.COMMUNITY_PORT }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
if docker run --rm alpine:3 wget -qO- -T 3 "http://${HOST_BIND}:${COMMUNITY_PORT}/health" 2>/dev/null | grep -q '"ok"'; then
|
||||||
|
echo "${CONTAINER} is healthy"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "attempt $i: not ready"
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
echo "${CONTAINER} did not answer /health within 150s — last 100 log lines:"
|
||||||
|
docker logs --tail 100 "${CONTAINER}" || true
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Prune old images
|
||||||
|
if: always() && steps.check.outputs.configured == 'true'
|
||||||
|
run: |
|
||||||
|
docker image prune -f || true
|
||||||
|
docker images "${IMAGE}" --format '{{.Tag}} {{.CreatedAt}}' \
|
||||||
|
| grep -v '^latest ' | sort -k2 -r | awk 'NR>3 {print $1}' \
|
||||||
|
| xargs -r -I{} docker image rm "${IMAGE}:{}" || true
|
||||||
73
.forgejo/workflows/deploy-forum.yml
Normal file
73
.forgejo/workflows/deploy-forum.yml
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
# =============================================================================
|
||||||
|
# Installér eller genopbyg forummet (Discourse, forum.sqlinsight.seosoft.dk) på appserveren.
|
||||||
|
#
|
||||||
|
# Kun manuelt (Actions → deploy forum → Run workflow): en rebuild tager 10-15 minutter, og forummet er nede imens.
|
||||||
|
# Discourse opdateres ellers fra /admin/upgrade.
|
||||||
|
#
|
||||||
|
# Runneren er host-mode, så launcheren køres i en helper-container med /var/discourse og docker.sock fra værten.
|
||||||
|
# app.yml udfyldes fra secrets af render.py; intet hemmeligt ligger i repoet.
|
||||||
|
#
|
||||||
|
# Påkrævede secrets: HOST_BIND FORUM_PORT KNOWN_PROXY FORUM_ADMIN_EMAIL FORUM_SMTP_ADDRESS FORUM_SMTP_USER FORUM_SMTP_PASSWORD
|
||||||
|
# Valgfri: FORUM_SMTP_PORT (587) FORUM_NOTIFICATION_EMAIL (= FORUM_SMTP_USER) FORUM_WORKERS (4) FORUM_DB_SHARED_BUFFERS (512MB)
|
||||||
|
# =============================================================================
|
||||||
|
name: deploy forum
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-sqlinsight-forum
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: [self-hosted, appserver]
|
||||||
|
timeout-minutes: 60
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build launcher helper
|
||||||
|
run: docker build -t sqlinsight-forum-launcher deploy/forum
|
||||||
|
|
||||||
|
- name: Rebuild Discourse
|
||||||
|
env:
|
||||||
|
FORUM_BIND: ${{ secrets.HOST_BIND }}
|
||||||
|
FORUM_PORT: ${{ secrets.FORUM_PORT }}
|
||||||
|
KNOWN_PROXY: ${{ secrets.KNOWN_PROXY }}
|
||||||
|
FORUM_ADMIN_EMAIL: ${{ secrets.FORUM_ADMIN_EMAIL }}
|
||||||
|
FORUM_SMTP_ADDRESS: ${{ secrets.FORUM_SMTP_ADDRESS }}
|
||||||
|
FORUM_SMTP_PORT: ${{ secrets.FORUM_SMTP_PORT }}
|
||||||
|
FORUM_SMTP_USER: ${{ secrets.FORUM_SMTP_USER }}
|
||||||
|
FORUM_SMTP_PASSWORD: ${{ secrets.FORUM_SMTP_PASSWORD }}
|
||||||
|
FORUM_NOTIFICATION_EMAIL: ${{ secrets.FORUM_NOTIFICATION_EMAIL }}
|
||||||
|
FORUM_WORKERS: ${{ secrets.FORUM_WORKERS }}
|
||||||
|
FORUM_DB_SHARED_BUFFERS: ${{ secrets.FORUM_DB_SHARED_BUFFERS }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
# Samme sti inde og ude, så launcherens bind mounts (/var/discourse/shared/...) peger på værtens mapper.
|
||||||
|
docker run --rm \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
-v /var/discourse:/var/discourse \
|
||||||
|
-e FORUM_BIND -e FORUM_PORT -e KNOWN_PROXY -e FORUM_ADMIN_EMAIL \
|
||||||
|
-e FORUM_SMTP_ADDRESS -e FORUM_SMTP_PORT -e FORUM_SMTP_USER -e FORUM_SMTP_PASSWORD \
|
||||||
|
-e FORUM_NOTIFICATION_EMAIL -e FORUM_WORKERS -e FORUM_DB_SHARED_BUFFERS \
|
||||||
|
sqlinsight-forum-launcher
|
||||||
|
|
||||||
|
- name: Wait for the forum
|
||||||
|
env:
|
||||||
|
HOST_BIND: ${{ secrets.HOST_BIND }}
|
||||||
|
FORUM_PORT: ${{ secrets.FORUM_PORT }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
if docker run --rm alpine:3 wget -qO- -T 3 "http://${HOST_BIND}:${FORUM_PORT}/srv/status" 2>/dev/null | grep -q ok; then
|
||||||
|
echo "forum is up"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "attempt $i: not ready"
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
docker logs --tail 100 app || true
|
||||||
|
exit 1
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
{$COMMUNITY_HOST} {
|
|
||||||
encode zstd gzip
|
|
||||||
reverse_proxy community:8080
|
|
||||||
header Strict-Transport-Security "max-age=31536000"
|
|
||||||
}
|
|
||||||
31
deploy/community/docker-compose.prod.yml
Normal file
31
deploy/community/docker-compose.prod.yml
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# SQLInsight Pro-webstedet (sqlinsight.seosoft.dk) i SEOSofts drift (appserver). Kopieres til INSTALL_DIR af
|
||||||
|
# .forgejo/workflows/deploy-community.yml sammen med en .env, der genereres fra Forgejo repo-secrets.
|
||||||
|
# Reverse proxyen terminerer TLS, som for hubben.
|
||||||
|
#
|
||||||
|
# Feedet læses direkte fra hubbens feed-mappe (skrivebeskyttet), så webstedet viser præcis det, agenterne får.
|
||||||
|
# Download-links går til hubbens offentlige /updates/.
|
||||||
|
#
|
||||||
|
# Manuel drift på appserveren:
|
||||||
|
# cd /opt/sqlinsight-community && docker compose ps | logs -f | restart
|
||||||
|
|
||||||
|
services:
|
||||||
|
community:
|
||||||
|
image: sqlinsight-community:latest
|
||||||
|
container_name: sqlinsight-community
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
SQLInsight__Community__Feed: /feed
|
||||||
|
SQLInsight__Community__DownloadUrl: https://hub.sqlinsight.seosoft.dk/updates/
|
||||||
|
SQLInsight__Community__ForumUrl: https://forum.sqlinsight.seosoft.dk/
|
||||||
|
volumes:
|
||||||
|
- /opt/sqlinsight-hub/data/feed:/feed:ro
|
||||||
|
ports:
|
||||||
|
# Bind til DMZ-IP (HOST_BIND i .env). Reverse proxyen terminerer TLS.
|
||||||
|
- "${HOST_BIND}:${COMMUNITY_PORT}:8080"
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
# The SQLInsight Pro website behind Caddy, which gets and renews the TLS certificate itself.
|
|
||||||
# DNS for COMMUNITY_HOST must point at this server, and ports 80 and 443 must be free (Discourse runs on its own server).
|
|
||||||
#
|
|
||||||
# COMMUNITY_HOST=sqlinsight.seosoft.dk docker compose up -d
|
|
||||||
services:
|
|
||||||
community:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: deploy/community/Dockerfile
|
|
||||||
image: sqlinsight-community
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
# The signed update feed: the hub's /updates/ (or a folder mounted into the container).
|
|
||||||
SQLInsight__Community__Feed: ${COMMUNITY_FEED:-https://hub.sqlinsight.seosoft.dk/updates/}
|
|
||||||
SQLInsight__Community__ForumUrl: ${COMMUNITY_FORUM:-https://forum.sqlinsight.seosoft.dk/}
|
|
||||||
SQLInsight__Community__ContactEmail: ${COMMUNITY_CONTACT:?set COMMUNITY_CONTACT (the address for support enquiries)}
|
|
||||||
# The public key releases are signed with, if it is not embedded in the build (the vendor tool's keygen prints it):
|
|
||||||
# SQLInsight__Community__TrustedKeys__k20260929: "MFkw..."
|
|
||||||
expose:
|
|
||||||
- "8080"
|
|
||||||
|
|
||||||
caddy:
|
|
||||||
image: caddy:2-alpine
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
- community
|
|
||||||
environment:
|
|
||||||
COMMUNITY_HOST: ${COMMUNITY_HOST:?set COMMUNITY_HOST}
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
- "443:443"
|
|
||||||
volumes:
|
|
||||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
||||||
- caddy_data:/data
|
|
||||||
- caddy_config:/config
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
caddy_data:
|
|
||||||
caddy_config:
|
|
||||||
8
deploy/forum/Dockerfile
Normal file
8
deploy/forum/Dockerfile
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Helper image that runs the discourse_docker launcher from the self-hosted runner (host-mode):
|
||||||
|
# /var/discourse and docker.sock are mounted from the host, so the launcher's host paths are the real ones.
|
||||||
|
ARG BASE=docker:cli
|
||||||
|
FROM ${BASE}
|
||||||
|
RUN apk add --no-cache bash git python3 coreutils sed grep gawk procps
|
||||||
|
WORKDIR /src
|
||||||
|
COPY app.yml render.py install.sh configure.py ./
|
||||||
|
ENTRYPOINT ["bash", "/src/install.sh"]
|
||||||
|
|
@ -1,26 +1,27 @@
|
||||||
## SQLInsight Pro community forum: Discourse, installed with the official discourse_docker launcher.
|
## SQLInsight Pro community forum (forum.sqlinsight.seosoft.dk): Discourse with the official discourse_docker launcher.
|
||||||
## See docs/community.md. Copy to /var/discourse/containers/app.yml, fill in the values marked CHANGE, and run
|
## See docs/community.md.
|
||||||
## ./launcher rebuild app
|
##
|
||||||
## (deploy/forum/install.sh does this for you.)
|
## This is a template: the double-braced names are filled in by render.py from environment variables (Forgejo secrets in
|
||||||
|
## .forgejo/workflows/deploy-forum.yml, or exported by hand before deploy/forum/install.sh). Nothing secret is committed.
|
||||||
|
##
|
||||||
|
## The forum runs behind SEOSoft's reverse proxy, which terminates TLS (like the hub). The proxy must send
|
||||||
|
## Host, X-Forwarded-For and X-Forwarded-Proto: https, and allow WebSocket/long polling (/message-bus/).
|
||||||
|
|
||||||
templates:
|
templates:
|
||||||
- "templates/postgres.template.yml"
|
- "templates/postgres.template.yml"
|
||||||
- "templates/redis.template.yml"
|
- "templates/redis.template.yml"
|
||||||
- "templates/web.template.yml"
|
- "templates/web.template.yml"
|
||||||
- "templates/web.ratelimited.template.yml"
|
- "templates/web.ratelimited.template.yml"
|
||||||
## TLS with Let's Encrypt. Remove both lines if a reverse proxy in front already terminates TLS.
|
|
||||||
- "templates/web.ssl.template.yml"
|
|
||||||
- "templates/web.letsencrypt.ssl.template.yml"
|
|
||||||
|
|
||||||
|
## Only on the DMZ address, where the reverse proxy reaches it.
|
||||||
expose:
|
expose:
|
||||||
- "80:80"
|
- "{{FORUM_BIND}}:{{FORUM_PORT}}:80"
|
||||||
- "443:443"
|
|
||||||
|
|
||||||
params:
|
params:
|
||||||
## Danish stemming for search.
|
## Danish stemming for search.
|
||||||
db_default_text_search_config: "pg_catalog.danish"
|
db_default_text_search_config: "pg_catalog.danish"
|
||||||
## About 25 % of the server's memory.
|
## About 25 % of the memory Discourse may use.
|
||||||
db_shared_buffers: "512MB"
|
db_shared_buffers: "{{FORUM_DB_SHARED_BUFFERS}}"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
LC_ALL: en_US.UTF-8
|
LC_ALL: en_US.UTF-8
|
||||||
|
|
@ -28,24 +29,22 @@ env:
|
||||||
LANGUAGE: en_US.UTF-8
|
LANGUAGE: en_US.UTF-8
|
||||||
DISCOURSE_DEFAULT_LOCALE: da
|
DISCOURSE_DEFAULT_LOCALE: da
|
||||||
## About 2 per CPU core.
|
## About 2 per CPU core.
|
||||||
UNICORN_WORKERS: 4
|
UNICORN_WORKERS: {{FORUM_WORKERS}}
|
||||||
|
|
||||||
## CHANGE: the forum's address (a DNS record must point to this server).
|
DISCOURSE_HOSTNAME: '{{FORUM_HOSTNAME}}'
|
||||||
DISCOURSE_HOSTNAME: forum.sqlinsight.seosoft.dk
|
## TLS is terminated by the reverse proxy; all links must still be https.
|
||||||
|
DISCOURSE_FORCE_HTTPS: true
|
||||||
|
|
||||||
## CHANGE: who becomes administrator when signing up with these addresses.
|
## Who becomes administrator when signing up with these addresses (comma separated).
|
||||||
DISCOURSE_DEVELOPER_EMAILS: "admin@example.com"
|
DISCOURSE_DEVELOPER_EMAILS: '{{FORUM_ADMIN_EMAIL}}'
|
||||||
|
|
||||||
## CHANGE: mail server. Discourse cannot work without it (sign-up, notifications, password reset).
|
## Mail server. Discourse cannot work without it (sign-up, notifications, password reset).
|
||||||
DISCOURSE_SMTP_ADDRESS: smtp.example.com
|
DISCOURSE_SMTP_ADDRESS: '{{FORUM_SMTP_ADDRESS}}'
|
||||||
DISCOURSE_SMTP_PORT: 587
|
DISCOURSE_SMTP_PORT: {{FORUM_SMTP_PORT}}
|
||||||
DISCOURSE_SMTP_USER_NAME: forum@example.com
|
DISCOURSE_SMTP_USER_NAME: '{{FORUM_SMTP_USER}}'
|
||||||
DISCOURSE_SMTP_PASSWORD: "CHANGE"
|
DISCOURSE_SMTP_PASSWORD: '{{FORUM_SMTP_PASSWORD}}'
|
||||||
DISCOURSE_SMTP_ENABLE_START_TLS: true
|
DISCOURSE_SMTP_ENABLE_START_TLS: true
|
||||||
DISCOURSE_NOTIFICATION_EMAIL: forum@example.com
|
DISCOURSE_NOTIFICATION_EMAIL: '{{FORUM_NOTIFICATION_EMAIL}}'
|
||||||
|
|
||||||
## CHANGE: address Let's Encrypt writes to about expiring certificates.
|
|
||||||
LETSENCRYPT_ACCOUNT_EMAIL: admin@example.com
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- volume:
|
- volume:
|
||||||
|
|
@ -65,4 +64,14 @@ hooks:
|
||||||
|
|
||||||
run:
|
run:
|
||||||
- exec: echo "Beginning of custom commands"
|
- exec: echo "Beginning of custom commands"
|
||||||
|
## Real client addresses from the reverse proxy. Without this every visitor looks like the proxy, and
|
||||||
|
## web.ratelimited would throttle them all together.
|
||||||
|
- replace:
|
||||||
|
filename: "/etc/nginx/conf.d/discourse.conf"
|
||||||
|
from: /server \{/
|
||||||
|
to: |
|
||||||
|
server {
|
||||||
|
set_real_ip_from {{KNOWN_PROXY}};
|
||||||
|
real_ip_header X-Forwarded-For;
|
||||||
|
real_ip_recursive on;
|
||||||
- exec: echo "End of custom commands"
|
- exec: echo "End of custom commands"
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,44 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Installs the SQLInsight Pro forum (Discourse) on a fresh Linux server with the official discourse_docker launcher.
|
# Installs or rebuilds the SQLInsight Pro forum (Discourse) with the official discourse_docker launcher.
|
||||||
# Run as root on the forum server, from a checkout of this repository, after filling in deploy/forum/app.yml:
|
#
|
||||||
# sudo deploy/forum/install.sh
|
# Normally run by .forgejo/workflows/deploy-forum.yml (in a helper container with /var/discourse and docker.sock
|
||||||
# Needs: DNS for the forum's host name pointing here, ports 80 and 443 free, 2 GB memory (4 GB recommended) and a mail server.
|
# from the host). By hand, as root on the server, with the values render.py needs exported:
|
||||||
|
# sudo -E deploy/forum/install.sh
|
||||||
|
# Needs 4 GB memory (2 GB minimum), 20 GB disk, and a mail server.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
TARGET=/var/discourse
|
TARGET=/var/discourse
|
||||||
|
|
||||||
if [[ $EUID -ne 0 ]]; then
|
if [[ $EUID -ne 0 ]]; then
|
||||||
echo "Kør som root (sudo)." >&2
|
echo "Kør som root (sudo -E)." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Refuse to install with the example values still in place: Discourse is useless without a working mail server.
|
command -v docker >/dev/null || { echo "Docker mangler." >&2; exit 1; }
|
||||||
if grep -nE 'example\.com|"CHANGE"' "$HERE/app.yml" | grep -vE '^[0-9]+:\s*#'; then
|
|
||||||
echo "Udfyld værdierne markeret CHANGE i $HERE/app.yml først (linjerne ovenfor)." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v docker >/dev/null; then
|
|
||||||
echo "Installerer Docker ..."
|
|
||||||
curl -fsSL https://get.docker.com | sh
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -d $TARGET/.git ]]; then
|
if [[ ! -d $TARGET/.git ]]; then
|
||||||
git clone --depth 1 https://github.com/discourse/discourse_docker.git "$TARGET"
|
git clone --depth 1 https://github.com/discourse/discourse_docker.git "$TARGET"
|
||||||
|
else
|
||||||
|
git -C "$TARGET" pull -q --ff-only
|
||||||
fi
|
fi
|
||||||
|
|
||||||
install -m 600 "$HERE/app.yml" "$TARGET/containers/app.yml"
|
mkdir -p "$TARGET/containers"
|
||||||
cd "$TARGET"
|
python3 "$HERE/render.py" "$HERE/app.yml" "$TARGET/containers/app.yml"
|
||||||
./launcher rebuild app
|
|
||||||
|
|
||||||
host=$(sed -n 's/^\s*DISCOURSE_HOSTNAME:\s*//p' "$HERE/app.yml" | tr -d '"' | head -1)
|
cd "$TARGET"
|
||||||
|
# The launcher asks before continuing on low memory; answer yes when running unattended.
|
||||||
|
./launcher rebuild app --skip-prereqs
|
||||||
|
|
||||||
|
host="${FORUM_HOSTNAME:-forum.sqlinsight.seosoft.dk}"
|
||||||
cat <<NEXT
|
cat <<NEXT
|
||||||
|
|
||||||
Discourse kører. Næste skridt:
|
Discourse kører på ${FORUM_BIND}:${FORUM_PORT}. Første gang:
|
||||||
1. Åbn https://$host og opret den første administrator med en af adresserne i DISCOURSE_DEVELOPER_EMAILS.
|
1. Peg reverse proxyen for $host på http://${FORUM_BIND}:${FORUM_PORT} (med X-Forwarded-Proto: https og WebSockets).
|
||||||
2. Lav en API-nøgle til opsætningen:
|
2. Åbn https://$host og opret administratoren med adressen i FORUM_ADMIN_EMAIL.
|
||||||
cd $TARGET && ./launcher enter app
|
3. Lav en API-nøgle til opsætningen:
|
||||||
cd /var/www/discourse && su discourse -c 'RAILS_ENV=production bundle exec rake api_key:create_master[forum-setup]'
|
docker exec app su discourse -c 'cd /var/www/discourse && RAILS_ENV=production bundle exec rake api_key:create_master[forum-setup]'
|
||||||
3. Opsæt kategorier, sprog og velkomstemne (kan køres igen):
|
4. Opsæt kategorier, sprog og velkomstemne (kan køres igen):
|
||||||
DISCOURSE_URL=https://$host DISCOURSE_API_KEY=<nøglen> CONTACT_EMAIL=<supportadresse> python3 $HERE/configure.py
|
DISCOURSE_URL=https://$host DISCOURSE_API_KEY=<nøglen> CONTACT_EMAIL=<supportadresse> python3 deploy/forum/configure.py
|
||||||
4. Slet API-nøglen igen under Admin → API, når opsætningen er færdig.
|
5. Slet API-nøglen igen under Admin → API.
|
||||||
NEXT
|
NEXT
|
||||||
|
|
|
||||||
65
deploy/forum/render.py
Executable file
65
deploy/forum/render.py
Executable file
|
|
@ -0,0 +1,65 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fills in deploy/forum/app.yml from environment variables and writes the result (mode 600).
|
||||||
|
|
||||||
|
render.py <template> <output>
|
||||||
|
|
||||||
|
Required: FORUM_BIND FORUM_PORT KNOWN_PROXY FORUM_ADMIN_EMAIL FORUM_SMTP_ADDRESS FORUM_SMTP_USER FORUM_SMTP_PASSWORD
|
||||||
|
Optional: FORUM_HOSTNAME (forum.sqlinsight.seosoft.dk), FORUM_SMTP_PORT (587), FORUM_NOTIFICATION_EMAIL (FORUM_SMTP_USER),
|
||||||
|
FORUM_WORKERS (4), FORUM_DB_SHARED_BUFFERS (512MB)
|
||||||
|
"""
|
||||||
|
import ipaddress
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
DEFAULTS = {
|
||||||
|
"FORUM_HOSTNAME": "forum.sqlinsight.seosoft.dk",
|
||||||
|
"FORUM_SMTP_PORT": "587",
|
||||||
|
"FORUM_WORKERS": "4",
|
||||||
|
"FORUM_DB_SHARED_BUFFERS": "512MB",
|
||||||
|
}
|
||||||
|
# Values that end up unquoted in YAML or in nginx/docker syntax must look exactly like this.
|
||||||
|
FORMATS = {
|
||||||
|
"FORUM_PORT": r"\d{1,5}",
|
||||||
|
"FORUM_SMTP_PORT": r"\d{1,5}",
|
||||||
|
"FORUM_WORKERS": r"\d{1,2}",
|
||||||
|
"FORUM_DB_SHARED_BUFFERS": r"\d+[MG]B",
|
||||||
|
"FORUM_HOSTNAME": r"[a-z0-9.-]+",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def value(name):
|
||||||
|
v = os.environ.get(name) or DEFAULTS.get(name)
|
||||||
|
if name == "FORUM_NOTIFICATION_EMAIL" and not v:
|
||||||
|
v = os.environ.get("FORUM_SMTP_USER")
|
||||||
|
if not v:
|
||||||
|
sys.exit(f"ERROR: {name} is not set")
|
||||||
|
if "\n" in v or "{{" in v:
|
||||||
|
sys.exit(f"ERROR: {name} contains a line break or {{{{")
|
||||||
|
if name in FORMATS and not re.fullmatch(FORMATS[name], v):
|
||||||
|
sys.exit(f"ERROR: {name} has an unexpected format")
|
||||||
|
if name in ("FORUM_BIND", "KNOWN_PROXY"):
|
||||||
|
try:
|
||||||
|
ipaddress.ip_network(v, strict=False) if name == "KNOWN_PROXY" else ipaddress.ip_address(v)
|
||||||
|
except ValueError:
|
||||||
|
sys.exit(f"ERROR: {name} is not an IP address" + (" or network" if name == "KNOWN_PROXY" else ""))
|
||||||
|
# All free-text values sit in single-quoted YAML strings.
|
||||||
|
return v.replace("'", "''")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
sys.exit(__doc__)
|
||||||
|
template, output = sys.argv[1], sys.argv[2]
|
||||||
|
with open(template, encoding="utf-8") as f:
|
||||||
|
text = f.read()
|
||||||
|
names = sorted(set(re.findall(r"\{\{([A-Z_]+)\}\}", text)))
|
||||||
|
text = re.sub(r"\{\{([A-Z_]+)\}\}", lambda m: value(m.group(1)), text)
|
||||||
|
fd = os.open(output, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||||
|
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||||
|
f.write(text)
|
||||||
|
print(f"{output}: {len(names)} values filled in")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -2,35 +2,53 @@
|
||||||
|
|
||||||
SQLInsight Pro har to offentlige websteder. Begge drives af SEOSoft:
|
SQLInsight Pro har to offentlige websteder. Begge drives af SEOSoft:
|
||||||
|
|
||||||
| Websted | Adresse (eksempel) | Hvad |
|
| Websted | Adresse | Hvad |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Community-webstedet | `https://sqlinsight.seosoft.dk` | Forside, funktioner, download af signerede udgivelser, dokumentation, SQL Server-versioner og supportdatoer, regler og supportaftaler. Projektet `src/SQLInsight.Community`. |
|
| Community-webstedet | `https://sqlinsight.seosoft.dk` | Forside, funktioner, download af signerede udgivelser, dokumentation, SQL Server-versioner og supportdatoer, regler og supportaftaler. Projektet `src/SQLInsight.Community`. |
|
||||||
| Forummet | `https://forum.sqlinsight.seosoft.dk` | Discourse på dansk: spørgsmål og svar (med løste svar), installation og drift, performance, regler, idéer og annonceringer. |
|
| Forummet | `https://forum.sqlinsight.seosoft.dk` | Discourse på dansk: spørgsmål og svar (med løste svar), installation og drift, performance, regler, idéer og annonceringer. |
|
||||||
|
|
||||||
Adresserne er eksempler. Ret dem i `deploy/forum/app.yml`, `deploy/community/docker-compose.yml` og i webbrugerfladens `SQLInsight:Web:CommunityUrl` og `SQLInsight:Web:ForumUrl`.
|
Alle tre navne (også `hub.sqlinsight.seosoft.dk`) peger på SEOSofts offentlige IP. Reverse proxyen dér terminerer TLS og sender videre til appserverens DMZ-adresse (`HOST_BIND`), på hver sin port:
|
||||||
|
|
||||||
Kør de to på hver sin server (eller VM). Discourse vil selv eje port 80 og 443 og er svær at dele en server med.
|
| Navn | Container | Port (secret) | Deploy |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `hub.sqlinsight.seosoft.dk` | `sqlinsight-hub` | `HOST_PORT` | `deploy-hub.yml`, ved push |
|
||||||
|
| `sqlinsight.seosoft.dk` | `sqlinsight-community` | `COMMUNITY_PORT` | `deploy-community.yml`, ved push |
|
||||||
|
| `forum.sqlinsight.seosoft.dk` | `app` (Discourse) | `FORUM_PORT` | `deploy-forum.yml`, kun manuelt |
|
||||||
|
|
||||||
|
Proxyen skal sende `Host`, `X-Forwarded-For` og `X-Forwarded-Proto: https`. For forummet skal den også tillade WebSockets og lange forespørgsler (Discourse bruger long polling på `/message-bus/`, så timeout skal være mindst 60 sekunder).
|
||||||
|
|
||||||
## Forummet (Discourse)
|
## Forummet (Discourse)
|
||||||
|
|
||||||
Discourse installeres med den officielle `discourse_docker`-launcher. Det er den eneste måde, Discourse understøtter.
|
Discourse installeres med den officielle `discourse_docker`-launcher, som er den eneste måde, Discourse understøtter. Workflowet `deploy-forum.yml` kører launcheren på appserveren i en helper-container, med `/var/discourse` og docker.sock fra værten. `deploy/forum/app.yml` er en skabelon, som `render.py` udfylder fra secrets. Intet hemmeligt ligger i repoet.
|
||||||
|
|
||||||
1. **Server:** Ubuntu LTS eller Debian, 2 GB RAM (4 GB anbefales), 2 CPU'er og 20 GB disk.
|
1. **Krav:** Discourse bruger 2-4 GB RAM og 20 GB disk under `/var/discourse` på appserveren.
|
||||||
2. **DNS:** Opret en A-post (og evt. AAAA) for forummets navn, der peger på serveren.
|
2. **Mail:** Discourse kan ikke fungere uden SMTP. Tilmelding, notifikationer og nulstilling af adgangskode sker på mail. Opret en postkasse til forummet, og sørg for SPF og DKIM for afsenderdomænet.
|
||||||
3. **Mail:** Discourse kan ikke fungere uden SMTP. Tilmelding, notifikationer og nulstilling af adgangskode sker på mail. Brug en rigtig mailudbyder med SPF og DKIM for afsenderdomænet.
|
3. **Secrets** i repoet (*Indstillinger → Actions → Secrets*):
|
||||||
4. **Konfiguration:** Udfyld værdierne markeret `CHANGE` i `deploy/forum/app.yml`: værtsnavn, administratoradresse, SMTP og Let's Encrypt-adresse.
|
|
||||||
5. **Installation:** Kør `sudo deploy/forum/install.sh`. Scriptet:
|
| Secret | Eksempel | |
|
||||||
- stopper, hvis der stadig står eksempelværdier;
|
|---|---|---|
|
||||||
- installerer Docker, hvis det mangler;
|
| `HOST_BIND`, `KNOWN_PROXY` | (findes allerede til hubben) | DMZ-adresse og reverse proxyens adresse |
|
||||||
- kloner `discourse_docker` til `/var/discourse` og kopierer `app.yml` på plads;
|
| `FORUM_PORT` | `8082` | Port på DMZ-adressen |
|
||||||
- kører `./launcher rebuild app`. Det tager 10-15 minutter.
|
| `FORUM_ADMIN_EMAIL` | din adresse | Bliver administrator ved tilmelding |
|
||||||
6. **Administrator:** Åbn forummet, og opret en bruger med en af adresserne i `DISCOURSE_DEVELOPER_EMAILS`. Den bliver administrator.
|
| `FORUM_SMTP_ADDRESS`, `FORUM_SMTP_USER`, `FORUM_SMTP_PASSWORD` | | Mailserver (STARTTLS) |
|
||||||
7. **Opsætning som kode:** Lav en midlertidig API-nøgle (scriptet skriver kommandoen), og kør:
|
| `FORUM_SMTP_PORT` | `587` | Valgfri |
|
||||||
|
| `FORUM_NOTIFICATION_EMAIL` | `forum@seosoft.dk` | Valgfri, ellers `FORUM_SMTP_USER` |
|
||||||
|
| `FORUM_WORKERS`, `FORUM_DB_SHARED_BUFFERS` | `4`, `512MB` | Valgfri: ca. 2 pr. CPU-kerne og 25 % af den RAM, Discourse må bruge |
|
||||||
|
|
||||||
|
4. **Reverse proxy:** Peg `forum.sqlinsight.seosoft.dk` på `http://HOST_BIND:FORUM_PORT`.
|
||||||
|
5. **Installation:** Kør *Actions → deploy forum → Run workflow*. Første gang tager det 10-15 minutter. Det samme workflow bruges, når `app.yml` eller secrets ændres. Selve Discourse opdateres fra */admin/upgrade*.
|
||||||
|
6. **Administrator:** Åbn forummet, og opret en bruger med `FORUM_ADMIN_EMAIL`. Den bliver administrator.
|
||||||
|
7. **Opsætning som kode:** Lav en midlertidig API-nøgle på appserveren:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker exec app su discourse -c 'cd /var/www/discourse && RAILS_ENV=production bundle exec rake api_key:create_master[forum-setup]'
|
||||||
|
```
|
||||||
|
|
||||||
|
Kør derefter:
|
||||||
|
|
||||||
```
|
```
|
||||||
DISCOURSE_URL=https://forum.sqlinsight.seosoft.dk DISCOURSE_API_KEY=... \
|
DISCOURSE_URL=https://forum.sqlinsight.seosoft.dk DISCOURSE_API_KEY=... \
|
||||||
COMMUNITY_URL=https://sqlinsight.seosoft.dk CONTACT_EMAIL=support@... \
|
CONTACT_EMAIL=support@... python3 deploy/forum/configure.py
|
||||||
python3 deploy/forum/configure.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`configure.py` gør følgende:
|
`configure.py` gør følgende:
|
||||||
|
|
@ -41,8 +59,10 @@ Discourse installeres med den officielle `discourse_docker`-launcher. Det er den
|
||||||
|
|
||||||
Scriptet kan køres igen. Det ændrer kun det, der afviger. Slet API-nøglen bagefter under *Admin → API*.
|
Scriptet kan køres igen. Det ændrer kun det, der afviger. Slet API-nøglen bagefter under *Admin → API*.
|
||||||
|
|
||||||
|
Uden Forgejo: Eksportér de samme værdier (`FORUM_BIND` i stedet for `HOST_BIND`), og kør `sudo -E deploy/forum/install.sh` på serveren.
|
||||||
|
|
||||||
**Drift:**
|
**Drift:**
|
||||||
- Opdatering: `cd /var/discourse && git pull && ./launcher rebuild app`, eller via */admin/upgrade*.
|
- Opdatering: via */admin/upgrade*. Discourses basisimage opdateres ved at køre `deploy-forum.yml` igen.
|
||||||
- Backup: Discourse tager dagligt backup til `/var/discourse/shared/standalone/backups`. Slå upload til S3 eller kopiering væk fra serveren til under *Admin → Backups*.
|
- Backup: Discourse tager dagligt backup til `/var/discourse/shared/standalone/backups`. Slå upload til S3 eller kopiering væk fra serveren til under *Admin → Backups*.
|
||||||
- Logge: `./launcher logs app`.
|
- Logge: `./launcher logs app`.
|
||||||
|
|
||||||
|
|
@ -60,16 +80,19 @@ Webstedet er en lille ASP.NET Core-applikation uden database. Det viser:
|
||||||
- **Dokumentation:** Filerne i `docs/`, bygget ind i programmet. Links mellem dokumenterne bliver til sider, og links til andre filer i repositoriet går til Forgejo. HTML i Markdown vises ikke.
|
- **Dokumentation:** Filerne i `docs/`, bygget ind i programmet. Links mellem dokumenterne bliver til sider, og links til andre filer i repositoriet går til Forgejo. HTML i Markdown vises ikke.
|
||||||
- **Forum:** De seneste emner fra forummets offentlige `/latest.json`. Hvis forummet er nede, virker webstedet stadig.
|
- **Forum:** De seneste emner fra forummets offentlige `/latest.json`. Hvis forummet er nede, virker webstedet stadig.
|
||||||
|
|
||||||
Kør det med Docker på sin egen server. Caddy henter og fornyr selv TLS-certifikatet:
|
`deploy-community.yml` bygger og starter webstedet ved hvert push til `main`, der ændrer kode, dokumentation eller regler. Det kræver tre secrets:
|
||||||
|
- `HOST_BIND` (findes allerede);
|
||||||
|
- `COMMUNITY_PORT`, fx `8081`;
|
||||||
|
- `COMMUNITY_CONTACT`: adressen til henvendelser om supportaftaler.
|
||||||
|
|
||||||
```
|
Peg `sqlinsight.seosoft.dk` på `http://HOST_BIND:COMMUNITY_PORT` i reverse proxyen.
|
||||||
cd deploy/community
|
|
||||||
COMMUNITY_HOST=sqlinsight.seosoft.dk COMMUNITY_CONTACT=salg@... docker compose up -d
|
Webstedet læser hubbens feed-mappe (`/opt/sqlinsight-hub/data/feed`) direkte og skrivebeskyttet. Download-links går til `https://hub.sqlinsight.seosoft.dk/updates/`. Dermed afhænger webstedet ikke af, at appserveren kan nå sin egen offentlige adresse. Forumemnerne på forsiden hentes derimod fra forummets offentlige adresse. Kan appserveren ikke nå den, vises forsiden bare uden emner.
|
||||||
```
|
|
||||||
|
|
||||||
| Indstilling (`SQLInsight:Community:…`) | Standard | Betydning |
|
| Indstilling (`SQLInsight:Community:…`) | Standard | Betydning |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `Feed` | (ingen) | Det signerede opdateringsfeed. Normalt hubbens `https://<hub>/updates/`, eller en mappe. Uden feed vises ingen udgivelser, og versionssiden bruger den indbyggede regelpakke. |
|
| `Feed` | (ingen) | Det signerede opdateringsfeed: en mappe eller en https-adresse. Uden feed vises ingen udgivelser, og versionssiden bruger den indbyggede regelpakke. |
|
||||||
|
| `DownloadUrl` | `Feed` | Offentlig adresse til download-links, når `Feed` er en mappe eller en intern adresse. |
|
||||||
| `ForumUrl` | (ingen) | Forummets adresse. Bruges til links, `/forum` og de seneste emner. |
|
| `ForumUrl` | (ingen) | Forummets adresse. Bruges til links, `/forum` og de seneste emner. |
|
||||||
| `SourceUrl` | Forgejo-repositoriet | Link til kildekoden og til filer, som dokumentationen henviser til. |
|
| `SourceUrl` | Forgejo-repositoriet | Link til kildekoden og til filer, som dokumentationen henviser til. |
|
||||||
| `ContactEmail` | `salg@example.com` | Adressen på supportsiden. Skal sættes. |
|
| `ContactEmail` | `salg@example.com` | Adressen på supportsiden. Skal sættes. |
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,12 @@ public sealed class CommunityOptions
|
||||||
/// <summary>The signed update feed (https or folder), where releases and rule packs are published.</summary>
|
/// <summary>The signed update feed (https or folder), where releases and rule packs are published.</summary>
|
||||||
public string? Feed { get; set; }
|
public string? Feed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Public address of the feed, for download links, when <see cref="Feed"/> is a folder or an internal address
|
||||||
|
/// (e.g. https://hub.sqlinsight.seosoft.dk/updates/). Defaults to <see cref="Feed"/> when that is http(s).
|
||||||
|
/// </summary>
|
||||||
|
public Uri? DownloadUrl { get; set; }
|
||||||
|
|
||||||
/// <summary>The Discourse forum, e.g. https://forum.sqlinsight.seosoft.dk/.</summary>
|
/// <summary>The Discourse forum, e.g. https://forum.sqlinsight.seosoft.dk/.</summary>
|
||||||
public Uri? ForumUrl { get; set; }
|
public Uri? ForumUrl { get; set; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,11 @@ public sealed partial class ReleaseService(IOptions<CommunityOptions> options, I
|
||||||
public Uri? DownloadUrl(FileRef file)
|
public Uri? DownloadUrl(FileRef file)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(file);
|
ArgumentNullException.ThrowIfNull(file);
|
||||||
return Uri.TryCreate(options.Value.Feed, UriKind.Absolute, out var root) && root.Scheme is "https" or "http"
|
var root = options.Value.DownloadUrl
|
||||||
? new Uri(new Uri(root.AbsoluteUri.EndsWith('/') ? root.AbsoluteUri : root.AbsoluteUri + "/"), file.Path)
|
?? (Uri.TryCreate(options.Value.Feed, UriKind.Absolute, out var feed) && feed.Scheme is "https" or "http" ? feed : null);
|
||||||
: null;
|
return root is null
|
||||||
|
? null
|
||||||
|
: new Uri(new Uri(root.AbsoluteUri.EndsWith('/') ? root.AbsoluteUri : root.AbsoluteUri + "/"), file.Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<FeedState> LoadAsync(CancellationToken cancellationToken)
|
private async Task<FeedState> LoadAsync(CancellationToken cancellationToken)
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,17 @@ public sealed class SiteTests : IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task A_folder_feed_links_downloads_to_the_public_address()
|
||||||
|
{
|
||||||
|
_feed.Publish("stable", release: _feed.MsiRelease("1.2.0"));
|
||||||
|
using var factory = Site(downloadUrl: "https://hub.example/updates/");
|
||||||
|
|
||||||
|
var html = await Get(factory, "/download");
|
||||||
|
|
||||||
|
Assert.Contains("href=\"https://hub.example/updates/releases/SQLInsight-Pro-1.2.0-x64.msi\"", html, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task A_manifest_with_a_bad_signature_is_not_shown()
|
public async Task A_manifest_with_a_bad_signature_is_not_shown()
|
||||||
{
|
{
|
||||||
|
|
@ -181,7 +192,7 @@ public sealed class SiteTests : IDisposable
|
||||||
Assert.Equal(new Uri("https://forum.example/"), response.Headers.Location);
|
Assert.Equal(new Uri("https://forum.example/"), response.Headers.Location);
|
||||||
}
|
}
|
||||||
|
|
||||||
private WebApplicationFactory<Program> Site(bool trustKey = true)
|
private WebApplicationFactory<Program> Site(bool trustKey = true, string? downloadUrl = null)
|
||||||
{
|
{
|
||||||
var settings = new Dictionary<string, string?>
|
var settings = new Dictionary<string, string?>
|
||||||
{
|
{
|
||||||
|
|
@ -189,6 +200,11 @@ public sealed class SiteTests : IDisposable
|
||||||
["SQLInsight:Community:ForumUrl"] = "https://forum.example/",
|
["SQLInsight:Community:ForumUrl"] = "https://forum.example/",
|
||||||
["SQLInsight:Community:SourceUrl"] = "https://git.example/seo/sqlinsight-pro",
|
["SQLInsight:Community:SourceUrl"] = "https://git.example/seo/sqlinsight-pro",
|
||||||
};
|
};
|
||||||
|
if (downloadUrl is not null)
|
||||||
|
{
|
||||||
|
settings["SQLInsight:Community:DownloadUrl"] = downloadUrl;
|
||||||
|
}
|
||||||
|
|
||||||
if (trustKey)
|
if (trustKey)
|
||||||
{
|
{
|
||||||
settings[$"SQLInsight:Community:TrustedKeys:{TestFeed.KeyId}"] = _feed.PublicKey;
|
settings[$"SQLInsight:Community:TrustedKeys:{TestFeed.KeyId}"] = _feed.PublicKey;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue