@@ -0,0 +1,80 @@
#!/bin/sh

# Copyright: Brandon Mitchell
# License: MIT
# Source: https://github.com/sudo-bmitch/docker-base/blob/master/bin/fix-perms

opt_h=0
opt_r=0

while getopts 'g:hru:' option; do
case $option in
g) opt_g="$OPTARG";;
h) opt_h=1;;
r) opt_r=1;;
u) opt_u="$OPTARG";;
esac
done
shift $(expr $OPTIND - 1)

if [ $# -lt 1 -o "$opt_h" = "1" -o \( -z "$opt_g" -a -z "$opt_u" \) ]; then
echo "Usage: $(basename $0) [opts] path"
echo " -g group_name: group name to adjust gid"
echo " -h: this help message"
echo " -r: recursively update uid/gid on root filesystem"
echo " -u user_name: user name to adjust uid"
echo "Either -u or -g must be provided in addition to a path. The uid and"
echo "gid of the path will be used to modify the uid/gid inside the"
echo "container. e.g.: "
echo " $0 -g app_group -u app_user -r /path/to/vol/data"
[ "$opt_h" = "1" ] && exit 0 || exit 1
fi

if [ "$(id -u)" != "0" ]; then
echo "Root required for $(basename $0)"
exit 1
fi

if [ ! -e "$1" ]; then
echo "File or directory does not exist, skipping fix-perms: $1"
exit 0
fi

if ! type usermod >/dev/null 2>&1 || \
! type groupmod >/dev/null 2>&1; then
if type apk /dev/null 2>&1; then
echo "Warning: installing shadow, this should be included in your image"
apk add --no-cache shadow
else
echo "Commands usermod and groupmod are required."
exit 1
fi
fi

set -e

# update the uid
if [ -n "$opt_u" ]; then
OLD_UID=$(getent passwd "${opt_u}" | cut -f3 -d:)
NEW_UID=$(stat -c "%u" "$1")
if [ "$OLD_UID" != "$NEW_UID" ]; then
echo "Changing UID of $opt_u from $OLD_UID to $NEW_UID"
usermod -u "$NEW_UID" -o "$opt_u"
if [ -n "$opt_r" ]; then
find / -xdev -user "$OLD_UID" -exec chown -h "$opt_u" {} \;
fi
fi
fi

# update the gid
if [ -n "$opt_g" ]; then
OLD_GID=$(getent group "${opt_g}" | cut -f3 -d:)
NEW_GID=$(stat -c "%g" "$1")
if [ "$OLD_GID" != "$NEW_GID" ]; then
echo "Changing GID of $opt_g from $OLD_GID to $NEW_GID"
groupmod -g "$NEW_GID" -o "$opt_g"
if [ -n "$opt_r" ]; then
find / -xdev -group "$OLD_GID" -exec chgrp -h "$opt_g" {} \;
fi
fi
fi
@@ -25,45 +25,65 @@

## Database settings
$wgDBname = $dockerDb;
$dockerMasterDb = [
'host' => "db-master",
'dbname' => $dockerDb,
'user' => 'root',
'password' => 'toor',
'type' => "mysql",
'flags' => DBO_DEFAULT,
'load' => 0,

// Decide on which services are available?
$mwddServices = [
// Configure a replica DB if it is running and we are not in unit tests
'db-replica' => gethostbyname('db-replica') !== 'db-replica' && !defined( 'MW_PHPUNIT_TEST' ),
'redis' => gethostbyname('redis') !== 'redis',
'graphite-statsd' => gethostbyname('graphite-statsd') !== 'graphite-statsd',
];
$dockerSlaveDb = [
'host' => "db-slave",
'dbname' => $dockerDb,
'user' => 'root',
'password' => 'toor',
'type' => "mysql",
'flags' => DBO_DEFAULT,
# Avoid switching to readonly too early (for example during update.php)
'max lag' => 60,
'load' => 1,

$wgDBservers = [
[
'host' => "db-master",
'dbname' => $dockerDb,
'user' => 'root',
'password' => 'toor',
'type' => "mysql",
'flags' => DBO_DEFAULT,
'load' => $mwddServices['db-replica'] ? 0 : 1,
],
];
// Unit tests fail when run with replication, due to not having the temporary tables.
// So for unit tests just run with the master.
if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
$wgDBservers = [ $dockerMasterDb, $dockerSlaveDb ];
} else {
$wgDBserver = $dockerMasterDb['host'];
$wgDBuser = $dockerMasterDb['user'];
$wgDBpassword = $dockerMasterDb['password'];
$wgDBtype = $dockerMasterDb['type'];
if($mwddServices['db-replica'] ) {
$wgDBservers[] = [
'host' => "db-replica",
'dbname' => $dockerDb,
'user' => 'root',
'password' => 'toor',
'type' => "mysql",
'flags' => DBO_DEFAULT,
# Avoid switching to readonly too early (for example during update.php)
'max lag' => 60,
'load' => 1,
];
}

// If a redis service is running, then configure an object cache (but don't use it)
if(gethostbyname('redis') !== 'redis') {
$wgObjectCaches['redis'] = [
'class' => 'RedisBagOStuff',
'servers' => [ 'redis:6379' ],
];
}

// Configure a statsd server if it is running
if(gethostbyname('graphite-statsd') !== 'graphite-statsd') {
$wgStatsdServer = "graphite-statsd";
}

require_once __DIR__ . '/MwddSpecialPage.php';
$wgSpecialPages['Mwdd'] = MwddSpecial::class;
$wgExtensionMessagesFiles['Mwdd'] = __DIR__ . '/special-aliases.php';

$wgShowHostnames = true;

// mysql only stuff (would need to change for sqlite)
$wgDBprefix = "";
$wgDBTableOptions = "ENGINE=InnoDB, DEFAULT CHARSET=binary";

## Site settings
$wgScriptPath = "/mediawiki";
$wgScriptPath = "";

$wgSitename = "docker-$dockerDb";
$wgMetaNamespace = "Project";
@@ -75,11 +95,9 @@
$wgTmpDirectory = "{$wgUploadDirectory}/tmp";
$wgCacheDirectory = "{$wgUploadDirectory}/cache";

$wgStatsdServer = "graphite-statsd";

## Dev & Debug

$dockerLogDirectory = "/var/log/mediawiki";
$dockerLogDirectory = getenv( 'MWDD_LOG_DIR' );
$wgDebugLogFile = "$dockerLogDirectory/debug.log";

ini_set( 'xdebug.var_display_max_depth', -1 );
@@ -0,0 +1,28 @@
<?php

class MwddSpecial extends SpecialPage {

public function __construct() {
parent::__construct( 'Mwdd' );
}

/**
* @see SpecialPage::execute
*
* @param string|null $subPage
*/
public function execute( $subPage ) {
parent::execute( $subPage );
global $mwddServices;

$this->getOutput()->addHTML( "Which services are running?" );
$this->getOutput()->addHTML( "</br>" );
$this->getOutput()->addHTML( json_encode( $mwddServices ) );
$this->getOutput()->addHTML( "</br>" );
$this->getOutput()->addHTML( "How does DB lag look?" );
$this->getOutput()->addHTML( "</br>" );
$this->getOutput()->addHTML( json_encode( \MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaxLag()[1] ) );

}

}

This file was deleted.

@@ -1,20 +1,30 @@
#!/bin/sh

# Make sure the master db is ready
/wait-for-it.sh db-master:3306

# Hide the current LocalSettings.php if it exists
if [ -f /var/www/mediawiki/LocalSettings.php ]
if [ -f /app/LocalSettings.php ]
then
mv /var/www/mediawiki/LocalSettings.php /var/www/mediawiki/LocalSettings.php.docker.tmp
mv /app/LocalSettings.php /app/LocalSettings.php.docker.tmp
fi

# Install the base Mediawiki tables on the db server & remove the generated LocalSettings.php
php /var/www/mediawiki/maintenance/install.php --dbuser root --dbpass toor --dbname $1 --dbserver db-master --lang en --pass dockerpass docker-$1 admin
rm /var/www/mediawiki/LocalSettings.php
php /app/maintenance/install.php --dbuser root --dbpass toor --dbname $1 --dbserver db-master --lang en --pass dockerpass docker-$1 admin

# Remove previous last generated file if it exists
if [ -f /app/LocalSettings.php.docker.lastgenereated ]
then
rm /app/LocalSettings.php.docker.lastgenereated
fi
# Move the generated LocalSettings file, but keep it around incase we want to look at it...
mv /app/LocalSettings.php /app/LocalSettings.php.docker.lastgenereated

# Move back the old LocalSettings if we had moved one!
if [ -f /var/www/mediawiki/LocalSettings.php.docker.tmp ]
if [ -f /app/LocalSettings.php.docker.tmp ]
then
mv /var/www/mediawiki/LocalSettings.php.docker.tmp /var/www/mediawiki/LocalSettings.php
mv /app/LocalSettings.php.docker.tmp /app/LocalSettings.php
fi

# Run update.php too
php /var/www/mediawiki/maintenance/update.php --wiki $1 --quick
php /app/maintenance/update.php --wiki $1 --quick
@@ -0,0 +1,5 @@
<?php
$specialPageAliases = [];
$specialPageAliases['en'] = [
'Mwdd' => [ 'Mwdd' ],
];
@@ -3,7 +3,7 @@
# On Windows if we mount the file directly it will end up having 777 permissions and mysql won't read the config
# So instead mount to a temporary directory and copy from there, then chmoding to 0444
rm -rf /etc/mysql/conf.d/*.cnf
cp /tmp/mwdd/master.cnf /etc/mysql/conf.d/master.cnf
cp /mwdd-custom/master.cnf /etc/mysql/conf.d/master.cnf
chmod 0444 /etc/mysql/conf.d/master.cnf

# Then execute the regular mysql / mariadb entrypoint
@@ -0,0 +1,29 @@
#!/bin/bash
# Split from https://tarunlalwani.com/post/mysql-master-slave-using-docker/
# This file grabs the position that we will want to start replication at and stores it in a file.
# This data is then used by the replica to start replicating

position_file=/mwdd-connector/master_position
file_file=/mwdd-connector/master_file

# Only save the data if the files don't already exist
# They might have been created during another container startup
if [ -e "$position_file" ]; then
echo "Position file already exists"
exit 0
fi

echo "Waiting for mysql master to start"
/wait-for-it.sh db-master:3306
# Wait and double check
sleep 1
/wait-for-it.sh db-master:3306

echo "* Get the binlog file and position"
MYSQL01_Position=$(eval "mysql --host db-master -uroot -p$MYSQL_MASTER_PASSWORD -e 'show master status \G' | grep Position | sed -n -e 's/^.*: //p'")
MYSQL01_File=$(eval "mysql --host db-master -uroot -p$MYSQL_MASTER_PASSWORD -e 'show master status \G' | grep File | sed -n -e 's/^.*: //p'")

echo "* Saving data to files"

echo $MYSQL01_Position > $position_file
echo $MYSQL01_File > $file_file
@@ -3,8 +3,8 @@
# On Windows if we mount the file directly it will end up having 777 permissions and mysql won't read the config
# So instead mount to a temporary directory and copy from there, then chmoding to 0444
rm -rf /etc/mysql/conf.d/*.cnf
cp /tmp/mwdd/slave.cnf /etc/mysql/conf.d/slave.cnf
chmod 0444 /etc/mysql/conf.d/slave.cnf
cp /mwdd-custom/replica.cnf /etc/mysql/conf.d/replica.cnf
chmod 0444 /etc/mysql/conf.d/replica.cnf

# Then execute the regular mysql / mariadb entrypoint
/usr/local/bin/docker-entrypoint.sh $@
@@ -0,0 +1,73 @@
#!/bin/bash
# Modified from https://tarunlalwani.com/post/mysql-master-slave-using-docker/

position_file=/mwdd-connector/master_position
file_file=/mwdd-connector/master_file

echo "Waiting for mysql replica to start"
/wait-for-it.sh db-master:3306
/wait-for-it.sh db-replica:3306
# Wait and double check
sleep 1
/wait-for-it.sh db-master:3306
/wait-for-it.sh db-replica:3306

# TODO add resilience? and wait for the position file to be created...?
# But this is probably okay, as the replica will take a while to start anyway..

# Only save the data if the files don't already exist
# They might have been created during another container startup
if [ ! -e "$position_file" ]; then
echo "Position file doesnt exist, can't start replication"
exit 1
fi

echo "* Create replication user"

mysql --host db-replica -uroot -p$MYSQL_REPLICA_PASSWORD -AN -e 'STOP SLAVE;';
mysql --host db-replica -uroot -p$MYSQL_MASTER_PASSWORD -AN -e 'RESET SLAVE ALL;';

mysql --host db-master -uroot -p$MYSQL_MASTER_PASSWORD -AN -e "CREATE USER '$MYSQL_REPLICATION_USER'@'%';"
mysql --host db-master -uroot -p$MYSQL_MASTER_PASSWORD -AN -e "GRANT REPLICATION SLAVE ON *.* TO '$MYSQL_REPLICATION_USER'@'%' IDENTIFIED BY '$MYSQL_REPLICATION_PASSWORD';"
mysql --host db-master -uroot -p$MYSQL_MASTER_PASSWORD -AN -e 'flush privileges;'


echo "* Set MySQL01 as master on MySQL02"

# Grab the position that should have been set from the first step of db-configure when the master was created
MYSQL01_Position=$(<$position_file)
MYSQL01_File=$(<$file_file)

MASTER_IP=$(eval "getent hosts db-master|awk '{print \$1}'")
echo $MASTER_IP
mysql --host db-replica -uroot -p$MYSQL_REPLICA_PASSWORD -AN -e "CHANGE MASTER TO master_host='db-master', master_port=3306, \
master_user='$MYSQL_REPLICATION_USER', master_password='$MYSQL_REPLICATION_PASSWORD', master_log_file='$MYSQL01_File', \
master_log_pos=$MYSQL01_Position;"

echo "* Set MySQL02 as master on MySQL01"

MYSQL02_Position=$(eval "mysql --host db-replica -uroot -p$MYSQL_REPLICA_PASSWORD -e 'show master status \G' | grep Position | sed -n -e 's/^.*: //p'")
MYSQL02_File=$(eval "mysql --host db-replica -uroot -p$MYSQL_REPLICA_PASSWORD -e 'show master status \G' | grep File | sed -n -e 's/^.*: //p'")

REPLICA_IP=$(eval "getent hosts db-replica|awk '{print \$1}'")
echo $REPLICA_IP
mysql --host db-master -uroot -p$MYSQL_MASTER_PASSWORD -AN -e "CHANGE MASTER TO master_host='db-replica', master_port=3306, \
master_user='$MYSQL_REPLICATION_USER', master_password='$MYSQL_REPLICATION_PASSWORD', master_log_file='$MYSQL02_File', \
master_log_pos=$MYSQL02_Position;"

echo "* Start Replica on both Servers"
mysql --host db-replica -uroot -p$MYSQL_REPLICA_PASSWORD -AN -e "start slave;"

echo "Increase the max_connections to 1000"
mysql --host db-master -uroot -p$MYSQL_MASTER_PASSWORD -AN -e 'set GLOBAL max_connections=1000';
mysql --host db-replica -uroot -p$MYSQL_REPLICA_PASSWORD -AN -e 'set GLOBAL max_connections=1000';

mysql --host db-replica -uroot -p$MYSQL_MASTER_PASSWORD -e "show slave status \G"

echo "MySQL servers created!"
echo "--------------------"
echo
echo Variables available fo you :-
echo
echo MYSQL01_IP : db-master
echo MYSQL02_IP : db-replica
File renamed without changes.
@@ -25,7 +25,7 @@ USAGE
wait_for()
{
if [[ $TIMEOUT -gt 0 ]]; then
echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT"
echoerr "$cmdname: waiting for $HOST:$PORT with a timeout of $TIMEOUT seconds"
else
echoerr "$cmdname: waiting for $HOST:$PORT without a timeout"
fi
@@ -136,7 +136,7 @@ if [[ "$HOST" == "" || "$PORT" == "" ]]; then
usage
fi

TIMEOUT=${TIMEOUT:-120}
TIMEOUT=${TIMEOUT:-240}
STRICT=${STRICT:-0}
CHILD=${CHILD:-0}
QUIET=${QUIET:-0}
@@ -174,4 +174,4 @@ if [[ $CLI != "" ]]; then
exec $CLI
else
exit $RESULT
fi
fi
@@ -0,0 +1 @@
vendor
@@ -0,0 +1,26 @@
FROM composer:1.10 as composer

COPY ./composer.json /app/composer.json
COPY ./composer.lock /app/composer.lock
RUN composer install --no-dev --no-progress


FROM php:7.2-cli

# Just install docker-compose which will allow the control app to interact with the docker setup
RUN apt-get update && \
apt-get install -y \
# TODO does the container really need git? I mean it doesn't even need docker-compose really...
git && \
curl -L "https://github.com/docker/compose/releases/download/1.25.5/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose && \
chmod +x /usr/local/bin/docker-compose

COPY --from=composer /app/vendor /mwdd-vendor

RUN echo "#!/bin/bash" >> /idle && echo "tail -f /dev/null" >> /idle && chmod +x /idle

WORKDIR /mwdd

# We don't need anything to be running, just for the container to be setup and running
# this needs to be running as mwdd uses docker-compose exec... (I couldn't make run work...)
ENTRYPOINT ["/idle"]
@@ -0,0 +1,16 @@
## mwdd control application

This application has been written as a second iteration of the mediawiki-docker-development environment.

This has been written in PHP to be as close as possible to the MediaWiki world, in the hopes of PRs etc.

Having a single command to do all of the things in a nice way has been [talked about for a while](https://github.com/addshore/mediawiki-docker-dev/issues/84)

The future plan would be that the only requirement is docker and or docker-compose in order to use this solution (no local PHP needed), but the details of that have not yet been finalized.

#### Directories

- Command - CLI commands that the application exposes
- DockerCompose - Classes that relate to the value docker-composer yml files used by the system
- Files - Classes that relate to the various files and directories that the system interacts with
- Shell - Classes that relate to the various applications this application shells out to
@@ -0,0 +1,114 @@
#!/usr/bin/env php
<?php

error_reporting(E_ALL);
ini_set('display_errors', 1);

if ( $_SERVER['PHP_SELF'] !== 'control/app.php' ) {
echo "You are running the control app from the wrong context.\n";
echo "Please run the app from the root mediawiki-docker-dev directory.\n";
die();
}

if(file_exists(__DIR__.'/vendor/autoload.php')){
// Load a local vendor dir if it exits
require_once __DIR__.'/vendor/autoload.php';
} elseif( file_exists('/mwdd-vendor') ) {
// If we are running in the control container, we can just copy the vendor that we made when building the image.
shell_exec('cp -R /mwdd-vendor ' . __DIR__ . '/vendor'); // XXX FIXME: this will copy as root?
// Try again with our copied vendor dir from the control image
require_once __DIR__.'/vendor/autoload.php';
} else {
echo "You either need to:\n";
echo " - Use a php environment locally and have done a composer install of the control directory.";
echo " - Use a docker-compose environment which will populate the control vendor directory for you. (as root FIXME)";
die();
}

define('MWDD_DIR', dirname( __DIR__ ));

$application = new \Symfony\Component\Console\Application('mwdd');

$application->add(new \Addshore\Mwdd\Command\Base\HostsAdd());

// TODO register these using a factory or something...
$mwHelp = <<<EOH
MediaWiki.
MediaWiki will be accessible at a location such as: http://default.web.mw.localhost:8080/api.php
EOH;
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Create('mw', $mwHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Suspend('mw', $mwHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Resume('mw', $mwHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Exec('mw', $mwHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Logs('mw', $mwHelp));
$application->add(new \Addshore\Mwdd\Command\MediaWiki\GetCode());
$application->add(new \Addshore\Mwdd\Command\MediaWiki\PHPUnit());
$application->add(new \Addshore\Mwdd\Command\MediaWiki\Composer());
$application->add(new \Addshore\Mwdd\Command\MediaWiki\Fresh());
$application->add(new \Addshore\Mwdd\Command\MediaWiki\Maint());
$application->add(new \Addshore\Mwdd\Command\MediaWiki\Quibble());
$application->add(new \Addshore\Mwdd\Command\MediaWiki\Install());

$adminerHelp = 'Adminer is a tool for managing content in MySQL databases.';
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Create('adminer', $adminerHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Suspend('adminer', $adminerHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Resume('adminer', $adminerHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Exec('adminer', $adminerHelp));

$phpMyAdminHelp = 'phpMyAdmin is a free and open source administration tool for MySQL and MariaDB.';
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Create('phpmyadmin', $phpMyAdminHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Suspend('phpmyadmin', $phpMyAdminHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Resume('phpmyadmin', $phpMyAdminHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Exec('phpmyadmin', $phpMyAdminHelp));

$redisHelp = 'Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker.';
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Create('redis', $redisHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Suspend('redis', $redisHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Resume('redis', $redisHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Exec('redis', $redisHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Cli('redis', 'redis-cli', $redisHelp));

$statsdHelp = 'Statsd and Graphite allow for simple time series data collection.';
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Create('statsd', $statsdHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Suspend('statsd', $statsdHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Resume('statsd', $statsdHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Exec('statsd', $statsdHelp));

$masterHelp = <<<EOH
A primary MySql server (master).
You can alter the image that is used in you local.env file.
EOH;
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Create('db', $masterHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Suspend('db', $masterHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Resume('db', $masterHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Exec('db', $masterHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Cli('db', 'mysql', $masterHelp));

$replicaHelp = <<< EOH
A second MySql server with automatic replication from the master.
You can alter the image that is used in you local.env file.
Upon startup it might take a short while for server to catch up with the master, depending on how much data has been written.
You can check the replication status using <info>SHOW SLAVE STATUS</info>
EOH;
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Create('db-replica', $replicaHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Suspend('db-replica', $replicaHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Resume('db-replica', $replicaHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Exec('db-replica', $replicaHelp));
$application->add(new \Addshore\Mwdd\Command\ServiceBase\Cli('db-replica', 'mysql', $replicaHelp));

$application->add(new \Addshore\Mwdd\Command\DockerCompose\Raw());
$application->add(new \Addshore\Mwdd\Command\DockerCompose\Ps());
$application->add(new \Addshore\Mwdd\Command\DockerCompose\Logs());
$application->add(new \Addshore\Mwdd\Command\DockerCompose\Destroy());
$application->add(new \Addshore\Mwdd\Command\DockerCompose\Bash());

$application->add(new \Addshore\Mwdd\Command\Control\Create());
$application->add(new \Addshore\Mwdd\Command\Control\Suspend());
$application->add(new \Addshore\Mwdd\Command\Control\Bash());

$application->run();
@@ -0,0 +1,12 @@
{
"require": {
"symfony/console": "^5.0",
"m1/env": "^2.2",
"symfony/yaml": "^5.0"
},
"autoload": {
"psr-4": {
"Addshore\\Mwdd\\": "src/"
}
}
}

Large diffs are not rendered by default.

@@ -0,0 +1,73 @@
<?php

namespace Addshore\Mwdd\Command\Base;

use Addshore\Mwdd\DockerCompose\Base;
use Addshore\Mwdd\Files\DotEnv;
use Addshore\Mwdd\Shell\Docker;
use Addshore\Mwdd\Shell\DockerCompose;
use Addshore\Mwdd\Shell\Id;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Create extends Command
{

protected static $defaultName = 'baseXXX:create';

protected function configure()
{
$this->setDescription('Create a the most basic development environment.');
$serviceString = implode( ', ', Base::SERVICES );
$this->setHelp(<<< EOT
Creates the most basic development environment, with the following services:
${serviceString}
EOT
);
}

protected function execute(InputInterface $input, OutputInterface $output)
{
(new DotEnv())->updateFromDefaultAndLocal();

# Start containers
$output->writeln('Starting services: ' . implode( ',', Base::SERVICES ));
(new DockerCompose())->upDetached( Base::SERVICES );

# Add document root index file (NOTE: docker-compose lacks a "cp" command)
# TODO why is this not just in the docker-compose yml?
(new Docker())->cp(
'config/mediawiki/index.php',
(new DockerCompose())->psQ(Base::SRV_MEDIAWIKI) . '://var/www/index.php'
);

# Chown some things...
# TODO should this be in the entrypoint? YES!
(new DockerCompose())->exec(
Base::SRV_MEDIAWIKI,
'chmod 777 //var/log/mediawiki //var/www/mediawiki/images/docker',
'--user root'
);

# Wait for the db server
$output->writeln('Waiting for the db server');
$output->writeln('Sometimes this can take some time...');
(new DockerCompose())->exec( Base::SRV_MEDIAWIKI, '//srv/wait-for-it.sh db-master:3306' );

# Reset local hosts file
if(file_exists(MWDD_DIR . '/.hosts')) {
unlink( MWDD_DIR . '/.hosts' );
}
$this->getApplication()->find('base:hosts-add')->run( new ArrayInput([ 'host' => 'proxy.mw.localhost' ]), $output );

$this->getApplication()->find('mw:installsite')->run( new ArrayInput([ 'site' => 'default' ]), $output );

$output->writeln('Your development environment is running');
$output->writeln('You may need to update your hosts file (see .hosts and hosts-sync files)!!');

return 0;

}
}
@@ -0,0 +1,27 @@
<?php

namespace Addshore\Mwdd\Command\Base;

use Addshore\Mwdd\Files\DotHosts;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class HostsAdd extends Command
{

protected static $defaultName = 'base:hosts-add';

protected function configure()
{
$this->setHidden(true);
$this->addArgument( 'host' );
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$host = $input->getArgument( 'host' );
(new DotHosts())->addHost( '127.0.0.1', $host );
return 0;
}
}
@@ -0,0 +1,27 @@
<?php

namespace Addshore\Mwdd\Command\Control;

use Addshore\Mwdd\DockerCompose\Control;
use Addshore\Mwdd\Shell\DockerCompose;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Bash extends Command
{

protected static $defaultName = 'ctrl:bash';

protected function configure()
{
$this->setDescription('Runs bash in the control container (if running).');
$this->setHidden(true);
}

protected function execute(InputInterface $input, OutputInterface $output)
{
(new DockerCompose())->exec( Control::SRV_CONTROL, 'bash' );
return 0;
}
}
@@ -0,0 +1,27 @@
<?php

namespace Addshore\Mwdd\Command\Control;

use Addshore\Mwdd\DockerCompose\Control;
use Addshore\Mwdd\Shell\DockerCompose;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Create extends Command
{

protected static $defaultName = 'ctrl:create';

protected function configure()
{
$this->setDescription('Creates the control container (building if needed).');
$this->setHidden(true);
}

protected function execute(InputInterface $input, OutputInterface $output)
{
(new DockerCompose())->upDetached( Control::SERVICES, $input->getOption('build'));
return 0;
}
}
@@ -0,0 +1,26 @@
<?php

namespace Addshore\Mwdd\Command\Control;

use Addshore\Mwdd\DockerCompose\Control;
use Addshore\Mwdd\Shell\DockerCompose;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Suspend extends Command
{

protected static $defaultName = 'ctrl:suspend';

protected function configure()
{
$this->setDescription('Suspends an already running control container.');
}

protected function execute(InputInterface $input, OutputInterface $output)
{
(new DockerCompose())->stop(Control::SERVICES);
return 0;
}
}
@@ -0,0 +1,32 @@
<?php

namespace Addshore\Mwdd\Command\DockerCompose;

use Addshore\Mwdd\Shell\DockerCompose;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Bash extends Command
{

protected static $defaultName = 'dc:bash';

protected function configure()
{
$this->setDescription('Run a shell on one of the service containers');
$this->addArgument( 'service' );
$this->addArgument( 'shell', InputArgument::OPTIONAL, '', 'bash' );

}

protected function execute(InputInterface $input, OutputInterface $output)
{
$service = $input->getArgument('service');
$shell = $input->getArgument('shell');

(new DockerCompose())->exec( $service, $shell );
return 0;
}
}
@@ -0,0 +1,32 @@
<?php

namespace Addshore\Mwdd\Command\DockerCompose;

use Addshore\Mwdd\Shell\DockerCompose;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Destroy extends Command
{

protected static $defaultName = 'dc:destroy';

protected function configure()
{
$this->setDescription('Shut down all containers, and destroy them. Also deletes databases and volumes.');
$this->setHelp('Shut down all containers, and destroy them. Also deletes databases and volumes.');
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln("Containers and volumes are being destroyed.");
(new DockerCompose())->downWithVolumesAndOrphans();

// This is not related to the destroy functionality, but this is nice to clean this up here...
if(file_exists(MWDD_DIR . '/.hosts')) {
unlink( MWDD_DIR . '/.hosts' );
}
return 0;
}
}
@@ -0,0 +1,32 @@
<?php

namespace Addshore\Mwdd\Command\DockerCompose;

use Addshore\Mwdd\Shell\DockerCompose;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Logs extends Command
{

protected static $defaultName = 'dc:logs';

protected function configure()
{
$this->setDescription('Tails service logs.');
$this->addArgument( 'service', InputArgument::REQUIRED );
$this->addArgument( 'lines', null, '', 25 );

}

protected function execute(InputInterface $input, OutputInterface $output)
{
$service = $input->getArgument('service');
$lines = $input->getArgument('lines');

(new DockerCompose())->logsTail( $service, $lines );
return 0;
}
}
@@ -0,0 +1,25 @@
<?php

namespace Addshore\Mwdd\Command\DockerCompose;

use Addshore\Mwdd\Shell\DockerCompose;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Ps extends Command
{

protected static $defaultName = 'dc:ps';

protected function configure()
{
$this->setDescription('Runs docker-compose ps in the correct context.');
}

protected function execute(InputInterface $input, OutputInterface $output)
{
(new DockerCompose())->ps();
return 0;
}
}
@@ -0,0 +1,37 @@
<?php

namespace Addshore\Mwdd\Command\DockerCompose;

use Addshore\Mwdd\Shell\DockerCompose;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Raw extends Command
{

protected static $defaultName = 'dc:raw';

protected function configure()
{
$this->setDescription('Runs a command in the docker-compose context.');
$this->setHelp( <<< EOT
Examples:
View the last 10 logs of the db-configure service:
dc:raw -- logs --tail=10 db-configure
EOT
);
$this->addArgument('args', InputArgument::IS_ARRAY );

}

protected function execute(InputInterface $input, OutputInterface $output)
{
$args = $input->getArgument('args');

(new DockerCompose())->raw( implode( ' ', $args ) );
return 0;
}
}
@@ -0,0 +1,73 @@
<?php

namespace Addshore\Mwdd\Command\MediaWiki;

use Addshore\Mwdd\DockerCompose\MwComposer;
use Addshore\Mwdd\Files\DotEnv;
use Addshore\Mwdd\Shell\DockerCompose;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Composer extends Command
{

protected static $defaultName = 'mw:composer';

protected function configure()
{
$this->setDescription('Runs composer for mediawiki or a subdirectory (when using the alias)');
$this->setHelp( <<< EOT
Runs the composer command in a container within the MediaWiki context.
By default this will run in the MediaWiki core directory.
If you are using the recommended mwdd alias this command will try to run composer in the context of the directory you run the command from.
This is only relevant when said directory is within the mediawiki core directory.
Commands that rely on other applications, such as git, will NOT work as expected.
An example of this would be the Wikibase phpcs-modified script.
EOT
);
$this->addUsage('version');
$this->addUsage('update --ignore-platform-reqs');

$this->addArgument('args', InputArgument::IS_ARRAY );
}

protected function execute(InputInterface $input, OutputInterface $output)
{
// Try to run in the correct directory to run the command in
// TODO reuse this code somewhere....
$shortcutEnv = getenv('MWDD_S_DIR');
$pathInsideMwPath = '';
if($shortcutEnv) {
$mwPath = (new DotEnv())->getValue('DOCKER_MW_PATH');
// Trim /'s from the start and end, and ~ if used as the MW path base
$shortcutEnv = trim( $shortcutEnv, '/' );
$mwPath = trim( $mwPath, '/~' );
// Determine some stuff
$isInMwDir = strstr( $shortcutEnv, $mwPath );
if($isInMwDir) {
$splitOnMwPath = explode( $mwPath, $shortcutEnv );
$pathInsideMwPath = $splitOnMwPath[1];
}
}

$args = $input->getArgument('args');

// The service must be created in order to be able to use docker run
// TODO don't always run this...
$output->writeln("MWDD: Sorry that this is a bit slow to run (need to think of a nice fix) as it runs up each time");
(new DockerCompose())->upDetached(MwComposer::SERVICES);

// TODO mount local .composer cache dir?! (done in getCode a bit already)
// User is specified in the docker-compose yml
(new DockerCompose())->run(
MwComposer::SRV_COMPOSER,
"composer " . implode( ' ', $args ),
'--rm --workdir=/app/' . $pathInsideMwPath
);
return 0;
}
}
@@ -0,0 +1,80 @@
<?php

namespace Addshore\Mwdd\Command\MediaWiki;

use Addshore\Mwdd\DockerCompose\MwComposer;
use Addshore\Mwdd\DockerCompose\MwFresh;
use Addshore\Mwdd\Files\DotEnv;
use Addshore\Mwdd\Shell\DockerCompose;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Fresh extends Command
{

protected static $defaultName = 'mw:fresh';

protected function configure()
{
$this->setDescription('Runs fresh for MediaWiki or a subdirectory (when using the alias)');
$this->addUsage('npm install');
$this->addUsage('npm run selenium');
$this->setHelp( <<< EOT
Runs 'fresh', a node js running environment, within the MediaWiki context.
See: https://github.com/wikimedia/fresh
By default this will run in the MediaWiki core directory.
If you are using the recommended mwdd alias this command will try to run composer in the context of the directory you run the command from.
This is only relevant when said directory is within the mediawiki core directory.
EOT
);

$this->addArgument('args', InputArgument::IS_ARRAY );
}

protected function execute(InputInterface $input, OutputInterface $output)
{
// Try to run in the correct directory to run the command in
// TODO reuse this code somewhere....
$shortcutEnv = getenv('MWDD_S_DIR');
$pathInsideMwPath = '';
if($shortcutEnv) {
$mwPath = (new DotEnv())->getValue('DOCKER_MW_PATH');
// Trim /'s from the start and end, and ~ if used as the MW path base
$shortcutEnv = trim( $shortcutEnv, '/' );
$mwPath = trim( $mwPath, '/~' );
// Determine some stuff
$isInMwDir = strstr( $shortcutEnv, $mwPath );
if($isInMwDir) {
$splitOnMwPath = explode( $mwPath, $shortcutEnv );
$pathInsideMwPath = $splitOnMwPath[1];
}
}

$args = $input->getArgument('args');

// The service must be created in order to be able to use docker run
// TODO don't always run this...
$output->writeln("MWDD: Sorry that this is a bit slow to run (need to think of a nice fix) as it runs up each time");
$output->writeln("MWDD: Fresh also seems to be a bit buggy currently and sometimes freeze? maybe?");
(new DockerCompose())->upDetached(MwFresh::SERVICES);


// TODO needs addslashes a little for " ?
if(strstr(implode( ' ', $args ), '"')) {
$output->writeln('MWDD: WARNING, Your arguments have a " in them, I currently predict something will go wrong.');
}

// User is specified in the docker-compose yml
(new DockerCompose())->run(
MwFresh::SRV_FRESH,
// TODO needs addslashes a little for " ?
'-c "' . implode( ' ', $args ) . '"',
'--rm --workdir=/app/' . $pathInsideMwPath
);
return 0;
}
}
@@ -0,0 +1,59 @@
<?php

namespace Addshore\Mwdd\Command\Mediawiki;

use Addshore\Mwdd\Files\DotEnv;
use Addshore\Mwdd\Files\MediaWikiDir;
use Addshore\Mwdd\Shell\Docker;
use Addshore\Mwdd\Shell\Git;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;

class GetCode extends Command {

protected static $defaultName = 'mw:getcode';

protected function configure() {
$this->setDescription('Gets MediaWiki code if you don\'t already have it.');
// TODO right now this doesnt auto gen the file if it already exists... so just always show the command, maybe we need a different config? :D
$this->setHidden((new MediaWikiDir((new DotEnv(true))->getValue('DOCKER_MW_PATH')))->hasDotGitDirectory());
}

protected function execute( InputInterface $input, OutputInterface $output ) {
$mwPath = (new DotEnv())->getValue('DOCKER_MW_PATH');

$output->writeln("Your currently configured MediaWiki path is: " . $mwPath);
$helper = $this->getHelper('question');
$question = new ConfirmationQuestion('Would you like to fetch code there??', false);

if (!$helper->ask($input, $output, $question)) {
$output->writeln("Please update your local.env before continuing.");
return 0;
}

$mwDir = new MediaWikiDir( $mwPath );

// Clone the minimum needed code
( new Git() )->clone( 'https://gerrit.wikimedia.org/r/mediawiki/core', $mwDir );
( new Git() )->clone( 'https://gerrit.wikimedia.org/r/mediawiki/skins/Vector',
$mwDir
. '/skins/Vector' );

// Run composer install (not as part of compose)
( new Docker() )->runComposerInstall( $mwDir );

// Create the basic local settings file...
$lsFile = $mwDir . '/LocalSettings.php';
$initialLocalSettings = <<<EOT
<?php
require_once '/mwdd-custom/LocalSettings.php';
wfLoadSkin( 'Vector' );
EOT;
file_put_contents( $lsFile, $initialLocalSettings );

return 0;
}

}
@@ -0,0 +1,63 @@
<?php

namespace Addshore\Mwdd\Command\Mediawiki;

use Addshore\Mwdd\Command\TraitForCommandsThatAddHosts;
use Addshore\Mwdd\DockerCompose\Base;
use Addshore\Mwdd\Shell\DockerCompose;
use Addshore\Mwdd\Shell\Id;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Install extends Command
{

use TraitForCommandsThatAddHosts;

protected static $defaultName = 'mw:install';

protected function configure()
{
$this->setDescription('Installs a new MediaWiki site.');
$this->setHelp(<<<EOH
This command does the following:
1) Creates some directories in the container for images, tmp storage and caching (777 permissions).
- /app/images/docker/<sitename>
- /app/images/docker/<sitename>/tmp
- /app/images/docker/<sitename>/cache
2) Runs the installdbs shell script which:
- Waits for db-master service to be ready
- Moves the user LocalSettings.php file out of the way
- Runs install.php
- Moves the user LocalSettings.php back
- Runs update.php
EOH
);

$this->addArgument( 'site', InputArgument::OPTIONAL, 'The site name to install', 'default' );
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$site = $input->getArgument( 'site' );
$output->writeln("Adding new site: " . $site);

// Make some directories that ONLY exist within the container (anon volume)
// TODO could chown the dirs to someone else?
(new DockerCompose())->exec( Base::SRV_MEDIAWIKI, 'mkdir -m 777 -p //app/images/docker/' . $site );
(new DockerCompose())->exec( Base::SRV_MEDIAWIKI, 'mkdir -m 777 -p //app/images/docker/' . $site . '/tmp' );
(new DockerCompose())->exec( Base::SRV_MEDIAWIKI, 'mkdir -m 777 -p //app/images/docker/' . $site . '/cache' );

$ug = (new Id())->ug();
// TODO try to output these commands as they are running to the user for observability..?
(new DockerCompose())->exec( Base::SRV_MEDIAWIKI, 'bash //mwdd-custom/installdbs ' . $site, "--user ${ug}" );

$this->addHostsAndPrintOutput( [ $site . '.web.mw.localhost' ], $output );
return 0;
}

}
@@ -0,0 +1,52 @@
<?php

namespace Addshore\Mwdd\Command\Mediawiki;

use Addshore\Mwdd\DockerCompose\Base;
use Addshore\Mwdd\Shell\DockerCompose;
use Addshore\Mwdd\Shell\Id;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class Maint extends Command
{

protected static $defaultName = 'mw:maint';

protected function configure()
{
$this->setDescription('Runs a MediaWiki maintenance script');

$this->addArgument('script', InputArgument::REQUIRED | InputArgument::IS_ARRAY );
$this->addOption('wiki', null, InputArgument::OPTIONAL, '', 'default' );
$this->addOption('debug', 'd', InputOption::VALUE_OPTIONAL, 'Enable debugger');
$this->ignoreValidationErrors();

$this->addUsage('-- maintenance/showJobs.php');
$this->addUsage('-- maintenance/showJobs.php --group');
$this->addUsage('--wiki=other -- maintenance/showJobs.php');
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$debugPrefix = '';
if($input->getOption('debug')) {
$debugPrefix = 'XDEBUG_CONFIG=\"remote_host=${XDEBUG_REMOTE_HOST}\" ';
}

$wiki = $input->getOption('wiki');
$script = implode( ' ', $input->getArgument('script') );

$ug = (new Id())->ug();
// exec instead of run so that we don't load the depends ons..... (but we could state not to load them?)
(new DockerCompose())->exec(
Base::SRV_MEDIAWIKI,
"sh -c \"${debugPrefix}php //app/${script} --wiki ${wiki}\"",
"--user ${ug}"
);
return 0;
}
}
@@ -0,0 +1,57 @@
<?php

namespace Addshore\Mwdd\Command\MediaWiki;

use Addshore\Mwdd\DockerCompose\Base;
use Addshore\Mwdd\Shell\DockerCompose;
use Addshore\Mwdd\Shell\Id;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class PHPUnit extends Command
{

protected static $defaultName = 'mw:phpunit';

protected function configure()
{
$this->setDescription('Runs the MediaWiki phpunit.php');

$this->addArgument('testPath', InputOption::VALUE_REQUIRED );
$this->addOption('wiki', 'w', InputOption::VALUE_OPTIONAL, 'The wiki to run the tests for', 'default');
$this->addOption('args', 'a', InputOption::VALUE_OPTIONAL, 'String of extra arguments to pass to phpunit.php', '');
$this->addOption('debug', 'd', InputOption::VALUE_OPTIONAL, 'Enable debugger');

$this->addUsage('tests/phpunit/includes/StatusTest.php');
$this->addUsage('-d=1 tests/phpunit/includes/StatusTest.php');
$this->addUsage('tests/phpunit/includes/StatusTest.php --wiki otherwiki');
$this->addUsage('extensions/Wikibase/lib/tests/phpunit/Store/Sql/TermSqlIndexTest.php');
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$path = $input->getArgument('testPath');
if(empty($path)) {
$output->writeln("path must be specified");
return 1;
}
$wiki = $input->getOption('wiki');
$args = $input->getOption('args');
$debugPrefix = '';
if($input->getOption('debug')) {
$debugPrefix = 'XDEBUG_CONFIG=\"remote_host=${XDEBUG_REMOTE_HOST}\" ';
}

$ug = (new Id())->ug();
// This runs in the mediawiki service currently, but we could consider running it as a tool (similar to fresh etc)
// exec instead of run so that we don't load the depends ons..... (but we could state not to load them?)
(new DockerCompose())->exec(
Base::SRV_MEDIAWIKI,
"sh -c \"${debugPrefix}php //app/tests/phpunit/phpunit.php ${args} --wiki ${wiki} //app/${path}\"",
"--user ${ug}"
);
return 0;
}
}
@@ -0,0 +1,65 @@
<?php

namespace Addshore\Mwdd\Command\MediaWiki;

use Addshore\Mwdd\DockerCompose\MwComposer;
use Addshore\Mwdd\DockerCompose\MwFresh;
use Addshore\Mwdd\DockerCompose\MwQuibble;
use Addshore\Mwdd\Files\DotEnv;
use Addshore\Mwdd\Shell\DockerCompose;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Quibble extends Command
{

protected static $defaultName = 'mw:quibble';

protected function configure()
{
$this->addArgument('args', InputArgument::IS_ARRAY );
}

protected function execute(InputInterface $input, OutputInterface $output)
{
// Try to run in the correct directory to run the command in
// TODO reuse this code somewhere....
// $shortcutEnv = getenv('MWDD_S_DIR');
$pathInsideMwPath = '';
// if($shortcutEnv) {
// $mwPath = (new DotEnv())->getValue('DOCKER_MW_PATH');
// // Trim /'s from the start and end, and ~ if used as the MW path base
// $shortcutEnv = trim( $shortcutEnv, '/' );
// $mwPath = trim( $mwPath, '/~' );
// // Determine some stuff
// $isInMwDir = strstr( $shortcutEnv, $mwPath );
// if($isInMwDir) {
// $splitOnMwPath = explode( $mwPath, $shortcutEnv );
// $pathInsideMwPath = $splitOnMwPath[1];
// }
// }

$args = $input->getArgument('args');

// The service must be created in order to be able to use docker run
// TODO don't always run this...
$output->writeln("MWDD: Sorry that this is a bit slow to run (need to think of a nice fix) as it runs up each time");
$output->writeln("MWDD: Quibble also han't been tested at all");
(new DockerCompose())->upDetached(MwQuibble::SERVICES);


// TODO needs addslashes a little for " ?
if(strstr(implode( ' ', $args ), '"')) {
$output->writeln('MWDD: WARNING, Your arguments have a " in them, I currently predict something will go wrong.');
}

(new DockerCompose())->run(
MwQuibble::SRV_QUIBBLE,
" --skip-zuul --skip-deps --skip-install " . implode( ' ', $args ),
'--rm --entrypoint=/usr/local/bin/quibble --workdir=/app/' . $pathInsideMwPath
);
return 0;
}
}
@@ -0,0 +1,39 @@
<?php

namespace Addshore\Mwdd\Command\ServiceBase;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Cli extends Command
{

protected $serviceSetName;
protected $cliTool;
protected $help;

public function __construct( string $serviceSetName, string $cliTool, string $help ) {
$this->serviceSetName = $serviceSetName;
$this->cliTool = $cliTool;
$this->help = $help;
parent::__construct();
}

protected function configure() {
$this->setName($this->serviceSetName . ':cli');
$this->setHelp(
$this->help . PHP_EOL . PHP_EOL .
'This command uses <info>docker-compose exec</info> internally.'
);
$this->setDescription('Runs the cli tool for this service in a container' );

$this->addUsage('');
}

protected function execute(InputInterface $input, OutputInterface $output)
{
return $this->getApplication()->find($this->serviceSetName . ':exec')->run( new ArrayInput([ Exec::COMMAND => $this->cliTool ]), $output );
}
}
@@ -0,0 +1,51 @@
<?php

namespace Addshore\Mwdd\Command\ServiceBase;

use Addshore\Mwdd\Command\TraitForCommandsThatAddHosts;
use Addshore\Mwdd\DockerCompose\ServiceSet;
use Addshore\Mwdd\Files\DotEnv;
use Addshore\Mwdd\Shell\DockerCompose;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Create extends Command
{

use TraitForCommandsThatAddHosts;

protected $serviceSetName;
protected $help;

public function __construct( string $serviceSetName, string $help ) {
$this->serviceSetName = $serviceSetName;
$this->help = $help;
parent::__construct();
}

protected function configure() {
$this->setName($this->serviceSetName . ':create' );
$this->setHelp(
$this->help . PHP_EOL . PHP_EOL .
'This command uses <info>docker-compose up -d</info> internally.'
);
$this->setDescription('Creates or recreates the service containers' );
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$serviceSet = new ServiceSet($this->serviceSetName);
$dotEnv = new DotEnv();
$dotEnv->updateFromDefaultAndLocal();

// Output which services we will be running
$serviceNames = $serviceSet->getServiceNames();
$output->writeln('Starting services (with deps): ' . implode( ',', $serviceNames ));
// TODO output what docker-composer command is being run? (slim version)
(new DockerCompose())->upDetached( $serviceNames );

$this->addHostsAndPrintOutput( $serviceSet->getVirtualHosts(), $output );
return 1;
}
}
@@ -0,0 +1,95 @@
<?php

namespace Addshore\Mwdd\Command\ServiceBase;

use Addshore\Mwdd\DockerCompose\ServiceSet;
use Addshore\Mwdd\Shell\DockerCompose;
use Addshore\Mwdd\Shell\Id;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class Exec extends Command
{

public const COMMAND = 'COMMAND';
public const SERVICE = 'SERVICE';
public const USER = 'user';

protected $serviceSetName;
protected $help;

protected const DEFAULT_COMMAND = 'Auto detect shell (sh/bash)';
protected const DEFAULT_SERVICE = 'Primary / first service';
protected const DEFAULT_USER = 'Host machine user';

public function __construct( string $serviceSetName, string $help ) {
$this->serviceSetName = $serviceSetName;
$this->help = $help;
parent::__construct();
}

protected function configure() {
$this->setName($this->serviceSetName . ':exec');
$this->setHelp(
$this->help . PHP_EOL . PHP_EOL .
'This command uses <info>docker-compose exec</info> internally.'
);
$this->setDescription('Runs a command a running service container' );

$this->addArgument(
self::COMMAND,
null,
'Which COMMAND to run inside the container.',
self::DEFAULT_COMMAND
);
$this->addArgument(
self::SERVICE,
null,
'Which container to run the COMMAND inside of? Defaults to the primary service.',
self::DEFAULT_SERVICE
);

$this->addOption(
self::USER,
null,
InputOption::VALUE_OPTIONAL,
'Which user to run as',
self::DEFAULT_USER
);

$this->addUsage('');
$this->addUsage('bash');
$this->addUsage('--user=root bash');
$this->addUsage('sh');
$this->addUsage('sh other-service');
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$serviceSet = new ServiceSet($this->serviceSetName);

$command = $input->getArgument( self::COMMAND );
if($command === self::DEFAULT_COMMAND) {
// use sh which we probably have to detect if bash is there and use the best one
$command = 'sh -c "if [ $(command -v bash) ]; then bash; else sh; fi"';
}

$service = $input->getArgument( self::SERVICE );
if($service === self::DEFAULT_SERVICE) {
// Get the first service listed in the docker-compose yml which should be the most important
$service = $serviceSet->getServiceNames()[0];
}

$user = $input->getOption( self::USER );
if($user === self::DEFAULT_USER) {
$output->writeln('MWDD: You may see a message saying the user or group ID doesnt exist, but you can ignore it');
$user = (new Id())->ug();
}

(new DockerCompose())->execIt( $service, $command, '--user ' . $user );

return 0;
}
}
@@ -0,0 +1,70 @@
<?php

namespace Addshore\Mwdd\Command\ServiceBase;

use Addshore\Mwdd\DockerCompose\ServiceSet;
use Addshore\Mwdd\Shell\DockerCompose;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Logs extends Command
{

private const LOG_DEFAULT_LS = 'Lists available log files';

protected $serviceSetName;
protected $help;

public function __construct( string $serviceSetName, string $help ) {
$this->serviceSetName = $serviceSetName;
$this->help = $help;
parent::__construct();
}

protected function configure()
{
$serviceSet = new ServiceSet($this->serviceSetName);

$logDir = $serviceSet->getEnvVarForFirstService('MWDD_LOG_DIR' );
// Don't show the command if the service doesnt have a log dir configured..
$this->setHidden(!$logDir);

/**
* In order to output more logs to this directory you might have to enable extra log groups.
https://www.mediawiki.org/wiki/Manual:\$wgDebugLogGroups
\$wgDebugLogGroups['debug'] = "/var/log/mediawiki/debug.log";
\$wgDebugLogGroups['Wikibase'] = "/var/log/mediawiki/wikibase.log";
*/

$this->setName($this->serviceSetName . ':logs');
$this->setHelp(
$this->help . PHP_EOL . PHP_EOL .
'This command uses <info>docker-compose exec</info> internally to run <info>tail -f</info> in the container.'
);
$this->setDescription('Tail service logs' );

$this->addArgument('log', InputArgument::OPTIONAL, '', self::LOG_DEFAULT_LS );

$this->addUsage('');
$this->addUsage('debug.log');
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$serviceSet = new ServiceSet($this->serviceSetName);

$log = $input->getArgument('log');

if( $log === self::LOG_DEFAULT_LS ) {
$output->writeln('Listing available log files:');
(new DockerCompose())->exec( $serviceSet->getFirstServiceName(), "ls /var/log/mediawiki/");
} else {
(new DockerCompose())->exec( $serviceSet->getFirstServiceName(), "tail -f /var/log/mediawiki/${log}");
}

return 0;
}
}
@@ -0,0 +1,38 @@
<?php

namespace Addshore\Mwdd\Command\ServiceBase;

use Addshore\Mwdd\DockerCompose\ServiceSet;
use Addshore\Mwdd\Shell\DockerCompose;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Resume extends Command
{

protected $serviceSetName;
protected $help;

public function __construct( string $serviceSetName, string $help ) {
$this->serviceSetName = $serviceSetName;
$this->help = $help;
parent::__construct();
}

protected function configure() {
$this->setName($this->serviceSetName . ':resume');
$this->setHelp(
$this->help . PHP_EOL . PHP_EOL .
'This command uses <info>docker-compose start</info> internally.'
);
$this->setDescription('Resumes a set of previously created containers' );
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$serviceSet = new ServiceSet($this->serviceSetName);
(new DockerCompose())->start( $serviceSet->getServiceNames());
return 0;
}
}
@@ -0,0 +1,39 @@
<?php

namespace Addshore\Mwdd\Command\ServiceBase;

use Addshore\Mwdd\DockerCompose\ServiceSet;
use Addshore\Mwdd\Shell\DockerCompose;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Suspend extends Command
{

protected $serviceSetName;
protected $help;

public function __construct( string $serviceSetName, string $help ) {
$this->serviceSetName = $serviceSetName;
$this->help = $help;
parent::__construct();
}

protected function configure() {
$this->setName($this->serviceSetName . ':suspend');
$this->setHelp(
$this->help . PHP_EOL . PHP_EOL .
'This command uses <info>docker-compose stop</info> internally.'
);
$this->setDescription('Suspends a set of previously created containers' );

}

protected function execute(InputInterface $input, OutputInterface $output)
{
$serviceSet = new ServiceSet($this->serviceSetName);
(new DockerCompose())->stop( $serviceSet->getServiceNames());
return 0;
}
}
@@ -0,0 +1,46 @@
<?php

namespace Addshore\Mwdd\Command;

use Addshore\Mwdd\Files\DotEnv;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\OutputInterface;

trait TraitForCommandsThatAddHosts {

private $addedHosts = [];

protected function addHostsAndPrintOutput( array $hosts, OutputInterface $output ) : void{
foreach( $hosts as $host ) {
$this->addHost( $host, $output );
}
$this->printInfoAboutAddedHosts( $output );
}

protected function addHosts( array $hosts, OutputInterface $output ) : void{
foreach( $hosts as $host ) {
$this->addHost( $host, $output );
}
}

protected function addHost( string $host, OutputInterface $output ) :void {
$this->getApplication()->find('base:hosts-add')->run( new ArrayInput([ 'host' => $host ]), $output );
$this->addedHosts[] = $host;
}

protected function printInfoAboutAddedHosts( OutputInterface $output ): void {
if( !$this->addedHosts ) {
return;
}

$dotEnv = new DotEnv();
$dotEnv->updateFromDefaultAndLocal();
$publicPort = $dotEnv->getValue('DOCKER_MW_PORT');

$output->writeln('Some processes added new host (accessible via HTTP):');
foreach( $this->addedHosts as $host ) {
$output->writeln( " - <href=http://${host}:${publicPort}>http://${host}:${publicPort}</>");
}
}

}
@@ -0,0 +1,21 @@
<?php

namespace Addshore\Mwdd\DockerCompose;

class Base {

public const SRV_MEDIAWIKI = 'mediawiki';
public const SRV_DPS = 'dps';
public const SRV_DB_MASTER = 'db-master';
public const SRV_DB_CONFIGURE = 'db-configure';
public const SRV_NGINX_PROXY = 'nginx-proxy';

public const SERVICES = [
self::SRV_DPS,
self::SRV_DB_MASTER,
self::SRV_DB_CONFIGURE,
self::SRV_NGINX_PROXY,
self::SRV_MEDIAWIKI,
];

}
@@ -0,0 +1,13 @@
<?php

namespace Addshore\Mwdd\DockerCompose;

class Control {

public const SRV_CONTROL = 'control';

public const SERVICES = [
self::SRV_CONTROL,
];

}
@@ -0,0 +1,13 @@
<?php

namespace Addshore\Mwdd\DockerCompose;

class MwComposer {

public const SRV_COMPOSER = 'mw-composer';

public const SERVICES = [
self::SRV_COMPOSER,
];

}
@@ -0,0 +1,13 @@
<?php

namespace Addshore\Mwdd\DockerCompose;

class MwFresh {

public const SRV_FRESH = 'mw-fresh';

public const SERVICES = [
self::SRV_FRESH,
];

}
@@ -0,0 +1,13 @@
<?php

namespace Addshore\Mwdd\DockerCompose;

class MwQuibble {

public const SRV_QUIBBLE = 'mw-quibble';

public const SERVICES = [
self::SRV_QUIBBLE,
];

}
@@ -0,0 +1,105 @@
<?php

namespace Addshore\Mwdd\DockerCompose;

use Addshore\Mwdd\Files\DotEnv;
use M1\Env\Parser;
use Symfony\Component\Yaml\Yaml;

/**
* A service set represents a docker-compose.yml file with a set of services within it.
* These services should be designed to fit into the mwdd espectations, which means:
* - dns configured?
* - VHOST exposed if desired?
* - matching docker-compose version....
*/
class ServiceSet {

/**
* @var string
*/
private $dcName;

/**
* @var array|null
*/
private $yaml;

/**
* @param string $dcName Name of the dc file, without path or extension
*/
public function __construct( string $dcName ) {
$this->dcName = $dcName;
}

private function getYaml() {
if(!$this->yaml) {
$this->yaml = Yaml::parseFile(__DIR__ . '/../../../docker-compose/' . $this->dcName . '.yml');
// TODO validate against expectations once loaded?
}
return $this->yaml;
}

private function getServices() {
return $this->getYaml()['services'];
}

private function getService( string $name ) {
return $this->getServices()[$name];
}

public function getFirstServiceName() {
return array_keys($this->getServices())[0];
}

public function getServiceNames() {
return array_keys($this->getServices());
}

private function getParsedEquivEnvFile( string $serviceName ) {
$service = $this->getService( $serviceName );

// Bail early if there are no environment values
if(!array_key_exists('environment', $service)) {
return new Parser('');
}

// Replace values from .env that need it
$dotEnv = (new DotEnv());
$dotEnv->updateFromDefaultAndLocal();
$cleanedEnvData = [];
foreach( $service['environment'] as $envLine ) {
preg_match_all( '/\$\{([^\}]*)\}/', $envLine, $matches );
foreach($matches[0] as $matchKey => $match) {
$matchesEnvKey = $matches[1][$matchKey];
$envLine = str_replace( $match, $dotEnv->getValue($matchesEnvKey), $envLine );
}
// XXX EVIL SUPER EVIL HACK
$envLine = str_replace( 'php.apc.enable_cli', 'php_apc_enable_cli', $envLine );
$cleanedEnvData[] = $envLine;
}

return new Parser(implode(PHP_EOL, $cleanedEnvData));
}

public function getEnvVar( string $serviceName, string $envVarName ) {
$environment = $this->getParsedEquivEnvFile( $serviceName );
return $environment->getContent( $envVarName );
}

public function getEnvVarForFirstService( string $envVarName ) {
return $this->getEnvVar( $this->getFirstServiceName(), $envVarName );
}

public function getVirtualHosts() {
$hosts = [];
foreach($this->getServiceNames() as $servicesName ) {
$host = $this->getEnvVar( $servicesName, 'VIRTUAL_HOST' );
if($host) {
$hosts[] = $host;
}
}
return $hosts;
}

}
@@ -0,0 +1,77 @@
<?php

namespace Addshore\Mwdd\Files;

use M1\Env\Parser;

class DotEnv {

private const FILE = MWDD_DIR . '/.env';
private const DEFAULT = MWDD_DIR . '/default.env';
private const LOCAL = MWDD_DIR . '/local.env';

private $forceFresh;

public function __construct( bool $forceFresh = false ) {
$this->forceFresh = $forceFresh;
}

private function useForceFreshFlag() {
if( $this->forceFresh ){
// Avoid keeping on getting fresh .env file
$this->forceFresh = false;
return true;
}
return false;
}

public function exists() : bool {
return file_exists(self::FILE);
}

public function getValue(string $key) : ?string {
if($this->useForceFreshFlag() || !$this->exists()) {
$this->updateFromDefaultAndLocal();
}
$dotEnv = (new Parser(file_get_contents(self::FILE)));
return $dotEnv->getContent($key);
}

/**
* Combines the default.env and local.env files for the environment into a single .env file
*/
public function updateFromDefaultAndLocal() {
$defaultEnv = (new Parser(file_get_contents(self::DEFAULT)));

// Sometimes users will not have specified a local env file just yet...
if(file_exists(self::LOCAL)) {
$localEnv = (new Parser(file_get_contents(self::LOCAL)));
$finalEnvLines = array_merge(
$defaultEnv->lines,
$localEnv->lines
);
} else {
$finalEnvLines = $defaultEnv->lines;
}

$finalEnvLines = $this->swapOutValues( $finalEnvLines );

$finalLines = '';
foreach( $finalEnvLines as $key => $line ) {
$finalLines .= $key . '=' . "${line}" . PHP_EOL;
}

file_put_contents( self::FILE, $finalLines );
}

private function swapOutValues( array $combinesLines ) {
if($combinesLines['UID'] === '{{id -u}}'){
$combinesLines['UID'] = trim(shell_exec('id -u'));
}
if($combinesLines['GID'] === '{{id -g}}'){
$combinesLines['GID'] = trim(shell_exec('id -g'));
}
return $combinesLines;
}

}
@@ -0,0 +1,23 @@
<?php

namespace Addshore\Mwdd\Files;

class DotHosts {

private const FILE = MWDD_DIR . '/.hosts';
private const SUFFIX = "# mediawiki-docker-dev";

public function addHost( string $ip, string $host ) {
$this->addLine( "${ip} ${host} " . self::SUFFIX );
}

private function ensureFileExists() {
touch(self::FILE);
}

private function addLine( string $line ) {
$this->ensureFileExists();
file_put_contents(self::FILE, trim($line).PHP_EOL , FILE_APPEND | LOCK_EX);
}

}
@@ -0,0 +1,45 @@
<?php

namespace Addshore\Mwdd\Files;

class MediaWikiDir {

private $dir;

/**
* Passed through realpath on the host
*/
private $realPath;

public function __construct( string $dir) {
$this->dir = $dir;
}

public function ensurePresent() {
// Ignore errors......
// Maybe we want to show errors though?
//@mkdir($this->dir, 0777, true);
passthru('mkdir -p ' . $this->dir);
}

private function getRealPath() {
if(!$this->realPath) {
$this->ensurePresent();
$dir = trim(shell_exec('realpath ' . $this->dir));
$this->realPath = $dir;
}
return $this->realPath;
}

public function hasDotGitDirectory() {
// Bash foo from https://stackoverflow.com/a/47677632/4746236
// Use bash instead of PHP as at least on Windows ~/dev/git/gerrit/mediawiki/core//.git returns false with file_exists when it really does
// TODO move to LS shell file?
return (int)trim(shell_exec('(ls ' . $this->getRealPath().'/.git' . ' >> /dev/null 2>&1 && echo 1) || echo 0'));
}

public function __toString() {
return $this->getRealPath();
}

}
@@ -0,0 +1,32 @@
<?php

namespace Addshore\Mwdd\Shell;

class Docker {

private const D = "docker";

public function cp( string $source, string $target ) {
$shell = self::D . " cp ${source} ${target}";
passthru( $shell );
}

public function runComposerInstall( string $dir ) {
// TODO should this method be somewhere else?
$homeComposerMntString = "";
// TODO make this ALWAYS mount the directory...?
// Otherwise the following happens...
// Cannot create cache directory /tmp/cache/repo/https---repo.packagist.org/, or directory is not writable. Proceeding without cache
// Cannot create cache directory /tmp/cache/files/, or directory is not writable. Proceeding without cache
if( file_exists( getenv('HOME') . '/.composer' ) ) {
// Note: this relies on the fact that the COMPOSER_HOME value is set to /tmp in the image by default.
$homeComposerMntString = "-v " . getenv('HOME') . '/.composer' . ":/tmp";
}

// This runs with the running user id, which is good and means no chown is needed...
$shell = self::D . " run -it --rm --user $(id -u):$(id -g) ${homeComposerMntString} -v ${dir}:/app composer install --ignore-platform-reqs";
passthru( $shell );
}

}

@@ -0,0 +1,122 @@
<?php

namespace Addshore\Mwdd\Shell;

use Addshore\Mwdd\Files\DotEnv;

class DockerCompose {

private $cmd;

public function __construct() {
$cmd = "docker-compose --project-directory . -p mediawiki-docker-dev";
foreach( $this->getAllYmlFiles() as $file ) {
$cmd .= " -f ${file}";
}
$this->cmd = $cmd;
}

/**
* The .env file must exist to run docker-compose commands without it screaming WARNING for no env vars..
* This would often be a problem on first initialization
*/
private function ensureDotEnv() {
$dotEnv = new DotEnv();
if(!$dotEnv->exists()) {
$dotEnv->updateFromDefaultAndLocal();
}
}

/**
* @param string $fullCommand including docker-compose
*/
private function passthruDc( string $fullCommand ) {
$this->ensureDotEnv();
passthru( $fullCommand );
}

/**
* @return string[] of files for example "docker-compose/foo.yml"
*/
private function getAllYmlFiles () : array {
$files = array_diff(scandir('docker-compose'), array('.', '..'));
array_walk( $files, function( &$value ) {
$value = 'docker-compose/' . $value;
} );
return $files;
}

/**
* @param string[] $services
* @param bool $build Should the --build flag be passed?
*/
public function upDetached( array $services, $build = false ) {
$buildString = $build ? ' --build' : '';
$shell = $this->cmd . " up -d ${buildString} " . implode( ' ', $services );
$this->passthruDc( $shell );
}

public function downWithVolumesAndOrphans() {
$shell = $this->cmd . " down --volumes --remove-orphans";
$this->passthruDc( $shell );
}

public function stop( array $services ) {
$shell = $this->cmd . " stop " . implode( ' ', $services );
$this->passthruDc( $shell );
}

public function start( array $services ) {
$shell = $this->cmd . " start " . implode( ' ', $services );;
$this->passthruDc( $shell );
}

// Command in an already running container
public function exec( string $service, $command, $extraArgString = '' ) {
$shell = $this->cmd . " exec ${extraArgString} \"${service}\" ${command}";
$this->passthruDc( $shell );
}

// Command in an already running container
public function execIt( string $service, $command, $extraArgString = '' ) {
$shell = $this->cmd . " exec -e COLUMNS=$(tput cols) -e LINES=$(tput lines) ${extraArgString} \"${service}\" ${command}";
$this->passthruDc( $shell );
}

// Command in a new container
public function run( string $service, $command, $extraArgString = '' ) {
$shell = $this->cmd . " run ${extraArgString} ${service} ${command}";
$this->passthruDc( $shell );
}

// Command in a new container
public function runDetatched( string $service, $command, $extraArgString = '' ) {
$shell = $this->cmd . " run -d ${extraArgString} \"${service}\" ${command}";
// TODO should this actually passthru, given it is detached??
$this->passthruDc( $shell );
}

public function psQ( string $service ) {
$shell = $this->cmd . " ps -q ${service}";
$output = shell_exec( $shell );
return trim( $output );
}

public function ps() {
$shell = $this->cmd . " ps";
$this->passthruDc( $shell );
}

public function logsTail( string $service, int $lines = 25 ) {
$shell = $this->cmd . " logs --tail=${lines} -f ${service}";
$this->passthruDc( $shell );
}

public function raw( string $rawCommand ) {
$shell = $this->cmd . " ${rawCommand}";
$this->passthruDc( $shell );
}


}

@@ -0,0 +1,15 @@
<?php

namespace Addshore\Mwdd\Shell;

class Git {

private const G = "git";

public function clone( string $repo, string $target, ?int $depth = 1 ) {
$depthString = $depth ? "--depth ${depth}" : "";
$shell = self::G . " clone ${depthString} ${repo} $target";
passthru( $shell );
}

}
@@ -0,0 +1,23 @@
<?php

namespace Addshore\Mwdd\Shell;

class Id {

private const I = "id";

public function ug() {
return $this->u() . ':' . $this->g();
}

public function u() {
$shell = self::I . " -u";
return trim( shell_exec( $shell ) );
}

public function g() {
$shell = self::I . " -g";
return trim( shell_exec( $shell ) );
}

}
57 create

This file was deleted.

@@ -1,3 +1,12 @@
# User & Group
#
# Used for services where file sharing happens by default to avoid permissions issues and the need to chmod.
# The {{}} templates are automatically swapped out for real values by the control application.
# You can override these values so that automatic calculation doesn't happen.
#
UID={{id -u}}
GID={{id -g}}

# Database
#
# Value: 'mariadb' or 'mysql'
@@ -40,3 +49,8 @@ DOCKER_MW_PATH=/srv/dev/git/gerrit/mediawiki
# External port (on host system) for webserver proxy
# Port to serve everything up through
DOCKER_MW_PORT=8080

# MediaWiki log directory volume mount
# mw-logs is a volume provided by the mw.yml
# You can use a local system path here if you want the logs to appear on your machine
DOCKER_MW_LOG_VOLUME=mw-logs

This file was deleted.

This file was deleted.

@@ -0,0 +1,15 @@
version: '2.2'

services:
adminer:
image: adminer
environment:
- VIRTUAL_HOST=adminer.mw.localhost
depends_on:
- dps
- nginx-proxy
hostname: adminer.mw.localhost
dns:
- 172.0.0.10
networks:
- dps
@@ -0,0 +1,41 @@
version: '2.2'

services:

dps:
image: defreitas/dns-proxy-server
volumes:
- /var/run/docker.sock:/var/run/docker.sock
hostname: dps.mw.localhost
networks:
dps:
ipv4_address: 172.0.0.10

nginx-proxy:
# TODO: replace with jwilder/nginx-proxy, once updated
image: silvanwmde/nginx-proxy:latest
environment:
- VIRTUAL_HOST=proxy.mw.localhost
- HOSTNAMES=.web.mw.localhost # wildcard name resolution, thanks to DPS
- HTTP_PORT=${DOCKER_MW_PORT} # internal port
ports:
- "${DOCKER_MW_PORT}:${DOCKER_MW_PORT}"
depends_on:
- dps
hostname: proxy.mw.localhost
dns:
- 172.0.0.10
dns_search:
- mw.localhost
networks:
- dps
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./config/nginx/client_max_body_size.conf:/etc/nginx/conf.d/client_max_body_size.conf:ro
- ./config/nginx/timeouts.conf:/etc/nginx/conf.d/timeouts.conf:ro

networks:
dps:
ipam:
config:
- subnet: 172.0.0.0/24
@@ -0,0 +1,11 @@
version: '2.2'

services:
control:
scale: 0
build: ./control
# The image tag should be updated whenever new composer dependencies are added...
image: mwdd-control:2020-05-13-19-44
volumes:
- ./:/mwdd
- /var/run/docker.sock:/var/run/docker.sock
@@ -0,0 +1,40 @@
version: '2.2'

services:
db-replica:
image: ${DB}:latest
environment:
- MYSQL_ROOT_PASSWORD=toor
hostname: db-replica.mw.localhost
depends_on:
- db-master
dns:
- 172.0.0.10
networks:
- dps
volumes:
- sql-data-replica:/var/lib/mysql
- ./config/mysql/replica:/mwdd-custom
entrypoint: "/mwdd-custom/entrypoint.sh"
command: "mysqld"

db-replica-configure-replication:
image: ${DB}:latest
environment:
- "MYSQL_REPLICA_PASSWORD=toor"
- "MYSQL_MASTER_PASSWORD=toor"
- "MYSQL_ROOT_PASSWORD=toor"
- "MYSQL_REPLICATION_USER=repl"
- "MYSQL_REPLICATION_PASSWORD=repl"
depends_on:
- db-replica
networks:
- dps
volumes:
- ./config/wait-for-it.sh:/wait-for-it.sh:ro
- ./config/mysql/replica:/mwdd-custom
- db-data-configure-replication:/mwdd-connector
command: /bin/bash -x /mwdd-custom/mysql_connector_replica.sh

volumes:
sql-data-replica:
@@ -0,0 +1,41 @@
version: '2.2'

services:
db-master:
image: ${DB}:latest
environment:
- MYSQL_ROOT_PASSWORD=toor
hostname: db-master.mw.localhost
dns:
- 172.0.0.10
networks:
- dps
volumes:
- db-data-master:/var/lib/mysql
- ./config/mysql/master:/mwdd-custom
entrypoint: "/mwdd-custom/entrypoint.sh"
command: "mysqld"
depends_on:
# This feels like a backward dependancy, but it makes sense as the replication configure services waits for the master anyway.
# and whenever the master is created we want the configure replciation service to run to write the initial file and position for replication..
- db-master-configure-replication

db-master-configure-replication:
image: ${DB}:latest
environment:
- "MYSQL_REPLICA_PASSWORD=toor"
- "MYSQL_MASTER_PASSWORD=toor"
- "MYSQL_ROOT_PASSWORD=toor"
- "MYSQL_REPLICATION_USER=repl"
- "MYSQL_REPLICATION_PASSWORD=repl"
networks:
- dps
volumes:
- ./config/wait-for-it.sh:/wait-for-it.sh:ro
- ./config/mysql/master:/mwdd-custom
- db-data-configure-replication:/mwdd-connector
command: /bin/bash -x /mwdd-custom/mysql_connector_master.sh

volumes:
db-data-master:
db-data-configure-replication:
@@ -0,0 +1,19 @@
version: '2.2'

# This service only exists to be able to run composer in its own container against the mediawiki code..
# docker-compose doesnt allow a container to be run unless a service has already been creased, so we need this...

services:
mw-composer:
image: composer
# Run as the host user as this might create files...
user: "${UID}:${GID}"
networks:
- dps
dns:
- 172.0.0.10
volumes:
# Only mount code and config, don't mount logs or image
# TODO is this bit of config even needed?
- "${DOCKER_MW_PATH}:/app:cached"
- ./config/mediawiki:/app/.docker:ro
@@ -0,0 +1,27 @@
version: '2.2'

# Service wrapper around what is provided by fresh
# https://github.com/wikimedia/fresh/blob/master/bin/fresh-node10

services:
mw-fresh:
image: docker-registry.wikimedia.org/releng/node10-test-browser:0.6.1
user: "${UID}:${GID}"
entrypoint: /bin/sh
command: -c "echo started"
working_dir: /app
networks:
- dps
dns:
- 172.0.0.10
environment:
# https://www.mediawiki.org/wiki/Selenium/How-to/Run_tests_targeting_MediaWiki-Docker_using_Fresh#Environment_variables
- MW_SERVER=http://default.web.mw.localhost:${DOCKER_MW_PORT}
- MW_SCRIPT_PATH=/mediawiki
- MEDIAWIKI_USER=Admin
- MEDIAWIKI_PASSWORD=dockerpass
volumes:
# Only mount code and config, don't mount logs or image
# TODO is this bit of config even needed?
- "${DOCKER_MW_PATH}:/app:cached"
- ./config/mediawiki:/app/.docker:ro
@@ -0,0 +1,30 @@
version: '2.2'

# Service wrapper around what is provided by fresh
# https://github.com/wikimedia/fresh/blob/master/bin/fresh-node10

services:
mw-quibble:
image: docker-registry.wikimedia.org/releng/quibble-stretch-php72:latest
user: "${UID}:${GID}"
entrypoint: /bin/sh
command: -c "echo started"
working_dir: /src
networks:
- dps
dns:
- 172.0.0.10
volumes:
# Only mount code and config, don't mount logs or image
# TODO is this bit of config even needed?
- "${DOCKER_MW_PATH}:/workspace/src:cached"
- ./config/mediawiki:/workspace/src/.docker:ro
# TODO cache should be mounted from the user machine?
- mw-quibble-workspace-cache:/workspace/cache
- mw-quibble-workspace-log:/workspace/log
- mw-quibble-workspace-ref:/workspace/ref

volumes:
mw-quibble-workspace-cache:
mw-quibble-workspace-log:
mw-quibble-workspace-ref:
@@ -0,0 +1,43 @@
version: '2.2'

services:

mediawiki:
image: webdevops/php-${WEBSERVER}-dev:${RUNTIMEVERSION}
environment:
# Used by various maintenance scripts to find MediaWiki.
# Also required for /var/www/index.php - https://phabricator.wikimedia.org/T153882
- MW_INSTALL_PATH=/app
- VIRTUAL_HOST=*.web.mw.localhost
- PHP_DEBUGGER=xdebug
- XDEBUG_REMOTE_AUTOSTART=${XDEBUG_REMOTE_AUTOSTART}
- XDEBUG_REMOTE_HOST=${IDELOCALHOST}
- XDEBUG_REMOTE_PORT=9000
- XDEBUG_REMOTE_CONNECT_BACK=0
- XDEBUG_PROFILER_ENABLE_TRIGGER=1
- PHP_IDE_CONFIG=serverName=docker
- PHP_UPLOAD_MAX_FILESIZE=1024M
- PHP_POST_MAX_SIZE=1024M
- php.apc.enable_cli=1
# The below env var is not needed by the container, only by MWDD
- MWDD_LOG_DIR=/var/log/mediawiki/
hostname: mediawiki.mw.localhost
depends_on:
- db-master
- nginx-proxy
dns:
- 172.0.0.10
dns_search:
- mw.localhost
networks:
- dps
volumes:
- "${DOCKER_MW_PATH}:/app:cached"
- ./config/mediawiki:/mwdd-custom:ro
- ./config/wait-for-it.sh:/wait-for-it.sh:ro
- mw-images:/app/images/docker:delegated
- "${DOCKER_MW_LOG_VOLUME}:/var/log/mediawiki:delegated"

volumes:
mw-logs:
mw-images:
@@ -0,0 +1,21 @@
version: '2.2'

services:
phpmyadmin:
image: phpmyadmin/phpmyadmin
environment:
- PMA_USER=root
- PMA_PASSWORD=toor
- PMA_HOSTS=db-master,db-replica
- PMA_ARBITRARY=1
- VIRTUAL_HOST=phpmyadmin.mw.localhost
depends_on:
- dps
- nginx-proxy
hostname: phpmyadmin.mw.localhost
dns:
- 172.0.0.10
networks:
- dps
volumes:
- ./config/phpmyadmin/config.user.inc.php:/etc/phpmyadmin/config.user.inc.php
@@ -0,0 +1,10 @@
version: '2.2'

services:
redis:
image: redis
hostname: redis.mw.localhost
dns:
- 172.0.0.10
networks:
- dps
@@ -0,0 +1,17 @@
version: '2.2'

services:
graphite-statsd:
image: hopsoft/graphite-statsd
environment:
- VIRTUAL_HOST=graphite.mw.localhost
hostname: graphite.mw.localhost
dns:
- 172.0.0.10
networks:
- dps
volumes:
- graphite-statsd-data:/opt/graphite/storage

volumes:
graphite-statsd-data:
3 help

This file was deleted.

This file was deleted.

2 hosts-remove 100755 → 100644
@@ -26,8 +26,6 @@ else
SEDFILE=/usr/bin/sed
fi

# TODO escalate / warn if file not accessible by current user

# Remove all of the lines we have added
$GREPFILE -v mediawiki-docker-dev $HOSTSFILE > ./.hosts.tmp
# Remove any excess whitespace
0 hosts-sync 100755 → 100644
Empty file.

This file was deleted.

37 mwdd
@@ -0,0 +1,37 @@
#!/bin/bash

# Allow the user to force and env, but default to php for now..
MWDD_ENV=${MWDD_ENV:-"php"}

# ### PHP only version....
if [[ $MWDD_ENV == "php" ]]
then
php control/app.php $@
fi

# ### Run everything in docker.. via docker-compose
if [[ $MWDD_ENV == "dc" ]]
then
# If control container is not already running, then we need to run up
CONTROL_CONTAINER_ID=$(docker-compose --project-directory . -p mediawiki-docker-dev -f docker-compose/control.yml ps --services --filter "status=running")
if [ -z "$CONTROL_CONTAINER_ID" ]; then

docker-compose \
--project-directory . \
-p mediawiki-docker-dev \
-f docker-compose/control.yml \
up -d --build \
control
fi

# Then run the command
# This currently runs as root
# We could pass in the uid and gid, but then by default it would not be able to access the docker socket unless we also tweaked more things
docker-compose \
--project-directory . \
-p mediawiki-docker-dev \
-f docker-compose/control.yml \
exec \
control \
php control/app.php $@
fi
9 mysql

This file was deleted.

This file was deleted.

This file was deleted.