Completed
Push — master ( 294b2e...9a0c7b )
by Morgan
04:19
created

DeployController::copyEnv()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
ccs 8
cts 8
cp 1
rs 9.4286
cc 1
eloc 5
nc 1
nop 0
crap 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
        // 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 12
            $domain = parse_url(config('app.url'), PHP_URL_HOST);
107 12
            $msg = \Swift_Message::newInstance('Project Deployed')
108 12
                    ->setFrom(["do_not_reply@$domain" => 'Laravel Auto-Deploy[$domain]'])
109 12
                    ->setTo($to)
110 12
                    ->setBody('', 'text/html');
111 12
            $handler = new SwiftMailerHandler(Mail::getSwiftMailer(), $msg, Logger::NOTICE);
112 12
            $handler->setFormatter(new HtmlFormatter());
113
            $this->log->pushHandler($handler);
114
        }
115 12
116 12
        // execute the configured steps
117 12
        $this->result = [
118 12
            'Commit_ID' => $this->commitId,
119
            'Timestamp' => date('r'),
120 12
            'output' => '',
121 12
        ];
122 12
        $whitelist = ['backupDatabase','pull','copyEnv','composer','npm','migrate','seed','deploy'];
123 4
        foreach ($steps as $step) {
124
            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 8
                $this->log->error('Deploy failed.', $this->result);
126
127 5
                return;
128 8
            }
129 8
        }
130
        $this->log->notice('Deploy succeeded!', $this->result);
131
    }
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 12
     * @return bool True if the command was successful, false on error
139
     */
140
    private function ex(CommandInterface $cmd)
141 12
    {
142 12
        // try to run the command
143 12
        $this->shell->run($cmd);
144
        $output = $this->shell->getOutput();
145
        $returnValue = $this->shell->getReturnValue();
146 12
147 12
        // record the result
148
        $output = count($output) ? implode("\n", $output)."\n" : '';
149
        $this->result['output'] .= "$cmd\n$output";
150 12
151
        // return a boolean
152
        return 0 === $returnValue;
153
    }
154
155
    /**
156
     * Backup the database.
157
     *
158 12
     * @return bool True if the database was successfully backed up. False on error.
159
     */
160
    private function backupDatabase()
161 12
    {
162 12
        // get the name of the DB to backed up and the connection to use
163 12
        $dbdir = database_path();
164
        $dbconn = config('database.default');
165
        $dbname = config("database.connections.$dbconn.database");
166 12
167 12
        // 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
            $cmd->addParam($dbdir.'/backups')
176 12
                ->addSubCommand('&&');
177 2
            switch ($dbconn) {
178 2
                case 'sqlite':
179 2
                    $cmd->addSubCommand('cp');
180
                    $cmd->addParam($dbname)
181 2
                        ->addParam('.');
182 5
183 6
                    return $this->ex($cmd);
184 6
                case 'mysql':
185 6
                    $cmd->addSubCommand('mysqldump');
186 6
                    $cmd->addParam($dbname)
187
                        ->addParam('>')
188 6
                        ->addParam("$dbname.sql");
189 3
190 2
                    return $this->ex($cmd);
191 2
                case 'pgsql':
192 2
                    $cmd->addSubCommand('pg_dump');
193 2
                    $cmd->addParam($dbname)
194
                        ->addParam('>')
195 2
                        ->addParam("$dbname.sql");
196
197 1
                    return $this->ex($cmd);
198
            }
199 2
        }
200
201
        return false;
202
    }
203
204
    /**
205
     * Create a new directory parallel to the webroot and clone the project into that directory.
206
     *
207 10
     * @return bool True if the clone is successful. False otherwise.
208
     */
209 10
    private function pull()
210 8
    {
211 8
        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
                    ->addParam($this->repoUrl)
222 8
                    ->addParam('.');
223
224
                return $this->ex($cmd);
225
            }
226 2
        }
227
228
        return false;
229
    }
230
231
    /**
232
     * Copy the .env file from the new deploy directory.
233
     *
234 8
     * @return bool True if the update is successful. False otherwise.
235
     */
236 8
    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 8
    {
238 8
        $cmd = new Command('cp');
239 8
        $cmd->addParam($this->webroot.'./env')
240 8
            ->addParam($this->installDir.'/.env');
241 8
242 8
        return $this->ex($cmd);
243 8
    }
244 8
245
    /**
246 8
     * Update composer and run composer update.
247
     *
248
     * @return bool True if the update is successful. False otherwise.
249
     */
250
    private function composer()
251
    {
252
        $cmd = new Command('cd');
253
        $cmd->addParam($this->installDir)
254 8
            ->addSubCommand('&&')
255
            ->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 8
278 8
        return $this->ex($cmd);
279 8
    }
280
281 8
    /**
282
     * Run any necessary database migrations.
283
     *
284
     * @return bool True if the migration is successful. False otherwise.
285
     */
286
    private function migrate()
287
    {
288
        $cmd = new Command('cd');
289 8
        $cmd->addParam($this->installDir)
290
            ->addSubCommand('&&')
291 8
            ->addSubCommand('php')
292 8
            ->addSubCommand('artisan')
293 8
            ->addParam('migrate')
294 8
            ->addArgument('force')
295 8
            ->addArgument('no-interaction');
296 8
297
        return $this->ex($cmd);
298 8
    }
299
300
    /**
301
     * Run any necessary database migrations.
302
     *
303
     * @return bool True if the migration is successful. False otherwise.
304
     */
305
    private function seed()
306 8
    {
307
        $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 8
314 8
        return $this->ex($cmd);
315
    }
316 8
317
    /**
318
     * Symlinks the new deploy directory to the webroot.
319
     *
320
     * @return bool True if the symlink is successful. False otherwise.
321
     */
322
    private function deploy()
323
    {
324
        $cmd = new Command('cd');
325
        $cmd->addParam(dirname($this->webroot))
326
            ->addSubCommand('&&')
327
            ->addSubCommand('ln')
328
            ->addFlag('fs')
329
            ->addParam($this->installDir)
330
            ->addParam($this->webroot);
331
332
        return $this->ex($cmd);
333
    }
334
}
335