Completed
Pull Request — master (#594)
by Mateusz
03:09
created

CapistranoDeploymentBackend::setJob()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 4
rs 10
cc 1
eloc 3
nc 1
nop 1
1
<?php
2
use \Symfony\Component\Process\Process;
3
4
class CapistranoDeploymentBackend extends Object implements DeploymentBackend {
5
6
	protected $packageGenerator;
7
8
	protected $job;
9
10
	public function getPackageGenerator() {
11
		return $this->packageGenerator;
12
	}
13
14
	public function setPackageGenerator(PackageGenerator $packageGenerator) {
15
		$this->packageGenerator = $packageGenerator;
16
	}
17
18
	public function getJob() {
19
		return $this->job;
20
	}
21
22
	public function setJob(DeploynautJob $job) {
23
		$this->job = $job;
24
		return $this;
25
	}
26
27
	/**
28
	 * Create a deployment strategy.
29
	 *
30
	 * @param DNEnvironment $environment
31
	 * @param array $options
32
	 *
33
	 * @return DeploymentStrategy
34
	 */
35
	public function planDeploy(DNEnvironment $environment, $options) {
36
		$strategy = new DeploymentStrategy($environment, $options);
37
38
		$currentBuild = $environment->CurrentBuild();
39
		$currentSha = $currentBuild ? $currentBuild->SHA : '-';
40
		if($currentSha !== $options['sha']) {
41
			$strategy->setChange('Code version', $currentSha, $options['sha']);
42
		}
43
		$strategy->setActionTitle('Confirm deployment');
44
		$strategy->setActionCode('fast');
45
		$strategy->setEstimatedTime('2');
46
47
		return $strategy;
48
	}
49
50
	/**
51
	 * Deploy the given build to the given environment.
52
	 *
53
	 * @param DNEnvironment $environment
54
	 * @param DeploynautLogFile $log
55
	 * @param DNProject $project
56
	 * @param array $options
57
	 */
58
	public function deploy(
59
		DNEnvironment $environment,
60
		DeploynautLogFile $log,
61
		DNProject $project,
62
		$options
63
	) {
64
		$name = $environment->getFullName();
65
		$repository = $project->getLocalCVSPath();
66
		$sha = $options['sha'];
67
		$url = $environment->getBareURL();
68
69
		$args = array(
70
			'branch' => $sha,
71
			'repository' => $repository,
72
		);
73
74
		$this->extend('deployStart', $environment, $sha, $log, $project);
75
76
		$log->write(sprintf('Deploying "%s" to "%s"', $sha, $name));
77
78
		$this->enableMaintenance($environment, $log, $project);
79
80
		// Use a package generator if specified, otherwise run a direct deploy, which is the default behaviour
81
		// if build_filename isn't specified
82
		if($this->packageGenerator) {
83
			$log->write(sprintf('Using package generator "%s"', get_class($this->packageGenerator)));
84
85
			try {
86
				$args['build_filename'] = $this->packageGenerator->getPackageFilename($project->Name, $sha, $repository, $log);
87
			} catch (Exception $e) {
88
				$log->write($e->getMessage());
89
				throw $e;
90
			}
91
92
			if(empty($args['build_filename'])) {
93
				throw new RuntimeException('Failed to generate package.');
94
			}
95
		}
96
97
		$command = $this->getCommand('deploy', 'web', $environment, $args, $log);
98
		$command->run(function($type, $buffer) use($log) {
99
			$log->write($buffer);
100
		});
101
102
		// Deployment cleanup. We assume it is always safe to run this at the end, regardless of the outcome.
103
		$self = $this;
104
		$cleanupFn = function() use($self, $environment, $args, $log, $sha, $project) {
105
			$command = $self->getCommand('deploy:cleanup', 'web', $environment, $args, $log);
106
			$command->run(function($type, $buffer) use($log) {
107
				$log->write($buffer);
108
			});
109
110
			if(!$command->isSuccessful()) {
111
				$self->extend('cleanupFailure', $environment, $sha, $log, $project);
112
				$log->write('Warning: Cleanup failed, but fine to continue. Needs manual cleanup sometime.');
113
			}
114
		};
115
116
		// Once the deployment has run it's necessary to update the maintenance page status
117
		if(!empty($options['leaveMaintenancePage'])) {
118
			$this->enableMaintenance($environment, $log, $project);
119
		}
120
121
		if ($url) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $url of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
122
			$runner = $this->getJob()->runOp(
123
				'Smoketest',
124
				new \SilverStripe\Platform\Core\Rainforest\Anthill\Operations\Params\Smoketest([
125
					'url' => $url,
126
				]),
127
				new DeploynautPsrOutputAdapter($log)
128
			);
129
			if ($runner->isSuccessful()) {
130
				$log->write(sprintf('Smoketest to "%s" succeeded.', $url));
131
132
			} else {
133
				$log->write('Warning: smoketest has failed.');
134
				$log->write($runner->getFailureException()->getMessage());
135
			}
136
		}
137
138
		if(!$command->isSuccessful()) {
139
			$cleanupFn();
140
			$this->extend('deployFailure', $environment, $sha, $log, $project);
141
			throw new RuntimeException($command->getErrorOutput());
142
		}
143
144
		// Check if maintenance page should be removed
145
		if(empty($options['leaveMaintenancePage'])) {
146
			$this->disableMaintenance($environment, $log, $project);
147
		}
148
149
		$cleanupFn();
150
151
		$log->write(sprintf('Deploy of "%s" to "%s" finished', $sha, $name));
152
153
		$this->extend('deployEnd', $environment, $sha, $log, $project);
154
	}
155
156
	/**
157
	 * Enable a maintenance page for the given environment using the maintenance:enable Capistrano task.
158
	 */
159 View Code Duplication
	public function enableMaintenance(DNEnvironment $environment, DeploynautLogFile $log, DNProject $project) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
160
		$name = $environment->getFullName();
161
		$command = $this->getCommand('maintenance:enable', 'web', $environment, null, $log);
162
		$command->run(function($type, $buffer) use($log) {
163
			$log->write($buffer);
164
		});
165
		if(!$command->isSuccessful()) {
166
			$this->extend('maintenanceEnableFailure', $environment, $log);
167
			throw new RuntimeException($command->getErrorOutput());
168
		}
169
		$log->write(sprintf('Maintenance page enabled on "%s"', $name));
170
	}
