commit bb3443a350da80856ee555fd8e6abfcfc789683a Author: Valentin Lab Date: Thu Sep 25 16:31:23 2025 +0200 first commit diff --git a/opensem/README.org b/opensem/README.org new file mode 100644 index 0000000..af02262 --- /dev/null +++ b/opensem/README.org @@ -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~. diff --git a/opensem/hooks/init b/opensem/hooks/init new file mode 100755 index 0000000..08c154b --- /dev/null +++ b/opensem/hooks/init @@ -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 \ No newline at end of file diff --git a/opensem/hooks/log_rotate-relation-joined b/opensem/hooks/log_rotate-relation-joined new file mode 100755 index 0000000..3099cd8 --- /dev/null +++ b/opensem/hooks/log_rotate-relation-joined @@ -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" <> "${OPENSEM_CONFIG_FILE}" + + +## meilisearch settings + +SCOUT_DRIVER=meilisearch +MEILISEARCH_HOST=http://${TARGET_SERVICE_NAME}:7700 +MEILISEARCH_KEY=${master_key} +EOF diff --git a/opensem/hooks/mysql_database-relation-joined b/opensem/hooks/mysql_database-relation-joined new file mode 100755 index 0000000..d024ff8 --- /dev/null +++ b/opensem/hooks/mysql_database-relation-joined @@ -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 <> "${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." diff --git a/opensem/hooks/pre_deploy b/opensem/hooks/pre_deploy new file mode 100755 index 0000000..11358bb --- /dev/null +++ b/opensem/hooks/pre_deploy @@ -0,0 +1,7 @@ +#!/bin/bash + +. lib/common + +set -e + +mv -v "${OPENSEM_CONFIG_FILE}" "${OPENSEM_CONFIG_FILE%.prepare}" diff --git a/opensem/hooks/publish_dir-relation-joined b/opensem/hooks/publish_dir-relation-joined new file mode 100755 index 0000000..e9d0cf5 --- /dev/null +++ b/opensem/hooks/publish_dir-relation-joined @@ -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" <> "${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." \ No newline at end of file diff --git a/opensem/hooks/smtp_server-relation-joined b/opensem/hooks/smtp_server-relation-joined new file mode 100755 index 0000000..034f422 --- /dev/null +++ b/opensem/hooks/smtp_server-relation-joined @@ -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 diff --git a/opensem/lib/common b/opensem/lib/common new file mode 100644 index 0000000..9e9d8e2 --- /dev/null +++ b/opensem/lib/common @@ -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 < "${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 + _ . - / @ :) doesn’t 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])[] | .[]' ) +} diff --git a/opensem/metadata.yml b/opensem/metadata.yml new file mode 100644 index 0000000..ef9b8c8 --- /dev/null +++ b/opensem/metadata.yml @@ -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: