Completed
Push — master ( 8724ba...8ef377 )
by Morgan
05:57
created

DeployController::pull()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 3.0022

Importance

Changes 2
Bugs 0 Features 2
Metric Value
c 2
b 0
f 2
dl 0
loc 21
ccs 15
cts 16
cp 0.9375
rs 9.3143
cc 3
eloc 15
nc 3
nop 0
crap 3.0022
1
<?php
2
3
namespace Morphatic\AutoDeploy\Controllers;
4
5
use Log;
6
use Mail;
7
use Monolog\Logger;
8
use Illuminate\Http\Request;
9
use Illuminate\Routing\Controller;
10
use AdamBrett\ShellWrapper\Command\Builder as Command;
11
use AdamBrett\ShellWrapper\Command\CommandInterface;
12
use AdamBrett\ShellWrapper\Runners\Exec;
13
use Monolog\Formatter\HtmlFormatter;
14
use Monolog\Handler\SwiftMailerHandler;
15
use Morphatic\AutoDeploy\Origins\OriginInterface;
16
17
class DeployController extends Controller
18
{
19
    /**
20
     * The origin of the webhook request.
21
     *
22
     * @var Morphatic\AutoDeploy\Origins\OriginInterface
23
     */
24
    private $origin;
25
26
    /**
27
     * The URL of the repo to be cloned.
28
     *
29
     * @var string
30
     */
31
    private $repoUrl;
32
33
    /**
34
     * The absolute path of the directory on the server that contains the project.
35
     *
36
     * @var string
37
     */
38
    private $webroot;
39
40
    /**
41
     * The absolute path of the directory where the new deployment will be set up.
42
     *
43
     * @var string
44
     */
45
    private $installDir;
46
47
    /**
48
     * A log of the results of the entire deploy process.
49
     *
50
     * @var Monolog\Logger
51
     */
52
    private $log;
53
54
    /**
55
     * The commit ID for this commit.
56
     *
57
     * @var string
58
     */
59
    private $commitId;
60
61
    /**
62
     * The commit ID for this commit.
63
     *
64
     * @var AdamBrett\ShellWrapper\Runners\Exec
65
     */
66
    private $shell;
67
68
    /**
69
     * The result of this commit.
70
     *
71
     * @var array
72
     */
73
    private $result;
74
75
    /**
76
     * Create a new DeployController instance.
77
     *
78
     * @param Morphatic\AutoDeploy\Origins\OriginInterface $origin The origin of the webhook
79
     * @param AdamBrett\ShellWrapper\Runners\Exec          $exec   The shell command execution class
80
     */
81 36
    public function __construct(OriginInterface $origin, Exec $exec)
82
    {
83
        // set class variables related to the webhook origin
84 36
        $this->origin = $origin;
85 36
        $this->repoUrl = $this->origin->getRepoUrl();
86 36
        $this->commitId = $this->origin->getCommitId();
87
88
        // create an instance of the shell exec
89 36
        $this->shell = $exec;
90 36
    }
91
92
    /**
93
     * Handles incoming webhook requests.
94
     */
95 12
    public function index()
96
    {
97
        // get the parameters for the event we're handling
98 12
        $configKey = "auto-deploy.{$this->origin->name}.{$this->origin->event()}";
99 12
        $this->webroot = config("$configKey.webroot");
100 12
        $this->installDir = dirname($this->webroot).'/'.date('Y-m-d').'_'.$this->commitId;
101 12
        $steps = config("$configKey.steps");
102
103
        // set up logging to email
104 12
        $this->log = Log::getMonolog();
105 12
        if ($to = config('auto-deploy.notify')) {
106
            $domain = parse_url(config('app.url'), PHP_URL_HOST);
107
            $msg = \Swift_Message::newInstance('Project Deployed')
108
                    ->setFrom(["do_not_reply@$domain" => 'Laravel Auto-Deploy[$domain]'])
109
                    ->setTo($to)
110
                    ->setBody('', 'text/html');
111
            $handler = new SwiftMailerHandler(Mail::getSwiftMailer(), $msg, Logger::NOTICE);
112
            $handler->setFormatter(new HtmlFormatter());
113
            $this->log->pushHandler($handler);
114
        }
115
116
        // execute the configured steps
117 12
        $this->result = [
118 12
            'Commit_ID' => $this->commitId,
119 12
            'Timestamp' => date('r'),
120 12
            'output' => '',
121
        ];
122 12
        $whitelist = ['backupDatabase','pull','copyEnv','composer','npm','migrate','seed','deploy'];
123 12
        foreach ($steps as $step) {
124 12
            if (in_array($step, $whitelist) && !$this->{$step}()) {
0 ignored issues
show
Security Code Execution introduced by
$step can contain request data and is used in code execution context(s) leading to a potential security vulnerability.

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
125 4
                $this->log->error('Deploy failed.', $this->result);
126
127 8
                return;
128
            }
129 5
        }
130 8
        $this->log->notice('Deploy succeeded!', $this->result);
131 8
    }
132
133
    /**
134
     * Runs a shell command, logs, and handles the result.
135
     *
136
     * @param AdamBrett\ShellWrapper\CommandInterface $cmd The text of the command to be run
137
     *
138
     * @return bool True if the command was successful, false on error
139
     */
140 12
    private function ex(CommandInterface $cmd)
141
    {
142
        // try to run the command
143 12
        $this->shell->run($cmd);
144 12
        $output = $this->shell->getOutput();
145 12
        $returnValue = $this->shell->getReturnValue();
146
147
        // record the result
148 12
        $output = count($output) ? implode("\n", $output)."\n" : '';
149 12
        $this->result['output'] .= "$cmd\n$output";
150
151
        // return a boolean
152 12
        return 0 === $returnValue;
153
    }
154
155
    /**
156
     * Backup the database.
157
     *
158
     * @return bool True if the database was successfully backed up. False on error.
159
     */
160 12
    private function backupDatabase()
161
    {
162
        // get the name of the DB to backed up and the connection to use
163 12
        $dbdir = database_path();
164 12
        $dbconn = config('database.default');
165 12
        $dbname = config("database.connections.$dbconn.database");
166
167
        // make a directory for the backup file and switch into that directory
168 12
        $cmd = new Command('cd');
169 12
        $cmd->addParam($dbdir)
170 12
            ->addSubCommand('&&')
171 12
            ->addSubCommand('mkdir')
172 12
            ->addParam('backups');
173 12
        if ($this->ex($cmd)) {
174 12
            $cmd = new Command('cd');
175 12
            $cmd->addParam($dbdir.'/backups')
176 12
                ->addSubCommand('&&');
177
            switch ($dbconn) {
178 12
                case 'sqlite':
179 2
                    $cmd->addSubCommand('cp');
180 2
                    $cmd->addParam($dbname)
181 2
                        ->addParam('.');
182
183 2
                    return $this->ex($cmd);
184 5
                case 'mysql':
185 6
                    $cmd->addSubCommand('mysqldump');
186 6
                    $cmd->addParam($dbname)
187 6
                        ->addParam('>')
188 6
                        ->addParam("$dbname.sql");
189
190 6
                    return $this->ex($cmd);
191 3
                case 'pgsql':
192 2
                    $cmd->addSubCommand('pg_dump');
193 2
                    $cmd->addParam($dbname)
194 2
                        ->addParam('>')
195 2
                        ->addParam("$dbname.sql");
196
197 2
                    return $this->ex($cmd);
198
            }
199 1
        }
200
201 2
        return false;
202
    }
203
204
    /**
205
     * Create a new directory parallel to the webroot and clone the project into that directory.
206
     *
207
     * @return bool True if the clone is successful. False otherwise.
208
     */
209 10
    private function pull()
210
    {
211 10
        if (is_writable(dirname($this->installDir))) {
212 8
            $cmd = new Command('mkdir');
213 8
            $cmd->addFlag('p')
214 8
                ->addParam($this->installDir);
215 8
            if ($this->ex($cmd)) {
216 8
                $cmd = new Command('cd');
217 8
                $cmd->addParam($this->installDir)
218 8
                    ->addSubCommand('&&')
219 8
                    ->addSubCommand('git')
220 8
                    ->addSubCommand('clone')
221 8
                    ->addParam($this->repoUrl)
222 8
                    ->addParam('.');
223
224 8
                return $this->ex($cmd);
225
            }
226
        }
227
228 2
        return false;
229
    }
230
231
    /**
232
     * Copy the .env file from the new deploy directory.
233
     *
234
     * @return bool True if the update is successful. False otherwise.
235
     */
236 2
    private function copyEnv()
0 ignored issues
show
Coding Style introduced by
function copyEnv() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
237
    {
238 2
        $cmd = new Command('cp');
239 2
        $cmd->addParam($this->webroot.'./env')
240 2
            ->addParam($this->installDir.'/.env');
241
242 2
        return $this->ex($cmd);
243
    }
244
245
    /**
246
     * Update composer and run composer update.
247
     *
248
     * @return bool True if the update is successful. False otherwise.
249
     */
250 8
    private function composer()
251
    {
252 8
        $cmd = new Command('cd');
253 8
        $cmd->addParam($this->installDir)
254 8
            ->addSubCommand('&&')
255 8
            ->addSubCommand('composer')
256 8
            ->addParam('self-update')
257 8
            ->addSubCommand('&&')
258 8
            ->addSubCommand('composer')
259 8
            ->addParam('update')
260 8
            ->addArgument('no-interaction');
261
262 8
        return $this->ex($cmd);
263
    }
264
265
    /**
266
     * Run npm update.
267
     *
268
     * @return bool True if npm is successful. False otherwise.
269
     */
270 8
    private function npm()
271
    {
272 8
        $cmd = new Command('cd');
273 8
        $cmd->addParam($this->installDir)
274 8
            ->addSubCommand('&&')
275 8
            ->addSubCommand('npm')
276 8
            ->addParam('update');
277
278 8
        return $this->ex($cmd);
279
    }
280
281
    /**
282
     * Run any necessary database migrations.
283
     *
284
     * @return bool True if the migration is successful. False otherwise.
285
     */
286 8
    private function migrate()
287
    {
288 8
        $cmd = new Command('cd');
289 8
        $cmd->addParam($this->installDir)
290 8
            ->addSubCommand('&&')
291 8
            ->addSubCommand('php')
292 8
            ->addSubCommand('artisan')
293 8
            ->addParam('migrate')
294 8
            ->addArgument('force')
295 8
            ->addArgument('no-interaction');
296
297 8
        return $this->ex($cmd);
298
    }
299
300
    /**
301
     * Run any necessary database migrations.
302
     *
303
     * @return bool True if the migration is successful. False otherwise.
304
     */
305 8
    private function seed()
306
    {
307 8
        $cmd = new Command('cd');
308 8
        $cmd->addParam($this->installDir)
309 8
            ->addSubCommand('&&')
310 8
            ->addSubCommand('php')
311 8
            ->addSubCommand('artisan')
312 8
            ->addParam('db:seed');
313
314 8
        return $this->ex($cmd);
315
    }
316
317
    /**
318
     * Symlinks the new deploy directory to the webroot.
319
     *
320
     * @return bool True if the symlink is successful. False otherwise.
321
     */
322 8
    private function deploy()
323
    {
324 8
        $cmd = new Command('cd');
325 8
        $cmd->addParam(dirname($this->webroot))
326 8
            ->addSubCommand('&&')
327 8
            ->addSubCommand('ln')
328 8
            ->addFlag('fs')
329 8
            ->addParam($this->installDir)
330 8
            ->addParam($this->webroot);
331
332 8
        return $this->ex($cmd);
333
    }
334
}
335