171
172
	/**
173
	 * Disable the maintenance page for the given environment using the maintenance:disable Capistrano task.
174
	 */
175 View Code Duplication
	public function disableMaintenance(DNEnvironment $environment, DeploynautLogFile $log, DNProject $project) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
176
		$name = $environment->getFullName();
177
		$command = $this->getCommand('maintenance:disable', 'web', $environment, null, $log);
178
		$command->run(function($type, $buffer) use($log) {
179
			$log->write($buffer);
180
		});
181
		if(!$command->isSuccessful()) {
182
			$this->extend('maintenanceDisableFailure', $environment, $log);
183
			throw new RuntimeException($command->getErrorOutput());
184
		}
185
		$log->write(sprintf('Maintenance page disabled on "%s"', $name));
186
	}
187
188
	/**
189
	 * Check the status using the deploy:check capistrano method
190
	 */
191
	public function ping(DNEnvironment $environment, DeploynautLogFile $log, DNProject $project) {
192
		$command = $this->getCommand('deploy:check', 'web', $environment, null, $log);
193
		$command->run(function($type, $buffer) use($log) {
194
			$log->write($buffer);
195
			echo $buffer;
196
		});
197
	}
198
199
	/**
200
	 * @inheritdoc
201
	 */
202
	public function dataTransfer(DNDataTransfer $dataTransfer, DeploynautLogFile $log) {
203
		if($dataTransfer->Direction == 'get') {
204
			$this->dataTransferBackup($dataTransfer, $log);
205
		} else {
206
			$environment = $dataTransfer->Environment();
207
			$project = $environment->Project();
208
			$workingDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID;
209
			$archive = $dataTransfer->DataArchive();
210
211
			// extract the sspak contents, we'll need these so capistrano can restore that content
212
			try {
213
				$archive->extractArchive($workingDir);
214
			} catch(Exception $e) {
215
				$log->write($e->getMessage());
216
				throw new RuntimeException($e->getMessage());
217
			}
218
219
			// validate the contents match the requested transfer mode
220
			$result = $archive->validateArchiveContents($dataTransfer->Mode);
221
			if(!$result->valid()) {
222
				// do some cleaning, get rid of the extracted archive lying around
223
				$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
224
				$process->setTimeout(120);
225
				$process->run();
226
227
				// log the reason why we can't restore the snapshot and halt the process
228
				$log->write($result->message());
229
				throw new RuntimeException($result->message());
230
			}
231
232
			// Put up a maintenance page during a restore of db or assets.
233
			$this->enableMaintenance($environment, $log, $project);
234
			$this->dataTransferRestore($workingDir, $dataTransfer, $log);
235
			$this->disableMaintenance($environment, $log, $project);
236
		}
237
	}
