Completed
Pull Request — master (#488)
by Helpful
1295:51 queued 1292:33
created

CapistranoDeploymentBackend::deploy()   C

Complexity

Conditions 8
Paths 14

Size

Total Lines 79
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 79
rs 6.0572
cc 8
eloc 45
nc 14
nop 4

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
use \Symfony\Component\Process\Process;
3
4
class CapistranoDeploymentBackend extends Object implements DeploymentBackend {
5
6
	protected $packageGenerator;
7
8
	public function getPackageGenerator() {
9
		return $this->packageGenerator;
10
	}
11
12
	public function setPackageGenerator(PackageGenerator $packageGenerator) {
13
		$this->packageGenerator = $packageGenerator;
14
	}
15
16
	/**
17
	 * Create a deployment strategy.
18
	 *
19
	 * @param DNEnvironment $environment
20
	 * @param array $options
21
	 *
22
	 * @return DeploymentStrategy
23
	 */
24
	public function planDeploy(DNEnvironment $environment, $options) {
25
		$strategy = new DeploymentStrategy($environment, $options);
26
27
		$currentBuild = $environment->CurrentBuild();
28
		$currentSha = $currentBuild ? $currentBuild->SHA : '-';
29
		if($currentSha !== $options['sha']) {
30
			$strategy->setChange('Code version', $currentSha, $options['sha']);
31
		}
32
		$strategy->setActionTitle('Confirm deployment');
33
		$strategy->setActionCode('fast');
34
		$strategy->setEstimatedTime('2');
35
36
		return $strategy;
37
	}
38
39
	/**
40
	 * Deploy the given build to the given environment.
41
	 *
42
	 * @param DNEnvironment $environment
43
	 * @param DeploynautLogFile $log
44
	 * @param DNProject $project
45
	 * @param array $options
46
	 */
47
	public function deploy(
48
		DNEnvironment $environment,
49
		DeploynautLogFile $log,
50
		DNProject $project,
51
		$options
52
	) {
53
		$name = $environment->getFullName();
54
		$repository = $project->getLocalCVSPath();
55
		$sha = $options['sha'];
56
57
		$args = array(
58
			'branch' => $sha,
59
			'repository' => $repository,
60
		);
61
62
		$this->extend('deployStart', $environment, $sha, $log, $project);
63
64
		$log->write(sprintf('Deploying "%s" to "%s"', $sha, $name));
65
66
		$this->enableMaintenance($environment, $log, $project);
67
68
		// Use a package generator if specified, otherwise run a direct deploy, which is the default behaviour
69
		// if build_filename isn't specified
70
		if($this->packageGenerator) {
71
			$log->write(sprintf('Using package generator "%s"', get_class($this->packageGenerator)));
72
73
			try {
74
				$args['build_filename'] = $this->packageGenerator->getPackageFilename($project->Name, $sha, $repository, $log);
75
			} catch (Exception $e) {
76
				$log->write($e->getMessage());
77
				throw $e;
78
			}
79
80
			if(empty($args['build_filename'])) {
81
				throw new RuntimeException('Failed to generate package.');
82
			}
83
		}
84
85
		$command = $this->getCommand('deploy', 'web', $environment, $args, $log);
86
		$command->run(function($type, $buffer) use($log) {
87
			$log->write($buffer);
88
		});
89
90
		// Deployment cleanup. We assume it is always safe to run this at the end, regardless of the outcome.
91
		$self = $this;
92
		$cleanupFn = function() use($self, $environment, $args, $log) {
93
			$command = $self->getCommand('deploy:cleanup', 'web', $environment, $args, $log);
94
			$command->run(function($type, $buffer) use($log) {
95
				$log->write($buffer);
96
			});
97
98
			if(!$command->isSuccessful()) {
99
				$this->extend('cleanupFailure', $environment, $sha, $log, $project);
100
				$log->write('Warning: Cleanup failed, but fine to continue. Needs manual cleanup sometime.');
101
			}
102
		};
103
104
		// Once the deployment has run it's necessary to update the maintenance page status
105
		if(!empty($options['leaveMaintenancePage'])) {
106
			$this->enableMaintenance($environment, $log, $project);
107
		}
108
109
		if(!$command->isSuccessful()) {
110
			$cleanupFn();
111
			$this->extend('deployFailure', $environment, $sha, $log, $project);
112
			throw new RuntimeException($command->getErrorOutput());
113
		}
114
115
		// Check if maintenance page should be removed
116
		if(empty($options['leaveMaintenancePage'])) {
117
			$this->disableMaintenance($environment, $log, $project);
118
		}
119
120
		$cleanupFn();
121
122
		$log->write(sprintf('Deploy of "%s" to "%s" finished', $sha, $name));
123
124
		$this->extend('deployEnd', $environment, $sha, $log, $project);
125
	}
126
127
	/**
128
	 * Enable a maintenance page for the given environment using the maintenance:enable Capistrano task.
129
	 */
130 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...
131
		$name = $environment->getFullName();
132
		$command = $this->getCommand('maintenance:enable', 'web', $environment, null, $log);
133
		$command->run(function($type, $buffer) use($log) {
134
			$log->write($buffer);
135
		});
136
		if(!$command->isSuccessful()) {
137
			$this->extend('maintenanceEnableFailure', $environment, $log);
138
			throw new RuntimeException($command->getErrorOutput());
139
		}
140
		$log->write(sprintf('Maintenance page enabled on "%s"', $name));
141
	}
142
143
	/**
144
	 * Disable the maintenance page for the given environment using the maintenance:disable Capistrano task.
145
	 */
146 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...
147
		$name = $environment->getFullName();
148
		$command = $this->getCommand('maintenance:disable', 'web', $environment, null, $log);
149
		$command->run(function($type, $buffer) use($log) {
150
			$log->write($buffer);
151
		});
152
		if(!$command->isSuccessful()) {
153
			$this->extend('maintenanceDisableFailure', $environment, $log);
154
			throw new RuntimeException($command->getErrorOutput());
155
		}
156
		$log->write(sprintf('Maintenance page disabled on "%s"', $name));
157
	}
