2 Commits

Author SHA1 Message Date
default
ec1eb83814 new: [add] new charm keycloak 2024-06-26 12:31:00 +02:00
default
b24dc346bb new: [outline] add new charm 2024-05-21 11:04:48 +02:00
33 changed files with 219 additions and 774 deletions

View File

@@ -1,66 +0,0 @@
# -*- ispell-local-dictionary: "english" -*-
* Info
From: Carbone https://hub.docker.com/r/carbone/carbone-ee#running-carbone-community-edition-forever-free
Upstream documentation:
- On-premise configuration: https://carbone.io/documentation/developer/on-premise-installation/configuration.html
- Template management: https://carbone.io/documentation/developer/on-premise-installation/template-management.html
- Studio Web Component: https://carbone.io/documentation/developer/embedding/studio-web-component.html
* What this charm enables
By default Carbone starts in the *stateless* mode: it only renders the
template sent with each request, exposes no Studio and keeps no template
metadata. The =init= hook turns it into the *stateful* service, which is
what the charm is meant to provide:
- =CARBONE_STUDIO=true= serves the Studio web interface and the
=/carbone-studio.js= Web Component.
- =CARBONE_TEMPLATE_MANAGEMENT=true= enables stable 64-bit template IDs,
versioning, the =GET/POST/PATCH /template= endpoints and the
=embedded-versioning= Studio mode.
- =CARBONE_BIND=0.0.0.0= makes Carbone listen on all interfaces so the
=web-proxy= can reach it (the upstream default is =127.0.0.1=).
* Usage
ex :
#+begin_src yaml
carbone:
relations:
web-proxy:
frontend:
domain: carbone.dev1.elabore.coop
#+end_src
* Persistence
| Path | Resource type | Content |
|-----------------+------------------+------------------------------------------------------|
| =/app/template= | =data-resources= | Templates and the metadata store (metadata.db) |
| =/app/config= | =config-resources= | Optional license, authentication public key (key.pub) |
Templates and their versioning metadata both live under =/app/template=,
so they survive redeploys and are covered by the =backup= relation.
* Enterprise license
Carbone runs fine without a license (Community Edition, forever free).
Advanced features (dynamic images/colors, barcodes, charts, HTML
aggregation, PDF operations, ...) require an Enterprise license.
To enable it, drop your =*.carbone-license= file into the config store
(=/app/config= on the host side, i.e.
=$SERVICE_CONFIGSTORE/app/config=) and redeploy the service.
* API authentication
Authentication is *disabled by default*: the API is reachable as soon as
the service is up, which is the expected setup behind the =web-proxy=.
If you ever need an API key (JWT), enable =CARBONE_AUTHENTICATION=true=,
generate an EC keypair with =generate-keys=, expose the public key as
=/app/config/key.pub= and generate a token with =generate-token=. See
the upstream "Deploy with Docker" documentation for the exact commands.

View File

@@ -1,47 +0,0 @@
#!/bin/bash
## Init is run on host
## For now it is run every time the script is launched, but
## it should be launched only once after build.
## Accessible variables are:
## - SERVICE_NAME Name of current service
## - DOCKER_BASE_IMAGE Base image from which this service might be built if any
## - SERVICE_DATASTORE Location on host of the DATASTORE of this service
## - SERVICE_CONFIGSTORE Location on host of the CONFIGSTORE of this service
set -e
## The image runs as the unprivileged "carbone" user, while the datastore
## and configstore bind mounts are created by root. Both /app/template
## (templates + metadata.db) and /app/config (config.json, keys, license)
## must be writable by that user, otherwise the container crash-loops with
## "EACCES: permission denied, open '/app/config/config.json'".
uid_gid=($(docker_get_uid_gid "$SERVICE_NAME" "carbone" "carbone")) || exit 1
uid="${uid_gid[0]}"
gid="${uid_gid[1]}"
mkdir -p "$SERVICE_DATASTORE/app/template" "$SERVICE_CONFIGSTORE/app/config"
chown "$uid:$gid" "$SERVICE_DATASTORE/app/template" "$SERVICE_CONFIGSTORE/app/config"
## Carbone starts in stateless mode (document generation only) and binds
## to 127.0.0.1 by default (see `carbone webserver --help`, v5.15.1).
## Inside Docker it must:
## - listen on all interfaces so the web-proxy can reach it (CARBONE_BIND) ;
## - serve the Studio web interface and /carbone-studio.js (CARBONE_STUDIO) ;
## - enable stateful template management: stable 64-bit template IDs,
## versioning and the GET/POST/PATCH /template endpoints
## (CARBONE_TEMPLATE_MANAGEMENT).
##
## CARBONE_TEMPLATE_METADATA_FLUSH_CRON defaults to once a day; flushing
## every 5 minutes shrinks the window where a hard crash (SIGKILL, OOM)
## would lose template metadata. The graceful-shutdown flush remains the
## safety net for redeploys (see docker-compose.stop_grace_period).
init-config-add "
$SERVICE_NAME:
environment:
CARBONE_BIND: \"0.0.0.0\"
CARBONE_STUDIO: \"true\"
CARBONE_TEMPLATE_MANAGEMENT: \"true\"
CARBONE_TEMPLATE_METADATA_FLUSH_CRON: \"*/5 * * * *\"
"