238
239
	/**
240
	 * @param string $action Capistrano action to be executed
241
	 * @param string $roles Defining a server role is required to target only the required servers.
242
	 * @param DNEnvironment $environment
243
	 * @param array<string>|null $args Additional arguments for process
244
	 * @param DeploynautLogFile $log
245
	 * @return \Symfony\Component\Process\Process
246
	 */
247
	public function getCommand($action, $roles, DNEnvironment $environment, $args = null, DeploynautLogFile $log) {
248
		$name = $environment->getFullName();
249
		$env = $environment->Project()->getProcessEnv();
250
251
		if(!$args) {
252
			$args = array();
253
		}
254
		$args['history_path'] = realpath(DEPLOYNAUT_LOG_PATH . '/');
255
		$args['environment_id'] = $environment->ID;
256
257
		// Inject env string directly into the command.
258
		// Capistrano doesn't like the $process->setEnv($env) we'd normally do below.
259
		$envString = '';
260
		if(!empty($env)) {
261
			$envString .= 'env ';
262
			foreach($env as $key => $value) {
263
				$envString .= "$key=\"$value\" ";
264
			}
265
		}
266
267
		$data = DNData::inst();
268
		// Generate a capfile from a template
269
		$capTemplate = file_get_contents(BASE_PATH . '/deploynaut/Capfile.template');
270
		$cap = str_replace(
271
			array('<config root>', '<ssh key>', '<base path>'),
272
			array($data->getEnvironmentDir(), DEPLOYNAUT_SSH_KEY, BASE_PATH),
273
			$capTemplate
274
		);
275
276
		if(defined('DEPLOYNAUT_CAPFILE')) {
277
			$capFile = DEPLOYNAUT_CAPFILE;
278
		} else {
279
			$capFile = ASSETS_PATH . '/Capfile';
280
		}
281
		file_put_contents($capFile, $cap);
282
283
		$command = "{$envString}cap -f " . escapeshellarg($capFile) . " -vv $name $action ROLES=$roles";
284
		foreach($args as $argName => $argVal) {
285
			$command .= ' -s ' . escapeshellarg($argName) . '=' . escapeshellarg($argVal);
286
		}
287
288
		$log->write(sprintf('Running command: %s', $command));
289
290
		$process = new Process($command);
291
		$process->setTimeout(3600);
292
		return $process;
293
	}
294
295
	/**
296
	 * Backs up database and/or assets to a designated folder,
297
	 * and packs up the files into a single sspak.
298
	 *
299
	 * @param DNDataTransfer    $dataTransfer
300
	 * @param DeploynautLogFile $log
301
	 */
