DeployController   A
last analyzed

Complexity

Total Complexity 23

Size/Duplication

Total Lines 323
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 92.25%

Importance

Changes 8
Bugs 0 Features 3
Metric Value
wmc 23
c 8
b 0
f 3
lcom 1
cbo 7
dl 0
loc 323
ccs 131
cts 142
cp 0.9225
rs 10

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
B index() 0 42 6
A ex() 0 14 2
B backupDatabase() 0 43 5
A pull() 0 21 3
A copyEnv() 0 8 1
A composer() 0 14 1
A npm() 0 10 1
A migrate() 0 13 1
A seed() 0 11 1
A deploy() 0 12 1
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
        // set up logging to email
98 12
        $this->log = Log::getMonolog();
99 12
        if ($to = config('auto-deploy.notify')) {
100
            $domain = parse_url(config('app.url'), PHP_URL_HOST);
101
            $msg = \Swift_Message::newInstance('Project Deployed')
102
                    ->setFrom(["do_not_reply@$domain" => "Laravel Auto-Deploy[$domain]"])
103
                    ->setTo($to)
104
                    ->setBody('', 'text/html');
105
            $handler = new SwiftMailerHandler(Mail::getSwiftMailer(), $msg, Logger::NOTICE);
106
            $handler->setFormatter(new HtmlFormatter());
107
            $this->log->pushHandler($handler);
108
        }
109
110
        // check to see if we should execute this event
111 12
        if (in_array($this->origin->event(), array_keys(config("auto-deploy.{$this->origin->name}")))) {
112
            // get the parameters for the event we're handling
113 12
            $configKey = "auto-deploy.{$this->origin->name}.{$this->origin->event()}";
114 12
            $this->webroot = config("$configKey.webroot");
115 12
            $this->installDir = dirname($this->webroot).'/'.date('Y-m-d').'_'.$this->commitId;
116 12
            $steps = config("$configKey.steps");
117
118
            // execute the configured steps
119 12
            $this->result = [
120 12
                'Commit_ID' => $this->commitId,
121 12
                'Timestamp' => date('r'),
122 12
                'output' => '',
123
            ];
124 12
            $whitelist = ['backupDatabase','pull','copyEnv','composer','npm','migrate','seed','deploy'];
125 12
            foreach ($steps as $step) {
126 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...
127 4
                    $this->log->error('Deploy failed.', $this->result);
128
129 8
                    return;
130
                }
131 5
            }
132 8
            $this->log->notice('Deploy succeeded!', $this->result);
133 4
        } else {
134
            $this->log->error('Deploy failed.', ['Reason' => 'This event was not configured.']);
135
        }
136 8
    }
137
138
    /**
139
     * Runs a shell command, logs, and handles the result.
140
     *
141
     * @param AdamBrett\ShellWrapper\CommandInterface $cmd The text of the command to be run
142
     *
143
     * @return bool True if the command was successful, false on error
144
     */
145 12
    private function ex(CommandInterface $cmd)
146
    {
147
        // try to run the command
148 12
        $this->shell->run($cmd);
149 12
        $output = $this->shell->getOutput();
150 12
        $returnValue = $this->shell->getReturnValue();
151
152
        // record the result
153 12
        $output = count($output) ? implode("\n", $output)."\n" : '';
154 12
        $this->result['output'] .= "$cmd\n$output";
155
156
        // return a boolean
157 12
        return 0 === $returnValue;
158
    }
159
160
    /**
161
     * Backup the database.
162
     *
163
     * @return bool True if the database was successfully backed up. False on error.
164
     */
165 12
    private function backupDatabase()
166
    {
167
        // get the name of the DB to backed up and the connection to use
168 12
        $dbdir = database_path();
169 12
        $dbconn = config('database.default');
170 12
        $dbname = config("database.connections.$dbconn.database");
171
172
        // make a directory for the backup file and switch into that directory
173 12
        $cmd = new Command('cd');
174 12
        $cmd->addParam($dbdir)
175 12
            ->addSubCommand('&&')
176 12
            ->addSubCommand('mkdir')
177 12
            ->addParam('backups');
178 12
        if ($this->ex($cmd)) {
179 12
            $cmd = new Command('cd');
180 12
            $cmd->addParam($dbdir.'/backups')
181 12
                ->addSubCommand('&&');
182
            switch ($dbconn) {
183 12
                case 'sqlite':
184 2
                    $cmd->addSubCommand('cp');
185 2
                    $cmd->addParam($dbname)
186 2
                        ->addParam('.');
187
188 2
                    return $this->ex($cmd);
189 5
                case 'mysql':
190 6
                    $cmd->addSubCommand('mysqldump');
191 6
                    $cmd->addParam($dbname)
192 6
                        ->addParam('>')
193 6
                        ->addParam("$dbname.sql");
194
195 6
                    return $this->ex($cmd);
196 3
                case 'pgsql':
197 2
                    $cmd->addSubCommand('pg_dump');
198 2
                    $cmd->addParam($dbname)
199 2
                        ->addParam('>')
200 2
                        ->addParam("$dbname.sql");
201
202 2
                    return $this->ex($cmd);
203
            }
204 1
        }
205
206 2
        return false;
207
    }