View File

@@ -1,35 +0,0 @@
## From carbone/carbone-ee:full-5.15.1
docker-image: docker.0k.io/carbone-ee:5.15.1
docker-compose:
## Carbone flushes its template metadata to disk on graceful shutdown.
## Docker's default 10s stop timeout is shorter than Carbone's shutdown
## sequence (exitQuietPeriod + flush), so a plain redeploy would SIGKILL
## it and lose the templates/versioning. Keep this above Carbone's
## exitDeadline (default 3 min).
stop_grace_period: 200s
data-resources:
## Templates and the template-management metadata store (metadata.db)
## both live here, so they survive redeploys and are covered by backup.
- /app/template
config-resources:
## Holds the optional Enterprise license (*.carbone-license) and the
## authentication public key (key.pub). See README.org.
- /app/config
uses:
web-proxy:
#constraint: required | recommended | optional
#auto: pair | summon | none ## default: pair
constraint: recommended
auto: pair
solves:
proxy: "Public access"
default-options:
target: !var-expand ${MASTER_BASE_SERVICE_NAME}:4000
backup:
constraint: recommended
auto: pair
solves:
backup: "Automatic regular backup"
default-options:

View File

@@ -1,2 +0,0 @@
carbone-test:
charm: carbone

View File

@@ -1,7 +0,0 @@
# -*- ispell-local-dictionary: "english" -*-
* Info
This charm is provided to work with plausible charm
* Usage

View File

