...
 
Commits (190)
......@@ -6,6 +6,9 @@ GRTAGS
GSYMS
GTAGS
# Documentation
docs
# Development
.idea
......
......@@ -39,11 +39,13 @@ before_install:
# Provision uses these variables to build the container.
- 'echo "web_user: docker" >> ~/.provision.yml'
- 'echo "web_user_uid: 999" >> ~/.provision.yml'
- 'echo "config_path: /home/travis/config" >> ~/.provision.yml'
- 'echo "contexts_path: /home/travis/config/contexts" >> ~/.provision.yml'
install:
- composer install
- sudo ln -s $PWD/bin/provision /usr/local/bin/provision
- mkdir /home/travis/config
- mkdir /home/travis/config/contexts -p
- ls -la
script:
......@@ -53,28 +55,28 @@ script:
- provision status -n
# Add server context.
- provision save server_master -n
- provision save --context=server_master -n
--context_type=server
--remote_host=provision.local.computer
--aegir_root=/var/aegir
--script_user=aegir
# Add services to server_master context.
- provision services server_master add http -n
- provision @server_master services add http -n
--service_type=apacheDocker
--http_port=80
--web_group=www-data
--restart_command="sudo apache2ctl graceful"
- provision services server_master add db -n
- provision @server_master services add db -n
--service_type=mysqlDocker
--master_db="mysql://root:root@db:3306"
--db_grant_all_hosts=1
- provision services server_master
- provision @server_master services
# Add platform context
- provision save platform_hostmaster -n
- provision save --context=platform_hostmaster -n
--context_type=platform
--root=/home/travis/hostmaster
--server_http=server_master
......@@ -83,10 +85,10 @@ script:
# add http service to platform.
# @TODO: This should already be done thanks to --server_http!!!
- provision services platform_hostmaster add http server_master
- provision @platform_hostmaster services add http server_master
# Add site context
- provision save hostmaster -n
- provision save --context=hostmaster -n
--context_type=site
--platform=platform_hostmaster
--server_db=server_master
......@@ -98,7 +100,7 @@ script:
--db_password=drupal
# Add a site context without a platform!
- provision save eight.local.computer -n
- provision save --context=eight.local.computer -n
--context_type=site
--server_db=server_master
--server_http=server_master
......@@ -112,28 +114,28 @@ script:
--makefile=https://raw.githubusercontent.com/aegir-project/provision/7.x-3.x/provision-tests/makes/drupal8.make
- provision status -n
- provision status server_master
- provision status platform_hostmaster
- provision status hostmaster
- provision status eight.local.computer
- provision @server_master status
- provision @platform_hostmaster status
- provision @hostmaster status
- provision @eight.local.computer status
- provision verify server_master
- provision @server_master verify
# Docker creates the host path when docker run happens, in server verify.
# Platform verify uses drush make, which won't run if folder exists.
# @TODO: Figure out how to deal with this within provision.
- sudo rm -rf /home/travis/hostmaster
- provision verify platform_hostmaster
- provision @platform_hostmaster verify
- ls -la /home/travis/hostmaster
- provision verify hostmaster
- provision @hostmaster verify
- sudo rm -rf /home/travis/eight
- provision verify eight.local.computer
- provision @eight.local.computer verify
- provision verify server_master
- provision @server_master verify
- docker ps
- docker logs servermaster_http_1
......
# Provision CLI
# Introduction
![](/assets/server-verify.png)
![](assets/server-verify.png)
Provision is a command-line interface for quickly launching websites on any computer for development, testing, or production.
Provision is awesome because it's designed to work with any system and because it's written in PHP making it familiar to web developers.
Provision is stable because it comes from a long history of automating Drupal hosting within the [Aegir Project](https://www.aegirproject.org/).
Provision is stable because it comes from a long history of automating Drupal hosting within the [Aegir Project](https://www.aegirproject.org/).
The 4.x version is a total rewrite, based on [Symfony Console](https://symfony.com/doc/3.4/console.html) and [Robo](https://robo.li/) components. We are working to make Provision a useful stand-alone command for managing any kind of web app.
......@@ -20,9 +20,9 @@ You add your site's URLs and path to your source code using `provision save`and
## Development
Provision 4.x is a work in progress. If you want open source Drupal hosting now, please see [the Aegir Project](https://www.aegirproject.org/).
Provision 4.x is a work in progress. If you want open source Drupal hosting now, please see [DevShop](https://www.getdevshop.com/) and [the Aegir Project](https://www.aegirproject.org/).
Currently being developed on GitHub: [github.com/aegir-project/provision](https://github.com/aegir-project/provision).
Currently being developed on GitHub: [github.com/provision4/provision](https://github.com/provision4/provision).
Documentation is still in progress, currently available at [aegir.gitbooks.io/provision](https://aegir.gitbooks.io/provision/).
......@@ -67,7 +67,3 @@ More documentation on the new Provision is coming soon. Thanks for your patience
--Jon
##
# Summary
* [Introduction](README.md)
* [Developing Provision](docs/developing-provision.md)
* [Customizing Provision](docs/customizing-provision.md)
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="96mm"
height="96mm"
viewBox="0 0 340.15748 340.15748"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="logo.svg"
inkscape:export-filename="/Users/jon/Projects/aegir4/provision/assets/logo.png"
inkscape:export-xdpi="300"
inkscape:export-ydpi="300">
<defs
id="defs4">
<linearGradient
inkscape:collect="always"
id="linearGradient5617">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop5619" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop5621" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5617"
id="linearGradient5627"
x1="303.82538"
y1="343.66541"
x2="456.00577"
y2="343.66541"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(2.6015015,11.692185)" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="2.0866842"
inkscape:cx="186.05456"
inkscape:cy="160.08285"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1366"
inkscape:window-height="698"
inkscape:window-x="0"
inkscape:window-y="1"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-72.39051,-136.1473)">
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:900;font-stretch:normal;font-size:135.48083496px;line-height:125%;font-family:'.SF NS Display Condensed';-inkscape-font-specification:'.SF NS Display Condensed, Heavy';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:0;stroke:#3a3a3a;stroke-width:6.455;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="164.60326"
y="300.63254"
id="text4136"
sodipodi:linespacing="125%"
transform="matrix(1.0310589,0,-0.22940975,0.96987669,0,0)"><tspan
sodipodi:role="line"
x="164.60326"
y="300.63254"
id="tspan4146"
style="stroke-width:6.455;stroke-miterlimit:4;stroke-dasharray:none;stroke-linejoin:round;stroke:#3a3a3a;stroke-opacity:1">Pro</tspan></text>
<text
transform="matrix(1.0310589,0,-0.22940975,0.96987669,0,0)"
sodipodi:linespacing="125%"
id="text4171"
y="445.43045"
x="302.11938"
style="font-style:normal;font-variant:normal;font-weight:900;font-stretch:normal;font-size:255.67454529px;line-height:125%;font-family:'.SF NS Display Condensed';-inkscape-font-specification:'.SF NS Display Condensed, Heavy';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:#404040;stroke-width:10.835;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
xml:space="preserve"><tspan
id="tspan4173"
y="445.43045"
x="302.11938"
sodipodi:role="line"
style="fill:#ffffff;fill-opacity:1;stroke-width:10.835;stroke-miterlimit:4;stroke-dasharray:none;stroke-linejoin:round;stroke:#404040;stroke-opacity:1">4</tspan></text>
</g>
</svg>
......@@ -22,7 +22,7 @@ use Aegir\Provision\Common\NotSetupException;
use Aegir\Provision\Console\ProvisionStyle;
use Robo\Common\TimeKeeper;
use Symfony\Component\Console\Input\ArgvInput;
use Aegir\Provision\Console\ArgvInput;
use Symfony\Component\Console\Exception\InvalidOptionException;
use Symfony\Component\Console\Exception\CommandNotFoundException;
......@@ -68,10 +68,10 @@ try {
}
// Create the app.
$app = new \Aegir\Provision\Provision($config, $input, $output);
$provision = new \Aegir\Provision\Provision($config, $input, $output);
// Run the app.
$status_code = $app->run($input, $output);
$status_code = $provision->run($input, $output);
}
catch (Exception $e) {
......
{
"name": "aegir/provision",
"description": "The CLI for the Aegir Hosting System.",
"name": "provision4/cli",
"description": "The Website Provisioning CLI",
"keywords": ["Hosting", "Drupal", "Aegir"],
"homepage": "http://aegirproject.org/",
"type": "library",
"homepage": "https://github.com/provision4/provision",
"type": "project",
"license": "GPL-2.0+",
"require": {
"consolidation/Robo": "^1.1",
"consolidation/Robo": "dev-block-output",
"consolidation/annotated-command": "~2",
"drupal/console-core": "1.0.2",
"drush/drush": "8.x",
......@@ -15,19 +15,17 @@
"symfony/console": "^3.2",
"symfony/yaml": "^3.2"
},
"repositories": [
{
"type": "vcs",
"url": "git@github.com:provision4/Robo.git"
}
],
"bin": ["bin/provision"],
"authors": [
{
"name": "Provision Contributors",
"homepage": "https://github.com/aegir-project/provision/graphs/contributors"
},
{
"name": "Hosting Contributors",
"homepage": "https://github.com/aegir-project/hosting/graphs/contributors"
},
{
"name": "Hostmaster Contributors",
"homepage": "https://github.com/aegir-project/hostmaster/graphs/contributors"
"homepage": "https://github.com/provision4/provision/graphs/contributors"
},
{
"name": "Jon Pugh",
......@@ -36,12 +34,15 @@
}
],
"support": {
"issues": "https://www.drupal.org/project/issues/provision?categories=All",
"docs": "https://docs.aegirproject.org/"
"issues": "https://github.com/provision4/provision/issues",
"docs": "https://aegir.gitbooks.io/provision/"
},
"config": {
"bin-dir": "bin/",
"sort-packages": true
"sort-packages": true,
"platform": {
"php": "5.5.9"
}
},
"minimum-stability": "dev",
"prefer-stable": true,
......
This diff is collapsed.
......@@ -31,6 +31,7 @@ RUN ln -s /var/$PROVISION_BASE_USER_NAME/config/apache.conf /etc/apache2/conf-av
RUN ln -s /etc/apache2/conf-available/$PROVISION_BASE_USER_NAME.conf /etc/apache2/conf-enabled/$PROVISION_BASE_USER_NAME.conf
RUN mkdir -p /var/$PROVISION_BASE_USER_NAME/config
RUN mkdir -p /var/$PROVISION_BASE_USER_NAME/platforms
RUN chown $PROVISION_BASE_USER_NAME:$PROVISION_BASE_USER_NAME /var/$PROVISION_BASE_USER_NAME -R
RUN ls -la /var/$PROVISION_BASE_USER_NAME
......@@ -51,10 +52,12 @@ ENV SERVER_NAME server_master
ENV COMPOSER_VERSION 1b137f8bf6db3e79a38a5bc45324414a6b1f9df2
RUN echo "$RUN_PREFIX Installing Composer version $COMPOSER_VERSION"
RUN wget https://raw.githubusercontent.com/composer/getcomposer.org/$COMPOSER_VERSION/web/installer -O - -q | php -- --quiet
RUN cp composer.phar /usr/local/bin/composer
RUN mv composer.phar /usr/local/bin/composer
RUN chmod +x /usr/local/bin/composer
ENV DRUSH_VERSION 8.1.16
RUN wget https://github.com/drush-ops/drush/releases/download/8.1.16/drush.phar -O - -q > /usr/local/bin/drush
RUN chmod +x /usr/local/bin/drush
USER $PROVISION_BASE_USER_NAME
......
# Use this Dockerfile for building a new image with your desired UID.
#
# Dockerfile.user
#
# This dockerfile is used by Provision on `provision verify` to create containers
# with UIDs that match the host user. This is so the files in volume mounts are
# owned by the same user inside and outside of the container.
#
#
# CUSTOM PROVISION DOCKERFILE SETUP
#
# Custom Dockerfiles for services that need the source code mapped as a volume
# must include the following code to get properly permissioned container.
#
# The build arguments PROVISION_USER_UID and PROVISION_WEB_UID are determined
# automatically by the Provision CLI, and are injected in the main
# docker-compose.yml template.
#
ARG IMAGE_NAME=http
ARG IMAGE_TAG=php7
FROM provision4/$IMAGE_NAME:$IMAGE_TAG
ENV IMAGE_TAG ${IMAGE_TAG:-php7}
ENV IMAGE_NAME ${IMAGE_NAME:-http}
# The UID and GID for the USER_NAME user.
ARG PROVISION_USER_UID=1000
ENV PROVISION_USER_UID ${PROVISION_USER_UID:-1000}
ARG PROVISION_WEB_UID=2000
ENV PROVISION_WEB_UID ${PROVISION_WEB_UID:-2000}
ENV PROVISION_USER_NAME provision
USER root
RUN echo "$RUN_PREFIX Creating set-user-ids script..."
COPY set-user-ids.sh /usr/local/bin/set-user-ids
RUN chmod +x /usr/local/bin/set-user-ids
ENV RUN_PREFIX '𝙋𝙍𝙊𝙑𝙄𝙎𝙄𝙊𝙉 Dockerfile.user ║'
RUN echo "$RUN_PREFIX Running /usr/local/bin/set-user-ids $PROVISION_USER_NAME $PROVISION_USER_UID $PROVISION_WEB_UID"
ARG PROVISION_USER_UID=1000
ENV PROVISION_USER_UID ${PROVISION_USER_UID:-1001}
ARG PROVISION_WEB_UID=1001
ENV PROVISION_WEB_UID ${PROVISION_WEB_UID:-1001}
ENV PROVISION_USER_NAME provision
RUN echo "𝙋𝙍𝙊𝙑𝙄𝙎𝙄𝙊𝙉 Dockerfile.user ║ Running /usr/local/bin/set-user-ids $PROVISION_USER_NAME $PROVISION_USER_UID $PROVISION_WEB_UID"
RUN /usr/local/bin/set-user-ids $PROVISION_USER_NAME $PROVISION_USER_UID $PROVISION_WEB_UID
USER $PROVISION_USER_NAME
\ No newline at end of file
USER $PROVISION_USER_NAME
#
# END CUSTOM PROVISION DOCKERFILE SETUP
#
......@@ -11,12 +11,17 @@ USER_NAME=$1
USER_UID=$2
WEB_UID=$3
echo "$PREFIX Changing user '$USER_NAME' UID/GID to '$USER_UID'...
echo "$PREFIX Recreating user '$USER_NAME' UID/GID to '$USER_UID'...
"
usermod -u $USER_UID $USER_NAME
groupmod -g $USER_UID $USER_NAME
userdel $USER_NAME
chown $USER_UID:$USER_UID /var/$USER_NAME -R
addgroup --gid $USER_UID $USER_NAME
useradd --no-log-init --uid $USER_UID --gid $USER_UID --system --home-dir /var/$USER_NAME $USER_NAME
echo "$PREFIX Changing user 'www-data' UID/GID to '$WEB_UID'...
"
usermod -u $WEB_UID www-data
groupmod -g $WEB_UID www-data
userdel www-data
addgroup --gid $WEB_UID www-data
useradd --no-log-init --uid $WEB_UID --gid $WEB_UID --system www-data
# Summary
* [Introduction](README.md)
* [Developing Provision](/docs/developing-provision.md "How to contribute to Provision.")
# Customizing Provision
## Docker Services
When using Provision's Docker services, a docker-compose.yml file is automatically generated, using pre-built images for Database & Web servers.
Each server has a "config path" where all server configuration is stored, such as apache config. Check `provision status` for the server config path. The server config folder is filled with files. Most files are generated automaticaly by provision verify. You can create the files labelled \(Optional\) below to customize the behavior of this server's stack.
```
~/.config/provision/$SERVER_NAME
/docker-compose.yml # Generated on provision verify
/docker-compose-overrides.yml # (Optional) Merged into docker-compose.yml on provision verify**
/mysql.cnf # (Optional) MySQL configuration can be put into this file.***
/apacheDocker.conf # Generated on provision verify
/platform.d # Generated Platform apache configs.
/pre.d # Custom Apache configs can be put in here.
/post.d # Custom Apache configs can be put in here.
/vhost.d # Generated Site virtualhost configs.
/platform.d
```
\*\* The docker-compose command supports automatic merging of docker-compose.yml files, by passing multiple `-f` options. Provision detects if this file is present and automatically adds this for you.
\*\*\* my.cnf file must follow the right format:
```
[mysqld]
max_allowed_packet = 32M
```
###
# Developing Provision
One of our missions is to be _Easy to Develop._
This is not just for the core team, but also so others can come in and customize the system to their needs without much difficulty.
## Code
The source code for provision is available at [github.com/aegir-project/provision](https://github.com/aegir-project/provision). The main branch is `4.x`.
The important files and folders are:
* composer.json - Defines the project and the dependencies.
* bin/provision - Executable. Run this to use provision.
* src/ - All the new classes.
* vendor/ - The new vendor directory. All composer libraries get installed here when you run `composer install`.
* .travis.yml - Automated tests for our new branch.
* README.md - The main documentation file.
NOTE: The entire codebase pre-4.x is still in the repository, so \_everything else \_is legacy code.
## Classes
### [class Provision](https://github.com/aegir-project/provision/blob/4.x/src/Provision.php)
Implements:
* ConfigAwareInterface: `$provision->getConfig()`to load the CLI config, optionally loaded from ~/.provision.yml
* ContainerAwareInterface: Uses [container.thephpleague.com](http://container.thephpleague.com) for dependency injection.
* LoggerAwareInterface: `$provision->getLogger()->info()` to `$provision->getLogger()->debug(),` easy PSR logging.
* BuilderAwareInterface: Access to the Robo Builder.
* and more... API docs coming soon.
### [class Context](https://github.com/aegir-project/provision/blob/4.x/src/Context.php)
A Context represents an object we are tracking, either a Server, Platform, or Site.
Each context type has different properties, defined in the `option_documentation()` method.
`$context->save()` will save the context properties into a YML file. Run provision status to reveal the path to a context's YML file.
`$context->verifyCommand() `is triggered when the `provision verify` command is run
### [class ServerContext](https://github.com/aegir-project/provision/blob/4.x/src/Context/ServerContext.php)
ServerContext "provide" services, while all others "subscribe" to them.
Use `ServerContext::shell_exec()` to easily run commands in the Server's config directory, while hiding output, throwing exception with error messages, and showing output when running with `-v`.
###
......@@ -2,12 +2,17 @@
namespace Aegir\Provision;
use Aegir\Provision\Command\CdCommand;
use Aegir\Provision\Command\EditCommand;
use Aegir\Provision\Command\SaveCommand;
use Aegir\Provision\Command\ServicesCommand;
use Aegir\Provision\Command\SetupCommand;
use Aegir\Provision\Command\ShellCommand;
use Aegir\Provision\Command\StatusCommand;
use Aegir\Provision\Command\Ui\CreateUiCommand;
use Aegir\Provision\Command\Ui\UiCreateCommand;
use Aegir\Provision\Command\VerifyCommand;
use Aegir\Provision\Command\InstallCommand;
use Aegir\Provision\Common\ProvisionAwareTrait;
use Aegir\Provision\Console\Config;
use Aegir\Provision\Console\ConsoleOutput;
......@@ -143,14 +148,18 @@ class Application extends BaseApplication
*/
protected function getDefaultCommands()
{
$commands[] = new CdCommand();
$commands[] = new HelpCommand();
$commands[] = new ListCommand();
$commands[] = new SaveCommand();
$commands[] = new EditCommand();
$commands[] = new SetupCommand();
$commands[] = new ServicesCommand();
// $commands[] = new ShellCommand();
$commands[] = new ShellCommand();
$commands[] = new StatusCommand();
$commands[] = new VerifyCommand();
$commands[] = new InstallCommand();
$commands[] = new UiCreateCommand();
return $commands;
}
......@@ -176,7 +185,7 @@ class Application extends BaseApplication
$exitCode = parent::doRunCommand($command, $input, $output);
return $exitCode;
}
/**
* {@inheritdoc}
*
......@@ -187,9 +196,9 @@ class Application extends BaseApplication
$inputDefinition = parent::getDefaultInputDefinition();
$inputDefinition->addOption(
new InputOption(
'--target',
'-t',
InputOption::VALUE_NONE,
'--context',
'-c',
InputOption::VALUE_OPTIONAL,
'The target context to act on.'
)
);
......
......@@ -28,6 +28,17 @@ abstract class Command extends BaseCommand
use ProvisionAwareTrait;
use LoggerAwareTrait;
/**
* Set if this command requires a context. If so provision will automatically ask for which one if not specified..
*/
const CONTEXT_REQUIRED = FALSE;
/**
* Set if this command is only for certain context types.
*/
const CONTEXT_REQUIRED_TYPES = [];
const CONTEXT_REQUIRED_QUESTION = 'Which context';
/**
* @var \Symfony\Component\Console\Input\InputInterface
*/
......@@ -72,31 +83,34 @@ abstract class Command extends BaseCommand
$this->io = new ProvisionStyle($input, $output);
// Load active context if a command uses the argument.
if ($this->input->hasArgument('context_name') && !empty($this->input->getArgument('context_name'))) {
if ($this->input->getOption('context') && !empty($this->input->getOption('context'))) {
try {
// Load context from context_name argument.
$this->context_name = $this->input->getArgument('context_name');
// Load context from context_name option.
$this->context_name = $this->input->getOption('context');
$this->context = $this->getProvision()->getContext($this->context_name);
}
catch (\Exception $e) {
// If no context with the specified name is found:
// if this is "save" command and option for --delete is used, throw exception: context must exist to delete.
if ($this->getName() == 'save' && $input->getOption('delete')) {
if ($this->getName() == 'context:save' && $input->getOption('delete')) {
throw new \Exception("No context named {$this->context_name}. Unable to delete.");
}
// If this is any other command, context is required.
elseif ($this->getName() != 'save') {
elseif ($this->getName() != 'context:save') {
throw new InvalidArgumentException($e->getMessage());
}
}
}
// If context_name is not specified, ask for it.
elseif ($this->input->hasArgument('context_name') && $this->getDefinition()->getArgument('context_name')->isRequired() && empty($this->input->getArgument('context_name'))) {
$this->askForContext();
$this->input->setArgument('context_name', $this->context_name);
elseif (($this::CONTEXT_REQUIRED && empty($this->input->getOption('context')))
|| ($this->getName() == 'save' && empty($this->input->getOption('context')))
) {
$this->askForContext($this::CONTEXT_REQUIRED_QUESTION, $this::CONTEXT_REQUIRED_TYPES);
$this->input->setOption('context', $this->context_name);
try {
$this->context = $this->getProvision()->getContext($this->context_name);
......@@ -110,12 +124,12 @@ abstract class Command extends BaseCommand
/**
* Show a list of Contexts to the user for them to choose from.
*/
public function askForContext($question = 'Choose a context') {
public function askForContext($question = self::CONTEXT_REQUIRED_QUESTION, $context_types = array()) {
if (empty($this->getProvision()->getAllContextsOptions())) {
throw new \Exception('No contexts available! use `provision save` to create one.');
}
$this->context_name = $this->io->choice($question, $this->getProvision()->getAllContextsOptions());
$this->context_name = $this->io->choice($question, $this->getProvision()->getAllContextsOptions($context_types));
}
/**
......
<?php
namespace Aegir\Provision\Command;
use Aegir\Provision\Command;
use Aegir\Provision\Provision;
use Psy\Shell;
use Psy\Configuration;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class CdCommand
*
* @package Aegir\Provision\Command
*/
class CdCommand extends Command
{
const CONTEXT_REQUIRED = TRUE;
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('cd')
->setDescription('Change into the directory of this site, server, or platform.')
->setHelp('If the chosen context is a site or platform, the `cd` command will put you into the directory of that sites source code. If the chosen context is a server, you will be put into the server config folder.')
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$process = new \Symfony\Component\Process\Process("bash");
$process->setTty(true);
if ($this->context->type == 'site') {
$dir = $this->context->getProperty('root');
}
elseif ($this->context->type == 'server') {
$dir = $this->context->getProperty('server_config_path');
}
if (getenv('SHELL')) {
$shell = getenv('SHELL');
}
else {
$shell = 'bash';
}
$process->setCommandLine("cd $dir && $shell");
$process->setEnv($_SERVER);
$messages[] = "Opening $shell shell in $dir...";
$this->io->commentBlock($messages);
$process->run();
}
}
<?php
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;
use Aegir\Provision\Context\SiteContext;
use Aegir\Provision\Property;
use Aegir\Provision\Provision;
use Aegir\Provision\Service;
use Symfony\Component\Console\Exception\InvalidOptionException;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class EditCommand
*
* @package Aegir\Provision\Command
*/
class EditCommand extends Command
{
const CONTEXT_REQUIRED = TRUE;
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('context:edit')
->setAliases(['edit'])
->setDescription('Edit a context file')
->setHelp(
'Use this command to interactively setup a new site, platform or server (known as "contexts"). Metadata is saved to .yml files in the "config_path" folder. Once you have create a context, use the `provision status` command to view the list of added contexts.'
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->context->process_exec('${VISUAL-${EDITOR-vi}} ' . $this->context->config_path);
}
}
<?php
namespace Aegir\Provision\Command;
use Aegir\Provision\Command;
use Aegir\Provision\Context;
use Aegir\Provision\Context\PlatformContext;
use Aegir\Provision\Context\ServerContext;
use Aegir\Provision\Context\SiteContext;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class VerifyCommand
*
* Replacement for drush provision-verify command
*
* @package Aegir\Provision\Command
* @see provision.drush.inc
* @see drush_provision_verify()
*/
class InstallCommand extends Command
{
/**
* This command needs a context.
*/
const CONTEXT_REQUIRED = TRUE;
const CONTEXT_REQUIRED_TYPES = ['site'];
const CONTEXT_REQUIRED_QUESTION = 'Install which site';
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('install')
->setDescription('Run the install process for a site.')
->setHelp(
'Run this command to prepare the site or web app, installing database tables and preparing folders, for example.'
)
->setDefinition($this->getCommandDefinition());
}
/**
* Generate the list of options derived from ProvisionContextType classes.
*
* @return \Symfony\Component\Console\Input\InputDefinition
*/
protected function getCommandDefinition()
{
$inputDefinition = [];
$inputDefinition[] = new InputOption(
'option',
null,
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL,
"Pass install options to the sites install command."
);
$inputDefinition[] = new InputOption(
'skip-verify',
null,
InputOption::VALUE_NONE,
"By default, the 'provision verify' command is run before the install process starts. Pass --skip-verify to skip it."
);
return new InputDefinition($inputDefinition);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->io->title(strtr("Verify %type: %name", [
'%name' => $this->context_name,
'%type' => $this->context->type,
]));
/**
* The provision-verify command function looks like:
*
*
function drush_provision_verify() {
provision_backend_invoke(d()->name, 'provision-save');
d()->command_invoke('verify');
}
*/
$this->context->runSteps('install');
}
}
......@@ -27,7 +27,6 @@ use Symfony\Component\Console\Output\OutputInterface;
*/
class SaveCommand extends Command
{
/**
* @var string
*/
......@@ -44,11 +43,11 @@ class SaveCommand extends Command
protected function configure()
{
$this
->setName('save')
->setAliases(['add'])
->setDescription('Save Provision Context.')
->setName('context:save')
->setAliases(['add', 'save'])
->setDescription('Create or update a site, platform, or server.')
->setHelp(
'Saves a ProvisionContext object to file. Currently just passes to "drush provision-save".'
'Use this command to interactively setup a new site, platform or server (known as "contexts"). Metadata is saved to .yml files in the "config_path" folder. Once you have create a context, use the `provision status` command to view the list of added contexts.'
)
->setDefinition($this->getCommandDefinition());
}
......@@ -61,17 +60,11 @@ class SaveCommand extends Command
protected function getCommandDefinition()
{
$inputDefinition = ServicesCommand::getCommandOptions();
$inputDefinition[] = new InputArgument(
'context_name',
InputArgument::REQUIRED,
'Context to save'
);
$inputDefinition[] = new InputOption(
'context_type',
null,
InputOption::VALUE_OPTIONAL,
'server, platform, or site',
'site'
'server, platform, or site'
);
$inputDefinition[] = new InputOption(
'delete',
......@@ -135,7 +128,7 @@ class SaveCommand extends Command
protected function initialize(InputInterface $input, OutputInterface $output)
{
parent::initialize($input,$output);
$this->context_name = $input->getArgument('context_name');
$this->context_name = $input->getOption('context');
$this->context_type = $input->getOption('context_type');
}
......@@ -151,14 +144,55 @@ class SaveCommand extends Command
$this->newContext = TRUE;
// If context_type is still empty, throw an exception. Happens if using -n
// If context_type is not specified,..
if (empty($context_type)) {
throw new \Exception('Option --context_type must be specified.');
// If user is interactive...
if ($input->isInteractive()){
// If name is not set, ask for it.
if (empty($this->context_name)) {
while (empty($this->context_name)) {
$this->context_name = $this->io->ask('Context Name:', '', function ($value) {
if (empty($value)) {
$this->io->error('You must specify a context name.');
}
else {
return $value;
}
});
}
$this->input->setOption('context', $this->context_name);
}
// If there are no contexts at all, default to "server".
$contexts = $this->getProvision()->getAllContexts();
if (count($contexts) == 0) {
// @TODO: This is the onboarding moment for CLI users.
$context_type = 'server';
$this->io->note('No contexts exist yet. First one must be a server.');
}
// If there are other contexts, ask what type the user wants to create.
else {
$context_type = $this->io->choice('What do you want to create?', Context::getContextTypeOptions());
}
// Save option to $input.
$input->setOption('context_type', $context_type);
}
else {
// Not interactive: --context_type is required.
throw new \Exception('Option --context_type must be specified.');
}
}
else {
$this->input->setOption('context_type', $context_type);
if (empty($this->context_name)) {
throw new \Exception('No context name set. You must use the --context option when running non-interactively.');
}
}
// Handle invalid context_type.
if (!class_exists(Context::getClassName($context_type))) {
$types = Context::getContextTypeOptions();
......@@ -199,7 +233,7 @@ class SaveCommand extends Command
$options['type'] = $this->context_type;
$class = Context::getClassName($this->input->getOption('context_type'));
$this->context = new $class($input->getArgument('context_name'), $this->getProvision(), $options);
$this->context = new $class($input->getOption('context'), $this->getProvision(), $options);
}
else {
$this->getProvision()->io()->helpBlock("Editing context {$this->context->name}...", ProvisionStyle::ICON_EDIT);
......@@ -257,7 +291,7 @@ class SaveCommand extends Command
$this->io->warningLite('Context not saved.');
return;
}
// $command = 'drush provision-save '.$input->getArgument('context_name');
// $command = 'drush provision-save '.$input->getOption('context');
// $this->process($command);
// If editing a context, exit here.
......@@ -276,7 +310,7 @@ class SaveCommand extends Command
// Offer to verify. (only if --verify option was specified or is interactive and confirmation is made.
if ($this->input->getOption('verify') || ($this->input->isInteractive() && $this->io->confirm('Would you like to run `provision verify` on this ' . $this->input->getOption('context_type') . '?'))) {
$command = $this->getApplication()->find('verify');
$arguments['context_name'] = $this->context_name;
$arguments['--context'] = $this->context_name;
$input = new ArrayInput($arguments);
exit($command->run($input, $this->output));
}
......@@ -321,7 +355,7 @@ class SaveCommand extends Command
* Override to add options
* @param string $question
*/
public function askForContext($question = 'Choose a context')
public function askForContext($question = 'Choose a context', $context_types = array())
{
$options = $this->getProvision()->getAllContextsOptions();
......@@ -334,19 +368,6 @@ class SaveCommand extends Command
if (empty($this->input->getOption('context_type'))) {
$type_options = Context::getContextTypeOptions();
// Check for platforms. If none. don't allow sites.
$platform_exists = FALSE;
foreach ($options as $name => $type_and_name) {
if (strpos($type_and_name, 'platform') === 0) {
$platform_exists = TRUE;
}
}
if (!$platform_exists) {
unset($type_options['site']);
$this->io->block("You cannot add a site until you have at least one platform.");
}
$context_type = $this->io->choice('Context Type?', $type_options);
}
else {
......@@ -437,7 +458,7 @@ class SaveCommand extends Command
}
$command = $this->getApplication()->find('services');
$arguments = [
'context_name' => $this->input->getArgument('context_name'),
'--context' => $this->input->getOption('context'),
'sub_command' => 'add',
];
while ($this->io->confirm('Add a service?')) {
......@@ -468,7 +489,7 @@ class SaveCommand extends Command
$command = $this->getApplication()->find('services');
$arguments = [
'context_name' => $this->input->getArgument('context_name'),
'--context' => $this->input->getOption('context'),
'sub_command' => 'add',
'service' => $type,
];
......
......@@ -28,6 +28,11 @@ use Symfony\Component\Console\Output\OutputInterface;
class ServicesCommand extends Command
{
/**
* This command needs a context.
*/
const CONTEXT_REQUIRED = TRUE;
/**
* "list" (default), "add", "remove", or "configure"
* @var string
......@@ -45,7 +50,8 @@ class ServicesCommand extends Command
->setHelp(
'Use this command to add new services to servers, or to add service subscriptions to platforms and sites.'
)
->setDefinition($this->getCommandDefinition());
->setDefinition($this->getCommandDefinition())
;
}
/**
......@@ -56,11 +62,7 @@ class ServicesCommand extends Command
protected function getCommandDefinition()
{
$inputDefinition = $this::getCommandOptions();
$inputDefinition[] = new InputArgument(
'context_name',
InputArgument::REQUIRED,
'Server to work on.'
);
$inputDefinition[] = new InputArgument(
'sub_command',
InputArgument::OPTIONAL,
......@@ -83,7 +85,6 @@ class ServicesCommand extends Command
InputOption::VALUE_OPTIONAL,
'The name of the service type to use.'
);
return new InputDefinition($inputDefinition);
}
......@@ -98,21 +99,25 @@ class ServicesCommand extends Command
// Load all service options
$options = Context::getServiceOptions();
// For each service type...
foreach ($options as $service => $service_name) {
$class = Service::getClassName($service);
// Load option_documentation() into input options.
foreach (Context::getContextTypeOptions() as $type => $type_name) {
$method = "{$type}_options";
foreach ($class::$method() as $option => $description) {
$description = "$type_name $service service: $description";
$inputDefinition[] = new InputOption($option, NULL, InputOption::VALUE_OPTIONAL, $description);
// Load every available service type.
foreach (Context::getServiceTypeOptions($service) as $service_type => $service_name) {
$class = Service::getClassName($service, $service_type);
Provision::getProvision()->getLogger()->debug("Loading options from $class $service_type");
// Load option_documentation() into input options.
foreach (Context::getContextTypeOptions() as $type => $type_name) {
$method = "{$type}_options";
foreach ($class::$method() as $option => $description) {
$description = "$type_name $service $service_name service: $description";
$inputDefinition[] = new InputOption($option, NULL, InputOption::VALUE_OPTIONAL, $description);
}
}
}
}
return $inputDefinition;
......@@ -130,13 +135,19 @@ class ServicesCommand extends Command
InputInterface $input,
OutputInterface $output
) {
//
// if ($input->getOption('context_name') == 'add') {
// $this->sub_command = $input->getArgument('context_name');
// $input->setArgument('context_name', NULL);
// }
// else {
// $this->sub_command = $input->getArgument('sub_command');
// }
$this->sub_command = $input->getArgument('sub_command');
if ($input->getArgument('context_name') == 'add') {
$this->sub_command = $input->getArgument('context_name');
$input->setArgument('context_name', NULL);
}
else {
$this->sub_command = $input->getArgument('sub_command');
// Ensure '@context' names can be used: Trim the "@"
if ($input->getArgument('server')) {
$input->setArgument('server', ltrim($input->getArgument('server'), '@'));
}
parent::initialize(
......@@ -214,7 +225,7 @@ class ServicesCommand extends Command
}
if ($this->context->hasService($service)) {
$this->getProvision()->io()->helpBlock("Editing service {$service} provded by server '{$this->context->name}'...", ProvisionStyle::ICON_EDIT);
$this->getProvision()->io()->helpBlock("Editing service {$service} provided by server '{$this->context->name}'...", ProvisionStyle::ICON_EDIT);
}
// Then ask for all options.
......@@ -281,12 +292,12 @@ class ServicesCommand extends Command
$property = Provision::newProperty($property);
}
if ($this->context->hasService($service) && $this->context->getService($service)->getProperty($name)) {
if ($this->context->hasService($service) && $this->context->getService($service)->hasProperty($name) && $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))) {
if ($this->input->hasOption($name) && !empty($this->input->getOption($name))) {
$properties[$name] = $this->input->getOption($name);
$this->io->comment("Using option {$name}={$properties[$name]}");
}
......
......@@ -193,7 +193,7 @@ YML;
$command = $this->getApplication()->find('save');
$parameters = $_SERVER['argv'];
$parameters['--context_type'] = 'server';
$parameters['context_name'] = 'server_master';
$parameters['--context'] = 'server_master';
$input = new ArrayInput($parameters);
$exit_code = $command->run($input, $this->output);
......
......@@ -3,6 +3,8 @@
namespace Aegir\Provision\Command;
use Aegir\Provision\Command;
use Aegir\Provision\Provision;
use Aegir\Provision\Service\DockerServiceInterface;
use Psy\Shell;
use Psy\Configuration;
use Symfony\Component\Console\Input\InputInterface;
......@@ -15,6 +17,7 @@ use Symfony\Component\Console\Output\OutputInterface;
*/
class ShellCommand extends Command
{
const CONTEXT_REQUIRED = TRUE;
/**
* {@inheritdoc}
......@@ -32,8 +35,51 @@ class ShellCommand extends Command
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$config = new Configuration;
$shell = new Shell($config);
$shell->run();
$messages = [];
$process = new \Symfony\Component\Process\Process("bash");
$process->setTty(TRUE);
$dir = $this->context->getWorkingDir();
if ($this->context->type == 'site') {
$ps1 = Provision::APPLICATION_FUN_NAME . ' \[\e[33m\]' . $this->context_name . '\[\e[m\] \w \[\e[36;40m\]\\$\[\e[m\] ';
$process->setWorkingDirectory($dir);
$process->setCommandLine("cd $dir && PS1='$ps1' bash");
//@TODO: Allow services to set environment variables for both shell command and virtualhost config.
$env = $_SERVER;
$env['db_type'] = $this->context->getSubscription('db')->service->getType();
$env['db_name'] = $this->context->getSubscription('db')->getProperty('db_name');
$env['db_user'] = $this->context->getSubscription('db')->getProperty('db_user');
$env['db_passwd'] = $this->context->getSubscription('db')->getProperty('db_password');
// @TODO: We shouldn't always rely on what remote_host says.
$env['db_host'] = $this->context->getSubscription('db')->service->provider->getProperty('remote_host');
$env['db_port'] = $this->context->getSubscription('db')->service->getCreds()['port'];
// If bin dir is found, add to path
if (file_exists($this->context->getProperty('root') . '/bin')) {
$env['PATH'] .= ':' . $this->context->getProperty('root') . '/bin';
}
if (file_exists($this->context->getProperty('root') . '/vendor/bin')) {
$env['PATH'] .= ':' . $this->context->getProperty('root') . '/vendor/bin';
}
$process->setEnv($env);
}
if ($this->context->hasService('http') && $this->context->getService('http') instanceof DockerServiceInterface) {
$process->setWorkingDirectory($this->context->service('http')->provider->getWorkingDir());
$process->setCommandLine("docker-compose exec http bash");
}
$messages[] = "Opening bash shell in " . $dir;
$messages[] = "Running the command: " . $process->getCommandLine();
$messages[] = 'The commands composer, drupal, drush and more are available.';
$messages[] = 'Type "exit" to leave.';
$this->io->commentBlock($messages);
$process->run();
}
}
......@@ -32,13 +32,7 @@ class StatusCommand extends Command
->setName('status')
->setDescription('Display system status.')
->setHelp('Lists helpful information about your system.')
->setDefinition([
new InputArgument(
'context_name',
InputArgument::OPTIONAL,
'Context to show info for.'
)
])
->setAliases(array('ls'))
;
}
......@@ -49,14 +43,14 @@ class StatusCommand extends Command
{
$this->getProvision();
if ($input->getArgument('context_name')) {
if ($input->getOption('context')) {
$rows = [['Configuration File', $this->context->config_path]];
foreach ($this->context->getProperties() as $name => $value) {
if (is_string($value)) {
$rows[] = [$name, $value];
}
}
$this->io->table(['Provision Context:', $input->getArgument('context_name')], $rows);
$this->io->table(['Provision Context:', $input->getOption('context')], $rows);
// Display services.
$this->context->showServices($this->io);
......@@ -104,7 +98,7 @@ class StatusCommand extends Command
$context = $this->io->choiceNoList('Get status for', $options, 'select a context');
if ($context != 'select a context') {
$command = $this->getApplication()->find('status');
$arguments['context_name'] = $context;
$arguments['--context'] = $context;
$input = new ArrayInput($arguments);
exit($command->run($input, $this->output));
}
......
This diff is collapsed.
......@@ -25,6 +25,11 @@ use Symfony\Component\Console\Output\OutputInterface;
class VerifyCommand extends Command
{
/**
* This command needs a context.
*/
const CONTEXT_REQUIRED = TRUE;
/**
* {@inheritdoc}
*/
......@@ -47,11 +52,6 @@ class VerifyCommand extends Command
protected function getCommandDefinition()
{
$inputDefinition = [];
$inputDefinition[] = new InputArgument(
'context_name',
InputArgument::REQUIRED,
'Context to verify'
);
return new InputDefinition($inputDefinition);
}
......@@ -80,7 +80,7 @@ class VerifyCommand extends Command
}
*/
$message = $this->context->verifyCommand();
$this->context->runSteps('verify');
}
}
<?php
namespace Aegir\Provision\Console;
use \Symfony\Component\Console\Input\ArgvInput as ArgvInputBase;
use \Symfony\Component\Console\Input\InputDefinition;
class ArgvInput extends ArgvInputBase {
/**
* @var string The name of the active context, extracted from the first argument if it has "@" prefix.
*/
public $activeContextName = NULL;
/**
* @param array|null $argv An array of parameters from the CLI (in the argv format)
* @param InputDefinition|null $definition A InputDefinition instance
*/
public function __construct(array $argv = [], InputDefinition $definition = null)
{
// If @alias is used, swap it out with --context=
if (isset($argv[1]) && strpos($argv[1], '@') === 0) {
$context_name = ltrim($argv[1], '@');
$argv[1] = "--context={$context_name}";
$this->activeContextName = $context_name;
}
// If --context option is used, use that.
elseif ($argv_filtered = array_filter($argv, function ($key) {
return strpos($key, '--context=') === 0;
})) {
$context_option = array_pop($argv_filtered);
$context_name = substr($context_option, strlen('--context='));
// Allow --context=@server_master.
$this->activeContextName = ltrim($context_name, '@');
}
parent::__construct($argv, $definition);
}
}
\ No newline at end of file
......@@ -5,11 +5,11 @@ namespace Aegir\Provision\Console;
use Aegir\Provision\Common\NotSetupException;
use Aegir\Provision\Common\ProvisionAwareTrait;
use Aegir\Provision\Provision;
use Aegir\Provision\Console\ArgvInput;
use Drupal\Console\Core\Style\DrupalStyle;
use Robo\Common\IO;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\Console\Exception\InvalidOptionException;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
......@@ -22,6 +22,7 @@ use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
class Config extends ProvisionConfig
{
const CONFIG_FILENAME = '.provision.yml';
const COMPOSER_INSTALL_DEFAULT = 'composer install --no-interaction';
use IO;
use ProvisionAwareTrait;
......@@ -116,12 +117,12 @@ class Config extends ProvisionConfig
// Check for missing everything. Tell the user to run the setup command.
// @TODO: Run the setup command here instead. I poked and prodded but could not get it to work. Config is instantiated before Application
if (
!file_exists($this->get('config_path')) &&
!file_exists($this->get('console_config_file'))
) {
throw new NotSetupException();
}
// if (