158
159
	/**
160
	 * Check the status using the deploy:check capistrano method
161
	 */
162
	public function ping(DNEnvironment $environment, DeploynautLogFile $log, DNProject $project) {
163
		$command = $this->getCommand('deploy:check', 'web', $environment, null, $log);
164
		$command->run(function($type, $buffer) use($log) {
165
			$log->write($buffer);
166
			echo $buffer;
167
		});
168
	}
169
170
	/**
171
	 * @inheritdoc
172
	 */
173
	public function dataTransfer(DNDataTransfer $dataTransfer, DeploynautLogFile $log) {
174
		if($dataTransfer->Direction == 'get') {
175
			$this->dataTransferBackup($dataTransfer, $log);
176
		} else {
177
			$environment = $dataTransfer->Environment();
178
			$project = $environment->Project();
179
			$workingDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID;
180
			$archive = $dataTransfer->DataArchive();
181
182
			// extract the sspak contents, we'll need these so capistrano can restore that content
183
			try {
184
				$archive->extractArchive($workingDir);
185
			} catch(Exception $e) {
186
				$log->write($e->getMessage());
187
				throw new RuntimeException($e->getMessage());
188
			}
189
190
			// validate the contents match the requested transfer mode
191
			$result = $archive->validateArchiveContents($dataTransfer->Mode);
192
			if(!$result->valid()) {
193
				// do some cleaning, get rid of the extracted archive lying around
194
				$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
195
				$process->run();
196
197
				// log the reason why we can't restore the snapshot and halt the process
198
				$log->write($result->message());
199
				throw new RuntimeException($result->message());
200
			}
201
202
			// Put up a maintenance page during a restore of db or assets.
203
			$this->enableMaintenance($environment, $log, $project);
204
			$this->dataTransferRestore($workingDir, $dataTransfer, $log);
205
			$this->disableMaintenance($environment, $log, $project);
206
		}
207
	}
208
209
	/**
210
	 * @param string $action Capistrano action to be executed
211
	 * @param string $roles Defining a server role is required to target only the required servers.
212
	 * @param DNEnvironment $environment
213
	 * @param array<string>|null $args Additional arguments for process
214
	 * @param DeploynautLogFile $log
215
	 * @return \Symfony\Component\Process\Process
216
	 */