@@ -1,21 +0,0 @@
#!/bin/bash
## Init is run on host
## For now it is run every time the script is launched, but
## it should be launched only once after build.
## Accessible variables are:
## - SERVICE_NAME Name of current service
## - DOCKER_BASE_IMAGE Base image from which this service might be built if any
## - SERVICE_DATASTORE Location on host of the DATASTORE of this service
## - SERVICE_CONFIGSTORE Location on host of the CONFIGSTORE of this service
set -e
init-config-add "
$SERVICE_NAME:
environment:
CLICKHOUSE_SKIP_USER_SETUP: 1
healthcheck:
test: [ \"CMD-SHELL\", \"wget --no-verbose --tries=1 -O - http://127.0.0.1:8123/ping || exit 1\" ]
"

View File

@@ -1,21 +0,0 @@
docker-image: docker.0k.io/clickhouse:24.12-alpine
#docker-image: clickhouse/clickhouse-server:24.12-alpine
data-resources:
- /var/lib/clickhouse
- /var/log/clickhouse-server
charm-resources:
- /etc/clickhouse-server/config.d/logs.xml
- /etc/clickhouse-server/config.d/ipv4-only.xml
- /etc/clickhouse-server/config.d/low-resources.xml
provides:
event-db:
uses:
log-rotate:
constraint: recommended
auto: pair
solves:
disk-leak: "/var/log/clickhouse-server"

View File

@@ -1,3 +0,0 @@
<clickhouse>
<listen_host>0.0.0.0</listen_host>
</clickhouse>

View File

@@ -1,28 +0,0 @@
<clickhouse>
<logger>
<level>warning</level>
<console>true</console>
</logger>
<query_log replace="1">
<database>system</database>
<table>query_log</table>
<flush_interval_milliseconds>7500</flush_interval_milliseconds>
<engine>
ENGINE = MergeTree
PARTITION BY event_date
ORDER BY (event_time)
TTL event_date + interval 30 day
SETTINGS ttl_only_drop_parts=1
</engine>
</query_log>
<!-- Stops unnecessary logging -->
<metric_log remove="remove" />
<asynchronous_metric_log remove="remove" />
<query_thread_log remove="remove" />
<text_log remove="remove" />
<trace_log remove="remove" />
<session_log remove="remove" />
<part_log remove="remove" />
</clickhouse>

View File

@@ -1,23 +0,0 @@
<!-- https://clickhouse.com/docs/en/operations/tips#using-less-than-16gb-of-ram -->
<clickhouse>
<!--
https://clickhouse.com/docs/en/operations/server-configuration-parameters/settings#mark_cache_size -->
<mark_cache_size>524288000</mark_cache_size>
<profile>
<default>
<!-- https://clickhouse.com/docs/en/operations/settings/settings#max_threads -->
<max_threads>1</max_threads>
<!-- https://clickhouse.com/docs/en/operations/settings/settings#max_block_size -->
<max_block_size>8192</max_block_size>
<!-- https://clickhouse.com/docs/en/operations/settings/settings#max_download_threads -->
<max_download_threads>1</max_download_threads>
<!--
https://clickhouse.com/docs/en/operations/settings/settings#input_format_parallel_parsing -->
<input_format_parallel_parsing>0</input_format_parallel_parsing>
<!--
https://clickhouse.com/docs/en/operations/settings/settings#output_format_parallel_formatting -->
<output_format_parallel_formatting>0</output_format_parallel_formatting>
</default>
</profile>
</clickhouse>

View File

@@ -0,0 +1,32 @@
Description
===========
Using ``keycloak`` version 24.0
DEV info : https://www.keycloak.org/server/containers
Usage
=====
To start with ``keycloak``, just put this service in your
``compose.yml``::
keycloak:
options:
admin-password: CHANGEME
relations:
web-proxy:
frontend:
domain: id.mydomain.fr
Customize theme
===============
You can customize theme by putting your theme in
``/srv/datastore/data/keycloak/opt/keycloak/themes``
For example copy the material folder from
https://github.com/MAXIMUS-DeltaWare/material-keycloak-theme and
restart ``keycloak``.
Then go to your admin console, log in and go to the realm/themes part
to choose you new theme

View File

@@ -0,0 +1,15 @@
#FROM keycloak/keycloak:24.0.4 as builder
#
#ENV KC_METRICS_ENABLED=true
#ENV KC_FEATURES=token-exchange
#ENV KC_DB=postgres
#
#WORKDIR /opt/keycloak
## for demonstration purposes only, please make sure to use proper certificates in production instead
#RUN /opt/keycloak/bin/kc.sh build
FROM keycloak/keycloak:24.0.4
#COPY --from=builder /opt/keycloak/ /opt/keycloak/
WORKDIR /opt/keycloak
ENV KC_LOG_LEVEL=INFO
ENTRYPOINT ["/opt/keycloak/bin/kc.sh", "start", "--optimized"]

View File

@@ -0,0 +1,16 @@
FROM docker.0k.io/keycloak:17.0.1 as builder
ENV KC_METRICS_ENABLED=true
ENV KC_FEATURES=token-exchange
ENV KC_DB=postgres
RUN /opt/keycloak/bin/kc.sh build
FROM builder as inspector
ENTRYPOINT ["ls", "-l", "/opt/keycloak/lib/"]
#FROM docker.0k.io/keycloak:17.0.0
#COPY --from=builder /opt/keycloak/lib/quarkus/ /opt/keycloak/lib/quarkus/
#WORKDIR /opt/keycloak
#ENV KC_LOG_LEVEL=INFO
#ENTRYPOINT ["/opt/keycloak/bin/kc.sh", "start"]

12
keycloak-elabore/hooks/init Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
set -e
admin_password=$(options-get admin-password) || exit 1
init-config-add "\
$MASTER_BASE_SERVICE_NAME:
environment:
KEYCLOAK_ADMIN: \"admin\"
KEYCLOAK_ADMIN_PASSWORD: \"$admin_password\"
"

View File

@@ -6,12 +6,12 @@ PASSWORD="$(relation-get password)"
USER="$(relation-get user)"
DBNAME="$(relation-get dbname)"
config-add "\
services:
$MASTER_BASE_SERVICE_NAME:
environment:
DATABASE_URL: postgres://$USER:$PASSWORD@$TARGET_SERVICE_NAME:5432/$DBNAME
KC_DB_URL: \"jdbc:postgresql://$MASTER_TARGET_SERVICE_NAME:5432/$DBNAME\"
KC_DB_USERNAME: \"$USER\"
KC_DB_PASSWORD: \"$PASSWORD\"
KC_DB: \"postgres\"
"
info "Configured $SERVICE_NAME code for $TARGET_SERVICE_NAME access."

View File

@@ -0,0 +1,21 @@
#!/bin/bash
. lib/common
DOMAIN=$(relation-get domain) || exit 1
#IP_HOST=$(hostname -I | awk '{print $1}')
set -e
keycloak:generate-key-if-not-exist "$DOMAIN"
config-add "\
services:
$MASTER_BASE_SERVICE_NAME:
environment:
KC_HOSTNAME: \"$DOMAIN\"
KC_PROXY: edge
KC_HTTP_ENABLED: \"true\"
KC_HOSTNAME_STRICT: \"false\"
"

View File

@@ -0,0 +1,46 @@
# -*- mode: bash -*-
KEYCLOAK_DIR=/opt/keycloak
DATASTORE_KEYCLOAK_DIR="$SERVICE_DATASTORE$KEYCLOAK_DIR"
HOST_DATASTORE_KEYCLOAK_DIR="$HOST_DATASTORE/$SERVICE_NAME$KEYCLOAK_DIR"
keycloak:generate-key-if-not-exist() {
local domain="$1" ip_host
[ -d "$DATASTORE_KEYCLOAK_DIR" ] && return 0
ip_host=$(set -o pipefail; getent ahostsv4 "$domain" | head -n 1 | cut -f 1 -d " ") || {
err "Couldn't resolve to ipv4 domain name '$domain'."
return 1
}
info "Resolved successfully '$domain' to ip '$ip_host'."
debug "DOCKER_BASE_IMAGE: $DOCKER_BASE_IMAGE"
debug "HOST_DATASTORE_KEYCLOAK_DIR:: $HOST_DATASTORE_KEYCLOAK_DIR"
mkdir -p "$DATASTORE_KEYCLOAK_DIR/conf" || return 0
docker_image_export_dir "$DOCKER_BASE_IMAGE" "/opt/keycloak" "$SERVICE_DATASTORE/opt" || return 1
uid=$(docker_get_uid "$SERVICE_NAME" "keycloak") || return 1
chown "$uid" "$DATASTORE_KEYCLOAK_DIR" -R
debug "DATASTORE_KEYCLOAK_DIR_LS:: $(ls $DATASTORE_KEYCLOAK_DIR)"
docker run -w /opt/keycloak \
-v "$HOST_DATASTORE_KEYCLOAK_DIR":"/opt/keycloak" \
--entrypoint bash \
"$DOCKER_BASE_IMAGE" -c "
export KC_METRICS_ENABLED=true
export KC_FEATURES=token-exchange
export KC_DB=postgres
keytool -genkeypair -storepass password \
-storetype PKCS12 -keyalg RSA \
-keysize 2048 -dname 'CN=$domain' \
-alias server -ext 'SAN:c=DNS:$domain,IP:$ip_host' \
-keystore conf/server.keystore || exit 1
echo 'Generated key'
/opt/keycloak/bin/kc.sh build
" || {
rmdir "$DATASTORE_KEYCLOAK_DIR/conf" 2>/dev/null
rmdir "$DATASTORE_KEYCLOAK_DIR" 2>/dev/null
return 1
}
}

