first commit

This commit is contained in:
Valentin Lab
2025-09-25 16:31:23 +02:00
commit bb3443a350
11 changed files with 642 additions and 0 deletions

10
opensem/README.org Normal file
View File

@@ -0,0 +1,10 @@
# -*- ispell-local-dictionary: "english" -*-
#+PROPERTY: TASK_CATEG opensem
* Build source code
Using a ~Dockerfile~ and forcing latest known working ~composer.lock~
and ~yarn.lock~ to ensure reproducible build.
We provide the resulting production ready application files in a
~tar.xz~.

21
opensem/hooks/init Executable file
View File

@@ -0,0 +1,21 @@
#!/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
. lib/common
set -e
opensem:init || exit 1
opensem:config || exit 1

View File

@@ -0,0 +1,86 @@
#!/bin/bash
## Should be executable N time in a row with same result.
. lib/common
set -e
LOGS=/var/log/opensem
php_fpm_service=$(service:traverse "$SERVICE_NAME":php-fpm) || {
err "Could not find php-fpm service for $SERVICE_NAME"
exit 1
}
uid_gid=$(get_service_base_image_dir_uid_gid "$php_fpm_service" /var/www/html) || {
err "Could not determine uid:gid for $php_fpm_service of dir /var/www/html"
exit 1
}
gid="${uid_gid#* }"
dirs=(
"$LOGS"
)
to_create=()
volumes=""
for d in "${dirs[@]}"; do
fdir="${SERVICE_DATASTORE}$d"
if [ -d "$fdir" ]; then
find "$fdir" \! -gid "$gid" -print0 | while read-0 f; do
chgrp -v "$gid" "$f" || return 1
done
find "$fdir" \! -perm -g+rwx -print0 | while read-0 f; do
chmod -v g+rwx "$f" || return 1
done
else
to_create+=("$fdir")
fi
done
if [ "${#to_create[@]}" -gt 0 ]; then
mkdir -p "${to_create[@]}" || return 1
chgrp -v "${gid}" "${to_create[@]}" || return 1
chmod g+rwx "${to_create[@]}" || return 1
fi
rotated_count=$(relation-get rotated-count 2>/dev/null) || true
rotated_count=${rotated_count:-52}
## XXXvlab: a lot of this intelligence should be moved away into ``logrotate`` charm
DST="$CONFIGSTORE/$TARGET_SERVICE_NAME/etc/logrotate.d/$SERVICE_NAME"
file_put "$DST" <<EOF
/var/log/docker/$SERVICE_NAME/laravel.log {
weekly
missingok
dateext
dateyesterday
dateformat _%Y-%m-%d
extension .log
rotate $rotated_count
compress
delaycompress
notifempty
create 640 root root
sharedscripts
}
EOF
opensem:config-set "LOG_CHANNEL" "single" || {
err "Could not set LOG_CHANNEL=single in opensem config"
exit 1
}
config-add "\
services:
$MASTER_TARGET_SERVICE_NAME:
volumes:
- $DST:/etc/logrotate.d/docker-${SERVICE_NAME}:ro
- $SERVICE_DATASTORE$LOGS:/var/log/docker/$SERVICE_NAME:rw
$php_fpm_service:
volumes:
- $SERVICE_DATASTORE$LOGS:/opt/apps/$SERVICE_NAME/storage/logs:rw
"

View File

@@ -0,0 +1,17 @@
#!/bin/bash
. lib/common
set -e
master_key=$(service:password:get "${TARGET_SERVICE_NAME}" "master_key" internal) || exit 1
cat <<EOF >> "${OPENSEM_CONFIG_FILE}"
## meilisearch settings
SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://${TARGET_SERVICE_NAME}:7700
MEILISEARCH_KEY=${master_key}
EOF

View File

@@ -0,0 +1,28 @@
#!/bin/bash
. lib/common
PASSWORD="$(relation-get password)"
USER="$(relation-get user)"
DBNAME="$(relation-get dbname)"
# control=$(H "$USER" "$DBNAME" "$PASSWORD")
# [ "$control" == "$(relation-get control || true)" ] && exit 0
set -e
cat <<EOF >> "${OPENSEM_CONFIG_FILE}"
DB_CONNECTION=mysql
DB_HOST=${TARGET_SERVICE_NAME}
DB_PORT=3306
DB_DATABASE=$DBNAME
DB_USERNAME=$USER
DB_PASSWORD=$PASSWORD
EOF
# relation-set control "$control"
info "Configured opensem code for mysql access."

7
opensem/hooks/pre_deploy Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
. lib/common
set -e
mv -v "${OPENSEM_CONFIG_FILE}" "${OPENSEM_CONFIG_FILE%.prepare}"

View File

