Failed Conditions
Push — master ( 1d3aa7...f32a49 )
by Çağrı
44:42 queued 42:36
created

src/Deployer/DefaultDeployer.php (1 issue)

1
<?php
2
3
namespace Bencagri\Artisan\Deployer\Deployer;
4
5
use Bencagri\Artisan\Deployer\Configuration\DefaultConfiguration;
6
use Bencagri\Artisan\Deployer\Configuration\Option;
7
use Bencagri\Artisan\Deployer\Exception\InvalidConfigurationException;
8
use Bencagri\Artisan\Deployer\Requirement\AllowsLoginViaSsh;
9
use Bencagri\Artisan\Deployer\Requirement\CommandExists;
10
use Bencagri\Artisan\Deployer\Server\Property;
11
use Bencagri\Artisan\Deployer\Server\Server;
12
use Bencagri\Artisan\Deployer\Task\TaskCompleted;
13
14
abstract class DefaultDeployer extends AbstractDeployer
15
{
16
    private $remoteProjectDirHasBeenCreated = false;
17
    private $remoteSymLinkHasBeenCreated = false;
18
19
    public function getConfigBuilder() : DefaultConfiguration
20
    {
21
        return new DefaultConfiguration($this->getContext()->getLocalProjectRootDir());
22
    }
23
24
    public function getRequirements() : array
25
    {
26
        $requirements = [];
27
        $localhost = $this->getContext()->getLocalHost();
28
        $allServers = $this->getServers()->findAll();
29
        $appServers = $this->getServers()->findByRoles([Server::ROLE_APP]);
30
31
        $requirements[] = new CommandExists([$localhost], 'git');
32
        $requirements[] = new CommandExists([$localhost], 'ssh');
33
34
        $requirements[] = new AllowsLoginViaSsh($allServers);
35
        $requirements[] = new CommandExists($appServers, $this->getConfig(Option::remoteComposerBinaryPath));
36
        if ('acl' === $this->getConfig(Option::permissionMethod)) {
37
            $requirements[] = new CommandExists($appServers, $this->getConfig('setfacl'));
38
        }
39
40
        return $requirements;
41
    }
42
43
    final public function deploy() : void
44
    {
45
        $this->initializeServerOptions();
46
        $this->createRemoteDirectoryLayout();
47
        $this->remoteProjectDirHasBeenCreated = true;
48
49
        $this->log('Executing <hook>beforeUpdating</> hook');
50
        $this->beforeUpdating();
51
        $this->log('<h1>Updating app code</>');
52
        $this->doUpdateCode();
53
54
        $this->log('Executing <hook>beforePreparing</> hook');
55
        $this->beforePreparing();
56
        $this->log('<h1>Preparing app</>');
57
        $this->doCreateCacheDir();
58
        $this->doCreateLogDir();
59
        $this->doCreateSharedDirs();
60
        $this->doCreateSharedFiles();
61
        $this->doSetPermissions();
62
        $this->doInstallDependencies();
63
64
        $this->log('Executing <hook>beforeOptimizing</> hook');
65
        $this->beforeOptimizing();
66
        $this->log('<h1>Optimizing app</>');
67
        $this->doWarmupCache();
68
        $this->doClearControllers();
69
        $this->doOptimizeComposer();
70
71
        $this->log('Executing <hook>beforePublishing</> hook');
72
        $this->beforePublishing();
73
        $this->log('<h1>Publishing app</>');
74
        $this->doCreateSymlink();
75
        $this->remoteSymLinkHasBeenCreated = true;
76
        $this->doResetOpCache();
77
        $this->doKeepReleases();
78
    }
79
80
    final public function cancelDeploy() : void
81
    {
82
        if (!$this->remoteSymLinkHasBeenCreated && !$this->remoteProjectDirHasBeenCreated) {
83
            $this->log('<h2>No changes need to be reverted on remote servers (neither the remote project dir nor the symlink were created)</>');
84
        }
85
86
        if ($this->remoteSymLinkHasBeenCreated) {
87
            $this->doSymlinkToPreviousRelease();
88
        }
89
90
        if ($this->remoteProjectDirHasBeenCreated) {
91
            $this->doDeleteLastReleaseDirectory();
92
        }
93
    }
94
95
    final public function rollback() : void
96
    {
97
        $this->initializeServerOptions();
98
99
        $this->log('Executing <hook>beforeRollingBack</> hook');
100
        $this->beforeRollingBack();
101
102
        $this->doCheckPreviousReleases();
103
        $this->doSymlinkToPreviousRelease();
104
        $this->doDeleteLastReleaseDirectory();
105
    }
106
107
    public function beforeUpdating()
108
    {
109
        $this->log('<h3>Nothing to execute</>');
110
    }
111
112
    public function beforePreparing()
113
    {
114
        $this->log('<h3>Nothing to execute</>');
115
    }
116
117
    public function beforeOptimizing()
118
    {
119
        $this->log('<h3>Nothing to execute</>');
120
    }
121
122
    public function beforePublishing()
123
    {
124
        $this->log('<h3>Nothing to execute</>');
125
    }
126
127
    public function beforeRollingBack()
128
    {
129
        $this->log('<h3>Nothing to execute</>');
130
    }
131
132
    private function doCheckPreviousReleases() : void
133
    {
134
        $this->log('<h2>Getting the previous releases dirs</>');
135
        $results = $this->runRemote('ls -r1 {{ deploy_dir }}/releases');
136
137
        if ($this->getContext()->isDryRun()) {
138
            return;
139
        }
140
141
        foreach ($results as $result) {
142
            $numReleases = count(array_filter(explode("\n", $result->getOutput())));
143
144
            if ($numReleases < 2) {
145
                throw new \RuntimeException(sprintf('The application cannot be rolled back because the "%s" server has only 1 release and it\'s not possible to roll back to a previous version.', $result->getServer()));
146
            }
147
        }
148
    }
149
150
    private function doSymlinkToPreviousRelease() : void
151
    {
152
        $this->log('<h2>Reverting the current symlink to the previous version</>');
153
        $this->runRemote('export _previous_release_dirname=$(ls -r1 {{ deploy_dir }}/releases | head -n 2 | tail -n 1) && rm -f {{ deploy_dir }}/current && ln -s {{ deploy_dir }}/releases/$_previous_release_dirname {{ deploy_dir }}/current');
154
    }
155
156
    private function doDeleteLastReleaseDirectory() : void
157
    {
158
        // this is needed to avoid rolling back in the future to this version
159
        $this->log('<h2>Deleting the last release directory</>');
160
        $this->runRemote('export _last_release_dirname=$(ls -r1 {{ deploy_dir }}/releases | head -n 1) && rm -fr {{ deploy_dir }}/releases/$_last_release_dirname');
161
    }
162
163
    private function initializeServerOptions() : void
164
    {
165
        $this->log('<h2>Initializing server options</>');
166
167
        /** @var Server[] $allServers */
168
        $allServers = array_merge([$this->getContext()->getLocalHost()], $this->getServers()->findAll());
169
        foreach ($allServers as $server) {
170
            if (true === $this->getConfig(Option::useSshAgentForwarding)) {
171
                $this->log(sprintf('<h3>Enabling SSH agent forwarding for <server>%s</> server</>', $server));
172
            }
173
            $server->set(Property::use_ssh_agent_forwarding, $this->getConfig(Option::useSshAgentForwarding));
174
        }
175
176
        $appServers = $this->getServers()->findByRoles([Server::ROLE_APP]);
177
        foreach ($appServers as $server) {
178
            $this->log(sprintf('<h3>Setting the %s property for <server>%s</> server</>', Property::deploy_dir, $server));
179
            $server->set(Property::deploy_dir, $this->getConfig(Option::deployDir));
180
        }
181
    }
182
183
    private function initializeDirectoryLayout(Server $server) : void
184
    {
185
        $this->log('<h2>Initializing server directory layout</>');
186
187
        $remoteProjectDir = $server->get(Property::project_dir);
188
        $server->set(Property::config_dir, sprintf('%s/%s', $remoteProjectDir, $this->getConfig(Option::configDir)));
189
        $server->set(Property::cache_dir, sprintf('%s/%s', $remoteProjectDir, $this->getConfig(Option::cacheDir)));
190
        $server->set(Property::log_dir, sprintf('%s/%s', $remoteProjectDir, $this->getConfig(Option::logDir)));
191
        $server->set(Property::src_dir, sprintf('%s/%s', $remoteProjectDir, $this->getConfig(Option::srcDir)));
192
        $server->set(Property::templates_dir, sprintf('%s/%s', $remoteProjectDir, $this->getConfig(Option::templatesDir)));
193
        $server->set(Property::web_dir, sprintf('%s/%s', $remoteProjectDir, $this->getConfig(Option::webDir)));
194
195
196
        $server->set(Property::console_bin, sprintf('%s artisan', $this->getConfig(Option::remotePhpBinaryPath)));
197
    }
198
199
200
    private function findConsoleBinaryPath(Server $server) : string
201
    {
202
        $consoleBinary = '{{ project_dir }}/artisan';
203
204
        $localConsoleBinary = $this->getContext()->getLocalHost()->resolveProperties($consoleBinary);
205
        if (is_executable($localConsoleBinary)) {
206
            return $server->resolveProperties($consoleBinary);
207
        }
208
209
        if (null === $server->get(Property::console_bin)) {
210
            throw new InvalidConfigurationException(sprintf('The artisan binary of your laravel application is not available in any of the following directories: %s. Configure the "baseDir" option and set it to the directory that contains the "artisan" binary.', $consoleBinary));
211
        }
0 ignored issues
show
Bug Best Practice introduced by
The function implicitly returns null when the if condition on line 209 is false. This is incompatible with the type-hinted return string. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
212
    }
213
214
    private function createRemoteDirectoryLayout() : void
215
    {
216
        $this->log('<h2>Creating the remote directory layout</>');
217
        $this->runRemote('mkdir -p {{ deploy_dir }} && mkdir -p {{ deploy_dir }}/releases && mkdir -p {{ deploy_dir }}/shared');
218
219
        /** @var TaskCompleted[] $results */
220
        $results = $this->runRemote('export _release_path="{{ deploy_dir }}/releases/$(date +%Y%m%d%H%M%S)" && mkdir -p $_release_path && echo $_release_path');
221
        foreach ($results as $result) {
222
            $remoteProjectDir = $this->getContext()->isDryRun() ? '(the remote project_dir)' : $result->getTrimmedOutput();
223
            $result->getServer()->set(Property::project_dir, $remoteProjectDir);
224
            $this->initializeDirectoryLayout($result->getServer());
225
        }
226
    }
227
228
    private function doGetcodeRevision() : string
229
    {
230
        $this->log('<h2>Getting the revision ID of the code repository</>');
231
        $result = $this->runLocal(sprintf('git ls-remote %s %s', $this->getConfig(Option::repositoryUrl), $this->getConfig(Option::repositoryBranch)));
232
        $revision = explode("\t", $result->getTrimmedOutput())[0];
233
        if ($this->getContext()->isDryRun()) {
234
            $revision = '(the code revision)';
235
        }
236
        $this->log(sprintf('<h3>Code revision hash = %s</>', $revision));
237
238
        return $revision;
239
    }
240
241
    private function doUpdateCode() : void
242
    {
243
        $repositoryRevision = $this->doGetcodeRevision();
244
245
        $this->log('<h2>Updating code base with remote_cache strategy</>');
246
        $this->runRemote(sprintf('if [ -d {{ deploy_dir }}/repo ]; then cd {{ deploy_dir }}/repo && git fetch -q origin && git fetch --tags -q origin && git reset -q --hard %s && git clean -q -d -x -f; else git clone -q -b %s %s {{ deploy_dir }}/repo && cd {{ deploy_dir }}/repo && git checkout -q -b deploy %s; fi', $repositoryRevision, $this->getConfig(Option::repositoryBranch), $this->getConfig(Option::repositoryUrl), $repositoryRevision));
247
248
        $this->log('<h3>Copying the updated code to the new release directory</>');
249
        $this->runRemote(sprintf('cp -RPp {{ deploy_dir }}/repo/* {{ project_dir }}'));
250
    }
251
252
    private function doCreateCacheDir() : void
253
    {
254
        $this->log('<h2>Creating cache directory</>');
255
        $this->runRemote('if [ -d {{ cache_dir }} ]; then rm -rf {{ cache_dir }}; fi; mkdir -p {{ cache_dir }}');
256
    }
257
258
    private function doCreateLogDir() : void
259
    {
260
        $this->log('<h2>Creating log directory</>');
261
        $this->runRemote('if [ -d {{ log_dir }} ] ; then rm -rf {{ log_dir }}; fi; mkdir -p {{ log_dir }}');
262
    }
263
264
    private function doCreateSharedDirs() : void
265
    {
266
        $this->log('<h2>Creating symlinks for shared directories</>');
267
        foreach ($this->getConfig(Option::sharedDirs) as $sharedDir) {
268
            $this->runRemote(sprintf('mkdir -p {{ deploy_dir }}/shared/%s', $sharedDir));
269
            $this->runRemote(sprintf('if [ -d {{ project_dir }}/%s ] ; then rm -rf {{ project_dir }}/%s; fi', $sharedDir, $sharedDir));
270
            $this->runRemote(sprintf('ln -nfs {{ deploy_dir }}/shared/%s {{ project_dir }}/%s', $sharedDir, $sharedDir));
271
        }
272
    }
273
274
    private function doCreateSharedFiles() : void
275
    {
276
        $this->log('<h2>Creating symlinks for shared files</>');
277
        if (!is_array($this->getConfig(Option::sharedFiles))) return;
278
279
        foreach ($this->getConfig(Option::sharedFiles) as $sharedFile) {
280
            $sharedFileParentDir = dirname($sharedFile);
281
            $this->runRemote(sprintf('mkdir -p {{ deploy_dir }}/shared/%s', $sharedFileParentDir));
282
            $this->runRemote(sprintf('touch {{ deploy_dir }}/shared/%s', $sharedFile));
283
            $this->runRemote(sprintf('ln -nfs {{ deploy_dir }}/shared/%s {{ project_dir }}/%s', $sharedFile, $sharedFile));
284
        }
285
    }
286
287
    // this method was inspired by https://github.com/deployphp/deployer/blob/master/recipe/deploy/writable.php
288
    // (c) Anton Medvedev <[email protected]>
289
    private function doSetPermissions() : void
290
    {
291
        $permissionMethod = $this->getConfig(Option::permissionMethod);
292
        $writableDirs = implode(' ', $this->getConfig(Option::writableDirs));
293
        $this->log(sprintf('<h2>Setting permissions for writable dirs using the "%s" method</>', $permissionMethod));
294
295
        if ('chmod' === $permissionMethod) {
296
            $this->runRemote(sprintf('chmod -R %s %s', $this->getConfig(Option::permissionMode), $writableDirs));
297
298
            return;
299
        }
300
301
        if ('chown' === $permissionMethod) {
302
            $this->runRemote(sprintf('sudo chown -RL %s %s', $this->getConfig(Option::permissionUser), $writableDirs));
303
304
            return;
305
        }
306
307
        if ('chgrp' === $permissionMethod) {
308
            $this->runRemote(sprintf('sudo chgrp -RH %s %s', $this->getConfig(Option::permissionGroup), $writableDirs));
309
310
            return;
311
        }
312
313
        if ('acl' === $permissionMethod) {
314
            $this->runRemote(sprintf('sudo setfacl -RL -m u:"%s":rwX -m u:`whoami`:rwX %s', $this->getConfig(Option::permissionUser), $writableDirs));
315
            $this->runRemote(sprintf('sudo setfacl -dRL -m u:"%s":rwX -m u:`whoami`:rwX %s', $this->getConfig(Option::permissionUser), $writableDirs));
316
317
            return;
318
        }
319
320
        throw new InvalidConfigurationException(sprintf('The "%s" permission method is not valid. Select one of the supported methods.', $permissionMethod));
321
    }
322
323
    private function doInstallDependencies() : void
324
    {
325
        if (true === $this->getConfig(Option::updateRemoteComposerBinary)) {
326
            $this->log('<h2>Self Updating the Composer binary</>');
327
            $this->runRemote(sprintf('%s self-update', $this->getConfig(Option::remoteComposerBinaryPath)));
328
        }
329
330
        $this->log('<h2>Installing Composer dependencies</>');
331
        $this->runRemote(sprintf('%s install %s', $this->getConfig(Option::remoteComposerBinaryPath), $this->getConfig(Option::composerInstallFlags)));
332
    }
333
334
    private function doWarmupCache() : void
335
    {
336
        if (true !== $this->getConfig(Option::warmupCache)) {
337
            return;
338
        }
339
340
        $this->log('<h2>Clearing Cache</>');
341
        $this->runRemote(sprintf('{{ console_bin }} cache:clear --no-debug --env=%s', $this->getConfig(Option::appEnvironment)));
342
        $this->runRemote('chmod -R g+w {{ cache_dir }}');
343
    }
344
345
    private function doClearControllers() : void
346
    {
347
        $this->log('<h2>Clearing controllers</>');
348
        foreach ($this->getServers()->findByRoles([Server::ROLE_APP]) as $server) {
349
            $absolutePaths = array_map(function ($relativePath) use ($server) {
350
                return $server->resolveProperties(sprintf('{{ project_dir }}/%s', $relativePath));
351
            }, $this->getConfig(Option::controllersToRemove));
352
353
            $this->safeDelete($server, $absolutePaths);
354
        }
355
    }
356
357
    private function doOptimizeComposer() : void
358
    {
359
        $this->log('<h2>Optimizing Composer autoloader</>');
360
        $this->runRemote(sprintf('%s dump-autoload %s', $this->getConfig(Option::remoteComposerBinaryPath), $this->getConfig(Option::composerOptimizeFlags)));
361
    }
362
363
    private function doCreateSymlink() : void
364
    {
365
        $this->log('<h2>Updating the symlink</>');
366
        $this->runRemote('rm -f {{ deploy_dir }}/current && ln -s {{ project_dir }} {{ deploy_dir }}/current');
367
    }
368
369
    private function doResetOpCache() : void
370
    {
371
        if (null === $homepageUrl = $this->getConfig(Option::resetOpCacheFor)) {
372
            return;
373
        }
374
375
        $this->log('<h2>Resetting the OPcache contents</>');
376
        $phpScriptPath = sprintf('__easy_deploy_opcache_reset_%s.php', bin2hex(random_bytes(8)));
377
        $this->runRemote(sprintf('echo "<?php opcache_reset();" > {{ web_dir }}/%s && wget %s/%s && rm -f {{ web_dir }}/%s', $phpScriptPath, $homepageUrl, $phpScriptPath, $phpScriptPath));
378
    }
379
380
    private function doKeepReleases() : void
381
    {
382
        if (-1 === $this->getConfig(Option::keepReleases)) {
383
            $this->log('<h3>No releases to delete</>');
384
385
            return;
386
        }
387
388
        $results = $this->runRemote('ls -1 {{ deploy_dir }}/releases');
389
        foreach ($results as $result) {
390
            $this->deleteOldReleases($result->getServer(), explode("\n", $result->getTrimmedOutput()));
391
        }
392
    }
393
394
    private function deleteOldReleases(Server $server, array $releaseDirs) : void
395
    {
396
        foreach ($releaseDirs as $releaseDir) {
397
            if (!preg_match('/\d{14}/', $releaseDir)) {
398
                $this->log(sprintf('[<server>%s</>] Skipping cleanup of old releases; unexpected "%s" directory found (all directory names should be timestamps)', $server, $releaseDir));
399
400
                return;
401
            }
402
        }
403
404
        if (count($releaseDirs) <= $this->getConfig(Option::keepReleases)) {
405
            $this->log(sprintf('[<server>%s</>] No releases to delete (there are %d releases and the config keeps %d releases).', $server, count($releaseDirs), $this->getConfig(Option::keepReleases)));
406
407
            return;
408
        }
409
410
        $relativeDirsToRemove = array_slice($releaseDirs, 0, -$this->getConfig(Option::keepReleases));
411
        $absoluteDirsToRemove = array_map(function ($v) {
412
            return sprintf('%s/releases/%s', $this->getConfig(Option::deployDir), $v);
413
        }, $relativeDirsToRemove);
414
415
        // the command must be run only on one server because the timestamps are
416
        // different for all servers, even when they belong to the same deploy and
417
        // because new servers may have been added to the deploy and old releases don't exist on them
418
        $this->log(sprintf('Deleting these old release directories: %s', implode(', ', $absoluteDirsToRemove)));
419
        $this->safeDelete($server, $absoluteDirsToRemove);
420
    }
421
}
422