View File

@@ -0,0 +1,24 @@
data-resources:
- /opt/keycloak
default-options:
uses:
web-proxy:
#constraint: required | recommended | optional
#auto: pair | summon | none ## default: pair
constraint: required
auto: pair
solves:
proxy: "Public access"
default-options:
target: !var-expand ${MASTER_BASE_SERVICE_NAME}:8080
postgres-database:
#constraint: required | recommended | optional
#auto: pair | summon | none ## default: pair
constraint: required
auto: summon
solves:
database: "main storage"
default-options:

View File

@@ -1,17 +0,0 @@
#!/bin/bash
set -e
DOMAIN=$(relation-get domain) || {
echo "Failed to get domain"
exit 1
}
config-add "\
services:
$MASTER_BASE_SERVICE_NAME:
environment:
N8N_HOST: \"${DOMAIN}\"
WEBHOOK_URL: \"https:\/\/${DOMAIN}\"
"

View File

@@ -1,4 +1,4 @@
docker-image: docker.0k.io/n8n:1.45.1
docker-image: docker.n8n.io/n8nio/n8n:1.23.0
uses:
postgres-database:
@@ -22,15 +22,6 @@ uses:
proxy: "Public access"
default-options:
target: !var-expand ${MASTER_BASE_SERVICE_NAME}:5678
apache-custom-rules:
- !var-expand |
## Use RewriteEngine to handle WebSocket connection upgrades
RewriteEngine On
RewriteCond %{HTTP:Upgrade} ^websocket$ [NC]
RewriteCond %{HTTP:Connection} Upgrade [NC]
RewriteRule /(.*)\$ ws://${MASTER_BASE_SERVICE_NAME}:5678/\$1 [P,L]
backup:
constraint: recommended
auto: pair

View File