217
	public function getCommand($action, $roles, DNEnvironment $environment, $args = null, DeploynautLogFile $log) {
218
		$name = $environment->getFullName();
219
		$env = $environment->Project()->getProcessEnv();
220
221
		if(!$args) {
222
			$args = array();
223
		}
224
		$args['history_path'] = realpath(DEPLOYNAUT_LOG_PATH . '/');
225
226
		// Inject env string directly into the command.
227
		// Capistrano doesn't like the $process->setEnv($env) we'd normally do below.
228
		$envString = '';
229
		if(!empty($env)) {
230
			$envString .= 'env ';
231
			foreach($env as $key => $value) {
232
				$envString .= "$key=\"$value\" ";
233
			}
234
		}
235
236
		$data = DNData::inst();
237
		// Generate a capfile from a template
238
		$capTemplate = file_get_contents(BASE_PATH . '/deploynaut/Capfile.template');
239
		$cap = str_replace(
240
			array('<config root>', '<ssh key>', '<base path>'),
241
			array($data->getEnvironmentDir(), DEPLOYNAUT_SSH_KEY, BASE_PATH),
242
			$capTemplate
243
		);
244
245
		if(defined('DEPLOYNAUT_CAPFILE')) {
246
			$capFile = DEPLOYNAUT_CAPFILE;
247
		} else {
248
			$capFile = ASSETS_PATH . '/Capfile';
249
		}
250
		file_put_contents($capFile, $cap);
251
252
		$command = "{$envString}cap -f " . escapeshellarg($capFile) . " -vv $name $action ROLES=$roles";
253
		foreach($args as $argName => $argVal) {
254
			$command .= ' -s ' . escapeshellarg($argName) . '=' . escapeshellarg($argVal);
255
		}
256
257
		$log->write(sprintf('Running command: %s', $command));
258
259
		$process = new Process($command);
260
		$process->setTimeout(3600);
261
		return $process;
262
	}
263
264
	/**
265
	 * Backs up database and/or assets to a designated folder,
266
	 * and packs up the files into a single sspak.
267
	 *
268
	 * @param DNDataTransfer    $dataTransfer
269
	 * @param DeploynautLogFile $log
270
	 */
271
	protected function dataTransferBackup(DNDataTransfer $dataTransfer, DeploynautLogFile $log) {
272
		$environment = $dataTransfer->Environment();
273
		$name = $environment->getFullName();
274
275
		// Associate a new archive with the transfer.
276
		// Doesn't retrieve a filepath just yet, need to generate the files first.
277
		$dataArchive = DNDataArchive::create();
278
		$dataArchive->Mode = $dataTransfer->Mode;
279
		$dataArchive->AuthorID = $dataTransfer->AuthorID;
280
		$dataArchive->OriginalEnvironmentID = $environment->ID;
281
		$dataArchive->EnvironmentID = $environment->ID;
282
		$dataArchive->IsBackup = $dataTransfer->IsBackupDataTransfer();
283
284
		// Generate directory structure with strict permissions (contains very sensitive data)
285
		$filepathBase = $dataArchive->generateFilepath($dataTransfer);
286
		mkdir($filepathBase, 0700, true);
287
288
		$databasePath = $filepathBase . DIRECTORY_SEPARATOR . 'database.sql';
289
290
		// Backup database
291 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...
292
			$log->write(sprintf('Backup of database from "%s" started', $name));
293
			$command = $this->getCommand('data:getdb', 'db', $environment, array('data_path' => $databasePath), $log);
294
			$command->run(function($type, $buffer) use($log) {
295
				$log->write($buffer);
296
			});
297
			if(!$command->isSuccessful()) {
298
				$this->extend('dataTransferFailure', $environment, $log);
299
				throw new RuntimeException($command->getErrorOutput());
300
			}
301
			$log->write(sprintf('Backup of database from "%s" done', $name));
302
		}
303
304
		// Backup assets
305 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...
306
			$log->write(sprintf('Backup of assets from "%s" started', $name));
307
			$command = $this->getCommand('data:getassets', 'web', $environment, array('data_path' => $filepathBase), $log);
308
			$command->run(function($type, $buffer) use($log) {
309
				$log->write($buffer);
310
			});
311
			if(!$command->isSuccessful()) {
312
				$this->extend('dataTransferFailure', $environment, $log);
313
				throw new RuntimeException($command->getErrorOutput());
314
			}
315
			$log->write(sprintf('Backup of assets from "%s" done', $name));
316
		}
317
318
		$sspakFilename = sprintf('%s.sspak', $dataArchive->generateFilename($dataTransfer));
319
		$sspakFilepath = $filepathBase . DIRECTORY_SEPARATOR . $sspakFilename;
320
321
		try {
322
			$dataArchive->attachFile($sspakFilepath, $dataTransfer);
323
			$dataArchive->setArchiveFromFiles($filepathBase);
324
		} catch(Exception $e) {
325
			$log->write($e->getMessage());
326
			throw new RuntimeException($e->getMessage());
327
		}
328
329
		// Remove any assets and db files lying around, they're not longer needed as they're now part
330
		// of the sspak file we just generated. Use --force to avoid errors when files don't exist,
331
		// e.g. when just an assets backup has been requested and no database.sql exists.
332
		$process = new Process(sprintf('rm -rf %s/assets && rm -f %s', $filepathBase, $databasePath));
333
		$process->run();
334
		if(!$process->isSuccessful()) {
335
			$log->write('Could not delete temporary files');
336
			throw new RuntimeException($process->getErrorOutput());
337
		}
338
339
		$log->write(sprintf('Creating sspak file done: %s', $dataArchive->ArchiveFile()->getAbsoluteURL()));
340
	}
341
342
	/**
343
	 * Utility function for triggering the db rebuild and flush.
344
	 * Also cleans up and generates new error pages.
345
	 * @param DeploynautLogFile $log
346
	 */
347 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...
348
		$name = $environment->getFullName();
349
		$command = $this->getCommand('deploy:migrate', 'web', $environment, null, $log);
350
		$command->run(function($type, $buffer) use($log) {
351
			$log->write($buffer);
352
		});
353
		if(!$command->isSuccessful()) {
354
			$log->write(sprintf('Rebuild of "%s" failed: %s', $name, $command->getErrorOutput()));
355
			throw new RuntimeException($command->getErrorOutput());
356
		}
357
		$log->write(sprintf('Rebuild of "%s" done', $name));
358
	}
359
360
	/**
361
	 * Extracts a *.sspak file referenced through the passed in $dataTransfer
362
	 * and pushes it to the environment referenced in $dataTransfer.
363
	 *
364
	 * @param string $workingDir Directory for the unpacked files.
365
	 * @param DNDataTransfer $dataTransfer
366
	 * @param DeploynautLogFile $log
367
	 */
368
	protected function dataTransferRestore($workingDir, DNDataTransfer $dataTransfer, DeploynautLogFile $log) {
369
		$environment = $dataTransfer->Environment();
370
		$name = $environment->getFullName();
371
372
		// Rollback cleanup.
373
		$self = $this;
374
		$cleanupFn = function() use($self, $workingDir, $environment, $log) {
375
			// Rebuild makes sense even if failed - maybe we can at least partly recover.
376
			$self->rebuild($environment, $log);
377
			$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
378
			$process->run();
379
		};
380
381
		// Restore database into target environment
382 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...
383
			$log->write(sprintf('Restore of database to "%s" started', $name));
384
			$args = array('data_path' => $workingDir . DIRECTORY_SEPARATOR . 'database.sql');
385
			$command = $this->getCommand('data:pushdb', 'db', $environment, $args, $log);
386
			$command->run(function($type, $buffer) use($log) {
387
				$log->write($buffer);
388
			});
389
			if(!$command->isSuccessful()) {
390
				$cleanupFn();
391
				$log->write(sprintf('Restore of database to "%s" failed: %s', $name, $command->getErrorOutput()));
392
				$this->extend('dataTransferFailure', $environment, $log);
393
				throw new RuntimeException($command->getErrorOutput());
394
			}
395
			$log->write(sprintf('Restore of database to "%s" done', $name));
396
		}
397
398
		// Restore assets into target environment
399 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...
400
			$log->write(sprintf('Restore of assets to "%s" started', $name));
401
			$args = array('data_path' => $workingDir . DIRECTORY_SEPARATOR . 'assets');
402
			$command = $this->getCommand('data:pushassets', 'web', $environment, $args, $log);
403
			$command->run(function($type, $buffer) use($log) {
404
				$log->write($buffer);
405
			});
406
			if(!$command->isSuccessful()) {
407
				$cleanupFn();
408
				$log->write(sprintf('Restore of assets to "%s" failed: %s', $name, $command->getErrorOutput()));
409
				$this->extend('dataTransferFailure', $environment, $log);
410
				throw new RuntimeException($command->getErrorOutput());
411
			}
412
			$log->write(sprintf('Restore of assets to "%s" done', $name));
413
		}
414
415
		$log->write('Rebuilding and cleaning up');
416
		$cleanupFn();
417
	}
418
419
}
420