208
209
    /**
210
     * Create a new directory parallel to the webroot and clone the project into that directory.
211
     *
212
     * @return bool True if the clone is successful. False otherwise.
213
     */
214 10
    private function pull()
215
    {
216 10
        if (is_writable(dirname($this->installDir))) {
217 8
            $cmd = new Command('mkdir');
218 8
            $cmd->addFlag('p')
219 8
                ->addParam($this->installDir);
220 8
            if ($this->ex($cmd)) {
221 8
                $cmd = new Command('cd');
222 8
                $cmd->addParam($this->installDir)
223 8
                    ->addSubCommand('&&')
224 8
                    ->addSubCommand('git')
225 8
                    ->addSubCommand('clone')
226 8
                    ->addParam($this->repoUrl)
227 8
                    ->addParam('.');
228
229 8
                return $this->ex($cmd);
230
            }
231
        }
232
233 2
        return false;
234
    }
235
236
    /**
237
     * Copy the .env file from the new deploy directory.
238
     *
239
     * @return bool True if the update is successful. False otherwise.
240
     */
241 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...
242
    {
243 2
        $cmd = new Command('cp');
244 2
        $cmd->addParam($this->webroot.'./env')
245 2
            ->addParam($this->installDir.'/.env');
246
247 2
        return $this->ex($cmd);
248
    }
249
250
    /**
251
     * Update composer and run composer update.
252
     *
253
     * @return bool True if the update is successful. False otherwise.
254
     */
255 8
    private function composer()
256
    {
257 8
        $cmd = new Command('cd');
258 8
        $cmd->addParam($this->installDir)
259 8
            ->addSubCommand('&&')
260 8
            ->addSubCommand('composer')
261 8
            ->addParam('self-update')
262 8
            ->addSubCommand('&&')
263 8
            ->addSubCommand('composer')
264 8
            ->addParam('update')
265 8
            ->addArgument('no-interaction');
266
267 8
        return $this->ex($cmd);
268
    }
269
270
    /**
271
     * Run npm update.
272
     *
273
     * @return bool True if npm is successful. False otherwise.
274
     */
275 8
    private function npm()
276
    {
277 8
        $cmd = new Command('cd');
278 8
        $cmd->addParam($this->installDir)
279 8
            ->addSubCommand('&&')
280 8
            ->addSubCommand('npm')
281 8
            ->addParam('update');
282
283 8
        return $this->ex($cmd);
284
    }
285
286
    /**
287
     * Run any necessary database migrations.
288
     *
289
     * @return bool True if the migration is successful. False otherwise.
290
     */
291 8
    private function migrate()
292
    {
293 8
        $cmd = new Command('cd');
294 8
        $cmd->addParam($this->installDir)
295 8
            ->addSubCommand('&&')
296 8
            ->addSubCommand('php')
297 8
            ->addSubCommand('artisan')
298 8
            ->addParam('migrate')
299 8
            ->addArgument('force')
300 8
            ->addArgument('no-interaction');
301
302 8
        return $this->ex($cmd);
303
    }
304
305
    /**
306
     * Run any necessary database migrations.
307
     *
308
     * @return bool True if the migration is successful. False otherwise.
309
     */
310 8
    private function seed()
311
    {
312 8
        $cmd = new Command('cd');
313 8
        $cmd->addParam($this->installDir)
314 8
            ->addSubCommand('&&')
315 8
            ->addSubCommand('php')
316 8
            ->addSubCommand('artisan')
317 8
            ->addParam('db:seed');
318
319 8
        return $this->ex($cmd);
320
    }
321
322
    /**
323
     * Symlinks the new deploy directory to the webroot.
324
     *
325
     * @return bool True if the symlink is successful. False otherwise.
326
     */
327 8
    private function deploy()
328
    {
329 8
        $cmd = new Command('cd');
330 8
        $cmd->addParam(dirname($this->webroot))
331 8
            ->addSubCommand('&&')
332 8
            ->addSubCommand('ln')
333 8
            ->addFlag('fs')
334 8
            ->addParam($this->installDir)
335 8
            ->addParam($this->webroot);
336
337 8
        return $this->ex($cmd);
338
    }
339
}
340