@@ -12,9 +12,8 @@ Config info: https://github.com/outline/outline/blob/main/.env.sample
Odoo config: if you configure odoo OIDC connector, the callback url
should be like this : https://<YOUR_OUTLINE>:443/auth/oidc.callback
#Requires a =smtp-server= provider to be functional, you can use
#=smtp-stub= charm to provide information to externally managed =SMTP=.
Requires a =smtp-server= provider to be functional, you can use
=smtp-stub= charm to provide information to externally managed =SMTP=.
#+begin_src yaml
outline:
@@ -27,58 +26,16 @@ outline:
oidc-user-info-uri: #the user info uri of your OIDC provider
oidc-logout-uri: #the login uri of your OIDC provider
#smtp-stub:
# options:
# host: smtp.myhost.com
# port: 465
# connection-security: "ssl/tls"
# auth-method: password #IMPORTANT: if not present login password doesn’t work
# login: myuser
# password: myp4ssw0rd
smtp-stub:
options:
host: smtp.myhost.com
port: 465
connection-security: "ssl/tls"
auth-method: password #IMPORTANT: if not present login password doesn’t work
login: myuser
password: myp4ssw0rd
#+end_src
** Odoo 14
We monkey-patch odoo in order to make it work, be sure to use latest version in 14.0 of galicea openIDConnection module
* Database ownership alignment
The =pre_deploy= hook ensures that every object of the database
(tables, sequences, views, materialized views, standalone types,
functions, procedures) is owned by the application role before the
container starts and runs its migrations.
Historical provisioning or restores executed as the =postgres=
superuser leave objects owned by =postgres=, which makes any later
=ALTER= on these objects fail with "must be owner of ..." and puts
outline in a crash-loop at migration time. This was seen on
2026-09-11 when upgrading elabore.coop from 1.6.1 to 1.10.0:
migration =20260714000000-add-mcp-to-search-queries-source.js=
failed on =enum_search_queries_source=. The same drift was found
on every managed server (lokavaluto.fr, lagemme.org, moneko.org).
Extensions are excluded from the realignment (they are managed by
the =postgres= charm). The hook is idempotent and silent when
there is no drift, and blocks the deployment (=exit 1=) if the
realignment fails, so the problem is visible at deploy time instead
of as a cryptic crash-loop.
* Datastore ownership alignment
The =init= hook aligns the ownership of the service datastore with
the user the Outline container runs as. Since 1.10.0 the image runs
as the unprivileged =nodejs= user (older images, up to 1.6.1, ran as
=root=), while the datastore is provisioned by =root=. Without
realignment the application cannot write its =uploads=, =public= and
=avatars= buckets and every attachment upload fails with "Permission
denied writing to ... Check the host machine file system
permissions". This was seen on 2026-09-11 on elabore.coop after the
1.6.1 to 1.10.0 upgrade, on every existing datastore.
The hook reads the user from the image's =Config.User=, so it stays
version-agnostic: images running as =root= are left untouched, and
re-running the hook on an already aligned datastore is a no-op.
* Building a new image
@@ -88,9 +45,33 @@ We use the official image with an added patch due to 2 bugs:
Note that a PR was pushed with a fix on the first bug. But this was not yet tested.
The fix are on 1.6.1
These fix are on 0.76.0
** Fix
** First fix
We need to add "url.port = '';" in ~build/server/middlewares/passport.js~ to remove the port. Note that this is a bad fix but works for our setup.
#+begin_src bash
IMAGE=docker.0k.io/outline:0.76.0-elabore
echo 'apk add patch bash' | dupd -u "$IMAGE" -- -u 1
cat <<'EOF1' | dupd -u "$IMAGE" -- -u 0
patch -p 1 <<'EOF2'
--- a/build/server/middlewares/passport.js
+++ b/build/server/middlewares/passport.js
@@ -40,6 +40,7 @@
const requestHost = ctx.get("host");
const url = new URL("".concat(reqProtocol, "://").concat(requestHost).concat(redirectUrl));
url.host = host;
+ url.port = '';
return ctx.redirect("".concat(url.toString()).concat(hasQueryString ? "&" : "?", "notice=").concat(notice));
}
if (_env.default.isDevelopment) {
EOF2
EOF1
#+end_src
** Second fix
Upon calling "/oidc" url, outline will return "Set-Cookie" header
with a "domain:" value that is incorrect (still the inner docker
@@ -103,14 +84,13 @@ The patches will change the "build/" files, so this is a very temporary and brit
#+begin_src bash
IMAGE=docker.0k.io/outline:1.6.1-elabore
IMAGE=docker.0k.io/outline:0.76.0-elabore
echo 'apt update && apt install patch' | dupd -u "$IMAGE" -- -u 0
cat <<'EOF1' | dupd -u "$IMAGE" -- -u 0
patch -p 1 <<'EOF2'
--- a/build/server/utils/passport.js.orig
+++ b/build/server/utils/passport.js
@@ -56,7 +56,7 @@
@@ -37,7 +37,7 @@
const state = buildState(host, token, client);
ctx.cookies.set(this.key, state, {
expires: (0, _dateFns.addMinutes)(new Date(), 10),
@@ -119,7 +99,7 @@ patch -p 1 <<'EOF2'
});
callback(null, token);
});
@@ -73,7 +73,7 @@
@@ -53,7 +53,7 @@
// Destroy the one-time pad token and ensure it matches
ctx.cookies.set(this.key, "", {
expires: (0, _dateFns.subMinutes)(new Date(), 1),

View File

@@ -47,7 +47,6 @@ oidc_logout_uri=$(options-get oidc-logout-uri) || exit 1
init-config-add "
$SERVICE_NAME:
restart: unless-stopped
volumes:
- $SERVICE_DATASTORE:/var/lib/outline/data
environment:
@@ -63,50 +62,12 @@ $SERVICE_NAME:
OIDC_LOGOUT_URI: \"$oidc_logout_uri\"
OIDC_SCOPES: \"openid\"
OIDC_USERNAME_CLAIM: \"preferred_username\"
OIDC_DISPLAY_NAME: \"OpenID Connect\"
NODE_ENV: \"production\"
LOG_LEVEL: \"debug\"
FORCE_HTTPS: \"false\"
FILE_STORAGE: \"local\"
#DEVELOPMENT_UNSAFE_INLINE_CSP: \"true\"
#DEBUG: \"http\"
DEBUG: \"http\"
"
## The datastore is bind-mounted into the container. Outline runs as
## an unprivileged user (image Config.User: root up to 1.6.1, "nodejs"
## since 1.10.0) and must write its uploads, public and avatars
## buckets. Provisioned by root, the datastore is not writable by
## that user and every upload fails with "Permission denied writing
## to ... Check the host machine file system permissions". Align the
## datastore ownership with the image user; skip images running as
## root. See README.org, "Datastore ownership alignment".
app_user=
if [ -n "$DOCKER_BASE_IMAGE" ]; then
app_user=$(docker image inspect "$DOCKER_BASE_IMAGE" \
--format '{{.Config.User}}') || exit 1
fi
case "$app_user" in
""|0|0:0|root)
## image runs as root: nothing to align
;;
*:*)
uid="${app_user%%:*}"
gid="${app_user#*:}"
;;
*)
uid_gid=($(docker_get_uid_gid "$SERVICE_NAME" "$app_user" "$app_user")) || exit 1
uid="${uid_gid[0]}"
gid="${uid_gid[1]}"
;;
esac
if [ -n "${uid:-}" ]; then
mkdir -p "$SERVICE_DATASTORE"
chown -R "$uid:$gid" "$SERVICE_DATASTORE" || {
err "Failed to align datastore ownership on '$uid:$gid'."
exit 1
}
info "Datastore ownership aligned on '$uid:$gid'."
fi

