Completed
Pull Request — master (#579)
by Mateusz
03:31
created

DNDeployment::ResqueStatus()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
dl 0
loc 3
rs 10
c 2
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
use Finite\State\StateInterface;
4
5
/**
6
 * Class representing a single deplyoment (passed or failed) at a time to a particular environment
7
 *
8
 * @property string $SHA
9
 * @property string $ResqueToken
10
 * @property string $State
11
 *
12
 * @method DNEnvironment Environment()
13
 * @property int EnvironmentID
14
 * @method Member Deployer()
15
 * @property int DeployerID
16
 */
17
class DNDeployment extends DataObject implements Finite\StatefulInterface, HasStateMachine {
18
19
	const STATE_NEW = 'New';
20
	const STATE_SUBMITTED = 'Submitted';
21
	const STATE_INVALID = 'Invalid';
22
	const STATE_QUEUED = 'Queued';
23
	const STATE_DEPLOYING = 'Deploying';
24
	const STATE_ABORTING = 'Aborting';
25
	const STATE_COMPLETED = 'Completed';
26
	const STATE_FAILED = 'Failed';
27
28
	const TR_SUBMIT = 'submit';
29
	const TR_INVALIDATE = 'invalidate';
30
	const TR_QUEUE = 'queue';
31
	const TR_DEPLOY = 'deploy';
32
	const TR_ABORT = 'abort';
33
	const TR_COMPLETE = 'complete';
34
	const TR_FAIL = 'fail';
35
36
	/**
37
	 * @var array
38
	 */
39
	private static $db = array(
40
		"SHA" => "GitSHA",
41
		"ResqueToken" => "Varchar(255)",
42
		// The branch that was used to deploy this. Can't really be inferred from Git history because
43
		// the commit could appear in lots of branches that are irrelevant to the user when it comes
44
		// to deployment history, and the branch may have been deleted.
45
		"Branch" => "Varchar(255)",
46
		"State" => "Enum('New, Submitted, Invalid, Queued, Deploying, Aborting, Completed, Failed', 'New')",
47
		// JSON serialised DeploymentStrategy.
48
		"Strategy" => "Text"
49
	);
50
51
	/**
52
	 * @var array
53
	 */
54
	private static $has_one = array(
55
		"Environment" => "DNEnvironment",
56
		"Deployer" => "Member",
57
	);
58
59
	private static $default_sort = '"LastEdited" DESC';
60
61
	public function getTitle() {
62
		return "#{$this->ID}: {$this->SHA} (Status: {$this->Status})";
0 ignored issues
show
Documentation introduced by
The property Status does not exist on object<DNDeployment>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
63
	}
64
65
	private static $summary_fields = array(
66
		'LastEdited' => 'Last Edited',
67
		'SHA' => 'SHA',
68
		'State' => 'State',
69
		'Deployer.Name' => 'Deployer'
70
	);
71
72
	public function getFiniteState() {
73
        return $this->State;
74
    }
75
76
    public function setFiniteState($state) {
77
		$this->State = $state;
78
		$this->write();
79
    }
80
81
	public function getStatus() {
82
		return $this->State;
83
	}
84
85
	public function getMachine() {
86
		$loader = new Finite\Loader\ArrayLoader([
87
			'class'   => 'DNDeployment',
88
			'states'  => [
89
				self::STATE_NEW => ['type' => StateInterface::TYPE_INITIAL],
90
				self::STATE_SUBMITTED => ['type' => StateInterface::TYPE_NORMAL],
91
				self::STATE_INVALID => ['type' => StateInterface::TYPE_NORMAL],
92
				self::STATE_QUEUED => ['type' => StateInterface::TYPE_NORMAL],
93
				self::STATE_DEPLOYING => ['type' => StateInterface::TYPE_NORMAL],
94
				self::STATE_ABORTING => ['type' => StateInterface::TYPE_NORMAL],
95
				self::STATE_COMPLETED => ['type' => StateInterface::TYPE_FINAL],
96
				self::STATE_FAILED => ['type' => StateInterface::TYPE_FINAL],
97
			],
98
			'transitions' => [
99
				self::TR_SUBMIT => ['from' => [self::STATE_NEW], 'to' => self::STATE_SUBMITTED],
100
				self::TR_QUEUE => ['from' => [self::STATE_SUBMITTED], 'to' => self::STATE_QUEUED],
101
				self::TR_INVALIDATE  => [
102
					'from' => [self::STATE_NEW, self::STATE_SUBMITTED],
103
					'to' => self::STATE_INVALID
104
				],
105
				self::TR_DEPLOY  => ['from' => [self::STATE_QUEUED], 'to' => self::STATE_DEPLOYING],
106
				self::TR_ABORT => [
107
					'from' => [
108
						self::STATE_QUEUED,
109
						self::STATE_DEPLOYING,
110
						self::STATE_ABORTING
111
					],
112
					'to' => self::STATE_ABORTING
113
				],
114
				self::TR_COMPLETE => ['from' => [self::STATE_DEPLOYING], 'to' => self::STATE_COMPLETED],
115
				self::TR_FAIL  => [
116
					'from' => [
117
						self::STATE_NEW,
118
						self::STATE_SUBMITTED,
119
						self::STATE_QUEUED,
120
						self::STATE_INVALID,
121
						self::STATE_DEPLOYING,
122
						self::STATE_ABORTING
123
					],
124
					'to' => self::STATE_FAILED
125
				],
126
			],
127
			'callbacks' => [
128
				'after' => [
129
					['to' => [self::STATE_QUEUED], 'do' => [$this, 'onQueue']],
130
					['to' => [self::STATE_ABORTING], 'do' => [$this, 'onAbort']],
131
				]
132
			]
133
		]);
134
		$stateMachine = new Finite\StateMachine\StateMachine($this);
135
		$loader->load($stateMachine);
136
		$stateMachine->initialize();
137
		return $stateMachine;
138
	}
139
140
141
	public function onQueue() {
142
		$log = $this->log();
143
		$token = $this->enqueueDeployment();
144
		$this->ResqueToken = $token;
145
		$this->write();
146
147
		$message = sprintf('Deploy queued as job %s (sigFile is %s)', $token, DeployJob::sig_file_for_data_object($this));
148
		$log->write($message);
149
	}
150
151
	public function onAbort() {
152
		// 2 is SIGINT - we can't use SIGINT constant in the mod_apache context.
153
		DeployJob::set_signal($this, 2);
154
	}
155
156
	public function Link() {
157
		return Controller::join_links($this->Environment()->Link(), 'deploy', $this->ID);
158
	}
159
160
	public function LogLink() {
161
		return $this->Link() . '/log';
162
	}
163
164
	public function canView($member = null) {
165
		return $this->Environment()->canView($member);
166
	}
167
168
	/**
169
	 * Return a path to the log file.
170
	 * @return string
171
	 */
172
	protected function logfile() {
173
		return sprintf(
174
			'%s.%s.log',
175
			$this->Environment()->getFullName('.'),
176
			$this->ID
177
		);
178
	}
179
180
	/**
181
	 * @return DeploynautLogFile
182
	 */
183
	public function log() {
184
		return Injector::inst()->createWithArgs('DeploynautLogFile', array($this->logfile()));
185
	}
186
187
	public function LogContent() {
188
		return $this->log()->content();
189
	}
190
191
	/**
192
	 * This remains here for backwards compatibility - we don't want to expose Resque status in here.
193
	 * Resque job (DeployJob) will change statuses as part of its execution.
194
	 *
195
	 * @return string
196
	 */
197
	public function ResqueStatus() {
198
		return $this->State;
199
	}
200
201
202
	/**
203
	 * Fetch the git repository
204
	 *
205
	 * @return \Gitonomy\Git\Repository|null
206
	 */
207
	public function getRepository() {
208
		if(!$this->SHA) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->SHA of type string|null is loosely compared to false; 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...
209
			return null;
210
		}
211
		return $this->Environment()->Project()->getRepository();
212
	}
213
214
215
	/**
216
	 * Gets the commit from source. The result is cached upstream in Repository.
217
	 *
218
	 * @return \Gitonomy\Git\Commit|null
219
	 */
220
	public function getCommit() {
221
		$repo = $this->getRepository();
222
		if($repo) {
223
			try {
224
				return $repo->getCommit($this->SHA);
225
			} catch(Gitonomy\Git\Exception\ReferenceNotFoundException $ex) {
226
				return null;
227
			}
228
		}
229
230
		return null;
231
	}
232
233
234
	/**
235
	 * Gets the commit message.
236
	 *
237
	 * @return string|null
238
	 */
239
	public function getCommitMessage() {
240
		$commit = $this->getCommit();
241
		if($commit) {
242
			try {
243
				return Convert::raw2xml($commit->getMessage());
244
			} catch(Gitonomy\Git\Exception\ReferenceNotFoundException $e) {
245
				return null;
246
			}
247
		}
248
		return null;
249
	}
250
251
	/**
252
	 * Return all tags for the deployed commit.
253
	 *
254
	 * @return ArrayList
255
	 */
256
	public function getTags() {
257
		$returnTags = array();
258
		$repo = $this->getRepository();
259
		if($repo) {
260
			$tags = $repo->getReferences()->resolveTags($this->SHA);
261
			if(!empty($tags)) {
262
				foreach($tags as $tag) {
263
					$field = Varchar::create('Tag', '255');
264
					$field->setValue($tag->getName());
265
					$returnTags[] = $field;
266
				}
267
			}
268
		}
269
		return new ArrayList($returnTags);
270
	}
271
272
	/**
273
	 * Collate the list of additional flags to affix to this deployment.
274
	 * Elements of the array will be rendered literally.
275
	 *
276
	 * @return ArrayList
277
	 */
278
	public function getFullDeployMessages() {
279
		$strategy = $this->getDeploymentStrategy();
280
		if ($strategy->getActionCode()!=='full') return null;
281
282
		$changes = $strategy->getChangesModificationNeeded();
283
		$messages = [];
284
		foreach ($changes as $change => $details) {
285
			if ($change==='Code version') continue;
286
287
			$messages[] = [
288
				'Flag' => sprintf(
289
					'<span class="label label-default full-deploy-info-item">%s</span>',
290
					$change[0]
291
				),
292
				'Text' => sprintf('%s changed', $change)
293
			];
294
		}
295
296
		if (empty($messages)) {
297
			$messages[] = [
298
				'Flag' => '',
299
				'Text' => '<i>Environment changes have been made.</i>'
300
			];
301
		}
302
303
		return new ArrayList($messages);
304
	}
305
306
	/**
307
	 * Fetches the latest tag for the deployed commit
308
	 *
309
	 * @return \Varchar|null
310
	 */
311
	public function getTag() {
312
		$tags = $this->getTags();
313
		if($tags->count() > 0) {
314
			return $tags->last();
315
		}
316
		return null;
317
	}
318
319
	/**
320
	 * @return DeploymentStrategy
321
	 */
322
	public function getDeploymentStrategy() {
323
		$environment = $this->Environment();
324
		$strategy = new DeploymentStrategy($environment);
325
		$strategy->fromJSON($this->Strategy);
0 ignored issues
show
Documentation introduced by
The property Strategy does not exist on object<DNDeployment>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
326
		return $strategy;
327
	}
328
329
	/**
330
	 * Return a list of things that are going to be deployed, such
331
	 * as the code version, and any infrastrucutral changes.
332
	 *
333
	 * @return ArrayList
334
	 */
335
	public function getChanges() {
336
		$list = new ArrayList();
337
		$strategy = $this->getDeploymentStrategy();
338
		foreach($strategy->getChanges() as $name => $change) {
339
			$changed = (isset($change['from']) && isset($change['to'])) ? $change['from'] != $change['to'] : null;
340
			$description = isset($change['description']) ? $change['description'] : '';
341
			$compareUrl = null;
342
343
			// if there is a compare URL, and a description or a change (something actually changed)
344
			// then show the URL. Otherwise don't show anything, as there is no comparison to be made.
345
			if ($changed || $description) {
346
				$compareUrl = isset($change['compareUrl']) ? $change['compareUrl'] : '';
347
			}
348
349
			$list->push(new ArrayData([
350
				'Name' => $name,
351
				'From' => isset($change['from']) ? $change['from'] : null,
352
				'To' => isset($change['to']) ? $change['to'] : null,
353
				'Description' => $description,
354
				'Changed' => $changed,
355
				'CompareUrl' => $compareUrl
356
			]));
357
		}
358
359
		return $list;
360
	}
361
362
	/**
363
	 * Start a resque job for this deployment
364
	 *
365
	 * @return string Resque token
366
	 */
367
	protected function enqueueDeployment() {
368
		$environment = $this->Environment();
369
		$project = $environment->Project();
370
		$log = $this->log();
371
372
		$args = array(
373
			'environmentName' => $environment->Name,
374
			'repository' => $project->getLocalCVSPath(),
375
			'logfile' => $this->logfile(),
376
			'projectName' => $project->Name,
377
			'env' => $project->getProcessEnv(),
378
			'deploymentID' => $this->ID,
379
			'sigFile' => DeployJob::sig_file_for_data_object($this)
380
		);
381
382
		$strategy = $this->getDeploymentStrategy();
383
		// Inject options.
384
		$args = array_merge($args, $strategy->getOptions());
385
		// Make sure we use the SHA as it was written into this DNDeployment.
386
		$args['sha'] = $this->SHA;
387
388
		if(!$this->DeployerID) {
389
			$this->DeployerID = Member::currentUserID();
390
		}
391
392 View Code Duplication
		if($this->DeployerID) {
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...
393
			$deployer = $this->Deployer();
394
			$message = sprintf(
395
				'Deploy to %s initiated by %s (%s), with IP address %s',
396
				$environment->getFullName(),
397
				$deployer->getName(),
398
				$deployer->Email,
399
				Controller::curr()->getRequest()->getIP()
400
			);
401
			$log->write($message);
402
		}
403
404
		return Resque::enqueue('deploy', 'DeployJob', $args, true);
405
	}
406
}
407