@@ -0,0 +1,88 @@
#!/bin/bash
. lib/common
set -e
domain=$(relation-get domain) || exit 1
url=$(relation-get url) || exit 1
location=$CONFIGSTORE/$BASE_SERVICE_NAME/var/www/$domain
php_fpm_service=$(service:traverse "$SERVICE_NAME":php-fpm) || {
err "Could not find php-fpm service for $SERVICE_NAME"
exit 1
}
uid_gid=$(get_service_base_image_dir_uid_gid "$php_fpm_service" /var/www/html) || {
err "Could not determine uid:gid for $php_fpm_service of dir /var/www/html"
exit 1
}
gid="${uid_gid#* }"
dirs=(
/var/cache/opensem/bootstrap
/var/cache/opensem/framework
/var/lib/opensem/views
/var/lib/opensem/sessions
/var/lib/opensem/app
/var/lib/opensem/app/public
/var/log/opensem
)
to_create=()
volumes=""
for d in "${dirs[@]}"; do
fdir="${SERVICE_DATASTORE}$d"
if [ -d "$fdir" ]; then
find "$fdir" \! -gid "$gid" -print0 | while read-0 f; do
chgrp -v "$gid" "$f" || return 1
done
find "$fdir" \! -perm -g+rwx -print0 | while read-0 f; do
chmod -v g+rwx "$f" || return 1
done
else
to_create+=("$fdir")
fi
done
if [ "${#to_create[@]}" -gt 0 ]; then
mkdir -p "${to_create[@]}" || return 1
chgrp -v "${gid}" "${to_create[@]}" || return 1
chmod g+rwx "${to_create[@]}" || return 1
fi
opensem:config-set APP_URL "$url"
dev=$(options-get dev 2>/dev/null) || true
if [ -n "$dev" ]; then
# if ! [ -d "$dev" ]; then
# err "The 'dev' option is set to '$dev' but this is not a directory."
# exit 1
# fi
OPENSEM_CODE="$dev"
fi
service:docker-compose:directive-merge "$MASTER_TARGET_SERVICE_NAME" <<EOF
volumes:
- $SERVICE_DATASTORE/var/lib/opensem/app/public:/var/www/$domain/storage:ro
EOF
config-add "
services:
$php_fpm_service:
volumes:
# - $OPENSEM_CODE/public:/var/www/$domain:ro
- $OPENSEM_CODE:/opt/apps/$SERVICE_NAME:ro
- $SERVICE_DATASTORE/var/cache/opensem/bootstrap:/opt/apps/$SERVICE_NAME/bootstrap/cache:rw
- $SERVICE_DATASTORE/var/cache/opensem/framework:/opt/apps/$SERVICE_NAME/storage/framework/cache:rw
- $SERVICE_DATASTORE/var/lib/opensem/views:/opt/apps/$SERVICE_NAME/storage/framework/views:rw
- $SERVICE_DATASTORE/var/lib/opensem/sessions:/opt/apps/$SERVICE_NAME/storage/framework/sessions:rw
## Required to give PHP write access to this dir
- $SERVICE_DATASTORE/var/lib/opensem/app:/opt/apps/$SERVICE_NAME/storage/app:rw
- $SERVICE_DATASTORE/var/lib/opensem/app/public:/opt/apps/$SERVICE_NAME/storage/app/public:rw
"

View File

@@ -0,0 +1,21 @@
#!/bin/bash
. lib/common
set -e
password=$(relation-get password) || {
err "Can't get password for '$SERVICE_NAME' from '$TARGET_SERVICE_NAME'."
exit 1
}
cat <<EOF >> "${OPENSEM_CONFIG_FILE}"
BROADCAST_DRIVER=redis
REDIS_HOST=${TARGET_SERVICE_NAME}
REDIS_PASSWORD=$password
REDIS_PORT=6379
EOF
info "Configured $SERVICE_NAME code for $TARGET_SERVICE_NAME access."

View File

@@ -0,0 +1,66 @@
#!/bin/bash
. lib/common
set -e
host=$(relation-get host)
port=$(relation-get port)
connection_security=$(relation-get connection-security)
auth_method=$(relation-get auth-method)
declare -A ENV
ENV[DRIVER]=smtp
ENV[HOST]="$host"
ENV[PORT]="$port"
case "$connection_security" in
"none")
ENV[ENCRYPTION]=null
;;
"ssl/tls")
ENV[ENCRYPTION]="tls"
;;
"ssl")
ENV[ENCRYPTION]="ssl"
;;
*)
error "Unsupported connection security: $connection_security"
exit 1
;;
esac
case "$auth_method" in
"none")
ENV[USERNAME]=null
;;
"password")
login=$(relation-get login) || true
ENV[USERNAME]="$login"
password=$(relation-get password) || true
ENV[PASSWORD]="$password"
;;
*)
error "Unsupported auth method: $auth_method"
exit 1
;;
esac
mail_from=$(relation-get mail-from) || true
if [ -n "$mail_from" ]; then
ENV[FROM_ADDRESS]="$mail_from"
fi
from_name=$(relation-get from-name) || true
if [ -n "$from_name" ]; then
ENV[FROM_NAME]="$from_name"
fi
for key in "${!ENV[@]}"; do
value=${ENV[$key]}
opensem:config-set "MAIL_$key" "$value"
done