View File

@@ -1,150 +0,0 @@
#!/bin/bash
## Should be executable N time in a row with same result.
##
## Ensure the outline application role owns every object of its
## database before the container boots and runs its migrations.
##
## Historical provisioning or restores executed as the "postgres"
## superuser leave objects owned by "postgres", which makes any
## later ALTER on these objects fail with "must be owner of ..."
## and puts outline in a crash-loop at migration time. See
## README.org, section "Database ownership alignment".
. lib/common
set -e
relation="postgres-database"
db_role=$(outline:named-relation-get "$relation" user) || {
err "Couldn't get ${WHITE}user${NORMAL} value" \
"in ${DARKCYAN}$relation${NORMAL} relation's data."
exit 1
}
## List every object of schema "public" not owned by the application
## role: tables, sequences, views, materialized views, standalone
## types and functions. Extensions are excluded (managed by the
## postgres charm).
audit_query="SET app.role = '$db_role';
SELECT obj FROM (
SELECT CASE c.relkind
WHEN 'S' THEN 'sequence '
WHEN 'v' THEN 'view '
WHEN 'm' THEN 'matview '
ELSE 'table '
END || c.relname AS obj
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public'
AND c.relkind IN ('r','p','S','v','m')
AND pg_get_userbyid(c.relowner) <> current_setting('app.role')
UNION ALL
SELECT 'type ' || t.typname AS obj
FROM pg_type t
JOIN pg_namespace n ON n.oid = t.typnamespace
WHERE n.nspname = 'public'
AND t.typtype IN ('e','d','c')
AND NOT EXISTS (SELECT 1 FROM pg_class c WHERE c.reltype = t.oid)
AND pg_get_userbyid(t.typowner) <> current_setting('app.role')
UNION ALL
SELECT CASE p.prokind
WHEN 'p' THEN 'procedure '
ELSE 'function '
END || p.proname AS obj
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname = 'public'
AND p.prokind IN ('f','p')
AND pg_get_userbyid(p.proowner) <> current_setting('app.role')
) drift
ORDER BY obj;"
drift=$(sql < <(e "$audit_query")) || {
err "Failed to audit database ownership for ${WHITE}$db_role${NORMAL}."
exit 1
}
if [ -z "$drift" ]; then
## fast-path: nothing to do
exit 0
fi
info "Found database objects not owned by '${db_role}', reassigning ownership:"
e "$drift" | prefix " ${GRAY}|${NORMAL} " >&2
dbname=$(outline:named-relation-get "$relation" dbname) || {
err "Couldn't get ${WHITE}dbname${NORMAL} value" \
"in ${DARKCYAN}$relation${NORMAL} relation's data."
exit 1
}
## Reassign ownership of every drifted object, then the database
## itself. Role name is passed through a session GUC and quoted
## with format('%I') in every generated statement.
repair_query="SET app.role = '$db_role';
DO \$\$
DECLARE
r record;
app_role text := current_setting('app.role');
BEGIN
FOR r IN
SELECT CASE c.relkind
WHEN 'S' THEN format('ALTER SEQUENCE public.%I OWNER TO %I', c.relname, app_role)
WHEN 'v' THEN format('ALTER VIEW public.%I OWNER TO %I', c.relname, app_role)
WHEN 'm' THEN format('ALTER MATERIALIZED VIEW public.%I OWNER TO %I', c.relname, app_role)
ELSE format('ALTER TABLE public.%I OWNER TO %I', c.relname, app_role)
END AS stmt
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public'
AND c.relkind IN ('r','p','S','v','m')
AND pg_get_userbyid(c.relowner) <> app_role
UNION ALL
SELECT format('ALTER TYPE public.%I OWNER TO %I', t.typname, app_role)
FROM pg_type t
JOIN pg_namespace n ON n.oid = t.typnamespace
WHERE n.nspname = 'public'
AND t.typtype IN ('e','d','c')
AND NOT EXISTS (SELECT 1 FROM pg_class c WHERE c.reltype = t.oid)
AND pg_get_userbyid(t.typowner) <> app_role
UNION ALL
SELECT CASE p.prokind
WHEN 'p' THEN format('ALTER PROCEDURE public.%I(%s) OWNER TO %I',
p.proname, pg_get_function_identity_arguments(p.oid), app_role)
ELSE format('ALTER FUNCTION public.%I(%s) OWNER TO %I',
p.proname, pg_get_function_identity_arguments(p.oid), app_role)
END AS stmt
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname = 'public'
AND p.prokind IN ('f','p')
AND pg_get_userbyid(p.proowner) <> app_role
LOOP
EXECUTE r.stmt;
END LOOP;
END \$\$;
ALTER DATABASE \"$dbname\" OWNER TO \"$db_role\";"
sql < <(e "$repair_query") || {
err "Failed to reassign ownership to '${db_role}'."
exit 1
}
## Fail-hard: verify the realignment actually worked.
remaining_drift=$(sql < <(e "$audit_query")) || {
err "Failed to re-audit database ownership for ${WHITE}$db_role${NORMAL}."
exit 1
}
if [ -n "$remaining_drift" ]; then
err "Some database objects are still not owned by '${db_role}':"
e "$remaining_drift" | prefix " ${GRAY}|${NORMAL} " >&2
err "Deployment halted. Fix the ownership manually and retry, e.g.:"
err " docker exec <postgres-container> psql -U postgres -d \"$dbname\" \\" >&2
err " -c 'ALTER TYPE public.<name> OWNER TO \"$db_role\"'" >&2
exit 1
fi
info "Database ownership aligned on '${db_role}'."

