Move wisski instance code to separate package
This commit is contained in:
parent
7c3c84e116
commit
063f3f9b7d
67 changed files with 533 additions and 409 deletions
65
internal/component/instances/create.go
Normal file
65
internal/component/instances/create.go
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
package instances
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/stringparser"
|
||||
)
|
||||
|
||||
var errInvalidSlug = errors.New("not a valid slug")
|
||||
|
||||
// Create fills the struct for a new WissKI instance.
|
||||
// It validates that slug is a valid name for an instance.
|
||||
//
|
||||
// It does not perform any checks if the instance already exists, or does the creation in the database.
|
||||
func (instances *Instances) Create(slug string) (wissKI *wisski.WissKI, err error) {
|
||||
|
||||
// make sure that the slug is valid!
|
||||
slug, err = stringparser.ParseSlug(instances.Environment, slug)
|
||||
if err != nil {
|
||||
return nil, errInvalidSlug
|
||||
}
|
||||
|
||||
wissKI = new(wisski.WissKI)
|
||||
instances.use(wissKI)
|
||||
|
||||
wissKI.Instance.Slug = slug
|
||||
wissKI.Instance.FilesystemBase = filepath.Join(instances.Path(), wissKI.Domain())
|
||||
|
||||
wissKI.Instance.OwnerEmail = ""
|
||||
wissKI.Instance.AutoBlindUpdateEnabled = true
|
||||
|
||||
// sql
|
||||
|
||||
wissKI.Instance.SqlDatabase = instances.Config.MysqlDatabasePrefix + slug
|
||||
wissKI.Instance.SqlUsername = instances.Config.MysqlUserPrefix + slug
|
||||
|
||||
wissKI.Instance.SqlPassword, err = instances.Config.NewPassword()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// triplestore
|
||||
|
||||
wissKI.Instance.GraphDBRepository = instances.Config.GraphDBRepoPrefix + slug
|
||||
wissKI.Instance.GraphDBUsername = instances.Config.GraphDBUserPrefix + slug
|
||||
|
||||
wissKI.Instance.GraphDBPassword, err = instances.Config.NewPassword()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// drupal
|
||||
|
||||
wissKI.DrupalUsername = "admin" // TODO: Change this!
|
||||
|
||||
wissKI.DrupalPassword, err = instances.Config.NewPassword()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// store the instance in the object and return it!
|
||||
return wissKI, nil
|
||||
}
|
||||
|
|
@ -5,9 +5,12 @@ import (
|
|||
"path/filepath"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component/meta"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component/snapshotslog"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component/sql"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component/triplestore"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
|
||||
"github.com/tkw1536/goprogram/exit"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
|
@ -17,8 +20,10 @@ import (
|
|||
type Instances struct {
|
||||
component.ComponentBase
|
||||
|
||||
TS *triplestore.Triplestore
|
||||
SQL *sql.SQL
|
||||
TS *triplestore.Triplestore
|
||||
SQL *sql.SQL
|
||||
Meta *meta.Meta
|
||||
SnapshotsLog *snapshotslog.SnapshotsLog
|
||||
}
|
||||
|
||||
func (Instances) Name() string {
|
||||
|
|
@ -37,40 +42,53 @@ var errSQL = exit.Error{
|
|||
ExitCode: exit.ExitGeneric,
|
||||
}
|
||||
|
||||
// Instance is a convenience function to return an instance based on a model slug.
|
||||
// When the instance does not exist, returns nil.
|
||||
func (instances *Instances) Instance(instance models.Instance) *WissKI {
|
||||
i, err := instances.WissKI(instance.Slug)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &i
|
||||
// use uses the non-nil wisski instance with this instances
|
||||
func (instances *Instances) use(wisski *wisski.WissKI) {
|
||||
wisski.Core = instances.Core
|
||||
wisski.SQL = instances.SQL
|
||||
wisski.TS = instances.TS
|
||||
wisski.Meta = instances.Meta
|
||||
wisski.SnapshotsLog = instances.SnapshotsLog
|
||||
}
|
||||
|
||||
// WissKI returns the WissKI with the provided slug, if it exists.
|
||||
// It the WissKI does not exist, returns ErrWissKINotFound.
|
||||
func (instances *Instances) WissKI(slug string) (i WissKI, err error) {
|
||||
func (instances *Instances) WissKI(slug string) (wissKI *wisski.WissKI, err error) {
|
||||
sql := instances.SQL
|
||||
if err := sql.WaitQueryTable(); err != nil {
|
||||
return i, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
table, err := sql.QueryTable(false, models.InstanceTable)
|
||||
if err != nil {
|
||||
return i, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create a struct
|
||||
wissKI = new(wisski.WissKI)
|
||||
|
||||
// find the instance by slug
|
||||
query := table.Where(&models.Instance{Slug: slug}).Find(&i.Instance)
|
||||
query := table.Where(&models.Instance{Slug: slug}).Find(&wissKI.Instance)
|
||||
switch {
|
||||
case query.Error != nil:
|
||||
return i, errSQL.WithMessageF(query.Error)
|
||||
return nil, errSQL.WithMessageF(query.Error)
|
||||
case query.RowsAffected == 0:
|
||||
return i, ErrWissKINotFound
|
||||
default:
|
||||
i.instances = instances
|
||||
return i, nil
|
||||
return nil, ErrWissKINotFound
|
||||
}
|
||||
|
||||
// use the wissKI instance
|
||||
instances.use(wissKI)
|
||||
return wissKI, nil
|
||||
}
|
||||
|
||||
// Instance is a convenience function to return an instance based on a model slug.
|
||||
// When the instance does not exist, returns nil.
|
||||
func (instances *Instances) Instance(instance models.Instance) *wisski.WissKI {
|
||||
wissKI, err := instances.WissKI(instance.Slug)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return wissKI
|
||||
}
|
||||
|
||||
// Has checks if a WissKI with the provided slug exists inside the database.
|
||||
|
|
@ -96,7 +114,7 @@ func (instances *Instances) Has(slug string) (ok bool, err error) {
|
|||
// All returns all instances of the WissKI Distillery in consistent order.
|
||||
//
|
||||
// There is no guarantee that this order remains identical between different api releases; however subsequent invocations are guaranteed to return the same order.
|
||||
func (instances *Instances) All() ([]WissKI, error) {
|
||||
func (instances *Instances) All() ([]*wisski.WissKI, error) {
|
||||
return instances.find(true, func(table *gorm.DB) *gorm.DB {
|
||||
return table
|
||||
})
|
||||
|
|
@ -104,14 +122,14 @@ func (instances *Instances) All() ([]WissKI, error) {
|
|||
|
||||
// WissKIs returns the WissKI instances with the provides slugs.
|
||||
// If a slug does not exist, it is omitted from the result.
|
||||
func (instances *Instances) WissKIs(slugs ...string) ([]WissKI, error) {
|
||||
func (instances *Instances) WissKIs(slugs ...string) ([]*wisski.WissKI, error) {
|
||||
return instances.find(true, func(table *gorm.DB) *gorm.DB {
|
||||
return table.Where("slug IN ?", slugs)
|
||||
})
|
||||
}
|
||||
|
||||
// Load is like All, except that when no slugs are provided, it calls All.
|
||||
func (instances *Instances) Load(slugs ...string) ([]WissKI, error) {
|
||||
func (instances *Instances) Load(slugs ...string) ([]*wisski.WissKI, error) {
|
||||
if len(slugs) == 0 {
|
||||
return instances.All()
|
||||
}
|
||||
|
|
@ -119,7 +137,7 @@ func (instances *Instances) Load(slugs ...string) ([]WissKI, error) {
|
|||
}
|
||||
|
||||
// find finds instances based on the provided query
|
||||
func (instances *Instances) find(order bool, query func(table *gorm.DB) *gorm.DB) (results []WissKI, err error) {
|
||||
func (instances *Instances) find(order bool, query func(table *gorm.DB) *gorm.DB) (results []*wisski.WissKI, err error) {
|
||||
sql := instances.SQL
|
||||
if err := sql.WaitQueryTable(); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -148,10 +166,11 @@ func (instances *Instances) find(order bool, query func(table *gorm.DB) *gorm.DB
|
|||
}
|
||||
|
||||
// make proper instances
|
||||
results = make([]WissKI, len(bks))
|
||||
results = make([]*wisski.WissKI, len(bks))
|
||||
for i, bk := range bks {
|
||||
results[i] = new(wisski.WissKI)
|
||||
results[i].Instance = bk
|
||||
results[i].instances = instances
|
||||
instances.use(results[i])
|
||||
}
|
||||
|
||||
return results, nil
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
DATA_PATH=${DATA_PATH}
|
||||
RUNTIME_DIR=${RUNTIME_DIR}
|
||||
GLOBAL_AUTHORIZED_KEYS_FILE=${GLOBAL_AUTHORIZED_KEYS_FILE}
|
||||
|
||||
SLUG=${SLUG}
|
||||
VIRTUAL_HOST=${VIRTUAL_HOST}
|
||||
DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}
|
||||
|
||||
HTTPS_ENABLED=${HTTPS_ENABLED}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# Ignore everything
|
||||
*
|
||||
|
||||
# allow the following files:
|
||||
!conf/*
|
||||
!scripts/*
|
||||
!patch/*
|
||||
!wisskiutils/*
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
FROM docker.io/library/php:8.0-apache-bullseye
|
||||
ARG COMPOSER_VERSION=2.3.8
|
||||
WORKDIR /var/www
|
||||
|
||||
# install and enable the various required php extension
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
default-mysql-client \
|
||||
git \
|
||||
imagemagick \
|
||||
libcurl4-openssl-dev \
|
||||
libfreetype6-dev \
|
||||
libicu-dev \
|
||||
libjpeg62-turbo-dev \
|
||||
libpng-dev \
|
||||
libssh2-1-dev \
|
||||
libwebp-dev \
|
||||
libxml2-dev \
|
||||
libxpm-dev \
|
||||
sudo \
|
||||
unzip \
|
||||
vim \
|
||||
zip \
|
||||
&& \
|
||||
docker-php-source extract && \
|
||||
mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" && \
|
||||
pear config-set php_ini "$PHP_INI_DIR/php.ini" && \
|
||||
docker-php-ext-configure gd \
|
||||
--enable-gd \
|
||||
--with-webp \
|
||||
--with-jpeg \
|
||||
--with-xpm \
|
||||
--with-freetype \
|
||||
--enable-gd-jis-conv \
|
||||
&& \
|
||||
docker-php-ext-install \
|
||||
curl \
|
||||
gd \
|
||||
intl \
|
||||
mysqli \
|
||||
opcache \
|
||||
pdo_mysql \
|
||||
soap \
|
||||
xml \
|
||||
&& \
|
||||
pecl install xmlrpc-1.0.0RC3 && \
|
||||
pecl install ssh2-1.3.1 && \
|
||||
pecl install apcu-5.1.21 && \
|
||||
pecl install uploadprogress-2.0.2 && \
|
||||
docker-php-ext-enable \
|
||||
apcu \
|
||||
curl \
|
||||
gd \
|
||||
intl \
|
||||
mysqli \
|
||||
mysqli \
|
||||
opcache \
|
||||
pdo_mysql \
|
||||
soap \
|
||||
ssh2 \
|
||||
uploadprogress \
|
||||
xml \
|
||||
xmlrpc \
|
||||
&& \
|
||||
docker-php-source delete
|
||||
|
||||
# enable the apache rewrite mod
|
||||
RUN a2enmod rewrite
|
||||
|
||||
# install composer and add it to path
|
||||
RUN curl -sS https://getcomposer.org/installer | php -- --version=$COMPOSER_VERSION && \
|
||||
mv composer.phar /usr/local/bin/composer
|
||||
ENV PATH "/usr/local/bin:/var/www/data/project/vendor/bin:$PATH"
|
||||
|
||||
# remove default configuration
|
||||
RUN rm /etc/apache2/sites-available/*.conf && \
|
||||
rm /etc/apache2/sites-enabled/*.conf
|
||||
|
||||
ADD patch/easyrdf.patch /patch/easyrdf.patch
|
||||
ADD patch/triples.patch /patch/triples.patch
|
||||
|
||||
# Add wisski configuration
|
||||
ADD conf/ports.conf /etc/apache2/ports.conf
|
||||
ADD conf/wisski.conf /etc/apache2/sites-available/wisski.conf
|
||||
ADD conf/wisski.ini /usr/local/etc/php/conf.d/wisski.ini
|
||||
RUN a2ensite wisski
|
||||
|
||||
# volumes for composer
|
||||
VOLUME /var/www/.composer
|
||||
VOLUME /var/www/data
|
||||
|
||||
# Add and configure the entrypoint
|
||||
ADD scripts/entrypoint.sh /entrypoint.sh
|
||||
|
||||
ENTRYPOINT [ "/bin/bash", "/entrypoint.sh" ]
|
||||
CMD ["apache2-foreground"]
|
||||
|
||||
# Add the provision script and WissKI utils
|
||||
ADD scripts/provision_container.sh /provision_container.sh
|
||||
ADD wisskiutils/ /wisskiutils
|
||||
|
||||
# Add the user_shell.sh
|
||||
ADD scripts/user_shell.sh /user_shell.sh
|
||||
|
||||
# expose port 8080
|
||||
EXPOSE 8080
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
# This file configures where apache should listen.
|
||||
# Because we are running as a limited user, we want to listen on a high port.
|
||||
# For this we use port 8080
|
||||
Listen 8080
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<VirtualHost *:8080>
|
||||
# the document root -- /var/www/data/project/web
|
||||
DocumentRoot /var/www/data/project/web
|
||||
|
||||
<Directory /var/www/data/project/web>
|
||||
# add types for .owl and .rdf
|
||||
AddType application/rdf+xml .owl
|
||||
AddType application/rdf+xml .rdf
|
||||
|
||||
# Rewrite the 'ontology' directory
|
||||
RewriteEngine On
|
||||
RewriteOptions InheritDownBefore
|
||||
ReWriteRule ^(ontology/[^/]+/).+ $1 [R=303,END]
|
||||
ReWriteRule ^(ontology/[^/]+)/$ sites/default/files/$1.owl [END]
|
||||
|
||||
# Allow overrides of symlinks
|
||||
Options Indexes FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
ErrorLog /dev/stderr
|
||||
CustomLog /dev/stdout combined
|
||||
</VirtualHost>
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
; File Uploads up to 1GB
|
||||
file_uploads = On
|
||||
upload_max_filesize = 1000M
|
||||
post_max_size = 1000M
|
||||
|
||||
; Composer uses an absurd amount of memory
|
||||
; 4GB ought to be enough
|
||||
memory_limit = 4G
|
||||
|
||||
; Increase various limits for some long running WissKI operations
|
||||
max_execution_time = 3000
|
||||
max_input_time = 600
|
||||
max_input_nesting_level = 640
|
||||
max_input_vars = 10000
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
version: "3.7"
|
||||
|
||||
services:
|
||||
barrel:
|
||||
build: .
|
||||
restart: always
|
||||
hostname: ${VIRTUAL_HOST}.wisski
|
||||
|
||||
# label it with the current slug
|
||||
labels:
|
||||
- "eu.wiss-ki.barrel.slug=${SLUG}"
|
||||
- "eu.wiss-ki.barrel.authfile=/var/www/.ssh/authorized_keys,/var/www/.ssh/global_authorized_keys"
|
||||
|
||||
- "traefik.enable=True"
|
||||
- "eu.wiss-ki.barrel.distillery=${DOCKER_NETWORK_NAME}"
|
||||
|
||||
- "traefik.http.routers.wisski_${SLUG}.rule=Host(`${VIRTUAL_HOST}`)"
|
||||
- "traefik.http.routers.wisski_${SLUG}.tls=${HTTPS_ENABLED}"
|
||||
- "traefik.http.routers.wisski_${SLUG}.tls.certresolver=distillery"
|
||||
- "traefik.http.services.wisski_${SLUG}.loadbalancer.server.port=8080"
|
||||
|
||||
# volumes that are mounted
|
||||
volumes:
|
||||
- ${GLOBAL_AUTHORIZED_KEYS_FILE}:/var/www/.ssh/global_authorized_keys:ro
|
||||
- ${DATA_PATH}/.composer:/var/www/.composer
|
||||
- ${DATA_PATH}/data:/var/www/data
|
||||
- ${DATA_PATH}/authorized_keys:/var/www/.ssh/authorized_keys
|
||||
- ${RUNTIME_DIR}:/runtime:ro
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: ${DOCKER_NETWORK_NAME}
|
||||
external: true
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
281c281
|
||||
< if (preg_match("|^HTTP/([\d\.x]+) (\d+) ([^\r\n]+)|", $status, $m)) {
|
||||
---
|
||||
> if(preg_match("|^HTTP/([\d\.x]+) (\d+) ([^\r\n]*)|", $status, $m)) {
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
100c100
|
||||
< if($result->o instanceof \EasyRdf_Resource) {
|
||||
---
|
||||
> if($result->o instanceof \EasyRdf\Resource) {
|
||||
118c118
|
||||
< $object_text = $result->o->getValue();
|
||||
---
|
||||
> $object_text = $result->o->dumpValue('string');
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# This script contains
|
||||
|
||||
# chown the volumes to make sure they can be read and written by the limited user
|
||||
chown www-data:www-data /var/www
|
||||
chown www-data:www-data /var/www/.composer
|
||||
chown www-data:www-data /var/www/data/
|
||||
|
||||
# run the original entrypoint
|
||||
docker-php-entrypoint "$@"
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
function log_info() {
|
||||
echo -e "\033[1m$1\033[0m"
|
||||
}
|
||||
|
||||
function log_ok() {
|
||||
echo -e "\033[0;32m$1\033[0m"
|
||||
}
|
||||
|
||||
log_info " => Reading configuration variables"
|
||||
|
||||
INSTANCE_DOMAIN="$1"
|
||||
echo "INSTANCE_DOMAIN=$INSTANCE_DOMAIN"
|
||||
shift 1
|
||||
|
||||
MYSQL_DATABASE="$1"
|
||||
echo "MYSQL_DATABASE=$MYSQL_DATABASE"
|
||||
MYSQL_USER="$2"
|
||||
echo "MYSQL_USER=$MYSQL_USER"
|
||||
MYSQL_PASSWORD="$3"
|
||||
echo "MYSQL_PASSWORD=$MYSQL_PASSWORD"
|
||||
|
||||
shift 3
|
||||
|
||||
GRAPHDB_REPO="$1"
|
||||
echo "GRAPHDB_REPO=$GRAPHDB_REPO"
|
||||
GRAPHDB_USER="$2"
|
||||
echo "GRAPHDB_USER=$GRAPHDB_USER"
|
||||
GRAPHDB_PASSWORD="$3"
|
||||
echo "GRAPHDB_PASSWORD=$GRAPHDB_PASSWORD"
|
||||
shift 3
|
||||
|
||||
GRAPHDB_HEADER="$(printf "%s:%s" "$GRAPHDB_USER" "$GRAPHDB_PASSWORD" | base64 -w 0)"
|
||||
|
||||
DRUPAL_USER="$1"
|
||||
echo "DRUPAL_USER=$DRUPAL_USER"
|
||||
DRUPAL_PASS="$2"
|
||||
echo "DRUPAL_PASS=$DRUPAL_PASS"
|
||||
shift 2
|
||||
|
||||
DRUPAL_VERSION="$1"
|
||||
echo "DRUPAL_VERSION=$DRUPAL_VERSION"
|
||||
shift 1
|
||||
|
||||
WISSKI_VERSION="$1"
|
||||
echo "WISSKI_VERSION=$WISSKI_VERSION"
|
||||
shift 1
|
||||
|
||||
log_info " => Preparing installation environment"
|
||||
BASE_DIR="/var/www/data"
|
||||
COMPOSER_DIR="$BASE_DIR/project"
|
||||
WEB_DIR="$COMPOSER_DIR/web"
|
||||
ONTOLOGY_DIR="$WEB_DIR/sites/default/files/ontology"
|
||||
|
||||
log_info " => Creating '$COMPOSER_DIR'"
|
||||
mkdir -p "$COMPOSER_DIR"
|
||||
cd "$COMPOSER_DIR"
|
||||
|
||||
# workaround for making the drupal sites directory writable
|
||||
function drupal_sites_permission_workaround() {
|
||||
chmod -R u+w "$WEB_DIR/sites/" || true
|
||||
}
|
||||
|
||||
# install a module with composer and enable it with drush
|
||||
# Example:
|
||||
#
|
||||
# composer_install_and_enable << EOF
|
||||
# drupal/some_module:1.23 some_module
|
||||
# drupal/other_module:2.34
|
||||
# EOF
|
||||
#
|
||||
# Will install both modules, but only enable the first one.
|
||||
function composer_install_and_enable() {
|
||||
while IFS= read -r line; do
|
||||
echo "$line" | (
|
||||
read composer drush;
|
||||
drupal_sites_permission_workaround
|
||||
composer require "$composer"
|
||||
if [ -n "$drush" ]; then
|
||||
drush pm-enable --yes "$drush"
|
||||
fi
|
||||
)
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
# Create a new composer project.
|
||||
log_info " => Creating composer project"
|
||||
if [ -z "${DRUPAL_VERSION}" ]; then
|
||||
composer --no-interaction create-project 'drupal/recommended-project:^9.0.0' .
|
||||
else
|
||||
composer --no-interaction create-project "drupal/recommended-project:$DRUPAL_VERSION" .
|
||||
fi
|
||||
|
||||
# needed for composer > 2.2
|
||||
composer --no-interaction config allow-plugins true
|
||||
|
||||
# Install drush so that we can automate a lot of things
|
||||
log_info " => Installing 'drush'"
|
||||
composer require drush/drush
|
||||
|
||||
# Use 'drush' to run the site-installation.
|
||||
# Here we need to use the username, password and database creds we made above.
|
||||
log_info " => Running drupal installation scripts"
|
||||
drush site-install standard --yes --site-name=${INSTANCE_DOMAIN} \
|
||||
--account-name=$DRUPAL_USER --account-pass=$DRUPAL_PASS \
|
||||
--db-url=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@sql/${MYSQL_DATABASE}
|
||||
drupal_sites_permission_workaround
|
||||
|
||||
# create a directory for ontologies.
|
||||
log_info " => Creating '$ONTOLOGY_DIR'"
|
||||
mkdir -p "$ONTOLOGY_DIR"
|
||||
|
||||
# Install the Wisski packages.
|
||||
log_info " => Installing Wisski packages"
|
||||
cd "$COMPOSER_DIR"
|
||||
|
||||
# install the development version when requested
|
||||
if [ -z "${WISSKI_VERSION}" ]; then
|
||||
composer require 'drupal/wisski'
|
||||
else
|
||||
composer require "drupal/wisski:$WISSKI_VERSION"
|
||||
fi
|
||||
|
||||
# Install dependencies of WissKI
|
||||
log_info " => Installing and patching Wisski dependencies"
|
||||
pushd "$WEB_DIR/modules/contrib/wisski"
|
||||
composer install
|
||||
|
||||
# Patch EasyRDF (for now)
|
||||
EASYRDF_RESPONSE="./vendor/easyrdf/easyrdf/lib/EasyRdf/Http/Response.php"
|
||||
if [ -f "$EASYRDF_RESPONSE" ]; then
|
||||
patch -N "$EASYRDF_RESPONSE" < "/patch/easyrdf.patch"
|
||||
fi
|
||||
popd
|
||||
|
||||
log_info " => Installing and enabling additional modules"
|
||||
composer_install_and_enable << EOF
|
||||
drupal/inline_entity_form:^1.0@RC
|
||||
drupal/imagemagick
|
||||
drupal/image_effects
|
||||
drupal/colorbox
|
||||
drupal/devel:^4.1 devel
|
||||
drupal/geofield:^1.40 geofield
|
||||
drupal/geofield_map:^2.85 geofield_map
|
||||
drupal/imce:^2.4 imce
|
||||
EOF
|
||||
|
||||
log_info " => Enable Wisski modules"
|
||||
drush pm-enable --yes wisski_core wisski_linkblock wisski_pathbuilder wisski_adapter_sparql11_pb wisski_salz
|
||||
drupal_sites_permission_workaround
|
||||
|
||||
log_info " => Setting up WissKI Salz Adapter"
|
||||
drush php:script /wisskiutils/create_adapter.php "$INSTANCE_DOMAIN" "$GRAPHDB_REPO" "$GRAPHDB_HEADER"
|
||||
|
||||
log_info " => Updating TRUSTED_HOST_PATTERNS in settings.php"
|
||||
|
||||
/bin/bash /wisskiutils/set_trusted_host.sh
|
||||
|
||||
log_info " => Running initial cron"
|
||||
drush core-cron
|
||||
|
||||
log_info " => Provisioning is now complete. "
|
||||
log_ok "Your installation details are as follows:"
|
||||
function printdetails() {
|
||||
echo "URL: http://$INSTANCE_DOMAIN"
|
||||
echo "Username: $DRUPAL_USER"
|
||||
echo "Password: $DRUPAL_PASS"
|
||||
}
|
||||
printdetails
|
||||
|
||||
exit 0
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# This script is used to start a user shell inside the docker container.
|
||||
cd "/var/www/data/project"
|
||||
sudo -u www-data "PATH=/var/www/data/project/vendor/bin:$PATH" /bin/bash "$@"
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This script will automatically create a WissKI Salz Adapter for use within the distillery.
|
||||
* It will not update any existing adapter and is rather primitive.
|
||||
*/
|
||||
|
||||
$argc = $_SERVER['argc']-3;
|
||||
$argv = array_slice($_SERVER['argv'], 3);
|
||||
|
||||
// read parameters from the command line
|
||||
if ($argc != 3) {
|
||||
die("Usage: drush php:script create_adapter.php INSTANCE_DOMAIN GRAPHDB_REPO HEADER");
|
||||
}
|
||||
$INSTANCE_DOMAIN = $argv[0];
|
||||
$GRAPHDB_REPO = $argv[1];
|
||||
$HEADER = $argv[2];
|
||||
|
||||
//
|
||||
// PROPERTIES FOR THE ADAPTER
|
||||
//
|
||||
|
||||
$id = 'default'; // id
|
||||
$type = 'sparql11_with_pb'; // plugin
|
||||
$machine_name = 'default'; // machine-name
|
||||
$label = 'Default WissKI Distillery Adapter';
|
||||
$description = 'Adapter for ' . $INSTANCE_DOMAIN; // description
|
||||
$writable = TRUE; // writable
|
||||
$is_preferred_local_store = TRUE; // is_preferred_local_store
|
||||
$header = $HEADER; // header
|
||||
$read_url = 'http://triplestore:7200/repositories/' . $GRAPHDB_REPO; // read_url
|
||||
$write_url = 'http://triplestore:7200/repositories/' . $GRAPHDB_REPO . '/statements'; // write_url
|
||||
$is_federatable = TRUE; // is_federatable
|
||||
$default_graph_uri = 'https://' . $INSTANCE_DOMAIN . '/';
|
||||
$same_as_properties = ['http://www.w3.org/2002/07/owl#sameAs']; // same_as_properties
|
||||
$ontology_graphs = []; // ontology_graphs
|
||||
|
||||
//
|
||||
// Do the creation!
|
||||
//
|
||||
|
||||
$storage = \Drupal::entityTypeManager()->getStorage('wisski_salz_adapter');
|
||||
$adapter = $storage->create([
|
||||
"id" => $id,
|
||||
"label" => $label,
|
||||
"description" => $description,
|
||||
]);
|
||||
$adapter->setEngineConfig([
|
||||
"id" => $type,
|
||||
"machine-name" => $machine_name,
|
||||
"header" => $header,
|
||||
"writeable" => $writable,
|
||||
"is_preferred_local_store" => $is_preferred_local_store,
|
||||
"read_url" => $read_url,
|
||||
"write_url" => $write_url,
|
||||
"is_federatable" => $is_federatable,
|
||||
"default_graph" => $default_graph_uri,
|
||||
"same_as_properties" => $same_as_properties,
|
||||
"ontology_graphs" => $ontology_graphs,
|
||||
]);
|
||||
$adapter->save();
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# This utility script can be used to configure the trusted host settings inside of settings.php.
|
||||
# It doesn't take care of corner cases and should only be used when needed.
|
||||
|
||||
INSTANCE_DOMAIN="$(hostname -f)"
|
||||
INSTANCE_DOMAIN="${INSTANCE_DOMAIN%.wisski}"
|
||||
|
||||
TRUSTED_HOST_PATTERN="${INSTANCE_DOMAIN//\./\\\\.}"
|
||||
TRUSTED_HOST_PATTERNS='["'$TRUSTED_HOST_PATTERN'"]'
|
||||
|
||||
echo "Setting 'trusted_host_patterns' to $TRUSTED_HOST_PATTERNS"
|
||||
bash /wisskiutils/settings_php_set.sh 'trusted_host_patterns' "$TRUSTED_HOST_PATTERNS"
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# settings_php_get.sh name
|
||||
# Gets the 'settings_php_get.php' setting 'name' as json-encoded value, or null when it does not exist.
|
||||
|
||||
NAME=$1
|
||||
|
||||
if [ -z "$NAME" ]; then
|
||||
echo "Usage: get_settings_setting.sh NAME"
|
||||
exit 1
|
||||
fi;
|
||||
|
||||
echo "$NAME" | drush php:eval '
|
||||
use \Drupal\Core\Site\Settings;
|
||||
$name=trim(file_get_contents("php://stdin"));
|
||||
echo json_encode(Settings::get($name));
|
||||
';
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# settings_php_set.sh name value
|
||||
# Sets the 'settings.php' setting 'name' to 'value'.
|
||||
# Value must be json-encoded.
|
||||
|
||||
NAME=$1
|
||||
VALUE=$2
|
||||
|
||||
if [ -z "$NAME" ]; then
|
||||
echo "Usage: settings_php_set.sh NAME VALUE"
|
||||
exit 1
|
||||
fi;
|
||||
|
||||
if [ -z "$VALUE" ]; then
|
||||
echo "Usage: settings_php_set.sh NAME VALUE"
|
||||
exit 1
|
||||
fi;
|
||||
|
||||
cd /var/www/data/project
|
||||
chmod u+w web/sites/default/settings.php
|
||||
|
||||
(echo "$NAME"; echo "$VALUE" ) | drush php:eval '
|
||||
if(is_file(DRUPAL_ROOT . "/internal/")) {
|
||||
include_once DRUPAL_ROOT . "/internal/core/includes/install.inc";
|
||||
} else {
|
||||
include_once DRUPAL_ROOT . "/core/includes/install.inc";
|
||||
}
|
||||
|
||||
// read NAME and VALUE from STDIN
|
||||
$content=file_get_contents("php://stdin");
|
||||
$newline=strpos($content, "\n");
|
||||
$name=trim(substr($content, 0, $newline));
|
||||
$jvalue=trim(substr($content, $newline + 1));
|
||||
|
||||
// decode json values
|
||||
$value = @json_decode($jvalue);
|
||||
if ($value === null && json_last_error() !== JSON_ERROR_NONE) {
|
||||
echo "Invalid JSON, cannot update settings.php. \n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// make parameters to drush_rewrite_settings
|
||||
$settings["settings"][$name] = (object)[
|
||||
"value" => $value,
|
||||
"required" => TRUE,
|
||||
];
|
||||
|
||||
// find the actual settings.php file to rewrite
|
||||
$filename = DRUPAL_ROOT . "/" . \Drupal::service("site.path") . "/settings.php";
|
||||
drupal_rewrite_settings($settings, $filename);
|
||||
|
||||
echo "Wrote " . $filename . "\n";
|
||||
return 0;
|
||||
';
|
||||
EXIT=$?
|
||||
|
||||
chmod u-w web/sites/default/settings.php
|
||||
|
||||
exit $?
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
SLUG=${SLUG}
|
||||
VIRTUAL_HOST=${VIRTUAL_HOST}
|
||||
|
||||
DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}
|
||||
HTTPS_ENABLED=${HTTPS_ENABLED}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
version: "3.7"
|
||||
|
||||
services:
|
||||
static:
|
||||
image: tkw01536/gostatic
|
||||
restart: always
|
||||
ports:
|
||||
- 8043
|
||||
|
||||
labels:
|
||||
- "traefik.enable=True"
|
||||
- "eu.wiss-ki.barrel.distillery=${DOCKER_NETWORK_NAME}"
|
||||
|
||||
- "traefik.http.routers.reserve_${SLUG}.rule=Host(`${VIRTUAL_HOST}`)"
|
||||
- "traefik.http.routers.reserve_${SLUG}.tls=${HTTPS_ENABLED}"
|
||||
- "traefik.http.routers.reserve_${SLUG}.tls.certresolver=distillery"
|
||||
- "traefik.http.services.reserve_${SLUG}.loadbalancer.server.port=8043"
|
||||
|
||||
|
||||
# volumes that are mounted
|
||||
volumes:
|
||||
- ./index.html:/srv/http/index.html:ro
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: ${DOCKER_NETWORK_NAME}
|
||||
external: true
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
This domain name is reserved.
|
||||
Content is a work in progress.
|
||||
|
|
@ -1,210 +0,0 @@
|
|||
package instances
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component/sql"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// MetaKey represents a key for metadata.
|
||||
type MetaKey string
|
||||
|
||||
// ErrMetadatumNotSet is returned by various [MetaStorage] functions when a metadatum is not set
|
||||
var ErrMetadatumNotSet = errors.New("metadatum not set")
|
||||
|
||||
// MetaStorage manages some metadata.
|
||||
type MetaStorage interface {
|
||||
// Get retrieves metadata with the provided key and deserializes the first one into target.
|
||||
// If no metadatum exists, returns [ErrMetadatumNotSet].
|
||||
Get(key MetaKey, target any) error
|
||||
|
||||
// GetAll receives all metadata with the provided keys.
|
||||
// For each received value, the targets function is called with the current index, and total number of results.
|
||||
// The function is intended to return a target for deserialization.
|
||||
//
|
||||
// When no metadatum exists, targets is not called, and nil error is returned.
|
||||
GetAll(key MetaKey, targets func(index, total int) any) error
|
||||
|
||||
// Delete deletes all metadata with the provided key.
|
||||
Delete(key MetaKey) error
|
||||
|
||||
// Set serializes value and stores it with the provided key.
|
||||
// Any other metadata with the same key is deleted.
|
||||
Set(key MetaKey, value any) error
|
||||
|
||||
// Set serializes values and stores them with the provided key.
|
||||
// Any other metadata with the same key is deleted.
|
||||
SetAll(key MetaKey, values ...any) error
|
||||
|
||||
// Purge removes all metadata, regardless of key.
|
||||
Purge() error
|
||||
}
|
||||
|
||||
// Metadata returns a system-wide [MetaStorage].
|
||||
func (instances *Instances) Metadata() MetaStorage {
|
||||
return &storage{
|
||||
SQL: instances.SQL,
|
||||
Slug: "", // not associated to any slug
|
||||
}
|
||||
}
|
||||
|
||||
// Metadata returns a [MetaStorage] that manages metadata related to this WissKI instance.
|
||||
// It will be automatically deleted once the instance is deleted.
|
||||
func (wisski *WissKI) Metadata() MetaStorage {
|
||||
return &storage{
|
||||
SQL: wisski.instances.SQL,
|
||||
Slug: wisski.Slug, // associated to this instance
|
||||
}
|
||||
}
|
||||
|
||||
// storage implements MetaStorage
|
||||
type storage struct {
|
||||
SQL *sql.SQL
|
||||
Slug string
|
||||
}
|
||||
|
||||
func (s *storage) Get(key MetaKey, target any) error {
|
||||
table, err := s.SQL.QueryTable(true, models.MetadataTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// read the datum from the database
|
||||
var datum models.Metadatum
|
||||
status := table.Where(&models.Metadatum{Slug: s.Slug, Key: string(key)}).Order("pk DESC").Find(&datum)
|
||||
|
||||
// check if there was an error
|
||||
if err := status.Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if e actually found it!
|
||||
if status.RowsAffected == 0 {
|
||||
return ErrMetadatumNotSet
|
||||
}
|
||||
|
||||
// and do the unmarshaling!
|
||||
return json.Unmarshal(datum.Value, target)
|
||||
}
|
||||
|
||||
func (s *storage) GetAll(key MetaKey, target func(index, total int) any) error {
|
||||
table, err := s.SQL.QueryTable(true, models.MetadataTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// read the datum from the database
|
||||
var data []models.Metadatum
|
||||
status := table.Where(&models.Metadatum{Slug: s.Slug, Key: string(key)}).Find(&data)
|
||||
|
||||
// check if there was an error
|
||||
if err := status.Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// unpack all of them into the destination
|
||||
for index, datum := range data {
|
||||
err := json.Unmarshal(datum.Value, target(index, len(data)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *storage) Delete(key MetaKey) error {
|
||||
table, err := s.SQL.QueryTable(true, models.MetadataTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete all the values
|
||||
status := table.Where(&models.Metadatum{Slug: s.Slug, Key: string(key)}).Delete(&models.Metadatum{})
|
||||
if err := status.Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *storage) Set(key MetaKey, value any) error {
|
||||
table, err := s.SQL.QueryTable(true, models.MetadataTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// marshal the value
|
||||
bytes, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return table.Transaction(func(tx *gorm.DB) error {
|
||||
// delete the old values
|
||||
status := tx.Where(&models.Metadatum{Slug: s.Slug, Key: string(key)}).Delete(&models.Metadatum{})
|
||||
if err := status.Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create the new item to insert
|
||||
status = tx.Create(&models.Metadatum{
|
||||
Key: string(key),
|
||||
Slug: s.Slug,
|
||||
Value: bytes,
|
||||
})
|
||||
if err := status.Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *storage) SetAll(key MetaKey, values ...any) error {
|
||||
table, err := s.SQL.QueryTable(true, models.MetadataTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return table.Transaction(func(tx *gorm.DB) error {
|
||||
// delete the old values
|
||||
status := tx.Where(&models.Metadatum{Slug: s.Slug, Key: string(key)}).Delete(&models.Metadatum{})
|
||||
if err := status.Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, value := range values {
|
||||
bytes, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create the new item to insert
|
||||
status := tx.Create(&models.Metadatum{
|
||||
Key: string(key),
|
||||
Slug: s.Slug,
|
||||
Value: bytes,
|
||||
})
|
||||
if err := status.Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *storage) Purge() error {
|
||||
table, err := s.SQL.QueryTable(true, models.MetadataTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
status := table.Where("slug = ?", s.Slug).Delete(&models.Metadatum{})
|
||||
if status.Error != nil {
|
||||
return status.Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Drupal\wisski_pathbuilder\Entity\WisskiPathEntity;
|
||||
|
||||
/** all_xml lists all pathbuilders, and returns the corresponding xml */
|
||||
function all_xml(): object {
|
||||
$all = \Drupal::entityTypeManager()->getStorage('wisski_pathbuilder')->loadMultiple();
|
||||
return (object)array_map("entity_to_xml", $all);
|
||||
}
|
||||
|
||||
|
||||
/** all_list lists the ids of all pathbuilders */
|
||||
function all_list(): Array {
|
||||
return array_keys(\Drupal::entityQuery('wisski_pathbuilder')->execute());
|
||||
}
|
||||
|
||||
/** one_xml serializes a single pathbuilder as xml */
|
||||
function one_xml(string $id): string {
|
||||
$pb = \Drupal::entityTypeManager()->getStorage('wisski_pathbuilder')->load($id);
|
||||
if ($pb === NULL) {
|
||||
return "";
|
||||
}
|
||||
return entity_to_xml($pb);
|
||||
}
|
||||
|
||||
// =================================================================================
|
||||
// =================================================================================
|
||||
|
||||
|
||||
function entity_to_xml($pb) {
|
||||
$xml = new \SimpleXMLElement("<pathbuilderinterface></pathbuilderinterface>");
|
||||
|
||||
$paths = $pb->getAllPaths();
|
||||
foreach ($paths as $key => $path) {
|
||||
$id = $path->getID();
|
||||
|
||||
$path = $pb->getPbPath($id);
|
||||
|
||||
$pathChild = $xml->addChild("path");
|
||||
$pathObject = WisskiPathEntity::load($id);
|
||||
|
||||
foreach ($path as $subkey => $value) {
|
||||
|
||||
if (in_array($subkey, ['relativepath'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($subkey == "parent") {
|
||||
$subkey = "group_id";
|
||||
}
|
||||
|
||||
$pathChild->addChild($subkey, htmlspecialchars($value));
|
||||
}
|
||||
|
||||
$pathArray = $pathChild->addChild('path_array');
|
||||
foreach ($pathObject->getPathArray() as $subkey => $value) {
|
||||
$pathArray->addChild($subkey % 2 == 0 ? 'x' : 'y', $value);
|
||||
}
|
||||
|
||||
$pathChild->addChild('datatype_property', htmlspecialchars($pathObject->getDatatypeProperty()));
|
||||
$pathChild->addChild('short_name', htmlspecialchars($pathObject->getShortName()));
|
||||
$pathChild->addChild('disamb', htmlspecialchars($pathObject->getDisamb()));
|
||||
$pathChild->addChild('description', htmlspecialchars($pathObject->getDescription()));
|
||||
$pathChild->addChild('uuid', htmlspecialchars($pathObject->uuid()));
|
||||
if ($pathObject->getType() == "Group" || $pathObject->getType() == "Smartgroup") {
|
||||
$pathChild->addChild('is_group', "1");
|
||||
} else {
|
||||
$pathChild->addChild('is_group', "0");
|
||||
}
|
||||
$pathChild->addChild('name', htmlspecialchars($pathObject->getName()));
|
||||
}
|
||||
|
||||
// turn it into XML
|
||||
$dom = dom_import_simplexml($xml)->ownerDocument;
|
||||
$dom->formatOutput = TRUE;
|
||||
return $dom->saveXML();
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* list_prefixes lists all content prefixes known to this WissKI.
|
||||
* Prefixes are not filtered, and may contain duplicates.
|
||||
*/
|
||||
function list_prefixes() {
|
||||
$prefixes = [];
|
||||
$storage = \Drupal::entityTypeManager()->getStorage('wisski_salz_adapter');
|
||||
foreach ($storage->loadMultiple() as $adapter) {
|
||||
// load all the prefixes from the triplestore
|
||||
$engine = $adapter->getEngine();
|
||||
getTriplestorePrefixes($adapter->getEngine(), $prefixes);
|
||||
|
||||
// read the configuration to check if we have a default graph
|
||||
$conf = $engine->getConfiguration();
|
||||
if(!array_key_exists('default_graph', $conf)) {
|
||||
continue;
|
||||
}
|
||||
$prefixes[] = $conf['default_graph'];
|
||||
}
|
||||
return $prefixes;
|
||||
}
|
||||
|
||||
function getTriplestorePrefixes($engine, &$prefixes) {
|
||||
// some adapters don't support a query method!
|
||||
if (!method_exists($engine, 'directQuery')) return NULL;
|
||||
|
||||
$results = $engine->directQuery('
|
||||
select distinct ?base where {
|
||||
{
|
||||
select distinct ?iri where {
|
||||
{
|
||||
select distinct (?s as ?iri) { ?s ?p ?o }
|
||||
} union {
|
||||
select distinct (?o as ?iri) { ?s ?p ?o FILTER(isiri(?o)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
BIND(replace(str(?iri), "/[^/]*/?$", "/") as ?base)
|
||||
FILTER(!REGEX(?base, "/wisski/navigate/[\\\\d]+/$"))
|
||||
} ORDER BY ?base');
|
||||
if (!$results) return FALSE;
|
||||
|
||||
foreach($results as $result) {
|
||||
$prefixes[] = $result->base->getValue();
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
<?php
|
||||
|
||||
// This file contains code to execute a php execution server.
|
||||
// It is passed as a *command line literal * directly to 'drush:script'.
|
||||
//
|
||||
// As such it is preprocessed and shortened.
|
||||
// It should only contain comments at the beginning of each line, and only starting with '//'.
|
||||
// See wisski_php_server.go.
|
||||
|
||||
// don't buffer stdin!
|
||||
stream_set_read_buffer(STDIN,0);
|
||||
|
||||
// stop all other output
|
||||
ob_start(null,0,PHP_OUTPUT_HANDLER_CLEANABLE);
|
||||
|
||||
while($line = fgets(STDIN)){
|
||||
// decode the command to run
|
||||
$code=@json_decode($line);
|
||||
|
||||
// execute it
|
||||
try{
|
||||
$json = json_encode([eval($code),""]);
|
||||
}catch(Throwable $t){
|
||||
$json = json_encode([null,(string)$t]);
|
||||
}
|
||||
if($json===false) {
|
||||
$json = '[null,"Error encoding result"]';
|
||||
}
|
||||
|
||||
// and write out the result
|
||||
ob_end_clean();
|
||||
fwrite(STDOUT,"$json\n");
|
||||
ob_start(null,0,PHP_OUTPUT_HANDLER_CLEANABLE);
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
<?php
|
||||
|
||||
/** gets a setting from 'settings.php' */
|
||||
function get_setting($name) {
|
||||
use \Drupal\Core\Site\Settings;
|
||||
return Settings::get($name);
|
||||
}
|
||||
|
||||
/** sets a setting in 'settings.php' */
|
||||
function set_setting($name, $value) {
|
||||
// load install.inc
|
||||
if(is_file(DRUPAL_ROOT . "/internal/")) {
|
||||
include_once DRUPAL_ROOT . "/internal/core/includes/install.inc";
|
||||
} else {
|
||||
include_once DRUPAL_ROOT . "/core/includes/install.inc";
|
||||
}
|
||||
|
||||
// update the provided setting
|
||||
$settings["settings"][$name] = (object)[
|
||||
"value" => $value,
|
||||
"required" => TRUE,
|
||||
];
|
||||
|
||||
// find the filename
|
||||
$filename = DRUPAL_ROOT . "/" . \Drupal::service("site.path") . "/settings.php";
|
||||
drupal_rewrite_settings($settings, $filename);
|
||||
|
||||
return True;
|
||||
}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
package instances
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/stringparser"
|
||||
"github.com/alessio/shellescape"
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
)
|
||||
|
||||
var errInvalidSlug = errors.New("not a valid slug")
|
||||
|
||||
// Create fills the struct for a new WissKI instance.
|
||||
// It validates that slug is a valid name for an instance.
|
||||
//
|
||||
// It does not perform any checks if the instance already exists, or does the creation in the database.
|
||||
func (instances *Instances) Create(slug string) (wisski WissKI, err error) {
|
||||
wisski.instances = instances
|
||||
|
||||
// make sure that the slug is valid!
|
||||
slug, err = stringparser.ParseSlug(instances.Environment, slug)
|
||||
if err != nil {
|
||||
return wisski, errInvalidSlug
|
||||
}
|
||||
|
||||
wisski.Instance.Slug = slug
|
||||
wisski.Instance.FilesystemBase = filepath.Join(instances.Path(), wisski.Domain())
|
||||
|
||||
wisski.Instance.OwnerEmail = ""
|
||||
wisski.Instance.AutoBlindUpdateEnabled = true
|
||||
|
||||
// sql
|
||||
|
||||
wisski.Instance.SqlDatabase = instances.Config.MysqlDatabasePrefix + slug
|
||||
wisski.Instance.SqlUsername = instances.Config.MysqlUserPrefix + slug
|
||||
|
||||
wisski.Instance.SqlPassword, err = instances.Config.NewPassword()
|
||||
if err != nil {
|
||||
return WissKI{}, err
|
||||
}
|
||||
|
||||
// triplestore
|
||||
|
||||
wisski.Instance.GraphDBRepository = instances.Config.GraphDBRepoPrefix + slug
|
||||
wisski.Instance.GraphDBUsername = instances.Config.GraphDBUserPrefix + slug
|
||||
|
||||
wisski.Instance.GraphDBPassword, err = instances.Config.NewPassword()
|
||||
if err != nil {
|
||||
return WissKI{}, err
|
||||
}
|
||||
|
||||
// drupal
|
||||
|
||||
wisski.DrupalUsername = "admin" // TODO: Change this!
|
||||
|
||||
wisski.DrupalPassword, err = instances.Config.NewPassword()
|
||||
if err != nil {
|
||||
return wisski, err
|
||||
}
|
||||
|
||||
// store the instance in the object and return it!
|
||||
return wisski, nil
|
||||
}
|
||||
|
||||
// Provision provisions an instance, assuming that the required databases already exist.
|
||||
func (wisski *WissKI) Provision(io stream.IOStream) error {
|
||||
|
||||
// build the container
|
||||
if err := wisski.Build(io, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
provisionParams := []string{
|
||||
wisski.Domain(),
|
||||
|
||||
wisski.SqlDatabase,
|
||||
wisski.SqlUsername,
|
||||
wisski.SqlPassword,
|
||||
|
||||
wisski.GraphDBRepository,
|
||||
wisski.GraphDBUsername,
|
||||
wisski.GraphDBPassword,
|
||||
|
||||
wisski.DrupalUsername,
|
||||
wisski.DrupalPassword,
|
||||
|
||||
"", // TODO: DrupalVersion
|
||||
"", // TODO: WissKIVersion
|
||||
}
|
||||
|
||||
// escape the parameter
|
||||
for i, param := range provisionParams {
|
||||
provisionParams[i] = shellescape.Quote(param)
|
||||
}
|
||||
|
||||
// figure out the provision script
|
||||
// TODO: Move the provision script into the control plane!
|
||||
provisionScript := "sudo PATH=$PATH -u www-data /bin/bash /provision_container.sh " + strings.Join(provisionParams, " ")
|
||||
|
||||
code, err := wisski.Barrel().Run(io, true, "barrel", "/bin/bash", "-c", provisionScript)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if code != 0 {
|
||||
return errors.New("unable to run provision script")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
package instances
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/tkw1536/goprogram/exit"
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
)
|
||||
|
||||
var errCronFailed = exit.Error{
|
||||
Message: "Failed to run cron script for instance %q: exited with code %s",
|
||||
ExitCode: exit.ExitGeneric,
|
||||
}
|
||||
|
||||
func (wisski *WissKI) Cron(io stream.IOStream) error {
|
||||
code, err := wisski.Shell(io, "/runtime/cron.sh")
|
||||
if err != nil {
|
||||
io.EPrintln(err)
|
||||
}
|
||||
if code != 0 {
|
||||
// keep going, because we want to run as many crons as possible
|
||||
err = errBlindUpdateFailed.WithMessageF(wisski.Slug, code)
|
||||
io.EPrintln(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wisski *WissKI) LastCron(server *PHPServer) (t time.Time, err error) {
|
||||
var timestamp int64
|
||||
err = wisski.EvalPHPCode(server, ×tamp, `$val = \Drupal::state()->get('system.cron_last'); return $val; `)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return time.Unix(timestamp, 0), nil
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
package instances
|
||||
|
||||
import "github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
|
||||
// WissKI represents a single WissKI Instance
|
||||
type WissKI struct {
|
||||
// Whatever is stored inside the bookkeeping database
|
||||
models.Instance
|
||||
|
||||
// Credentials to Drupal
|
||||
DrupalUsername string
|
||||
DrupalPassword string
|
||||
|
||||
// reference to the component!
|
||||
instances *Instances
|
||||
}
|
||||
|
||||
// Save saves this instance in the bookkeeping table
|
||||
func (wisski *WissKI) Save() error {
|
||||
db, err := wisski.instances.SQL.QueryTable(false, models.InstanceTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// it has never been created => we need to create it in the database
|
||||
if wisski.Instance.Created.IsZero() {
|
||||
return db.Create(&wisski.Instance).Error
|
||||
}
|
||||
|
||||
// Update based on the primary key!
|
||||
return db.Where("pk = ?", wisski.Instance.Pk).Updates(&wisski.Instance).Error
|
||||
}
|
||||
|
||||
// Delete deletes this instance from the bookkeeping table
|
||||
func (wisski *WissKI) Delete() error {
|
||||
db, err := wisski.instances.SQL.QueryTable(false, models.InstanceTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// doesn't exist => nothing to delete
|
||||
if wisski.Instance.Created.IsZero() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// delete it directly
|
||||
return db.Delete(&wisski.Instance).Error
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package instances
|
||||
|
||||
import (
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
)
|
||||
|
||||
// Shell executes a shell command inside the instance.
|
||||
func (wisski *WissKI) Shell(io stream.IOStream, argv ...string) (int, error) {
|
||||
return wisski.Barrel().Exec(io, "barrel", "/bin/sh", append([]string{"/user_shell.sh"}, argv...)...)
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
package instances
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// WissKIInfo represents information about this WissKI Instance.
|
||||
type WissKIInfo struct {
|
||||
Time time.Time // Time this info was built
|
||||
|
||||
// Generic Information
|
||||
Slug string // slug
|
||||
URL string // complete URL, including http(s)
|
||||
|
||||
Locked bool // Is this instance currently locked?
|
||||
|
||||
// Information about the running instance
|
||||
Running bool
|
||||
LastRebuild time.Time
|
||||
LastUpdate time.Time
|
||||
LastCron time.Time
|
||||
|
||||
// List of backups made
|
||||
Snapshots []models.Export
|
||||
|
||||
// WissKI content information
|
||||
NoPrefixes bool // TODO: Move this into the database
|
||||
Prefixes []string // list of prefixes
|
||||
Pathbuilders map[string]string // all the pathbuilders
|
||||
}
|
||||
|
||||
// Info fetches information about this WissKI.
|
||||
// TODO: Rework this to be able to determine what kind of information is available.
|
||||
func (wisski *WissKI) Info(quick bool) (info WissKIInfo, err error) {
|
||||
var group errgroup.Group
|
||||
wisski.infoQuick(&info, &group)
|
||||
|
||||
if !quick {
|
||||
server, err := wisski.NewPHPServer()
|
||||
if err == nil {
|
||||
defer server.Close()
|
||||
}
|
||||
wisski.infoSlow(&info, server, &group)
|
||||
}
|
||||
|
||||
err = group.Wait()
|
||||
return
|
||||
}
|
||||
|
||||
func (wisski *WissKI) infoQuick(info *WissKIInfo, group *errgroup.Group) {
|
||||
info.Time = time.Now().UTC()
|
||||
info.Slug = wisski.Slug
|
||||
info.URL = wisski.URL().String()
|
||||
|
||||
group.Go(func() (err error) {
|
||||
info.Running, err = wisski.Running()
|
||||
return
|
||||
})
|
||||
|
||||
group.Go(func() (err error) {
|
||||
info.Locked = wisski.IsLocked()
|
||||
return
|
||||
})
|
||||
|
||||
group.Go(func() (err error) {
|
||||
info.LastRebuild, _ = wisski.LastRebuild()
|
||||
return
|
||||
})
|
||||
|
||||
group.Go(func() (err error) {
|
||||
info.LastUpdate, _ = wisski.LastUpdate()
|
||||
return
|
||||
})
|
||||
|
||||
group.Go(func() (err error) {
|
||||
info.LastRebuild, _ = wisski.LastRebuild()
|
||||
return
|
||||
})
|
||||
|
||||
group.Go(func() (err error) {
|
||||
info.NoPrefixes = wisski.NoPrefix()
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func (wisski *WissKI) infoSlow(info *WissKIInfo, server *PHPServer, group *errgroup.Group) {
|
||||
group.Go(func() (err error) {
|
||||
info.Prefixes, _ = wisski.Prefixes(server)
|
||||
return nil
|
||||
})
|
||||
|
||||
group.Go(func() (err error) {
|
||||
info.Snapshots, _ = wisski.Snapshots()
|
||||
return nil
|
||||
})
|
||||
|
||||
group.Go(func() (err error) {
|
||||
info.Pathbuilders, _ = wisski.AllPathbuilders(server)
|
||||
return nil
|
||||
})
|
||||
|
||||
group.Go(func() (err error) {
|
||||
info.LastCron, _ = wisski.LastCron(server)
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// Running checks if this WissKI is currently running.
|
||||
func (wisski *WissKI) Running() (bool, error) {
|
||||
ps, err := wisski.Barrel().Ps(stream.FromNil())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return len(ps) > 0, nil
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
package instances
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
)
|
||||
|
||||
var ErrLocked = errors.New("instance is locked")
|
||||
|
||||
// TryLock attemps to lock this WissKI
|
||||
// If this is not possible, returns ErrLocked
|
||||
func (wisski WissKI) TryLock() error {
|
||||
table, err := wisski.instances.SQL.QueryTable(true, models.LockTable)
|
||||
if err != nil {
|
||||
return ErrLocked
|
||||
}
|
||||
|
||||
result := table.FirstOrCreate(&models.Lock{}, models.Lock{Slug: wisski.Slug})
|
||||
locked := result.Error == nil && result.RowsAffected == 1
|
||||
|
||||
if !locked {
|
||||
return ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wisski WissKI) IsLocked() (locked bool) {
|
||||
table, err := wisski.instances.SQL.QueryTable(true, models.LockTable)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// check if this instance is locked
|
||||
table.Select("count(*) > 0").Where("slug = ?", wisski.Slug).Find(&locked)
|
||||
return
|
||||
}
|
||||
|
||||
// Unlock unlocks this WissKI instance and returns if it succeeded
|
||||
func (wisski WissKI) Unlock() bool {
|
||||
table, err := wisski.instances.SQL.QueryTable(true, models.LockTable)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
result := table.Where("slug = ?", wisski.Slug).Delete(&models.Lock{})
|
||||
return result.Error == nil && result.RowsAffected == 1
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
package instances
|
||||
|
||||
import (
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/environment"
|
||||
"github.com/tkw1536/goprogram/lib/collection"
|
||||
)
|
||||
|
||||
// ExportLogFor retrieves (and prunes) the ExportLog.
|
||||
// Slug determines if entries for Backups (empty slug)
|
||||
// or a specific Instance (non-empty slug) are returned.
|
||||
func (instances *Instances) ExportLogFor(slug string) (exports []models.Export, err error) {
|
||||
exports, err = instances.ExportLog()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return collection.Filter(exports, func(s models.Export) bool {
|
||||
return s.Slug == slug
|
||||
}), nil
|
||||
}
|
||||
|
||||
// ExportLog retrieves (and prunes) all entries in the snapshot log.
|
||||
func (instances *Instances) ExportLog() ([]models.Export, error) {
|
||||
// query the table!
|
||||
table, err := instances.SQL.QueryTable(false, models.ExportTable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// find all the exports
|
||||
var exports []models.Export
|
||||
res := table.Find(&exports)
|
||||
if res.Error != nil {
|
||||
return nil, res.Error
|
||||
}
|
||||
|
||||
// partition out the exports that have been deleted!
|
||||
parts := collection.Partition(exports, func(s models.Export) bool {
|
||||
_, err := instances.Core.Environment.Stat(s.Path)
|
||||
return !environment.IsNotExist(err)
|
||||
})
|
||||
|
||||
// go and delete them!
|
||||
if len(parts[false]) > 0 {
|
||||
if err := table.Delete(parts[false]).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// return the ones that still exist
|
||||
return parts[true], nil
|
||||
}
|
||||
|
||||
// Snapshots returns the list of snapshots of this WissKI
|
||||
func (wisski *WissKI) Snapshots() (snapshots []models.Export, err error) {
|
||||
return wisski.instances.ExportLogFor(wisski.Slug)
|
||||
}
|
||||
|
||||
// AddToExportLog adds the provided export to the log.
|
||||
func (instances *Instances) AddToExportLog(export models.Export) error {
|
||||
// find the table
|
||||
table, err := instances.SQL.QueryTable(false, models.ExportTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// and save it!
|
||||
res := table.Create(&export)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
package instances
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
//go:embed php/export_pathbuilder.php
|
||||
var exportPathbuilderPHP string
|
||||
|
||||
// Pathbuilders returns the ids of all pathbuilders in consistent order.
|
||||
//
|
||||
// server is the server to fetch the pathbuilders from, any may be nil.
|
||||
func (wisski *WissKI) Pathbuilders(server *PHPServer) (ids []string, err error) {
|
||||
err = wisski.ExecPHPScript(server, &ids, exportPathbuilderPHP, "all_list")
|
||||
slices.Sort(ids)
|
||||
return
|
||||
}
|
||||
|
||||
// Pathbuilder returns a single pathbuilder as xml.
|
||||
// If it does not exist, it returns the empty string and nil error.
|
||||
//
|
||||
// server is the server to fetch the pathbuilders from, any may be nil.
|
||||
func (wisski *WissKI) Pathbuilder(server *PHPServer, id string) (xml string, err error) {
|
||||
err = wisski.ExecPHPScript(server, &xml, exportPathbuilderPHP, "one_xml", id)
|
||||
return
|
||||
}
|
||||
|
||||
// AllPathbuilders returns all pathbuilders serialized as xml
|
||||
//
|
||||
// server is the server to fetch the pathbuilders from, any may be nil.
|
||||
func (wisski *WissKI) AllPathbuilders(server *PHPServer) (pathbuilders map[string]string, err error) {
|
||||
err = wisski.ExecPHPScript(server, &pathbuilders, exportPathbuilderPHP, "all_xml")
|
||||
return
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
package instances
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed php/settings.php
|
||||
var settingsPHP string
|
||||
|
||||
func (wisski *WissKI) GetSettingsPHP(server *PHPServer, key string) (value any, err error) {
|
||||
err = wisski.ExecPHPScript(server, &value, settingsPHP, "get_setting", key)
|
||||
return
|
||||
}
|
||||
|
||||
func (wisski *WissKI) SetSettingsPHP(server *PHPServer, key string, value any) error {
|
||||
return wisski.ExecPHPScript(server, nil, settingsPHP, "set_setting", key, value)
|
||||
}
|
||||
|
|
@ -1,312 +0,0 @@
|
|||
package instances
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/alessio/shellescape"
|
||||
"github.com/tkw1536/goprogram/lib/collection"
|
||||
"github.com/tkw1536/goprogram/lib/nobufio"
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
)
|
||||
|
||||
// Common PHP Error
|
||||
var (
|
||||
errPHPInit = "Unable to initialize"
|
||||
errPHPMarshal = "Marshal failed"
|
||||
errPHPInvalid = PHPServerError{Message: "Invalid code to execute"}
|
||||
errPHPReceive = "Failed to receive response"
|
||||
errPHPClosed = PHPServerError{Message: "Server closed"}
|
||||
)
|
||||
|
||||
// PHPError represents an error during PHPServer logic
|
||||
type PHPServerError struct {
|
||||
Message string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (err PHPServerError) Unwrap() error {
|
||||
return err.Err
|
||||
}
|
||||
|
||||
func (err PHPServerError) Error() string {
|
||||
if err.Err == nil {
|
||||
return fmt.Sprintf("PHPServer: %s", err.Message)
|
||||
}
|
||||
return fmt.Sprintf("PHPServer: %s: %s", err.Message, err.Err)
|
||||
}
|
||||
|
||||
// PHPThrowable represents an error during php code
|
||||
type PHPThrowable string
|
||||
|
||||
func (throwable PHPThrowable) Error() string {
|
||||
return string(throwable)
|
||||
}
|
||||
|
||||
// NewPHPServer returns a new server that can execute code within this distillery.
|
||||
// When err == nil, the caller must call server.Close().
|
||||
//
|
||||
// See [PHPServer].
|
||||
func (wisski *WissKI) NewPHPServer() (*PHPServer, error) {
|
||||
// create input and output pipes
|
||||
ir, iw, err := os.Pipe()
|
||||
if err != nil {
|
||||
return nil, PHPServerError{errPHPInit, err}
|
||||
}
|
||||
or, ow, err := os.Pipe()
|
||||
if err != nil {
|
||||
ir.Close()
|
||||
iw.Close()
|
||||
return nil, PHPServerError{errPHPInit, err}
|
||||
}
|
||||
|
||||
// create a context to close the server
|
||||
context, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// start the shell process, which will close everything once done
|
||||
go func() {
|
||||
defer func() {
|
||||
ir.Close()
|
||||
iw.Close()
|
||||
or.Close()
|
||||
ow.Close()
|
||||
|
||||
cancel()
|
||||
}()
|
||||
|
||||
// start the server
|
||||
io := stream.NewIOStream(ow, nil, ir, 0)
|
||||
wisski.Shell(io, "-c", shellescape.QuoteCommand([]string{"drush", "php:eval", serverPHP}))
|
||||
}()
|
||||
|
||||
// return the seerver
|
||||
return &PHPServer{
|
||||
in: iw,
|
||||
out: or,
|
||||
c: context,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PHPServer represents a server that executes code within a distillery.
|
||||
// A typical use-case is to define functions using [MarshalEval], and then call those functions [MarshalCall].
|
||||
//
|
||||
// A nil PHPServer will return [ErrServerBroken] on every function call.
|
||||
type PHPServer struct {
|
||||
m sync.Mutex
|
||||
|
||||
in io.WriteCloser
|
||||
out io.Reader
|
||||
c context.Context
|
||||
}
|
||||
|
||||
// MarshalEval evaluates code on the server and Marshals the result into value.
|
||||
// When value is nil, the results are discarded.
|
||||
//
|
||||
// code is directly passed to php's "eval" function.
|
||||
// as such any functions defined will remain in server memory.
|
||||
//
|
||||
// When an exception is thrown by the PHP Code, error is not nil, and dest remains unchanged.
|
||||
func (server *PHPServer) MarshalEval(value any, code string) error {
|
||||
server.m.Lock()
|
||||
defer server.m.Unlock()
|
||||
|
||||
// quick hack: when the server is already done
|
||||
if err := server.c.Err(); err != nil {
|
||||
return errPHPClosed
|
||||
}
|
||||
|
||||
// marshal the code, and send it to the server
|
||||
bytes, err := json.Marshal(code)
|
||||
if err != nil {
|
||||
return PHPServerError{Message: errPHPMarshal, Err: err}
|
||||
}
|
||||
|
||||
// send it to the server
|
||||
io.WriteString(server.in, string(bytes)+"\n")
|
||||
|
||||
// read the next line (as a response)
|
||||
data, err := nobufio.ReadLine(server.out)
|
||||
if err != nil {
|
||||
return PHPServerError{Message: errPHPReceive, Err: err}
|
||||
}
|
||||
|
||||
// read whatever we received
|
||||
var received [2]json.RawMessage
|
||||
if err := json.Unmarshal([]byte(data), &received); err != nil {
|
||||
return PHPServerError{Message: errPHPMarshal, Err: err}
|
||||
}
|
||||
|
||||
// check if there was an error
|
||||
var errString string
|
||||
if err := json.Unmarshal(received[1], &errString); err == nil && errString != "" {
|
||||
return PHPThrowable(errString)
|
||||
}
|
||||
|
||||
// special case: no return value => no unmarshaling needed
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// read the actual result!
|
||||
return json.Unmarshal(received[0], value)
|
||||
}
|
||||
|
||||
// Eval is like [MarshalEval], but returns the value as an any
|
||||
func (server *PHPServer) Eval(code string) (value any, err error) {
|
||||
err = server.MarshalEval(&value, code)
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalCall calls a previously defined function with the given arguments.
|
||||
// Arguments are sent to php using json Marshal, and are 'json_decode'd on the php side.
|
||||
//
|
||||
// Return values are received as in [MarshalEval].
|
||||
func (server *PHPServer) MarshalCall(value any, function string, args ...any) error {
|
||||
// marshal a code for the call
|
||||
userFunction, err := marshalPHP(function)
|
||||
if err != nil {
|
||||
return PHPServerError{Message: errPHPMarshal, Err: err}
|
||||
}
|
||||
|
||||
userFunctionArgs := "[]"
|
||||
if len(args) > 0 {
|
||||
userFunctionArgs, err = marshalPHP(args)
|
||||
if err != nil {
|
||||
return PHPServerError{Message: errPHPMarshal, Err: err}
|
||||
}
|
||||
}
|
||||
code := "return call_user_func_array(" + userFunction + "," + userFunctionArgs + ");"
|
||||
|
||||
// and return the evaluated code!
|
||||
return server.MarshalEval(value, code)
|
||||
}
|
||||
|
||||
// Call is like [MarshalCall] but returns the return value of the function as an any
|
||||
func (server *PHPServer) Call(function string, args ...any) (value any, err error) {
|
||||
err = server.MarshalCall(&value, function, args...)
|
||||
return
|
||||
}
|
||||
|
||||
const marshalRune = 'F' // press to pay respect
|
||||
|
||||
// marshalPHP marshals some data which can be marshaled using [json.Encode] into a PHP Expression.
|
||||
// the string can be safely used directly within php.
|
||||
func marshalPHP(data any) (string, error) {
|
||||
// this function uses json as a data format to transport the data into php.
|
||||
// then we build a heredoc to encode it safely, and decode it in php
|
||||
|
||||
// Step 1: Encode the data as json
|
||||
jbytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
jstring := string(jbytes)
|
||||
|
||||
// Step 2: Find a delimiter for the heredoc.
|
||||
// Step 2a: Find the longest sequence of [marshalRune]s inside the encoded string.
|
||||
var current, longest int
|
||||
for _, r := range jstring {
|
||||
|
||||
if r == marshalRune {
|
||||
current++
|
||||
} else {
|
||||
current = 0
|
||||
}
|
||||
|
||||
if current > longest {
|
||||
longest = current
|
||||
}
|
||||
}
|
||||
// Step 2b: Build a string of marshalRune that is one longer!
|
||||
delim := strings.Repeat(string(marshalRune), longest+1)
|
||||
|
||||
// Step 3: Assemble the encoded string!
|
||||
result := "call_user_func(function(){$x=<<<'" + delim + "'\n" + jstring + "\n" + delim + ";return json_decode(trim($x));})" // press to doubt
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Close closes this server and prevents any further code from being run.
|
||||
func (server *PHPServer) Close() error {
|
||||
server.m.Lock()
|
||||
defer server.m.Unlock()
|
||||
|
||||
// if the context is already closed
|
||||
if err := server.c.Err(); err != nil {
|
||||
return errPHPClosed
|
||||
}
|
||||
|
||||
server.in.Close()
|
||||
<-server.c.Done()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecPHPScript executes the PHP code as a script on the given server.
|
||||
// When server is nil, creates a new server and automatically closes it after execution.
|
||||
// Calling this function repeatedly with server = nil is inefficient.
|
||||
//
|
||||
// The script should define a function called entrypoint, and may define additional functions.
|
||||
//
|
||||
// Code must start with "<?php" and may not contain a closing tag.
|
||||
// Code is expected not to mess with PHPs output buffer.
|
||||
// Code should not contain user input.
|
||||
// Code breaking these conventions may or may not result in an error.
|
||||
//
|
||||
// It's arguments are encoded as json using [json.Marshal] and decoded within php.
|
||||
//
|
||||
// The return value of the function is again marshaled with json and returned to the caller.
|
||||
func (wisski *WissKI) ExecPHPScript(server *PHPServer, value any, code string, entrypoint string, args ...any) (err error) {
|
||||
if server == nil {
|
||||
server, err = wisski.NewPHPServer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer server.Close()
|
||||
}
|
||||
|
||||
if code != "" {
|
||||
if err := server.MarshalEval(nil, strings.TrimPrefix(code, "<?php")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return server.MarshalCall(value, entrypoint, args...)
|
||||
}
|
||||
|
||||
func (wisski *WissKI) EvalPHPCode(server *PHPServer, value any, code string) (err error) {
|
||||
if server == nil {
|
||||
server, err = wisski.NewPHPServer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer server.Close()
|
||||
}
|
||||
|
||||
return server.MarshalEval(value, code)
|
||||
}
|
||||
|
||||
//go:embed php/server.php
|
||||
var serverPHP string
|
||||
|
||||
// pre-process the server.php code to make it shorter
|
||||
func init() {
|
||||
// remove the first '<?php' line
|
||||
lines := strings.Split(serverPHP, "\n")[1:]
|
||||
for i, line := range lines {
|
||||
lines[i] = strings.TrimSpace(line)
|
||||
}
|
||||
|
||||
// remove comment lines
|
||||
lines = collection.Filter(lines, func(line string) bool {
|
||||
return !strings.HasPrefix(line, "//")
|
||||
})
|
||||
|
||||
serverPHP = strings.Join(lines, "")
|
||||
}
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
package instances
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/fsx"
|
||||
"github.com/tkw1536/goprogram/lib/collection"
|
||||
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
// NoPrefix checks if this WissKI instance is excluded from generating prefixes.
|
||||
// TODO: Move this to the database!
|
||||
func (wisski *WissKI) NoPrefix() bool {
|
||||
return fsx.IsFile(wisski.instances.Environment, filepath.Join(wisski.FilesystemBase, "prefixes.skip"))
|
||||
}
|
||||
|
||||
//go:embed php/list_uri_prefixes.php
|
||||
var listURIPrefixesPHP string
|
||||
|
||||
// Prefixes returns the prefixes applying to this WissKI
|
||||
//
|
||||
// server is an optional server to fetch prefixes from.
|
||||
// server may be nil.
|
||||
func (wisski *WissKI) Prefixes(server *PHPServer) ([]string, error) {
|
||||
prefixes, err := wisski.dbPrefixes(server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prefixes2, err := wisski.filePrefixes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append(prefixes, prefixes2...), nil
|
||||
}
|
||||
|
||||
func (wisski *WissKI) dbPrefixes(server *PHPServer) (prefixes []string, err error) {
|
||||
// get all the ugly prefixes
|
||||
err = wisski.ExecPHPScript(server, &prefixes, listURIPrefixesPHP, "list_prefixes")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// filter out sequential prefixes
|
||||
prefixes = collection.NonSequential(prefixes, func(prev, now string) bool {
|
||||
return strings.HasPrefix(now, prev)
|
||||
})
|
||||
|
||||
// load the list of blocked prefixes
|
||||
blocks, err := wisski.instances.blockedPrefixes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// filter out blocked prefixes
|
||||
return collection.Filter(prefixes, func(uri string) bool { return !hasAnyPrefix(uri, blocks) }), nil
|
||||
}
|
||||
|
||||
func (instances *Instances) blockedPrefixes() ([]string, error) {
|
||||
// open the resolver block file
|
||||
file, err := instances.Environment.Open(instances.Config.SelfResolverBlockFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var lines []string
|
||||
|
||||
// read all the lines that aren't a comment!
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "//") || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
|
||||
// check if there was an error
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// and done!
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
func hasAnyPrefix(candidate string, prefixes []string) bool {
|
||||
return collection.Any(
|
||||
prefixes,
|
||||
func(prefix string) bool {
|
||||
return strings.HasPrefix(candidate, prefix)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (wisski *WissKI) filePrefixes() (prefixes []string, err error) {
|
||||
path := filepath.Join(wisski.FilesystemBase, "prefixes")
|
||||
if !fsx.IsFile(wisski.instances.Environment, path) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
file, err := wisski.instances.Environment.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if len(line) == 0 || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
prefixes = append(prefixes, line)
|
||||
}
|
||||
|
||||
if scanner.Err() != nil {
|
||||
return nil, scanner.Err()
|
||||
}
|
||||
return prefixes, nil
|
||||
}
|
||||
|
||||
// CACHING
|
||||
|
||||
var PrefixConfigKey MetaKey = "prefix"
|
||||
|
||||
// Prefixes returns the cached prefixes from the given instance
|
||||
func (wisski *WissKI) PrefixesCached() (results []string, err error) {
|
||||
err = wisski.Metadata().GetAll(PrefixConfigKey, func(index, total int) any {
|
||||
if results == nil {
|
||||
results = make([]string, total)
|
||||
}
|
||||
return &results[index]
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// UpdatePrefixes updates the cached prefixes of this instance
|
||||
func (wisski *WissKI) UpdatePrefixes() error {
|
||||
prefixes, err := wisski.Prefixes(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return wisski.Metadata().SetAll(PrefixConfigKey, collection.AsAny(prefixes)...)
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package instances
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Domain returns the full domain name of this WissKI
|
||||
func (wisski WissKI) Domain() string {
|
||||
return fmt.Sprintf("%s.%s", wisski.Slug, wisski.instances.Config.DefaultDomain)
|
||||
}
|
||||
|
||||
// URL returns the public URL of this instance
|
||||
func (wisski WissKI) URL() *url.URL {
|
||||
// setup domain and path
|
||||
url := &url.URL{
|
||||
Host: wisski.Domain(),
|
||||
Path: "/",
|
||||
}
|
||||
|
||||
// use http or https scheme depending on if the distillery has it enabled
|
||||
if wisski.instances.Config.HTTPSEnabled() {
|
||||
url.Scheme = "https"
|
||||
} else {
|
||||
url.Scheme = "http"
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
package instances
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component"
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
)
|
||||
|
||||
//go:embed all:instances/barrel instances/barrel.env
|
||||
var barrelResources embed.FS
|
||||
|
||||
// Barrel returns a stack representing the running WissKI Instance
|
||||
func (wisski *WissKI) Barrel() component.StackWithResources {
|
||||
return component.StackWithResources{
|
||||
Stack: component.Stack{
|
||||
Dir: wisski.FilesystemBase,
|
||||
Env: wisski.instances.Environment,
|
||||
},
|
||||
|
||||
Resources: barrelResources,
|
||||
ContextPath: filepath.Join("instances", "barrel"),
|
||||
EnvPath: filepath.Join("instances", "barrel.env"),
|
||||
|
||||
EnvContext: map[string]string{
|
||||
"DOCKER_NETWORK_NAME": wisski.instances.Config.DockerNetworkName,
|
||||
|
||||
"SLUG": wisski.Slug,
|
||||
"VIRTUAL_HOST": wisski.Domain(),
|
||||
"HTTPS_ENABLED": wisski.instances.Config.HTTPSEnabledEnv(),
|
||||
|
||||
"DATA_PATH": filepath.Join(wisski.FilesystemBase, "data"),
|
||||
"RUNTIME_DIR": wisski.instances.Config.RuntimeDir(),
|
||||
"GLOBAL_AUTHORIZED_KEYS_FILE": wisski.instances.Config.GlobalAuthorizedKeysFile,
|
||||
},
|
||||
|
||||
MakeDirs: []string{"data", ".composer"},
|
||||
|
||||
TouchFiles: []string{
|
||||
filepath.Join("data", "authorized_keys"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const KeyLastRebuild MetaKey = "lastRebuild"
|
||||
|
||||
func (wisski *WissKI) LastRebuild() (t time.Time, err error) {
|
||||
var epoch int64
|
||||
|
||||
// read the epoch!
|
||||
err = wisski.Metadata().Get(KeyLastRebuild, &epoch)
|
||||
if err == ErrMetadatumNotSet {
|
||||
return t, nil
|
||||
}
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
|
||||
// and turn it into time!
|
||||
return time.Unix(epoch, 0), nil
|
||||
}
|
||||
|
||||
func (wisski *WissKI) setLastRebuild() error {
|
||||
return wisski.Metadata().Set(KeyLastRebuild, time.Now().Unix())
|
||||
}
|
||||
|
||||
// Build builds or rebuilds the barel connected to this instance.
|
||||
//
|
||||
// It also logs the current time into the metadata belonging to this instance.
|
||||
func (wisski *WissKI) Build(stream stream.IOStream, start bool) error {
|
||||
if err := wisski.TryLock(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer wisski.Unlock()
|
||||
|
||||
barrel := wisski.Barrel()
|
||||
|
||||
var context component.InstallationContext
|
||||
|
||||
{
|
||||
err := barrel.Install(stream, context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
err := barrel.Update(stream, start)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// store the current last rebuild
|
||||
return wisski.setLastRebuild()
|
||||
}
|
||||
|
||||
//go:embed all:instances/reserve instances/reserve.env
|
||||
var reserveResources embed.FS
|
||||
|
||||
// Reserve returns a stack representing the reserve instance
|
||||
func (wisski *WissKI) Reserve() component.StackWithResources {
|
||||
return component.StackWithResources{
|
||||
Stack: component.Stack{
|
||||
Dir: wisski.FilesystemBase,
|
||||
Env: wisski.instances.Environment,
|
||||
},
|
||||
|
||||
Resources: reserveResources,
|
||||
ContextPath: filepath.Join("instances", "reserve"),
|
||||
EnvPath: filepath.Join("instances", "reserve.env"),
|
||||
|
||||
EnvContext: map[string]string{
|
||||
"DOCKER_NETWORK_NAME": wisski.instances.Config.DockerNetworkName,
|
||||
|
||||
"SLUG": wisski.Slug,
|
||||
"VIRTUAL_HOST": wisski.Domain(),
|
||||
"HTTPS_ENABLED": wisski.instances.Config.HTTPSEnabledEnv(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
package instances
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/environment"
|
||||
"github.com/tkw1536/goprogram/exit"
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
)
|
||||
|
||||
var errBlindUpdateFailed = exit.Error{
|
||||
Message: "Failed to run blind update script for instance %q: exited with code %s",
|
||||
ExitCode: exit.ExitGeneric,
|
||||
}
|
||||
|
||||
// BlinUpdate performs a blind update of the given instance
|
||||
func (wisski *WissKI) BlindUpdate(io stream.IOStream) error {
|
||||
code, err := wisski.Shell(io, "/runtime/blind_update.sh")
|
||||
if err != nil {
|
||||
return errBlindUpdateFailed.WithMessageF(wisski.Slug, environment.ExecCommandError)
|
||||
}
|
||||
if code != 0 {
|
||||
return errBlindUpdateFailed.WithMessageF(wisski.Slug, code)
|
||||
}
|
||||
|
||||
return wisski.setLastUpdate()
|
||||
}
|
||||
|
||||
const KeyLastUpdate MetaKey = "lastUpdate"
|
||||
|
||||
func (wisski *WissKI) LastUpdate() (t time.Time, err error) {
|
||||
var epoch int64
|
||||
|
||||
// read the epoch!
|
||||
err = wisski.Metadata().Get(KeyLastUpdate, &epoch)
|
||||
if err == ErrMetadatumNotSet {
|
||||
return t, nil
|
||||
}
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
|
||||
// and turn it into time!
|
||||
return time.Unix(epoch, 0), nil
|
||||
}
|
||||
|
||||
func (wisski *WissKI) setLastUpdate() error {
|
||||
return wisski.Metadata().Set(KeyLastUpdate, time.Now().Unix())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue