├── .gitignore ├── README.md └── src ├── AlanKent └── MagentoDev │ ├── CommandParser.php │ ├── Config.php │ ├── EnvironmentInterface.php │ ├── Environments │ └── VagrantRsync │ │ ├── README.md │ │ ├── VagrantRsyncEnvironment.php │ │ └── scripts │ │ ├── Vagrantfile │ │ ├── install-gulp │ │ ├── install-gulp-run-gulp.sh │ │ ├── install-magento │ │ ├── install-magento-apache2.conf │ │ ├── install-magento-magento.conf │ │ └── install-nodejs │ ├── MdException.php │ ├── ProviderInterface.php │ └── Providers │ ├── GoDaddy │ └── GoDaddyProvider.php │ └── MagentoCloud │ ├── MagentoCloudProvider.php │ └── README.md ├── autoloader.php └── magento-dev.php /.gitignore: -------------------------------------------------------------------------------- 1 | src/Vagrantfile 2 | src/scripts/ 3 | src/.magento-dev.json 4 | .idea/ 5 | tmp/ 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Magento Development Environment creation tool 2 | 3 | This project contains my experimentation with a tool to create development environments 4 | for Magento 2 projects. If successful, this project will get sucked into something more 5 | permanent. For now, this is a good way to share what I am doing with any external people 6 | who are interested. 7 | 8 | For example, one option is to merge this work into the Magento CLI via a Composer package. 9 | I have not done that here however as I am hoping this tool will not supposed to be tied 10 | to specific versions of Magento (although maybe it should be in case of configuration 11 | changes). 12 | 13 | There are two concepts supported by this tool, *environments* and *providers*. 14 | Environments are for local development and may be created using configuration files for 15 | technologies such as Docker or Vagrant. 16 | Providers are hosting providers for when you want to push your project code changes into 17 | production. 18 | Both environments and providers here are intended for simple projects. It is likely 19 | that advanced users would build their own variation of tools and deployment processes. 20 | The goal here is to get people going. 21 | 22 | Currently there is no installer. You need to get this code directly from GitHub and create 23 | a shell script or BAT file to run the `magento-dev.php` PHP script. The script takes command 24 | line arguments as follows: 25 | 26 | ``` 27 | Usage: magento-dev where 'command' is one of: 28 | 29 | create ... Create new development environment. 30 | destroy [--force] Destroy all files created for the environment. 31 | connect ... Connect to a production host. 32 | pull-code Pull a copy of the code from the current provider. 33 | push-code Push the code and run deployment actions. 34 | disconnect Forget about the last environment connected to. 35 | ``` 36 | 37 | Available environments: 38 | 39 | - [**vagrant-rsync**](src/AlanKent/MagentoDev/Environments/VagrantRsync/README.md): 40 | Creates a Vagrantfile and support files to use Vagrant to set up a M2 development 41 | environment. Uses `vagrant rsync-auto` to copy local file system changes into the box. 42 | This works well on Windows, allowing the source code to be kept on your laptop natively 43 | (which is generally better for an IDE) while having the full database and other tools 44 | run in a VM, avoiding problems with multiple versions of PHP and MySQL etc installed. 45 | 46 | Available providers: 47 | 48 | - [**magento-cloud**](src/AlanKent/MagentoDev/Providers/MagentoCloud/README.md): 49 | Displays instructions how to push/pull code etc for Magento Cloud. 50 | 51 | 52 | Feedback welcome. Talk to me before you plan any big contributions to avoid merge conflicts 53 | (I am still doing reasonably dramatic changes to the code). 54 | 55 | If successful, this code may be merged into the Magento code base and so follow standard 56 | Magento copyright ownership and licenses. All external code contributions should keep this 57 | in mind. 58 | 59 | ## TODO 60 | 61 | * "Environment" might be confusing for Magento Cloud users, as it is something different. 62 | * "Provider" might be better called "Hosting Provider" or "Connector". 63 | -------------------------------------------------------------------------------- /src/AlanKent/MagentoDev/CommandParser.php: -------------------------------------------------------------------------------- 1 | environments = [ 31 | 'vagrant-rsync' => new VagrantRsyncEnvironment() 32 | ]; 33 | $this->providers = [ 34 | 'magento-cloud' => new MagentoCloudProvider(), 35 | 'godaddy-cloud' => new GoDaddyProvider() 36 | ]; 37 | } 38 | 39 | /** 40 | * The main program. Parse command line options and process the command appropriately. 41 | * @param string[] $args Script command line arguments. 42 | * @return int Exit status code, 0 = success. Returning the exit code rather than calling exit($status) 43 | * makes testing easier. 44 | */ 45 | public function main($args) 46 | { 47 | // TODO: Consider using Symfony parser for better command line parsing and error message etc. 48 | 49 | 50 | try { 51 | if (count($args) < 2) { 52 | throw new MdException("Insufficient arguments provided to command.\n" . $this->usage(), 1); 53 | } 54 | 55 | switch ($args[1]) { 56 | 57 | case 'create': { 58 | $this->createCommand($args); 59 | break; 60 | } 61 | 62 | case 'destroy': { 63 | $this->destroyCommand($args); 64 | break; 65 | } 66 | 67 | case 'connect': { 68 | $this->connectCommand($args); 69 | break; 70 | } 71 | 72 | case 'pull-code': { 73 | $this->pullCodeCommand($args); 74 | break; 75 | } 76 | 77 | case 'push-code': { 78 | $this->pushCodeCommand($args); 79 | break; 80 | } 81 | 82 | case 'disconnect': { 83 | $this->disconnectCommand($args); 84 | break; 85 | } 86 | 87 | case 'help': { 88 | $this->helpCommand($args); 89 | break; 90 | } 91 | 92 | default: { 93 | throw new MdException("Unknown command '$args[1]'\n" . $this->usage(), 1); 94 | } 95 | } 96 | } catch (MdException $e) { 97 | echo $e->getMessage(); 98 | return $e->getCode(); 99 | } 100 | 101 | return 0; 102 | } 103 | 104 | /** 105 | * Return the usage message for display on the screen. 106 | * @return string The usage message. 107 | */ 108 | private function usage() 109 | { 110 | $msg = "Usage: magento-dev where 'command' is one of:\n\n"; 111 | $msg .= " create ... Create new development environment.\n"; 112 | $msg .= " destroy [--force] Destroy all files created for the environment.\n"; 113 | $msg .= " connect ... Connect to a production host.\n"; 114 | $msg .= " pull-code Pull a copy of the code from the current provider.\n"; 115 | $msg .= " push-code Push the code and run deployment actions.\n"; 116 | $msg .= " disconnect Forget about the last environment connected to.\n"; 117 | $msg .= " help Display help information.\n"; 118 | $msg .= "\n"; 119 | $msg .= "Environments: " . implode(", ", array_keys($this->environments)) . "\n"; 120 | $msg .= "Providers: " . implode(", ", array_keys($this->providers)) . "\n"; 121 | return $msg; 122 | } 123 | 124 | /** 125 | * Parse and invoke a 'create' command. 126 | * @param string[] $args Optional additional command line arguments to parse. 127 | * @throws MdException Thrown on error. 128 | */ 129 | private function createCommand($args) 130 | { 131 | $config = Config::load(); 132 | if (isset($config['environment'])) { 133 | throw new MdException("You have already created an '${config['environment']}' environment.\n", 1); 134 | } 135 | if (count($args) < 3) { 136 | throw new MdException("Missing environment name for 'create' command.\n" . $this->usage(), 1); 137 | } 138 | $envName = $args[2]; 139 | $environment = $this->findEnvironment($envName); 140 | if ($environment == null) { 141 | $msg = "Unknown environment name '$envName'.\n"; 142 | $msg .= "Supported environment names are:\n\n"; 143 | foreach ($this->environments as $envName => $env) { 144 | $msg .= " $envName\n"; 145 | } 146 | $msg .= "\n"; 147 | throw new MdException($msg, 1); 148 | } 149 | $config['environment'] = $envName; 150 | $environment->create($config, array_splice($args, 3)); 151 | Config::save($config); 152 | echo "Environment '$envName' has been created.\n"; 153 | } 154 | 155 | /** 156 | * Parse and invoke a 'destroy' command. 157 | * @param string[] $args Optional additional command line arguments to parse. 158 | * @throws MdException Thrown on error. 159 | */ 160 | private function destroyCommand($args) 161 | { 162 | // TODO: Should do proper command line argument parsing! 163 | $force = false; 164 | if (count($args) == 3 && $args[2] == '--force') { 165 | $force = true; 166 | } elseif (count($args) != 2) { 167 | throw new MdException($this->usage(), 1); 168 | } 169 | 170 | $config = Config::load(); 171 | if (!isset($config['environment'])) { 172 | throw new MdException("No environment currently exists, so cannot 'destroy' environment.\n", $force ? 0 : 1); 173 | } 174 | 175 | $envName = $config['environment']; 176 | $environment = $this->findEnvironment($envName); 177 | if ($environment == null) { 178 | $filename = Config::CONFIG_FILE_NAME; 179 | throw new MdException("The '$filename' file contains a reference to unknown environment '$envName'.\n", $force ? 0 : 1); 180 | } 181 | 182 | $environment->destroy($config, $force); 183 | unset($config['environment']); 184 | Config::save($config); 185 | echo "Environment '$envName' has been destroyed.\n"; 186 | } 187 | 188 | /** 189 | * Parse and invoke a 'connect' command. 190 | * @param string[] $args Optional additional command line arguments to parse. 191 | * @throws MdException Thrown on error. 192 | */ 193 | private function connectCommand($args) 194 | { 195 | $config = Config::load(); 196 | if (isset($config['provider'])) { 197 | throw new MdException("You have already connected to '${config['provider']}'.\n", 1); 198 | } 199 | if (count($args) < 3) { 200 | throw new MdException("Missing provider name for 'connect' command.\n" . $this->usage(), 1); 201 | } 202 | $providerName = $args[2]; 203 | $provider = $this->findProvider($providerName); 204 | if ($provider == null) { 205 | $msg = "Unknown provider name '$providerName'.\n"; 206 | $msg .= "Supported provider names are:\n\n"; 207 | foreach ($this->providers as $providerName => $provider) { 208 | $msg .= " $providerName\n"; 209 | } 210 | $msg .= "\n"; 211 | throw new MdException($msg, 1); 212 | } 213 | $oldConfig = $config; 214 | $config['provider'] = $providerName; 215 | $provider->connect($config, array_splice($args, 3)); 216 | Config::save($config); 217 | try { 218 | $provider->checkConnection($config); 219 | } catch (MdException $e) { 220 | Config::save($oldConfig); 221 | throw $e; 222 | } 223 | echo "Provider '$providerName' has been connected to.\n"; 224 | } 225 | 226 | /** 227 | * Parse and invoke a 'pull-code' command. 228 | * @param string[] $args Optional additional command line arguments to parse. 229 | * @throws MdException Thrown on error. 230 | */ 231 | private function pullCodeCommand($args) 232 | { 233 | if (count($args) != 2) { 234 | throw new MdException("Unexpected additional arguments for 'pull-code' command.\n" . $this->usage(), 1); 235 | } 236 | $config = Config::load(); 237 | if (!isset($config['provider'])) { 238 | throw new MdException("Please use 'connect' to specify the provider name first.\n", 1); 239 | } 240 | $providerName = $config['provider']; 241 | $provider = $this->findProvider($providerName); 242 | if ($provider == null) { 243 | $msg = "Unknown provider name '$providerName'.\n"; 244 | $msg .= "Supported provider names are:\n\n"; 245 | foreach ($this->providers as $providerName => $provider) { 246 | $msg .= " $providerName\n"; 247 | } 248 | $msg .= "\n"; 249 | throw new MdException($msg, 1); 250 | } 251 | 252 | $environment = null; 253 | if (isset($config['environment'])) { 254 | $environment = $this->findEnvironment($config['environment']); 255 | } 256 | 257 | $provider->pullCode($config, $environment); 258 | Config::save($config); 259 | } 260 | 261 | /** 262 | * Parse and invoke a 'disconnect' command. 263 | * @param string[] $args Optional additional command line arguments to parse. 264 | * @throws MdException Thrown on error. 265 | */ 266 | private function disconnectCommand($args) 267 | { 268 | if (count($args) != 2) { 269 | throw new MdException("Unexpected additional arguments for 'disconnect' command.\n" . $this->usage(), 1); 270 | } 271 | 272 | $config = Config::load(); 273 | if (!isset($config['provider'])) { 274 | echo "Not connected to a provider.\n"; 275 | return; 276 | } 277 | 278 | $providerName = $config['provider']; 279 | $provider = $this->findProvider($providerName); 280 | if ($provider == null) { 281 | $filename = Config::CONFIG_FILE_NAME; 282 | throw new MdException("The '$filename' file contains a reference to unknown provider '$providerName'.\n", 1); 283 | } 284 | 285 | $provider->disconnect($config); 286 | unset($config['provider']); 287 | Config::save($config); 288 | echo "Disconnected from provider '$providerName'.\n"; 289 | } 290 | 291 | /** 292 | * Parse and invoke a 'push-code' command. 293 | * @param string[] $args Optional additional command line arguments to parse. 294 | * @throws MdException Thrown on error. 295 | */ 296 | private function pushCodeCommand($args) 297 | { 298 | if (count($args) != 2) { 299 | throw new MdException("Unexpected additional arguments for 'push-code' command.\n" . $this->usage(), 1); 300 | } 301 | $config = Config::load(); 302 | if (!isset($config['provider'])) { 303 | throw new MdException("Please use 'connect' to specify the provider name first.\n", 1); 304 | } 305 | $providerName = $config['provider']; 306 | $provider = $this->findProvider($providerName); 307 | if ($provider == null) { 308 | $msg = "Unknown provider name '$providerName'.\n"; 309 | $msg .= "Supported provider names are:\n\n"; 310 | foreach ($this->providers as $providerName => $provider) { 311 | $msg .= " $providerName\n"; 312 | } 313 | $msg .= "\n"; 314 | throw new MdException($msg, 1); 315 | } 316 | 317 | $environment = null; 318 | if (isset($config['environment'])) { 319 | $environment = $this->findEnvironment($config['environment']); 320 | } 321 | 322 | $provider->pushCode($config, $environment); 323 | Config::save($config); 324 | } 325 | 326 | /** 327 | * Parse and invoke a 'help' command. 328 | * @param string[] $args Optional additional command line arguments to parse. 329 | * @throws MdException Thrown on error. 330 | */ 331 | private function helpCommand(/** @noinspection PhpUnusedParameterInspection */ $args) 332 | { 333 | echo $this->usage(); 334 | } 335 | 336 | /** 337 | * The the environment handle for the specified environment name. 338 | * @param string $envName The environment handler name. 339 | * @return EnvironmentInterface|null The environment, or null if not found. 340 | */ 341 | private function findEnvironment($envName) 342 | { 343 | if (!isset($this->environments[$envName])) { 344 | return null; 345 | } 346 | return $this->environments[$envName]; 347 | } 348 | 349 | /** 350 | * The the provider handle for the specified provider name. 351 | * @param string $providerName The provider handler name. 352 | * @return ProviderInterface|null The provider, or null if not found. 353 | */ 354 | private function findProvider($providerName) 355 | { 356 | if (!isset($this->providers[$providerName])) { 357 | return null; 358 | } 359 | return $this->providers[$providerName]; 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /src/AlanKent/MagentoDev/Config.php: -------------------------------------------------------------------------------- 1 | $run\n"; 142 | system($run); 143 | } 144 | 145 | /** 146 | * @inheritdoc 147 | */ 148 | public function syncToEnvironment($config) 149 | { 150 | $run = "sh -c 'vagrant rsync'"; 151 | echo "> $run\n"; 152 | // If rsync-auto is running in background, give it a chance to complete (hacky!) 153 | sleep(3); 154 | system($run); 155 | } 156 | 157 | /** 158 | * @inheritdoc 159 | */ 160 | public function excludeFiles() 161 | { 162 | return ["Vagrantfile", "scripts"]; 163 | } 164 | } -------------------------------------------------------------------------------- /src/AlanKent/MagentoDev/Environments/VagrantRsync/scripts/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby ai sw=2 : 3 | 4 | # Version 2. 5 | Vagrant.configure(2) do |config| 6 | 7 | # Base box. 8 | config.vm.box = "paliarush/magento2.ubuntu" 9 | 10 | # Create a private network allowing host-only access. 11 | config.vm.network "private_network", ip: "192.168.33.33" 12 | 13 | # Extra sync folder. 14 | config.vm.synced_folder "scripts", "/scripts" 15 | 16 | # Auth.json + composer cache, don't sync changes back to avoid collisions. 17 | config.vm.synced_folder ENV['HOME'] + '/.composer/', 18 | '/home/vagrant/.composer/', 19 | type: 'rsync', 20 | rsync__auto: false 21 | 22 | # Source code 23 | config.vm.synced_folder '.', '/vagrant', 24 | type: 'rsync', 25 | rsync__exclude: [ 26 | 'Vagrantfile', # Vagrant control file 27 | '.vagrant/', # Vagrant work area 28 | '.git/', # Git repository 29 | '.gitignore', # Git support file 30 | '.gitattributes', # Git support file 31 | 'var/', # Temporary files used by Magento 32 | 'pub/media/', # Don't wipe uploaded media files pub/media 33 | 'pub/static/', # Don't wipe generated assets under pub/static 34 | 'scripts/', # Support shell scripts 35 | 'vendor/', # Compose download area 36 | '.idea/', # PHP Storm project files 37 | 'app/etc/env.php', # Don't want to overwrite DB settings 38 | '.magento' # Used by Magento Cloud 39 | ], 40 | rsync__auto: true 41 | 42 | # Virtualbox specific configuration. 43 | config.vm.provider "virtualbox" do |vb| 44 | # Customize the amount of memory on the VM: 45 | vb.memory = "2048" 46 | end 47 | 48 | # Final provisioning. (Should move into a base box.) 49 | config.vm.provision "shell", inline: <<-SHELL 50 | echo ==== Installing NodeJS ==== 51 | sh -x /scripts/install-nodejs 52 | echo ==== Installing Gulp ==== 53 | sh -x /scripts/install-gulp 54 | echo ==== Installing Magento web server configuration ==== 55 | sudo -i -u vagrant sh -x /scripts/install-magento 56 | SHELL 57 | end 58 | -------------------------------------------------------------------------------- /src/AlanKent/MagentoDev/Environments/VagrantRsync/scripts/install-gulp: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir /gulp 4 | cd /gulp 5 | 6 | sudo apt-get update 7 | sudo apt-get install -y unzip 8 | 9 | curl -L -o /tmp/frontool.zip https://github.com/SnowdogApps/magento2-frontools/archive/master.zip 10 | unzip /tmp/frontool.zip 11 | rm /tmp/frontool.zip 12 | mv magento2-frontools-master/* . 13 | 14 | cp /scripts/install-gulp-run-gulp.sh /usr/local/bin/run-gulp.sh 15 | chmod +rx /usr/local/bin/run-gulp.sh 16 | 17 | npm install -g gulp 18 | npm install --save-dev gulp 19 | npm install jshint gulp-less gulp-concat gulp-uglify gulp-rename gulp-livereload gulp-sourcemaps gulp-util notify-send --save-dev 20 | npm install 21 | 22 | cat < configs/browser-sync.json 23 | { 24 | "proxy": "http://localhost" 25 | } 26 | EOF 27 | 28 | cat < configs/themes.json 29 | { 30 | "backend": { 31 | "src": "/magento2/app/design/adminhtml/Magento/backend", 32 | "dest": "/magento2/pub/static/adminhtml/Magento/backend", 33 | "locale": ["en_US"], 34 | "lang": "less", 35 | "default": true, 36 | "area": "adminhtml", 37 | "vendor": "Magento", 38 | "name": "backend", 39 | "files": [ 40 | "css/styles-old", 41 | "css/styles" 42 | ] 43 | }, 44 | "blank": { 45 | "src": "/magento2/app/design/frontend/Magento/blank", 46 | "dest": "/magento2/pub/static/frontend/Magento/blank", 47 | "locale": ["en_US"], 48 | "lang": "less", 49 | "default": true, 50 | "area": "frontend", 51 | "vendor": "Magento", 52 | "name": "blank", 53 | "files": [ 54 | "css/styles-m", 55 | "css/styles-l", 56 | "css/print", 57 | "css/email", 58 | "css/email-inline" 59 | ] 60 | }, 61 | "luma": { 62 | "src": "/magento2/app/design/frontend/Magento/luma", 63 | "dest": "/magento2/pub/static/frontend/Magento/luma", 64 | "locale": ["en_US"], 65 | "lang": "less", 66 | "default": true, 67 | "area": "frontend", 68 | "vendor": "Magento", 69 | "name": "luma", 70 | "files": [ 71 | "css/styles-m", 72 | "css/styles-l", 73 | "css/print", 74 | "css/email", 75 | "css/email-inline" 76 | ] 77 | } 78 | } 79 | EOF 80 | 81 | # Use 'magento', not '../bin/magento' as we installed this somewhere else. 82 | sed -i -e 's/..\/bin\/magento/magento/' /gulp/tasks/deploy.js 83 | 84 | -------------------------------------------------------------------------------- /src/AlanKent/MagentoDev/Environments/VagrantRsync/scripts/install-gulp-run-gulp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -x 4 | 5 | # Clear out some caches 6 | rm -rf /magento/var/{cache,page_cache,session,view_preprocessed} 7 | 8 | # Turn off the config cache - it is caching the host/port causing problems 9 | # if you use browsersync and the default port. 10 | magento cache:disable config 11 | 12 | # Redeploy to pick up new files, compile styles, watch for file changes. 13 | cd /gulp 14 | gulp deploy 15 | gulp styles 16 | gulp docs 17 | gulp dev 18 | -------------------------------------------------------------------------------- /src/AlanKent/MagentoDev/Environments/VagrantRsync/scripts/install-magento: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Remove the default sites enabled. 4 | sudo rm /etc/apache2/sites-enabled/* 5 | 6 | # Add the Apache virtual host file 7 | sudo cp /scripts/install-magento-magento.conf /etc/apache2/sites-enabled/magento.conf 8 | 9 | # Overwrite apache config file to use /vagrant instead of /var/www/html 10 | sudo cp /scripts/install-magento-apache2.conf /etc/apache2/apache2.conf 11 | 12 | # Register cron entries. 13 | ( 14 | echo "*/1 * * * * /usr/local/bin/php -c /usr/local/etc/php/php.ini /vagrant/bin/magento cron:run" 15 | echo "*/1 * * * * /usr/local/bin/php -c /usr/local/etc/php/php.ini /vagrant/update/cron.php" 16 | echo "*/1 * * * * /usr/local/bin/php -c /usr/local/etc/php/php.ini /vagrant/bin/magento setup:cron:run" 17 | ) | crontab -u vagrant - 18 | 19 | # Restart the web server 20 | sudo apache2ctl restart 21 | 22 | # Load all the other composer packages. 23 | cd /vagrant 24 | id 25 | composer install 26 | chmod +x bin/magento 27 | 28 | # Create the database 29 | echo 'CREATE DATABASE IF NOT EXISTS `magento`;' | mysql -uroot 30 | 31 | # Create tables and env.php with local settings. 32 | bin/magento setup:install --backend-frontname=admin \ 33 | --cleanup-database --db-host=127.0.0.1 \ 34 | --db-name=magento --db-user=root --db-password= \ 35 | --admin-firstname=Magento --admin-lastname=User \ 36 | --admin-email=user@example.com \ 37 | --admin-user=admin --admin-password=admin123 --language=en_US \ 38 | --currency=USD --timezone=America/Chicago --use-rewrites=1 39 | 40 | # Flip to developer mode (above sets it to 'default'). 41 | sed -i -e "s/'MAGE_MODE' => 'default'/'MAGE_MODE' => 'developer'/" app/etc/env.php 42 | 43 | # Clear all the caches for good luck 44 | rm -rf var/* 45 | 46 | # Update bashrc file 47 | echo 'export PATH=${PATH}:/vagrant/bin' >> ~vagrant/.bashrc 48 | 49 | -------------------------------------------------------------------------------- /src/AlanKent/MagentoDev/Environments/VagrantRsync/scripts/install-magento-apache2.conf: -------------------------------------------------------------------------------- 1 | # see http://sources.debian.net/src/apache2/2.4.10-1/debian/config-dir/apache2.conf 2 | # (Modified to use /vagrant instead of /var/www/html) 3 | 4 | Mutex file:/var/lock/apache2 default 5 | PidFile /var/run/apache2/apache2.pid 6 | Timeout 300 7 | KeepAlive On 8 | MaxKeepAliveRequests 100 9 | KeepAliveTimeout 5 10 | User vagrant 11 | Group vagrant 12 | HostnameLookups Off 13 | ErrorLog /proc/self/fd/2 14 | LogLevel warn 15 | 16 | IncludeOptional mods-enabled/*.load 17 | IncludeOptional mods-enabled/*.conf 18 | 19 | # ports.conf 20 | Listen 80 21 | 22 | Listen 443 23 | 24 | 25 | Listen 443 26 | 27 | 28 | 29 | Options FollowSymLinks 30 | AllowOverride None 31 | Require all denied 32 | 33 | 34 | 35 | AllowOverride All 36 | Require all granted 37 | 38 | 39 | DocumentRoot /vagrant 40 | 41 | AccessFileName .htaccess 42 | 43 | Require all denied 44 | 45 | 46 | LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined 47 | LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined 48 | LogFormat "%h %l %u %t \"%r\" %>s %O" common 49 | LogFormat "%{Referer}i -> %U" referer 50 | LogFormat "%{User-agent}i" agent 51 | 52 | CustomLog /proc/self/fd/1 combined 53 | 54 | 55 | SetHandler application/x-httpd-php 56 | 57 | 58 | # Multiple DirectoryIndex directives within the same context will add 59 | # to the list of resources to look for rather than replace 60 | # https://httpd.apache.org/docs/current/mod/mod_dir.html#directoryindex 61 | DirectoryIndex disabled 62 | DirectoryIndex index.php index.html 63 | 64 | IncludeOptional conf-enabled/*.conf 65 | IncludeOptional sites-enabled/*.conf 66 | -------------------------------------------------------------------------------- /src/AlanKent/MagentoDev/Environments/VagrantRsync/scripts/install-magento-magento.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | DocumentRoot /vagrant 4 | 5 | 6 | Options Indexes FollowSymLinks 7 | AllowOverride All 8 | Order allow,deny 9 | allow from all 10 | 11 | 12 | ErrorLog ${APACHE_LOG_DIR}/error.log 13 | LogLevel warn 14 | CustomLog ${APACHE_LOG_DIR}/access.log combined 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/AlanKent/MagentoDev/Environments/VagrantRsync/scripts/install-nodejs: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Install NodeJS (after curl is installed above). 4 | curl -sL https://deb.nodesource.com/setup_4.x | bash - 5 | apt-get update 6 | apt-get install -y nodejs 7 | 8 | -------------------------------------------------------------------------------- /src/AlanKent/MagentoDev/MdException.php: -------------------------------------------------------------------------------- 1 | 0) { 49 | if (count($args) < 2) { 50 | throw new MdException($this->goDaddyConnectUsage(), 1); 51 | } 52 | switch ($args[0]) { 53 | case '--ssh-user': { 54 | $sshUser = $args[1]; 55 | break; 56 | } 57 | case '--ssh-host': { 58 | $sshHost = $args[1]; 59 | break; 60 | } 61 | case '--ssh-port': { 62 | $sshPort = $args[1]; 63 | break; 64 | } 65 | case '--ssh-identity': { 66 | $sshIdentity = $args[1]; 67 | break; 68 | } 69 | default: { 70 | throw new MdException("Unknown option '$args[0]'.\n" . $this->goDaddyConnectUsage(), 1); 71 | } 72 | } 73 | $args = array_splice($args, 2); 74 | } 75 | 76 | if ($sshHost == "" || $sshUser == "") { 77 | throw new MdException($this->goDaddyConnectUsage(), 1); 78 | } 79 | 80 | // Save away configuration settings (saved to disk by calling code) 81 | $sshConfig = []; 82 | if ($sshHost != '') { 83 | $sshConfig['host'] = $sshHost; 84 | } 85 | if ($sshPort != '') { 86 | $sshConfig['port'] = $sshPort; 87 | } 88 | if ($sshUser != '') { 89 | $sshConfig['user'] = $sshUser; 90 | } 91 | if ($sshIdentity != '') { 92 | $sshConfig['identity'] = $sshIdentity; 93 | } 94 | $config['godaddy'] = ['ssh'=>$sshConfig]; 95 | 96 | if ($sshIdentity == "") { 97 | $sshIdentity = "~/.ssh/" . self::IDENTITY_FILE; 98 | } 99 | if (!file_exists($this->expandPath($sshIdentity))) { 100 | 101 | // TODO: Could consider performing these files automatically. 102 | $msg = "The SSH identify file '$sshIdentity' does not exist.\n"; 103 | $msg .= "Use the --ssh-identity option to the connect command to specify a different file,\n"; 104 | $msg .= "or create a new file using a command such as (inserting your real email address):\n\n"; 105 | $msg .= " ssh-keygen -t rsa -C your.email@example.com -N \"\" -f $sshIdentity\n\n"; 106 | throw new MdException($msg, 1); 107 | } 108 | 109 | echo "Copying the SSH public key to your production server.\n\n"; 110 | echo "** If prompted, please enter you server password **\n\n"; 111 | $cmd = "sh -x -c 'cat $sshIdentity.pub | ssh -oStrictHostKeyChecking=no -i $sshIdentity $sshUser@$sshHost \"mkdir -p ~/.ssh; cat >> ~/.ssh/authorized_keys\""; 112 | echo "> $cmd\n"; 113 | system($cmd); 114 | 115 | echo "Testing SSH certification (you should not be asked for a password).\n"; 116 | $cmd = "sh -c 'ssh -i $sshIdentity $sshUser@$sshHost echo Working'"; 117 | echo "> $cmd\n"; 118 | system($cmd); 119 | } 120 | 121 | /** 122 | * Return usage message with all the command line options. 123 | * @return string 124 | */ 125 | private function goDaddyConnectUsage() 126 | { 127 | $idFile = self::IDENTITY_FILE; 128 | $msg = "GoDaddy Cloud provider connection command line options:\n\n"; 129 | $msg .= " --ssh-user - username to connect with on production host\n"; 130 | $msg .= " --ssh-host - hostname or IP address of production host\n"; 131 | $msg .= " --ssh-port - SSH port number to use if not 22 (optional)\n"; 132 | $msg .= " --ssh-identity - SSH identity file if not ~/.ssh/$idFile (optional)\n"; 133 | $msg .= "\n"; 134 | $msg .= "You must set at least --ssh-user and --ssh-host.\n"; 135 | return $msg; 136 | } 137 | 138 | /** 139 | * Expand "~" at front of path for PHP file operations, allowing "~" to be kept when 140 | * passing to "ssh" etc commands to make the path more portable. E.g. cygwin does not 141 | * like C:\Users\foo - it wants /cygdrive/c/home/foo. But other shells use /c/home/foo. 142 | * So using "~" is good when possible. 143 | * @param $path 144 | * @return string The expanded path, suitable for passing to PHP functions. 145 | * @throws MdException 146 | */ 147 | private function expandPath($path) 148 | { 149 | if ($path[0] == '~') { 150 | $home = $this->getHomeDir(); 151 | if ($home == "") { 152 | throw new MdException("Unable to determine user's home directory, aborting.\n", 1); 153 | } 154 | return $home . "/" . substr($path, 1); 155 | } else { 156 | return $path; 157 | } 158 | } 159 | 160 | /** 161 | * Return the user's home directory. (Borrowed from Drush.) 162 | */ 163 | private function getHomeDir() 164 | { 165 | // Cannot use $_SERVER superglobal since that's empty during UnitUnishTestCase 166 | // getenv('HOME') isn't set on Windows and generates a Notice. 167 | $home = getenv('HOME'); 168 | if (!empty($home)) { 169 | // home should never end with a trailing slash. 170 | $home = rtrim($home, '/'); 171 | } 172 | elseif (!empty($_SERVER['HOMEDRIVE']) && !empty($_SERVER['HOMEPATH'])) { 173 | // home on windows 174 | $home = $_SERVER['HOMEDRIVE'] . $_SERVER['HOMEPATH']; 175 | // If HOMEPATH is a root directory the path can end with a slash. Make sure 176 | // that doesn't happen. 177 | $home = rtrim($home, '\\/'); 178 | } 179 | return empty($home) ? NULL : $home; 180 | } 181 | 182 | /** 183 | * @inheritdoc 184 | */ 185 | public function pullCode($config, $environment) 186 | { 187 | $this->checkConnection($config); 188 | 189 | echo "==== Make sure 'magento' CLI is in PATH.\n"; 190 | $htdocs = self::HTDOCS_PATH; 191 | $output = $this->runOnProd($config, "cat ~/.bashrc"); 192 | if (strpos(implode($output), "magento") === false) { 193 | $this->runOnProd($config, "echo export PATH=\\\${PATH}:$htdocs/bin >> ~/.bashrc"); 194 | $this->runOnProd($config, "echo umask 002 >> ~/.bashrc"); 195 | } 196 | 197 | echo "==== Fetching code from production.\n"; 198 | // Note: The trailing "/" on "./" is important for correct operation. 199 | $this->copyRemoteToLocal($config, "/opt/bitnami/apps/magento/htdocs/", "./", self::EXCLUDE_LIST); 200 | foreach (self::EXCLUDE_LIST as $dir) { 201 | if (!file_exists($dir)) { 202 | mkdir($dir, 0777, true); 203 | } 204 | } 205 | 206 | echo "==== Downloading any next patches or extensions.\n"; 207 | // Tell composer not to override local changes. 208 | // Eventually this step will not be required. 209 | echo "Adding Magento deployment strategy to composer.json (ignore the 'none' warning)\n"; 210 | $oldComposerJson = file_get_contents('composer.json'); 211 | // This could parse the file as JSON, but this works. 212 | $newComposerJson = str_replace('extra": {', 'extra": { "magento-deploystrategy": "none",', $oldComposerJson); 213 | file_put_contents('composer.json', $newComposerJson); 214 | echo "> sh -c 'composer update'\n"; 215 | system("sh -c 'composer update'"); 216 | echo "Restoring composer.json file\n"; 217 | file_put_contents('composer.json', $oldComposerJson); 218 | echo "> sh -c 'cd update; composer update'\n"; 219 | system("sh -c 'cd update; composer update'"); 220 | 221 | echo "==== Refreshing development environment with changes.\n\n"; 222 | if ($environment == null) { 223 | echo "Warning - no environment has been configured, so skipping this step.\n"; 224 | } else { 225 | $environment->syncToEnvironment($config); 226 | $environment->runCommand($config, "cd /vagrant; composer install"); 227 | $environment->runCommand($config, "cd /vagrant; bin/magento cache:clean"); 228 | $environment->runCommand($config, "cd /vagrant; bin/magento setup:upgrade"); 229 | $environment->runCommand($config, "cd /vagrant; bin/magento indexer:reindex"); 230 | $environment->runCommand($config, "cd /vagrant; bin/magento maintenance:disable"); 231 | 232 | // TODO: Should be able to remove this one day. 233 | // Above commands result in 'localhost' being in cached files - clear 234 | // the cache to lose that setting. 235 | $environment->runCommand($config, "cd /vagrant; rm -rf var/cache"); 236 | } 237 | } 238 | 239 | /** 240 | * @inheritdoc 241 | */ 242 | public function pushCode($config, $environment) 243 | { 244 | $this->checkConnection($config); 245 | 246 | echo "==== Put production store into mainenance mode.\n"; 247 | $htdocs = self::HTDOCS_PATH; 248 | $this->runOnProd($config, "cd $htdocs; sudo -u daemon bin/magento maintenance:enable"); 249 | 250 | echo "==== Merge development changes on production.\n"; 251 | $this->copyLocalToRemote($config, "./", $htdocs, array_merge(self::EXCLUDE_LIST, $environment->excludeFiles())); 252 | $this->runOnProd($config, "cd $htdocs; sudo chgrp -R daemon .; sudo chmod -R g+w ."); 253 | 254 | echo "==== Refresh any composer installed libraries.\n"; 255 | // This turns off the Magento installer installing 'base' package changes 256 | // over the top of any locally committed changes. Eventually this will 257 | // no longer be required. For now, do not do this in production. 258 | $this->runOnProd($config, "cd $htdocs; mv composer.json composer.json.original"); 259 | $this->runOnProd($config, 'cd '.$htdocs.'; sed < composer.json.original > composer.json -e \"/extra.:/ a \\\\\\\\\"magento-deploystrategy\\\\\\\\\": \\\\\\\\\"none\\\\\\\\\",\"'); 260 | $this->runOnProd($config, "cd $htdocs; composer install"); 261 | $this->runOnProd($config, "cd $htdocs; cp composer.json.original composer.json"); 262 | $this->runOnProd($config, "cd $htdocs; sudo chown -R daemon:daemon var pub/static"); 263 | 264 | echo "==== Update the database schema.\n"; 265 | $this->runOnProd($config, "cd $htdocs; sudo rm -rf var/*"); // TODO: setup:upgrade failed due to 'mage-tags' had wrong file permissions. 266 | $this->runOnProd($config, "cd $htdocs; sudo -u daemon bin/magento setup:upgrade"); 267 | 268 | echo "==== Switching production mode, triggering compile and content deployment.\n"; 269 | $this->runOnProd($config, "cd $htdocs; sudo -u daemon bin/magento deploy:mode:set production; exit 0"); // TODO: deploy:mode:set on success exits with status code 1? 270 | $this->runOnProd($config, "cd $htdocs; sudo rm -rf var/*"); // TODO: 'mage-tags' got created with wrong file permissions - wiping. 271 | $this->runOnProd($config, "cd $htdocs; sudo -u daemon bin/magento maintenance:disable"); 272 | $this->runOnProd($config, "cd $htdocs; sudo rm -rf var/*"); // TODO: 'mage-tags' got created with wrong file permissions - wiping. 273 | $this->runOnProd($config, "cd $htdocs; sudo chmod -R g+ws var pub/static"); 274 | 275 | echo "==== Turning off bitnami banner\n"; 276 | $this->runOnProd($config, "sudo /opt/bitnami/apps/magento/bnconfig --disable_banner 1"); 277 | $this->runOnProd($config, "sudo /opt/bitnami/ctlscript.sh restart apache"); 278 | 279 | echo "==== Ready for use.\n"; 280 | } 281 | 282 | /** 283 | * Disconnect from hosting provider. 284 | * @throws MdException Thrown on error. 285 | */ 286 | public function disconnect(&$config) 287 | { 288 | if (!isset($config['godaddy'])) { 289 | echo "Not currently connected.\n"; 290 | return; 291 | } 292 | unset($config['godaddy']); 293 | } 294 | 295 | /** 296 | * Run a command on the remote host using SSH, capturing output. 297 | * @param array $config Configuration details to connect to server. 298 | * @param string $cmd Command to run on remote server. Be careful with quotes and escaping rules! 299 | * @return string[] The output of the command that was run 300 | * @throws MdException Thrown on error. 301 | */ 302 | private function runOnProd(&$config, $cmd) 303 | { 304 | $host = $this->getSshHost($config); 305 | $user = $this->getSshUser($config); 306 | $port = $this->getSshPort($config); 307 | $identity = $this->getSshIdentity($config); 308 | 309 | $opts = ''; 310 | 311 | if ($port != '22') { 312 | $opts .= " -p $port"; 313 | } 314 | 315 | if ($identity != '') { 316 | $opts .= " -i $identity"; 317 | } 318 | 319 | $cmd = "sh -c 'ssh$opts $user@$host \"$cmd\"'"; 320 | echo "> $cmd\n"; 321 | exec($cmd, $output, $exitStatus); 322 | if ($exitStatus != 0) { 323 | throw new MdException("Failed to execute SSH command on production server (exit status $exitStatus).", 1); 324 | } 325 | return $output; 326 | } 327 | 328 | /** 329 | * Copy files from production server to the local environment. 330 | * @param array $config Configuration settings. 331 | * @param string $fromDir The source directory (typically "htdocs") on the production server. 332 | * @param string $toDir The destination directory (typically "."). 333 | * @param string[] $excludes A list of files and directories to not copy. 334 | * @throws MdException Thrown on error for user to see. 335 | */ 336 | private function copyRemoteToLocal($config, $fromDir, $toDir, $excludes) 337 | { 338 | $host = $this->getSshHost($config); 339 | $user = $this->getSshUser($config); 340 | $port = $this->getSshPort($config); 341 | $identity = $this->getSshIdentity($config); 342 | 343 | $opts = ''; 344 | 345 | if ($port != '22') { 346 | $opts .= " -p $port"; 347 | } 348 | 349 | if ($identity != '') { 350 | $opts .= " -i $identity"; 351 | } 352 | 353 | $rsyncOpts = ""; 354 | if (!empty($excludes)) { 355 | $rsyncOpts .= " --exclude " . implode(" --exclude ", $excludes); 356 | } 357 | 358 | $cmd = "sh -c 'rsync -r -e \"ssh$opts\"$rsyncOpts $user@$host:$fromDir $toDir'"; 359 | echo "> $cmd\n"; 360 | system($cmd, $exitStatus); 361 | if ($exitStatus != 0) { 362 | throw new MdException("Failed to execute scp command on production server.", 1); 363 | } 364 | } 365 | 366 | /** 367 | * Copy files from local environment to the production environment. 368 | * @param array $config Configuration settings. 369 | * @param string $fromDir The source directory (typically "."). 370 | * @param string $toDir The destination directory (typically "htdocs") on the production server. 371 | * @param string[] $excludes A list of files and directories to not copy. 372 | * @throws MdException Thrown on error for user to see. 373 | */ 374 | private function copyLocalToRemote($config, $fromDir, $toDir, $excludes) 375 | { 376 | $host = $this->getSshHost($config); 377 | $user = $this->getSshUser($config); 378 | $port = $this->getSshPort($config); 379 | $identity = $this->getSshIdentity($config); 380 | 381 | $opts = ''; 382 | 383 | if ($port != '22') { 384 | $opts .= " -p $port"; 385 | } 386 | 387 | if ($identity != '') { 388 | $opts .= " -i $identity"; 389 | } 390 | 391 | $rsyncOpts = ""; 392 | if (!empty($excludes)) { 393 | $rsyncOpts .= " --exclude " . implode(" --exclude ", $excludes); 394 | } 395 | 396 | $cmd = "sh -c 'rsync -r -e \"ssh$opts\"$rsyncOpts $fromDir $user@$host:$toDir'"; 397 | echo "> $cmd\n"; 398 | system($cmd, $exitStatus); 399 | if ($exitStatus != 0) { 400 | throw new MdException("Failed to execute scp command on production server.", 1); 401 | } 402 | } 403 | 404 | /** 405 | * Get the specified path out of the configuration file. 406 | * @param array $config Configuration file loaded from disk. 407 | * @param string[] $path Path to requested configuration settings (e.g. ['ssh', 'port']). 408 | * @return string Configuration setting for the specified path, or empty string if not found. 409 | * @throws MdException Thrown on error. 410 | */ 411 | private function getGoDaddyConfig($config, $path) 412 | { 413 | if (!isset($config['godaddy'])) { 414 | throw new MdException("Please use the 'connect' command to provide connection details.\n", 1); 415 | } 416 | $c = $config['godaddy']; 417 | foreach ($path as $name) { 418 | if (!isset($c[$name])) { 419 | return ''; 420 | } 421 | $c = $c[$name]; 422 | } 423 | return $c; 424 | } 425 | 426 | /** 427 | * @param $config 428 | * @return string 429 | * @throws MdException 430 | */ 431 | private function getSshHost($config) 432 | { 433 | $host = $this->getGoDaddyConfig($config, ['ssh', 'host']); 434 | if ($host == '') { 435 | throw new MdException("godaddy.ssh.host is not set\n", 1); 436 | } 437 | return $host; 438 | } 439 | 440 | /** 441 | * @param $config Configuration file loaded from disk. 442 | * @return string SSH user name to use to connect. 443 | * @throws MdException Thrown on error. 444 | */ 445 | private function getSshUser($config) 446 | { 447 | $user = $this->getGoDaddyConfig($config, ['ssh', 'user']); 448 | if ($user == '') { 449 | throw new MdException("godaddy.ssh.user is not set\n", 1); 450 | } 451 | return $user; 452 | } 453 | 454 | /** 455 | * @param $config 456 | * @return string 457 | */ 458 | private function getSshPort($config) 459 | { 460 | $port = $this->getGoDaddyConfig($config, ['ssh', 'port']); 461 | if ($port == '') { 462 | $port = '22'; 463 | } 464 | return $port; 465 | } 466 | 467 | /** 468 | * @param $config 469 | * @return string 470 | */ 471 | private function getSshIdentity($config) 472 | { 473 | $identity = $this->getGoDaddyConfig($config, ['ssh', 'identity']); 474 | if ($identity == '') { 475 | $identity = "~/.ssh/" . self::IDENTITY_FILE; 476 | } 477 | return $identity; 478 | } 479 | 480 | /** 481 | * Check the connection to GoDaddy and the UID of the logged in account due to some timing 482 | * issues with fast, automated deployments. 483 | * @para array $config Holds connection details. 484 | * @throws MdException Thrown on error. 485 | */ 486 | public function checkConnection($config) 487 | { 488 | echo "==== Checking SSH connection to production server.\n"; 489 | 490 | // Make sure we can run commands remotely successfully 491 | //$this->runOnProd($config, "true"); 492 | 493 | // I don't understand, but the user id changes after a while. 494 | // If we jump in too early, the file permissions won't work. 495 | // So wait until the user id flip occurs. 496 | // (This also checks the connection to the remote server is working.) 497 | $retryLimit = 10; 498 | while ($retryLimit-- > 0) { 499 | $output = $this->runOnProd($config, "id"); 500 | if (strpos(implode("", $output), "bitnami") !== false) { 501 | break; 502 | } 503 | echo "User ID is not bitnami yet, retrying...\n"; 504 | sleep(2); 505 | } 506 | } 507 | } 508 | -------------------------------------------------------------------------------- /src/AlanKent/MagentoDev/Providers/MagentoCloud/MagentoCloudProvider.php: -------------------------------------------------------------------------------- 1 | main(['main.php', 'connect', 'godaddy-cloud', 12 | '--ssh-user', 'admin', '--ssh-host', '107.180.107.179']); 13 | echo "===========================\n"; 14 | (new CommandParser())->main(['main.php', 'destroy', '--force']); 15 | echo "===========================\n"; 16 | (new CommandParser())->main(['main.php', 'create', 'vagrant-rsync']); 17 | echo "===========================\n"; 18 | (new CommandParser())->main(['main.php', 'destroy']); 19 | echo "===========================\n"; 20 | (new CommandParser())->main(['main.php', 'destroy']); 21 | echo "===========================\n"; 22 | exit(0); 23 | } 24 | 25 | global $argv; 26 | $parser = new CommandParser(); 27 | exit($parser->main($argv)); 28 | 29 | --------------------------------------------------------------------------------