221
opensem/lib/common Normal file
View File

@@ -0,0 +1,221 @@
# -*- mode: shell-script -*-
OPENSEM_DIR="/opt/apps/opensem"
OPENSEM_CODE="$SERVICE_CONFIGSTORE$OPENSEM_DIR"
OPENSEM_RELEASE=1.0.0-rc.1
OPENSEM_URL=https://docker.0k.io/downloads/opensem-"${OPENSEM_RELEASE}".tar.xz
OPENSEM_CONFIG_FILE="${OPENSEM_CODE}"/.env.prepare
opensem:init() {
current_version=""
if [ -e "${OPENSEM_CODE}/.version" ]; then
current_version="$(cat "${OPENSEM_CODE}/.version")" || return 1
fi
## Note: previous content will be removed, if not in `.git` and no
## version matching current one
if ! [ -d "${OPENSEM_CODE}/.git" ]; then
mkdir -p "${OPENSEM_CODE}" &&
cd "${OPENSEM_CODE}" &&
git init . &&
git config user.email "root@localhost" &&
git config user.name "Root" || {
err "Couldn't create directory ${OPENSEM_CODE}, or init it with git."
return 1
}
fi
## Check if we need to upgrade code.
if [ "$current_version" == "$OPENSEM_RELEASE" ]; then
return 0
fi
cd "${OPENSEM_CODE}" || return 1
if [ -d "$PWD"/.git ]; then
rm -rf "${PWD:?}"/* "$PWD"/{.version,.env} || return 1
else
err "Can't find the '.git' directory in ${OPENSEM_CODE}."
return 1
fi
curl -L "$OPENSEM_URL" | tar xJ || {
err "Couldn't download $OPENSEM_URL."
return 1
}
sed -ri "s% __DIR__.'/../% '/opt/apps/$SERVICE_NAME/%g" public/index.php || {
err "Couldn't patch public/index.php."
return 1
}
echo "$OPENSEM_RELEASE" > .version
git add -A . || {
err "'git add -A .' in '${OPENSEM_CODE}' failed."
return 1
}
if git diff --staged -s --exit-code; then
info "No differences with last saved version."
else
git commit -m "Release $OPENSEM_RELEASE" || {
err "'git commit' failed."
return 1
}
rm -rf "$SERVICE_DATASTORE/var/cache/opensem/"*
fi
}
opensem:config() {
APP_ENV=$(options-get app-env 2>/dev/null) || true
APP_ENV=${APP_ENV:-production}
cat <<EOF > "${OPENSEM_CONFIG_FILE}"
APP_NAME=OpenSEM
APP_ENV=${APP_ENV}
APP_KEY=base64:$(password:get app_key internal 32 base64)
APP_DEBUG=false
LOG_CHANNEL=stderr
CACHE_DRIVER=array
SESSION_DRIVER=file
SESSION_LIFETIME=180
QUEUE_CONNECTION=sync
CLOCKWORK_ENABLE=false
XRAY_ENABLED=false
DEBUGBAR_ENABLED=false
QUERY_DETECTOR_ENABLED=false
MICROSCOPE_ENABLED=false
WKHTML_PDF_BINARY='"wkhtmltopdf"'
WKHTML_IMG_BINARY='"wkhtmltoimage"'
LIMIT_UUID_LENGTH_32=true
AUTHENTICATION_LOG_NOTIFY=false
REPORTING_API_ENABLED=false
EOF
service_def=$(get_compose_service_def "$SERVICE_NAME") || return 1
echo "Service def: '$service_def'" >&2
env=$(e "$service_def" | shyaml get-value -y options.env 2>/dev/null) || true
echo "Env: '$env'" >&2
if [ -n "$env" ]; then
e "$env" | opensem:config-merge || return 1
fi
}
artisan() {
export COMPOSE_IGNORE_ORPHANS=true
php_fpm_service=$(service:traverse "$SERVICE_NAME":php-fpm) || return 1
## We don't want post deploy that is doing the final http initialization.
compose --debug -q --no-init --no-post-deploy --no-pre-deploy \
--without-relation="$SERVICE_NAME":publish-dir \
run \
"${artisan_docker_run_opts[@]}" \
-T --rm -w /opt/apps/"$SERVICE_NAME" \
--entrypoint php \
-u www-data "$php_fpm_service" artisan "$@" | cat
return "${PIPESTATUS[0]}"
}
dotenv:quote() {
local val="$1"
# Empty string → quoted so the emptiness is explicit
if [[ -z "$val" ]]; then
printf "''"
return
fi
# Plain token (alnum + _ . - / @ :) doesnt need quoting
if [[ "$val" =~ ^[A-Za-z0-9_.:/@-]+$ ]]; then
printf '%s' "$val"
return
fi
# If it has no single quotes or newlines, prefer single quotes
if [[ "$val" != *"'"* && "$val" != *$'\n'* && "$val" != *$'\r'* ]]; then
printf "'%s'" "$val"
return
fi
# Fallback: double quotes with escaped specials + newlines
local escaped="$val"
escaped=${escaped//\\/\\\\}
escaped=${escaped//$'\n'/\\n}
escaped=${escaped//$'\r'/\\r}
escaped=${escaped//$'\t'/\\t}
escaped=${escaped//\"/\\\"}
escaped=${escaped//\$/\\$}
escaped=${escaped//\`/\\\`}
printf "\"%s\"" "$escaped"
}
## Set or add a single key/value to .env
opensem:config-set() {
local key="$1" val="$2"
## sanity check on key:
if ! [[ "$key" =~ ^[A-Z_]+$ ]]; then
err "Invalid key name '$key', only [A-Z_] are allowed."
return 1
fi
val=$(dotenv:quote "$val") || return 1
## modify or add in "$OPENSEM_CONFIG_FILE"
if grep -qE "^[[:space:]]*$key=" "${OPENSEM_CONFIG_FILE}"; then
sed -ri "s%^[[:space:]]*${key}=.*$%${key}=${val}%" "${OPENSEM_CONFIG_FILE}" || return 1
else
echo "${key}=${val}" >> "${OPENSEM_CONFIG_FILE}" || return 1
fi
}
opensem:config-merge() {
local key val type
local sep=
local prefix=
if [ -n "$1" ]; then
prefix="$(IFS="_"; echo "$*")_"
fi
while read-0 key type val; do
## sanity check on key:
if ! [[ "$key" =~ ^[a-z-]+$ ]]; then
err "Invalid key name '$key', only [a-z-] are allowed."
return 1
fi
case "${type##*\!}" in
map|seq)
e "$val" | opensem:config-merge "$@" "$key" || return 1
continue
;;
bool)
val="${val%$'\n'}"
case "${val,,}" in
true|ok|yes|y)
val=true
;;
false|ko|nok|no|n)
val=false
;;
*)
die "Invalid value for ${WHITE}$key$NORMAL, please use a boolean value."
;;
esac
;;
str|*)
val="${val%$'\n'}"
val="$(dotenv:quote "$val")"
;;
esac
key=${key//-/_}
key=${key^^}
key=${prefix^^}$key
opensem:config-set "$key" "$val" || return 1
done < <( yq -0 'to_entries | map([.key, .value | type, .value | to_yaml])[] | .[]' )
}

77
opensem/metadata.yml Normal file
View File

@@ -0,0 +1,77 @@
description: OpenSem
subordinate: true
requires:
web-publishing-directory:
interface: publish-dir
scope: container
uses:
mysql-database:
#constraint: required | recommended | optional
#auto: pair | summon | none ## default: pair
constraint: required
auto: summon
solves:
database: "main storage"
publish-dir:
#constraint: required | recommended | optional
#auto: pair | summon | none ## default: pair
scope: container
constraint: required
auto: summon
solves:
container: "main running server"
default-options:
location: !var-expand "$CONFIGSTORE/$BASE_SERVICE_NAME/opt/apps/opensem/public"
backup:
constraint: recommended
auto: pair
solves:
backup: "Automatic regular backup"
default-options:
## First pattern matching wins, no pattern matching includes.
## include-patterns are checked first, then exclude-patterns
## Patterns rules:
## - ending / for directory
## - '*' authorized
## - must start with a '/', will start from $SERVICE_DATASTORE
# include-patterns:
# - /var/backups/pg/
exclude-patterns:
- "/var/cache/" ## cache
- "/var/lib/opensem/sessions/" ## sessions
- "/var/lib/opensem/views/" ## compiled blade cache
php-fpm:
#constraint: required | recommended | optional
#auto: pair | summon | none ## default: pair
constraint: required
auto: summon
solves:
container: "main php interpreter"
default-options:
extensions:
- pdo_mysql gd intl
sys-tools:
- fonts wkhtmltopdf
meilisearch-engine:
#constraint: required | recommended | optional
#auto: pair | summon | none ## default: pair
constraint: required
auto: summon
solves:
container: "meilisearch engine"
smtp-server:
constraint: required
auto: pair
solves:
mail: "verify email"
log-rotate:
#constraint: required | recommended | optional
#auto: pair | summon | none ## default: pair
constraint: recommended
auto: pair
solves:
unmanaged-logs: "in docker logs"
#default-options: