Completed
Push — master ( f31c89...76c4af )
by Morgan
27:30 queued 03:39
created

DeployController::ex()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 14
ccs 0
cts 0
cp 0
rs 9.4286
cc 2
eloc 7
nc 2
nop 1
crap 6
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 2
use Morphatic\AutoDeploy\Origins\OriginInterface;
16
17
class DeployController extends Controller
18 2
{
19 2
    /**
20
     * The origin of the webhook request.
21 2
     *
22
     * @var Morphatic\AutoDeploy\Origins\OriginInterface
23
     */
24
    private $origin;
25
26 2
    /**
27
     * The URL of the repo to be cloned.
28
     *
29 2
     * @var string
30
     */
31
    private $repo_url;
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 $install_dir;
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 $commit_id;
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
    public function __construct(OriginInterface $origin, Exec $exec)
76
    {
77
        // set class variables related to the webhook origin
78
        $this->origin = $origin;
1 ignored issue
show
Documentation Bug introduced by
It seems like $origin of type object<Morphatic\AutoDep...rigins\OriginInterface> is incompatible with the declared type object<Morphatic\AutoDep...rigins\OriginInterface> of property $origin.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
79
        $this->repo_url = $this->origin->getRepoUrl();
80
        $this->commit_id = $this->origin->getCommitId();
81
82
        // create an instance of the shell exec
83
        $this->shell = $exec;
1 ignored issue
show
Documentation Bug introduced by
It seems like $exec of type object<AdamBrett\ShellWrapper\Runners\Exec> is incompatible with the declared type object<Morphatic\AutoDep...llWrapper\Runners\Exec> of property $shell.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
84
    }
85
86
    /**
87
     * Handles incoming webhook requests.
88
     *
89
     * @param Request $request The payload from the webhook origin, e.g. Github
90
     */
91
    public function index(Request $request)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
92
    {
93
        // get the parameters for the event we're handling
94
        $config_key = "auto-deploy.{$this->origin->name}.{$this->origin->event()}";
95
        $this->webroot = config("$config_key.webroot");
96
        $this->install_dir = dirname($this->webroot).'/'.date('Y-m-d').'_'.$this->commit_id;
97
        $steps = config("$config_key.steps");
98
99
        // set up logging to email
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(config('auto-deploy.notify'))
104
                ->setBody('', 'text/html');
105
        $handler = new SwiftMailerHandler(Mail::getSwiftMailer(), $msg, Logger::NOTICE);
106
        $handler->setFormatter(new HtmlFormatter());
107
        $this->log = Log::getMonolog();
108
        $this->log->pushHandler($handler);
109
110
        // execute the configured steps
111
        $this->result = [
112
            'Commit_ID' => $this->commit_id,
113
            'Timestamp' => date('r'),
114
            'output' => '',
115
        ];
116
        foreach ($steps as $step) {
117
            if (!$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...
118
                $this->log->error('Deploy failed.', $this->result);
119
120
                return;
121
            }
122
        }
123
        $this->log->notice('Deploy succeeded!', $this->result);
124
    }
125
126
    /**
127
     * Runs a shell command, logs, and handles the result.
128
     *
129
     * @param AdamBrett\ShellWrapper\CommandInterface $cmd The text of the command to be run
130
     *
131
     * @return bool True if the command was successful, false on error
132
     */
133
    private function ex(CommandInterface $cmd)
134
    {
135
        // try to run the command
136
        $last_line = $this->shell->run($cmd);
0 ignored issues
show
Unused Code introduced by
$last_line is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
137
        $output = $this->shell->getOutput();
138
        $return_var = $this->shell->getReturnValue();
139
140
        // record the result
141
        $output = count($output) ? implode("\n", $output)."\n" : '';
142
        $this->result['output'] .= "$cmd\n$output";
143
144
        // return a boolean
145
        return 0 === $return_var;
146
    }
147
148
    /**
149
     * Backup the database.
150
     *
151
     * @return bool True if the database was successfully backed up. False on error.
152
     */
153
    private function backupDatabase()
154
    {
155
        // get the name of the DB to backed up and the connection to use
156
        $dbdir = database_path();
157
        $dbconn = config('database.default');
158
        $dbname = config("database.connections.$dbconn.database");
159
160
        // make a directory for the backup file and switch into that directory
161
        $cmd = new Command('cd');
162
        $cmd->addParam($dbdir)
163
            ->addSubCommand('&&')
164
            ->addSubCommand('mkdir')
165
            ->addParam('backups');
166
        if ($this->ex($cmd)) {
167
            $cmd = new Command('cd');
168
            $cmd->addParam($dbdir.'/backups')
169
                ->addSubCommand('&&');
170
            switch ($dbconn) {
171
                case 'sqlite':
172
                    $cmd->addSubCommand('cp');
173
                    $cmd->addParam($dbname)
174
                        ->addParam('.');
175
176
                    return $this->ex($cmd);
177
                case 'mysql':
178
                    $cmd->addSubCommand('mysqldump');
179
                    $cmd->addParam($dbname)
180
                        ->addParam('>')
181
                        ->addParam("$dbname.sql");
182
183
                    return $this->ex($cmd);
184
                case 'pgsql':
185
                    $cmd->addSubCommand('pg_dump');
186
                    $cmd->addParam($dbname)
187
                        ->addParam('>')
188
                        ->addParam("$dbname.sql");
189
190
                    return $this->ex($cmd);
191
            }
192
        }
193
194
        return false;
195
    }
196
197
    /**
198
     * Create a new directory parallel to the webroot and clone the project into that directory.
199
     *
200
     * @return bool True if the clone is successful. False otherwise.
201
     */
202
    private function pull()
203
    {
204
        if (is_writable(dirname($this->install_dir))) {
205
            $cmd = new Command('mkdir');
206
            $cmd->addFlag('p')
207
                ->addParam($this->install_dir);
208
            if ($this->ex($cmd)) {
209
                $cmd = new Command('cd');
210
                $cmd->addParam($this->install_dir)
211
                    ->addSubCommand('&&')
212
                    ->addSubCommand('git')
213
                    ->addSubCommand('clone')
214
                    ->addParam($this->repo_url)
215
                    ->addParam('.');
216
217
                return $this->ex($cmd);
218
            }
219
        }
220
221
        return false;
222
    }
223
224
    /**
225
     * Update composer and run composer update.
226
     *
227
     * @return bool True if the update is successful. False otherwise.
228
     */
229
    private function composer()
230
    {
231
        $cmd = new Command('cd');
232
        $cmd->addParam($this->install_dir)
233
            ->addSubCommand('&&')
234
            ->addSubCommand('composer')
235
            ->addParam('self-update')
236
            ->addSubCommand('&&')
237
            ->addSubCommand('composer')
238
            ->addParam('update')
239
            ->addArgument('no-interaction');
240
241
        return $this->ex($cmd);
242
    }
243
244
    /**
245
     * Run npm update.
246
     *
247
     * @return bool True if npm is successful. False otherwise.
248
     */
249
    private function npm()
250
    {
251
        $cmd = new Command('cd');
252
        $cmd->addParam($this->install_dir)
253
            ->addSubCommand('&&')
254
            ->addSubCommand('npm')
255
            ->addParam('update');
256
257
        return $this->ex($cmd);
258
    }
259
260
    /**
261
     * Run any necessary database migrations.
262
     *
263
     * @return bool True if the migration is successful. False otherwise.
264
     */
265
    private function migrate()
266
    {
267
        $cmd = new Command('cd');
268
        $cmd->addParam($this->install_dir)
269
            ->addSubCommand('&&')
270
            ->addSubCommand('php')
271
            ->addSubCommand('artisan')
272
            ->addParam('migrate')
273
            ->addArgument('force')
274
            ->addArgument('no-interaction');
275
276
        return $this->ex($cmd);
277
    }
278
279
    /**
280
     * Run any necessary database migrations.
281
     *
282
     * @return bool True if the migration is successful. False otherwise.
283
     */
284
    private function seed()
285
    {
286
        $cmd = new Command('cd');
287
        $cmd->addParam($this->install_dir)
288
            ->addSubCommand('&&')
289
            ->addSubCommand('php')
290
            ->addSubCommand('artisan')
291
            ->addParam('db:seed');
292
293
        return $this->ex($cmd);
294
    }
295
296
    /**
297
     * Symlinks the new deploy directory to the webroot.
298
     *
299
     * @return bool True if the symlink is successful. False otherwise.
300
     */
301
    private function deploy()
302
    {
303
        $cmd = new Command('cd');
304
        $cmd->addParam(dirname($this->webroot))
305
            ->addSubCommand('&&')
306
            ->addSubCommand('ln')
307
            ->addFlag('fs')
308
            ->addParam($this->install_dir)
309
            ->addParam($this->webroot);
310
311
        return $this->ex($cmd);
312
    }
313
}
314