View File

@@ -13,9 +13,9 @@ services:
$MASTER_BASE_SERVICE_NAME:
environment:
SMTP_USERNAME: \"$user\"
SMTP_PASSWORD: \"${password//\$/\$\$}\"
SMTP_PASS: \"${password//\$/\$\$}\"
SMTP_HOST: \"$host\"
SMTP_PORT: \"$port\"
SMTP_FROM_EMAIL: \"$user\"
#SMTP_SECURE: \"false\"
"

View File

@@ -1,65 +0,0 @@
# -*- mode: shell-script -*-
##
## Database access helpers (from cyclos/immich pattern in 0k-charms)
##
## Get target service name for a named relation
outline:relation-get-target-service() {
local relation="$1" ts
if ! read-0 ts _ _ < <(get_service_relation "$SERVICE_NAME" "$relation"); then
err "Couldn't find relation ${DARKCYAN}$relation${NORMAL}."
return 1
fi
e "$ts"
}
## Get the raw data of a named relation
outline:relation-get-config() {
local relation="$1" ts relation_dir
ts=$(outline:relation-get-target-service "$relation") || return 1
relation_dir=$(get_relation_data_dir "$SERVICE_NAME" "$ts" "$relation") || return 1
cat "${relation_dir}/data"
}
## Get a key from the relation data
outline:named-relation-get() {
local relation="$1" key="$2" config
config=$(outline:relation-get-config "$relation") || return 1
e "$config" | shyaml get-value "$key" || {
err "Couldn't get ${WHITE}$key${NORMAL} value" \
"in ${DARKCYAN}$relation${NORMAL} relation's data."
return 1
}
}
## Run SQL as the postgres superuser on the database related to this
## service through the "postgres-database" relation.
## Usage: sql < <(echo "SELECT ...")
## echo "SELECT ..." | sql
sql() {
(
local dbname ts target_charm target_charm_path
dbname="$(outline:named-relation-get "postgres-database" dbname)" || exit 1
ts=$(outline:relation-get-target-service "postgres-database") || exit 1
export SERVICE_NAME="$ts"
export SERVICE_DATASTORE="$DATASTORE/$SERVICE_NAME"
DOCKER_BASE_IMAGE=$(service_ensure_image_ready "$SERVICE_NAME") || exit 1
export DOCKER_BASE_IMAGE
target_charm=$(get_service_charm "$ts") || exit 1
target_charm_path=$(charm.get_dir "$target_charm") || exit 1
set +e
. "$target_charm_path/lib/common"
set -e
ensure_db_docker_running
ddb -d "$dbname" -v ON_ERROR_STOP=1
)
}

View File

