Commit 00cf999a authored by Jon Pugh's avatar Jon Pugh Committed by GitHub

Merge pull request #18 from aegir-project/4.x-docker-compose

Change docker support to use docker-compose.
parents 70f784b9 ddf676e1
......@@ -59,9 +59,9 @@ script:
--restart_command="sudo apache2ctl graceful"
- provision services server_master add db -n
--service_type=mysql
--master_db="mysql://root:root@0.0.0.0:3307"
--db_grant_all_hosts=0
--service_type=mysqlDocker
--master_db="mysql://root:root@db:3306"
--db_grant_all_hosts=1
- provision services server_master
......@@ -94,13 +94,21 @@ script:
- provision verify server_master -v
# Docker creates the host path when docker run happens, in server verify.
- sudo rm -rf /home/travis/hostmaster
- provision verify platform_hostmaster -v
- ls -la /home/travis/hostmaster
- provision verify hostmaster -v
- provision verify server_master
- docker ps
- docker logs provision_http_server_master
- docker exec provision_http_server_master sudo apache2ctl -S
- docker logs servermaster_http_1
# @TODO This fails right now.
# - docker exec servermaster_http_1 sudo apache2ctl -S
# Curling localhost is failing always. I'm commenting this out for now. This works!
#- sleep 5
#- curl http://localhost
#- curl http://provision.local.computer
<Directory <?php print $this->root; ?>>
<Directory <?php print $document_root; ?>>
Order allow,deny
Allow from all
Satisfy any
......@@ -8,9 +8,9 @@
<?php
if (is_readable("{$this->root}/.htaccess")) {
if (is_readable("{$document_root}/.htaccess")) {
print "\n# Include the platform's htaccess file\n";
print "Include {$this->root}/.htaccess\n";
print "Include {$document_root}/.htaccess\n";
}
?>
......
......@@ -10,7 +10,7 @@ if (!$aegir_root && $server->aegir_root) {
}
?>
DocumentRoot <?php print $this->root; ?>
DocumentRoot <?php print $document_root; ?>
ServerName <?php print $this->uri; ?>
......
......@@ -4,6 +4,7 @@ namespace Aegir\Provision\Command;
use Aegir\Provision\Application;
use Aegir\Provision\Command;
use Aegir\Provision\Console\ProvisionStyle;
use Aegir\Provision\Context;
use Aegir\Provision\Context\PlatformContext;
use Aegir\Provision\Context\ServerContext;
......@@ -32,6 +33,11 @@ class SaveCommand extends Command
*/
private $context_type;
/**
* @var bool
*/
private $newContext = FALSE;
/**
* {@inheritdoc}
*/
......@@ -135,6 +141,8 @@ class SaveCommand extends Command
// If this is a new context...
if (empty($this->context)) {
$this->newContext = TRUE;
// If context_type is still empty, throw an exception. Happens if using -n
if (empty($context_type)) {
throw new \Exception('Option --context_type must be specified.');
......@@ -175,7 +183,6 @@ class SaveCommand extends Command
exit(1);
}
$options = $this->askForContextProperties();
$options['name'] = $this->context_name;
$options['type'] = $this->context_type;
......@@ -183,6 +190,30 @@ class SaveCommand extends Command
$class = Context::getClassName($this->input->getOption('context_type'));
$this->context = new $class($input->getArgument('context_name'), $this->getProvision(), $options);
}
else {
$icon = ProvisionStyle::ICON_EDIT;
$this->getProvision()->io()->block(
" {$icon} Editing context {$this->context->name} ",
NULL,
'bg=black;fg=blue',
NULL,
TRUE
);
// Save over existing contexts.
$this->newContext = FALSE;
$this->input->setOption('context_type', $this->context->type);
$properties = $this->askForContextProperties();
// Write over each property with new values.
foreach ($properties as $name => $value) {
$this->context->setProperty($name, $value);
}
$context_type = $this->context->type;
$this->input->setOption('context_type', $this->context->type);
}
// Delete context config.
if ($input->getOption('delete')) {
......@@ -199,12 +230,13 @@ class SaveCommand extends Command
}
foreach ($this->context->getProperties() as $name => $value) {
if ($name == 'services' || $name == 'service_subscriptions') {
continue;
}
$value = is_array($value)? implode(', ', $value): $value;
$rows[] = [$name, $value];
}
$this->io->table(['Saving Context:', $this->context->name], $rows);
if ($this->io->confirm("Write configuration for <fg=blue>{$this->context->type}</> context <fg=blue>{$this->context->name}</> to <fg=blue>{$this->context->config_path}</>?")) {
......@@ -222,6 +254,11 @@ class SaveCommand extends Command
// $command = 'drush provision-save '.$input->getArgument('context_name');
// $this->process($command);
// If editing a context, exit here.
if (!$this->newContext) {
return;
}
// Offer to add services.
if ($context_type == 'server') {
$this->askForServices();
......@@ -324,13 +361,18 @@ class SaveCommand extends Command
$property = Provision::newProperty($property);
}
// If we are editing a context, override the default property.
if (!$this->newContext && $current_value = $this->context->getProperty($name)) {
$property->default = $current_value;
}
// If option does not exist, ask for it.
if (!empty($this->input->getOption($name))) {
$properties[$name] = $this->input->getOption($name);
$this->io->comment("Using option {$name}={$properties[$name]}");
}
else {
$properties[$name] = $this->io->ask("{$name}({$property->description})", $property->default, $property->validate);
$properties[$name] = $this->io->ask("{$name} ({$property->description})", $property->default, $property->validate);
}
}
return $properties;
......
......@@ -3,6 +3,7 @@
namespace Aegir\Provision\Command;
use Aegir\Provision\Command;
use Aegir\Provision\Console\ProvisionStyle;
use Aegir\Provision\Context;
use Aegir\Provision\Context\PlatformContext;
use Aegir\Provision\Context\ServerContext;
......@@ -212,6 +213,18 @@ class ServicesCommand extends Command
]));
}
if ($this->context->hasService($service)) {
$icon = ProvisionStyle::ICON_EDIT;
$this->getProvision()->io()->block(
" {$icon} Editing service {} provded by {$this->context->name} ",
NULL,
'bg=black;fg=blue',
NULL,
TRUE
);
}
// Then ask for all options.
$properties = $this->askForServiceProperties($service, $service_type);
......@@ -246,9 +259,9 @@ class ServicesCommand extends Command
try {
$this->context->config[$services_key][$service] = $service_info;
if (!empty($properties)) {
$this->context->config[$services_key][$service]['properties'] = $properties;
}
$this->context->config[$services_key][$service]['properties'] = $properties;
$this->context->setProperty($services_key, $this->context->config[$services_key]);
$this->context->save();
$this->io->success('Service saved to Context!');
}
......@@ -276,6 +289,10 @@ class ServicesCommand extends Command
$property = Provision::newProperty($property);
}
if ($this->context->hasService($service) && $this->context->getService($service)->getProperty($name)) {
$property->default = $this->context->getService($service)->getProperty($name);
}
// If option does not exist, ask for it.
if (!empty($this->input->getOption($name))) {
$properties[$name] = $this->input->getOption($name);
......
......@@ -41,6 +41,7 @@ class Config extends ProvisionConfig
$this->set('php_version', PHP_VERSION);
$this->set('php_ini', get_cfg_var('cfg_file_path'));
$this->set('script', $this->getProvisionScript());
$this->set('interactive_task_sleep', 500);
$os_string = explode(' ', php_uname('v'));
$os = array_shift($os_string);
......@@ -49,6 +50,7 @@ class Config extends ProvisionConfig
$this->set('aegir_root', $this->getHomeDir());
$this->set('script_user', $this->getScriptUser());
$this->set('script_uid', $this->getScriptUid());
// If user has a ~/.config path, use it.
if (file_exists($this->getHomeDir() . '/.config')) {
......@@ -235,4 +237,11 @@ class Config extends ProvisionConfig
$real_script_user = posix_getpwuid(posix_geteuid());
return $real_script_user['name'];
}
/**
* Determine the user running provision.
*/
static public function getScriptUid() {
return posix_getuid();
}
}
<?php
namespace Aegir\Provision\Console;
use Aegir\Provision\Provision;
use Drupal\Console\Core\Style\DrupalStyle;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Terminal;
class ProvisionStyle extends DrupalStyle {
/**
* @var BufferedOutput
*/
protected $bufferedOutput;
protected $input;
protected $lineLength;
/**
* Icons
*/
const ICON_EDIT = '🖉';
const ICON_START = '▷';
const ICON_FINISH = '🏁';
const ICON_FAILED = '🔥';
const ICON_COMMAND = '$';
public function __construct(InputInterface $input, OutputInterface $output)
{
$this->input = $input;
$this->bufferedOutput = new BufferedOutput($output->getVerbosity(), false, clone $output->getFormatter());
// Windows cmd wraps lines as soon as the terminal width is reached, whether there are following chars or not.
$width = (new Terminal())->getWidth() ?: self::MAX_LINE_LENGTH;
$this->lineLength = min($width - (int) (DIRECTORY_SEPARATOR === '\\'), self::MAX_LINE_LENGTH);
parent::__construct($input, $output);
}
public function taskInfoBlock($task_id, $op, $status = 'none') {
switch ($op) {
case 'started':
default:
$bg = 'black';
$fg = 'blue';
$icon = ' ' . self::ICON_START;
$op = ucfirst($op);
break;
case 'completed':
$bg = 'black';
$fg = 'green';
$icon = self::ICON_FINISH;
$op = ucfirst($op);
break;
case 'failed':
$bg = 'black';
$fg = 'red';
$icon = self::ICON_FAILED;
$op = ucfirst($op);
break;
}
$app_name = Provision::APPLICATION_FUN_NAME;
$task_word = 'Task';
$message = "{$app_name} {$icon} {$task_word} {$op}";
$timestamp = date('r');
$message_suffix = $task_id;
$spaces = $this::MAX_LINE_LENGTH - strlen($message . $message_suffix) - 2;
$message .= str_repeat(' ', $spaces) . $message_suffix;
$message .= "\n" . $timestamp;
$this->autoPrependBlock();
$this->block(
$message,
NULL,
"bg=$bg;fg=$fg",
' ',
TRUE
);
}
public function commandBlock($message, $directory = '') {
$this->autoPrependBlock();
$this->customLite($message, $directory . ' <fg=yellow>' . self::ICON_COMMAND . '</>', '');
}
public function outputBlock($message) {
$this->block(
$message,
NULL,
'fg=yellow;bg=black',
' ╎ ',
TRUE
);
}
/**
* Replacement for parent::autoPrependBlock(), allowing access and setting newLine to 1 - instead of 2 -.
*/
private function autoPrependBlock()
{
$chars = substr(str_replace(PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2);
if (!isset($chars[0])) {
return $this->newLine(); //empty history, so we should start with a new line.
}
//Prepend new line for each non LF chars (This means no blank line was output before)
$this->newLine(1 - substr_count($chars, "\n"));
}
}
\ No newline at end of file
......@@ -426,6 +426,17 @@ class Context implements BuilderAwareInterface
}
}
/**
* Set a specific property.
*
* @param $name
* @return mixed
* @throws \Exception
*/
public function setProperty($name, $value) {
$this->properties[$name] = $value;
}
/**
* Saves the config class to file.
*
......@@ -439,7 +450,7 @@ class Context implements BuilderAwareInterface
$dumper = new Dumper();
try {
$fs->dumpFile($this->config_path, $dumper->dump($this->config, 10));
$fs->dumpFile($this->config_path, $dumper->dump($this->getProperties(), 10));
return true;
} catch (IOException $e) {
return false;
......
......@@ -44,6 +44,14 @@ class PlatformContext extends ContextSubscriber implements ConfigurationInterfac
// Load "web_server" context.
// There is no need to validate for $this->properties['web_server'] because the config system does that.
// $this->web_server = $application->getContext($this->properties['web_server']);
// Make document root property absolute, and set to root if there is no docroot.
if ($this->getProperty('document_root')) {
$this->setProperty('document_root', $this->getProperty('root') . DIRECTORY_SEPARATOR . $this->getProperty('document_root'));
}
else {
$this->setProperty('document_root', $this->getProperty('root'));
}
}
static function option_documentation()
......@@ -51,7 +59,7 @@ class PlatformContext extends ContextSubscriber implements ConfigurationInterfac
$options = [
'root' =>
Provision::newProperty()
->description('platform: path to the Drupal installation. You may use a relative or absolute path.')
->description('platform: path to the source code for this platform. You may use a relative or absolute path. May be different from document root.')
->defaultValue(getcwd())
->required(TRUE)
->validate(function($path) {
......@@ -135,6 +143,11 @@ class PlatformContext extends ContextSubscriber implements ConfigurationInterfac
return $git_url;
})
,
'document_root' =>
Provision::newProperty()
->description('platform: Relative path to the "document root" in your source code. Leave blank if docroot is the root.')
->required(FALSE)
,
];
return $options;
......
......@@ -6,7 +6,12 @@ use Aegir\Provision\Console\Config;
use Aegir\Provision\ContextProvider;
use Aegir\Provision\Property;
use Aegir\Provision\Provision;
use Aegir\Provision\Service\DockerServiceInterface;
use Psr\Log\LogLevel;
use Robo\ResultData;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Yaml\Yaml;
/**
* Class ServerContext
......@@ -23,8 +28,40 @@ class ServerContext extends ContextProvider implements ConfigurationInterface
*/
public $type = 'server';
const TYPE = 'server';
/**
* @var string
* The path to store the server's configuration files in. ie. /var/aegir/config/server_master.
*/
public $server_config_path;
/**
* ServerContext constructor.
*
* Prepares "server_config_path" as the place to store this server's service
* configuration files (apache configs, etc.).
*
* @param $name
* @param Provision $provision
* @param array $options
*/
function __construct($name, Provision $provision, array $options = [])
{
// @TODO: Create a 'servers_path' to keep things nice and clean.
parent::__construct($name, $provision, $options);
// If server_config_path property is empty, generate it from provision config_path + server name.
if (empty($this->getProperty('server_config_path'))) {
$this->server_config_path = $this->getProvision()->getConfig()->get('config_path') . DIRECTORY_SEPARATOR . $name;
$this->setProperty('server_config_path', $this->server_config_path);
}
else {
$this->server_config_path = $this->getProperty('server_config_path');
}
$this->fs = new Filesystem();
}
/**
* @return string|Property[]
*/
......@@ -58,29 +95,75 @@ class ServerContext extends ContextProvider implements ConfigurationInterface
'master_url' =>
Provision::newProperty()
->description('server: Hostmaster URL')
->required(FALSE),
'server_config_path' =>
Provision::newProperty()
->description('server: The location to store the server\'s configuration files. If left empty, will be generated automatically.')
->required(FALSE)
,
];
}
/**
* @return array
*/
public function verify()
{
$tasks = [];
return $tasks;
}
/**
* Run a shell command on this server.
*
* @TODO: Run remote commands correctly.
* @param $cmd string The command to run
* @param $dir string The directory to run the command in. Defaults to this server's config path.
* @param $return string What to return. Can be 'output' or 'exit'.
*
* @param $cmd
* @return string
* @throws \Exception
*/
public function shell_exec($cmd) {
$output = '';
$exit = 0;
exec($cmd, $output, $exit);
public function shell_exec($command, $dir = NULL, $return = 'stdout') {
$cwd = getcwd();
$original_command = $command;
$tmpdir = sys_get_temp_dir() . '/provision';
if (!$this->fs->exists($tmpdir)){
$this->fs->mkdir($tmpdir);
}
$datestamp = date('c');
$tmp_output_file = tempnam($tmpdir, 'task.' . $datestamp . '.output.');
$tmp_error_file = tempnam($tmpdir, 'task.' . $datestamp . '.error.');
$effective_wd = $dir? $dir:
$this->getProperty('server_config_path');
if ($this->getProvision()->getOutput()->isVerbose()) {
$this->getProvision()->io()->commandBlock($command, $effective_wd);
}
// Output and Errors to files.
$command .= "> $tmp_output_file 2> $tmp_error_file";
chdir($effective_wd);
exec($command, $output, $exit);
chdir($cwd);
$stderr = file_get_contents($tmp_error_file);
$stdout = file_get_contents($tmp_output_file);
if (!empty($stdout)){
if ($this->getProvision()->getOutput()->isVerbose()) {
$this->getProvision()->io()->outputBlock($stdout);
}
}
if ($exit != 0) {
throw new \Exception("Command failed: $cmd");
if ($exit != ResultData::EXITCODE_OK) {
throw new \Exception($stderr);
}
return implode("\n", $output);
return ${$return};
}
}
......@@ -57,6 +57,10 @@ class SiteContext extends ContextSubscriber implements ConfigurationInterface
$uri = $this->getProperty('uri');
$this->properties['site_path'] = "sites/{$uri}";
// If site_path property is empty, generate it from platform root + uri.
if (empty($this->getProperty('site_path'))) {
$this->setProperty('site_path', $this->platform->getConfig()->get('root') . DIRECTORY_SEPARATOR . $this->uri);
}
}
static function option_documentation()
......@@ -72,6 +76,13 @@ class SiteContext extends ContextSubscriber implements ConfigurationInterface
// 'install_method' => 'site: How to install the site; default profile. When set to "profile" the install profile will be run automatically. Otherwise, an empty database will be created. Additional modules may provide additional install_methods.',
'profile' => 'site: Drupal profile to use; default standard',
// 'drush_aliases' => 'site: Comma-separated list of additional Drush aliases through which this site can be accessed.',
'site_path' =>
Provision::newProperty()
->description('site: The site configuration path (sites/domain.com). If left empty, will be generated automatically.')
->required(FALSE)
,
];
}
......
......@@ -64,6 +64,21 @@ class ContextProvider extends Context
throw new \Exception("Service '$type' does not exist in the context '{$this->name}'.");
}
}
/**
* Whether or not this Server has a service.
*
* @param $type
* @return bool
*/
public function hasService($type) {
if (isset($this->services[$type])) {
return TRUE;
}
else {
return FALSE;
}
}
/**
* Return all services for this context.
......
......@@ -99,4 +99,19 @@ class ContextSubscriber extends Context
->end()
->end();
}
/**
* Whether or not this Server has a service.
*
* @param $type
* @return bool
*/
public function hasService($type) {
if (isset($this->services[$type])) {
return TRUE;
}
else {
return FALSE;
}
}
}
......@@ -7,6 +7,7 @@ use Aegir\Provision\Console\Config;
use Aegir\Provision\Commands\ExampleCommands;
use Aegir\Provision\Console\ConsoleOutput;
use Aegir\Provision\Console\ProvisionStyle;
use Aegir\Provision\Robo\ProvisionCollectionBuilder;
use Aegir\Provision\Robo\ProvisionExecutor;
use Aegir\Provision\Robo\ProvisionTasks;
......@@ -53,7 +54,7 @@ class Provision implements ConfigAwareInterface, ContainerAwareInterface, Logger
* The path within config_path to write contexts to.
*/
const CONTEXTS_PATH = 'contexts';
use BuilderAwareTrait;
use ConfigAwareTrait;
use ContainerAwareTrait;
......@@ -83,7 +84,7 @@ class Provision implements ConfigAwareInterface, ContainerAwareInterface, Logger
/**
* @var \Aegir\Provision\Context[]
*/
private $contexts = [];
public $contexts = [];
/**
* @var array[]
......@@ -264,12 +265,12 @@ class Provision implements ConfigAwareInterface, ContainerAwareInterface, Logger
/**
* Provide access to DrupalStyle object.
*
* @return \Drupal\Console\Core\Style\DrupalStyle
* @return ProvisionStyle
*/
public function io()
{
if (!$this->io) {
$this->io = new DrupalStyle($this->input(), $this->output());
$this->io = new ProvisionStyle($this->input(), $this->output());
}
return $this->io;
}
......@@ -322,7 +323,27 @@ class Provision implements ConfigAwareInterface, ContainerAwareInterface, Logger
}
return $servers;
}
/**
* Return all available contexts.
*
* @return array|Context
*/
public function getAllPlatforms() {
$platforms = [];
$context_files = $this->getAllContexts();
if (empty($context_files)) {
throw new \Exception('No contexts found. Use `provision save` to create one.');
}
foreach ($context_files as $context) {
if ($context->type == 'platform') {
$platforms[$context->name] = $context;
}
}
return $platforms;
}
/**
* Get a simple array of all contexts, for use in an options list.
* @return array
......
......@@ -43,19 +43,23 @@ class ProvisionCollection extends Collection {
/** @var \Aegir\Provision\Task $task */
$task = $this->getConfig()->get($name);
if ($this->getProvision()->getOutput()->isVerbose()) {
$this->getProvision()->io()->customLite('STARTED ' . $name, '○');
}
// Show starting message.
// If task is not in "logging" group
if (strpos($name, 'logging.') !== 0) {
// If -v flag is used, show task start indicator.
if ($this->getProvision()->getOutput()->isVerbose()) {
$this->getProvision()->io()->taskInfoBlock($name, 'started');
}
// Show starting message.