302
	protected function dataTransferBackup(DNDataTransfer $dataTransfer, DeploynautLogFile $log) {
303
		$environment = $dataTransfer->Environment();
304
		$name = $environment->getFullName();
305
306
		// Associate a new archive with the transfer.
307
		// Doesn't retrieve a filepath just yet, need to generate the files first.
308
		$dataArchive = DNDataArchive::create();
309
		$dataArchive->Mode = $dataTransfer->Mode;
310
		$dataArchive->AuthorID = $dataTransfer->AuthorID;
311
		$dataArchive->OriginalEnvironmentID = $environment->ID;
312
		$dataArchive->EnvironmentID = $environment->ID;
313
		$dataArchive->IsBackup = $dataTransfer->IsBackupDataTransfer();
314
315
		// Generate directory structure with strict permissions (contains very sensitive data)
316
		$filepathBase = $dataArchive->generateFilepath($dataTransfer);
317
		mkdir($filepathBase, 0700, true);
318
319
		$databasePath = $filepathBase . DIRECTORY_SEPARATOR . 'database.sql';
320
321
		// Backup database
322 View Code Duplication
		if(in_array($dataTransfer->Mode, array('all', 'db'))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
323
			$log->write(sprintf('Backup of database from "%s" started', $name));
324
			$command = $this->getCommand('data:getdb', 'db', $environment, array('data_path' => $databasePath), $log);
325
			$command->run(function($type, $buffer) use($log) {
326
				$log->write($buffer);
327
			});
328
			if(!$command->isSuccessful()) {
329
				$this->extend('dataTransferFailure', $environment, $log);
330
				throw new RuntimeException($command->getErrorOutput());
331
			}
332
			$log->write(sprintf('Backup of database from "%s" done', $name));
333
		}
334
335
		// Backup assets
336 View Code Duplication
		if(in_array($dataTransfer->Mode, array('all', 'assets'))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
337
			$log->write(sprintf('Backup of assets from "%s" started', $name));
338
			$command = $this->getCommand('data:getassets', 'web', $environment, array('data_path' => $filepathBase), $log);
339
			$command->run(function($type, $buffer) use($log) {
340
				$log->write($buffer);
341
			});
342
			if(!$command->isSuccessful()) {
343
				$this->extend('dataTransferFailure', $environment, $log);
344
				throw new RuntimeException($command->getErrorOutput());
345
			}
346
			$log->write(sprintf('Backup of assets from "%s" done', $name));
347
		}
348
349
		// ensure the database connection is re-initialised, which is needed if the transfer
350
		// above took a really long time because the handle to the db may have become invalid.
351
		global $databaseConfig;
352
		DB::connect($databaseConfig);
353
354
		$log->write('Creating sspak...');
355
356
		$sspakFilename = sprintf('%s.sspak', $dataArchive->generateFilename($dataTransfer));
357
		$sspakFilepath = $filepathBase . DIRECTORY_SEPARATOR . $sspakFilename;
358
359
		try {
360
			$dataArchive->attachFile($sspakFilepath, $dataTransfer);
361
			$dataArchive->setArchiveFromFiles($filepathBase);
362
		} catch(Exception $e) {
363
			$log->write($e->getMessage());
364
			throw new RuntimeException($e->getMessage());
365
		}
366
367
		// Remove any assets and db files lying around, they're not longer needed as they're now part
368
		// of the sspak file we just generated. Use --force to avoid errors when files don't exist,
369
		// e.g. when just an assets backup has been requested and no database.sql exists.
370
		$process = new Process(sprintf('rm -rf %s/assets && rm -f %s', escapeshellarg($filepathBase), escapeshellarg($databasePath)));
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 128 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
371
		$process->setTimeout(120);
372
		$process->run();
373
		if(!$process->isSuccessful()) {
374
			$log->write('Could not delete temporary files');
375
			throw new RuntimeException($process->getErrorOutput());
376
		}
377
378
		$log->write(sprintf('Creating sspak file done: %s', $dataArchive->ArchiveFile()->getAbsoluteURL()));
379
	}
380
381
	/**
382
	 * Utility function for triggering the db rebuild and flush.
383
	 * Also cleans up and generates new error pages.
384
	 * @param DeploynautLogFile $log
385
	 */
386 View Code Duplication
	public function rebuild(DNEnvironment $environment, $log) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
387
		$name = $environment->getFullName();
388
		$command = $this->getCommand('deploy:migrate', 'web', $environment, null, $log);
389
		$command->run(function($type, $buffer) use($log) {
390
			$log->write($buffer);
391
		});
392
		if(!$command->isSuccessful()) {
393
			$log->write(sprintf('Rebuild of "%s" failed: %s', $name, $command->getErrorOutput()));
394
			throw new RuntimeException($command->getErrorOutput());
395
		}
396
		$log->write(sprintf('Rebuild of "%s" done', $name));
397
	}
398
399
	/**
400
	 * Extracts a *.sspak file referenced through the passed in $dataTransfer
401
	 * and pushes it to the environment referenced in $dataTransfer.
402
	 *
403
	 * @param string $workingDir Directory for the unpacked files.
404
	 * @param DNDataTransfer $dataTransfer
405
	 * @param DeploynautLogFile $log
406
	 */
407
	protected function dataTransferRestore($workingDir, DNDataTransfer $dataTransfer, DeploynautLogFile $log) {
408
		$environment = $dataTransfer->Environment();
409
		$name = $environment->getFullName();
410
411
		// Rollback cleanup.
412
		$self = $this;
413 View Code Duplication
		$cleanupFn = function() use($self, $workingDir, $environment, $log) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
414
			// Rebuild makes sense even if failed - maybe we can at least partly recover.
415
			$self->rebuild($environment, $log);
416
			$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
417
			$process->setTimeout(120);
418
			$process->run();
419
		};
420
421
		// Restore database into target environment
422 View Code Duplication
		if(in_array($dataTransfer->Mode, array('all', 'db'))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
423
			$log->write(sprintf('Restore of database to "%s" started', $name));
424
			$args = array('data_path' => $workingDir . DIRECTORY_SEPARATOR . 'database.sql');
425
			$command = $this->getCommand('data:pushdb', 'db', $environment, $args, $log);
426
			$command->run(function($type, $buffer) use($log) {
427
				$log->write($buffer);
428
			});
429
			if(!$command->isSuccessful()) {
430
				$cleanupFn();
431
				$log->write(sprintf('Restore of database to "%s" failed: %s', $name, $command->getErrorOutput()));
432
				$this->extend('dataTransferFailure', $environment, $log);
433
				throw new RuntimeException($command->getErrorOutput());
434
			}
435
			$log->write(sprintf('Restore of database to "%s" done', $name));
436
		}
437
438
		// Restore assets into target environment
439 View Code Duplication
		if(in_array($dataTransfer->Mode, array('all', 'assets'))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
440
			$log->write(sprintf('Restore of assets to "%s" started', $name));
441
			$args = array('data_path' => $workingDir . DIRECTORY_SEPARATOR . 'assets');
442
			$command = $this->getCommand('data:pushassets', 'web', $environment, $args, $log);
443
			$command->run(function($type, $buffer) use($log) {
444
				$log->write($buffer);
445
			});
446
			if(!$command->isSuccessful()) {
447
				$cleanupFn();
448
				$log->write(sprintf('Restore of assets to "%s" failed: %s', $name, $command->getErrorOutput()));
449
				$this->extend('dataTransferFailure', $environment, $log);
450
				throw new RuntimeException($command->getErrorOutput());
451
			}
452
			$log->write(sprintf('Restore of assets to "%s" done', $name));
453
		}
454
455
		$log->write('Rebuilding and cleaning up');
456
		$cleanupFn();
457
	}
458
459
}
460