@@ -1,4 +1,4 @@
docker-image: docker.0k.io/outline:0.83.0-elabore
docker-image: docker.0k.io/outline:0.76.0-elabore
uses:
postgres-database:
@@ -11,8 +11,6 @@ uses:
default-options:
extensions:
- uuid-ossp
- unaccent
- pg_trgm
redis-database:
constraint: required
auto: summon
@@ -32,15 +30,6 @@ uses:
proxy: "Public access"
default-options:
target: !var-expand ${MASTER_BASE_SERVICE_NAME}:3000
apache-custom-rules:
- !var-expand |
## Use RewriteEngine to handle WebSocket connection upgrades
RewriteEngine On
RewriteCond %{HTTP:Connection} Upgrade [NC]
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteRule /(.*)\$ ws://${MASTER_BASE_SERVICE_NAME}:3000/\$1 [P,L]
backup:
constraint: recommended
auto: pair

View File

@@ -1,13 +0,0 @@
outline:
options:
sender-email: outline@example.com
oidc-client-id: test-client
oidc-client-secret: test-secret
oidc-auth-uri: https://example.com/auth
oidc-token-uri: https://example.com/token
oidc-user-info-uri: https://example.com/userinfo
oidc-logout-uri: https://example.com/logout
smtp-stub:
options:
host: smtp.example.com

View File

@@ -1,7 +0,0 @@
# -*- ispell-local-dictionary: "english" -*-
* Info
From: https://github.com/plausible/community-edition/
* Usage

View File

@@ -1,14 +0,0 @@
#!/bin/bash
set -e
config-add "\
services:
$MASTER_BASE_SERVICE_NAME:
environment:
CLICKHOUSE_DATABASE_URL: http://$TARGET_SERVICE_NAME:8123/$TARGET_SERVICE_NAME
"
info "Configured $SERVICE_NAME code for $TARGET_SERVICE_NAME access."

View File

@@ -1,27 +0,0 @@
#!/bin/bash
SECRET_KEY_BASE="$SERVICE_DATASTORE"/secret-key
SHARE_DIR="$SERVICE_DATASTORE"/var/lib/plausible
mkdir -p $SHARE_DIR
uid=$(docker_get_uid "$SERVICE_NAME" "plausible")
if ! [ -f "$SECRET_KEY_BASE" ]; then
info "Generating secret key"
mkdir -p "${SECRET_KEY_BASE%/*}"
umask 077
openssl rand -base64 64 > "$SECRET_KEY_BASE"
else
info "Using existing secret key"
fi
secret_key_base=$(cat "$SECRET_KEY_BASE")
init-config-add "
$SERVICE_NAME:
environment:
SECRET_KEY_BASE: \"$secret_key_base\"
"
chown -v "$uid" "$SHARE_DIR"

View File

@@ -1,22 +0,0 @@
#!/bin/bash
set -e
host=$(relation-get host) || exit 1
port=$(relation-get port) || exit 1
user=$(relation-get login) || exit 1
password="$(relation-get password)" || exit 1
config-add "\
services:
$MASTER_BASE_SERVICE_NAME:
environment:
SMTP_USER_NAME: \"$user\"
SMTP_USER_PWD: \"${password//\$/\$\$}\"
SMTP_HOST_ADDR: \"$host\"
SMTP_HOST_PORT: \"$port\"
SMTP_HOST_SSL_ENABLE: \"true\"
MAILER_EMAIL: \"$user\"
"

View File

@@ -1,16 +0,0 @@
#!/bin/bash
set -e
DOMAIN=$(relation-get domain) || {
echo "Failed to get domain"
exit 1
}
config-add "\
services:
$MASTER_BASE_SERVICE_NAME:
environment:
BASE_URL: \"https:\/\/${DOMAIN}\"
"

View File

@@ -1,58 +0,0 @@
docker-image: docker.0k.io/plausible:3.0.1
#docker-image: ghcr.io/plausible/community-edition:v3.0.1
data-resources:
- /var/lib/plausible
docker-compose:
entrypoint: sh -c "/entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
#entrypoint: sh -c "/entrypoint.sh run"
uses:
event-db:
#constraint: required | recommended | optional
#auto: pair | summon | none ## default: pair
constraint: required
auto: summon
solves:
database: "event db"
postgres-database:
#constraint: required | recommended | optional
#auto: pair | summon | none ## default: pair
constraint: required
auto: summon
solves:
database: "main storage"
default-options:
extensions:
- citext
smtp-server:
constraint: required
auto: summon
solves:
proxy: "Public access"
web-proxy:
#constraint: required | recommended | optional
#auto: pair | summon | none ## default: pair
constraint: recommended
auto: pair
solves:
proxy: "Public access"
default-options:
target: !var-expand ${MASTER_BASE_SERVICE_NAME}:8000
apache-custom-rules:
- !var-expand |
ProxyPreserveHost On
#Set web sockets
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/(live/websocket)$ ws://${MASTER_BASE_SERVICE_NAME}:8000/\$1 [P,L]
backup:
constraint: recommended
auto: pair
solves:
backup: "Automatic regular backup"
default-options: