1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* DNProject represents a project that relates to a group of target |
5
|
|
|
* environments. |
6
|
|
|
* |
7
|
|
|
* @property string Name |
8
|
|
|
* @property string CVSPath |
9
|
|
|
* @property int DiskQuotaMB |
10
|
|
|
* |
11
|
|
|
* @method HasManyList Environments() |
12
|
|
|
* @method ManyManyList Viewers() |
13
|
|
|
*/ |
14
|
|
|
class DNProject extends DataObject { |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* @var array |
18
|
|
|
*/ |
19
|
|
|
public static $db = array( |
20
|
|
|
"Name" => "Varchar", |
21
|
|
|
"CVSPath" => "Varchar(255)", |
22
|
|
|
"DiskQuotaMB" => "Int", |
23
|
|
|
"AllowedEnvironmentType" => "Varchar(255)", |
24
|
|
|
"Client" => "Varchar(255)", |
25
|
|
|
); |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* @var array |
29
|
|
|
*/ |
30
|
|
|
public static $has_many = array( |
31
|
|
|
"Environments" => "DNEnvironment", |
32
|
|
|
"CreateEnvironments" => "DNCreateEnvironment" |
33
|
|
|
); |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @var array |
37
|
|
|
*/ |
38
|
|
|
public static $many_many = array( |
39
|
|
|
"Viewers" => "Group", |
40
|
|
|
'StarredBy' => "Member" |
41
|
|
|
); |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @var array |
45
|
|
|
*/ |
46
|
|
|
public static $summary_fields = array( |
47
|
|
|
"Name", |
48
|
|
|
"ViewersList", |
49
|
|
|
); |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* @var array |
53
|
|
|
*/ |
54
|
|
|
public static $searchable_fields = array( |
55
|
|
|
"Name", |
56
|
|
|
); |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @var string |
60
|
|
|
*/ |
61
|
|
|
private static $singular_name = 'Project'; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* @var string |
65
|
|
|
*/ |
66
|
|
|
private static $plural_name = 'Projects'; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* @var string |
70
|
|
|
*/ |
71
|
|
|
private static $default_sort = 'Name'; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Display the repository URL on the project page. |
75
|
|
|
* |
76
|
|
|
* @var bool |
77
|
|
|
*/ |
78
|
|
|
private static $show_repository_url = false; |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* In-memory cache for currentBuilds per environment since fetching them from |
82
|
|
|
* disk is pretty resource hungry. |
83
|
|
|
* |
84
|
|
|
* @var array |
85
|
|
|
*/ |
86
|
|
|
protected static $relation_cache = array(); |
87
|
|
|
|
88
|
|
|
/** |
89
|
|
|
* @var bool|Member |
90
|
|
|
*/ |
91
|
|
|
protected static $_current_member_cache = null; |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* Used by the sync task |
95
|
|
|
* |
96
|
|
|
* @param string $path |
97
|
|
|
* @return \DNProject |
98
|
|
|
*/ |
99
|
|
|
public static function create_from_path($path) { |
100
|
|
|
$project = DNProject::create(); |
101
|
|
|
$project->Name = $path; |
102
|
|
|
$project->write(); |
103
|
|
|
|
104
|
|
|
// add the administrators group as the viewers of the new project |
105
|
|
|
$adminGroup = Group::get()->filter('Code', 'administrators')->first(); |
106
|
|
|
if($adminGroup && $adminGroup->exists()) { |
107
|
|
|
$project->Viewers()->add($adminGroup); |
108
|
|
|
} |
109
|
|
|
return $project; |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* Return the used quota in MB. |
114
|
|
|
* |
115
|
|
|
* @param int $round Number of decimal places to round to |
116
|
|
|
* @return double The used quota size in MB |
117
|
|
|
*/ |
118
|
|
|
public function getUsedQuotaMB($round = 2) { |
119
|
|
|
$size = 0; |
120
|
|
|
|
121
|
|
|
foreach($this->Environments() as $environment) { |
122
|
|
|
foreach($environment->DataArchives()->filter('IsBackup', 0) as $archive) { |
123
|
|
|
$size += $archive->ArchiveFile()->getAbsoluteSize(); |
124
|
|
|
} |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
// convert bytes to megabytes and round |
128
|
|
|
return round(($size / 1024) / 1024, $round); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* Getter for DiskQuotaMB field to provide a default for existing |
133
|
|
|
* records that have no quota field set, as it will need to default |
134
|
|
|
* to a globally set size. |
135
|
|
|
* |
136
|
|
|
* @return string|int The quota size in MB |
137
|
|
|
*/ |
138
|
|
|
public function getDiskQuotaMB() { |
139
|
|
|
$size = $this->getField('DiskQuotaMB'); |
140
|
|
|
|
141
|
|
|
if(empty($size)) { |
142
|
|
|
$defaults = $this->config()->get('defaults'); |
143
|
|
|
$size = (isset($defaults['DiskQuotaMB'])) ? $defaults['DiskQuotaMB'] : 0; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
return $size; |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
/** |
150
|
|
|
* Has the disk quota been exceeded? |
151
|
|
|
* |
152
|
|
|
* @return boolean |
153
|
|
|
*/ |
154
|
|
|
public function HasExceededDiskQuota() { |
155
|
|
|
return $this->getUsedQuotaMB(0) >= $this->getDiskQuotaMB(); |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* Is there a disk quota set for this project? |
160
|
|
|
* |
161
|
|
|
* @return boolean |
162
|
|
|
*/ |
163
|
|
|
public function HasDiskQuota() { |
164
|
|
|
return $this->getDiskQuotaMB() > 0; |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* Returns the current disk quota usage as a percentage |
169
|
|
|
* |
170
|
|
|
* @return int |
171
|
|
|
*/ |
172
|
|
|
public function DiskQuotaUsagePercent() { |
173
|
|
|
$quota = $this->getDiskQuotaMB(); |
174
|
|
|
if($quota > 0) { |
175
|
|
|
return $this->getUsedQuotaMB() * 100 / $quota; |
176
|
|
|
} |
177
|
|
|
return 100; |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
* Get the menu to be shown on projects |
182
|
|
|
* |
183
|
|
|
* @return ArrayList |
184
|
|
|
*/ |
185
|
|
View Code Duplication |
public function Menu() { |
|
|
|
|
186
|
|
|
$list = new ArrayList(); |
187
|
|
|
|
188
|
|
|
$controller = Controller::curr(); |
189
|
|
|
$actionType = $controller->getField('CurrentActionType'); |
190
|
|
|
|
191
|
|
|
if(DNRoot::FlagSnapshotsEnabled()) { |
192
|
|
|
$list->push(new ArrayData(array( |
193
|
|
|
'Link' => sprintf('naut/project/%s/snapshots', $this->Name), |
194
|
|
|
'Title' => 'Snapshots', |
195
|
|
|
'IsCurrent' => $this->isSection() && $controller->getAction() == 'snapshots', |
196
|
|
|
'IsSection' => $this->isSection() && $actionType == DNRoot::ACTION_SNAPSHOT |
197
|
|
|
))); |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
$this->extend('updateMenu', $list); |
201
|
|
|
|
202
|
|
|
return $list; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
/** |
206
|
|
|
* Is this project currently at the root level of the controller that handles it? |
207
|
|
|
* |
208
|
|
|
* @return bool |
209
|
|
|
*/ |
210
|
|
|
public function isCurrent() { |
211
|
|
|
return $this->isSection() && Controller::curr()->getAction() == 'project'; |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/** |
215
|
|
|
* Return the current object from $this->Menu() |
216
|
|
|
* Good for making titles and things |
217
|
|
|
* |
218
|
|
|
* @return DataObject |
219
|
|
|
*/ |
220
|
|
|
public function CurrentMenu() { |
221
|
|
|
return $this->Menu()->filter('IsSection', true)->First(); |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* Is this project currently in a controller that is handling it or performing a sub-task? |
226
|
|
|
* |
227
|
|
|
* @return bool |
228
|
|
|
*/ |
229
|
|
|
public function isSection() { |
230
|
|
|
$controller = Controller::curr(); |
231
|
|
|
$project = $controller->getField('CurrentProject'); |
232
|
|
|
return $project && $this->ID == $project->ID; |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
/** |
236
|
|
|
* Restrict access to viewing this project |
237
|
|
|
* |
238
|
|
|
* @param Member|null $member |
239
|
|
|
* @return boolean |
240
|
|
|
*/ |
241
|
|
|
public function canView($member = null) { |
242
|
|
|
if(!$member) { |
243
|
|
|
$member = Member::currentUser(); |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
if(Permission::checkMember($member, 'ADMIN')) { |
247
|
|
|
return true; |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
return $member->inGroups($this->Viewers()); |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* @param Member|null $member |
255
|
|
|
* |
256
|
|
|
* @return bool |
257
|
|
|
*/ |
258
|
|
View Code Duplication |
public function canRestore($member = null) { |
|
|
|
|
259
|
|
|
if ($this->allowedAny( |
260
|
|
|
array( |
261
|
|
|
DNRoot::ALLOW_PROD_SNAPSHOT, |
262
|
|
|
DNRoot::ALLOW_NON_PROD_SNAPSHOT |
263
|
|
|
), |
264
|
|
|
$member |
265
|
|
|
)) { |
266
|
|
|
return true; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
return (bool)$this->Environments()->filterByCallback(function($env) use($member) { |
270
|
|
|
return $env->canRestore($member); |
271
|
|
|
})->Count(); |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
/** |
275
|
|
|
* @param Member|null $member |
276
|
|
|
* @return bool |
277
|
|
|
*/ |
278
|
|
View Code Duplication |
public function canBackup($member = null) { |
|
|
|
|
279
|
|
|
if ($this->allowedAny( |
280
|
|
|
array( |
281
|
|
|
DNRoot::ALLOW_PROD_SNAPSHOT, |
282
|
|
|
DNRoot::ALLOW_NON_PROD_SNAPSHOT |
283
|
|
|
), |
284
|
|
|
$member |
285
|
|
|
)) { |
286
|
|
|
return true; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
return (bool)$this->Environments()->filterByCallback(function($env) use($member) { |
290
|
|
|
return $env->canBackup($member); |
291
|
|
|
})->Count(); |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
/** |
295
|
|
|
* @param Member|null $member |
296
|
|
|
* @return bool |
297
|
|
|
*/ |
298
|
|
View Code Duplication |
public function canUploadArchive($member = null) { |
|
|
|
|
299
|
|
|
if ($this->allowedAny( |
300
|
|
|
array( |
301
|
|
|
DNRoot::ALLOW_PROD_SNAPSHOT, |
302
|
|
|
DNRoot::ALLOW_NON_PROD_SNAPSHOT |
303
|
|
|
), |
304
|
|
|
$member |
305
|
|
|
)) { |
306
|
|
|
return true; |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
return (bool)$this->Environments()->filterByCallback(function($env) use($member) { |
310
|
|
|
return $env->canUploadArchive($member); |
311
|
|
|
})->Count(); |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
/** |
315
|
|
|
* @param Member|null $member |
316
|
|
|
* @return bool |
317
|
|
|
*/ |
318
|
|
View Code Duplication |
public function canDownloadArchive($member = null) { |
|
|
|
|
319
|
|
|
if ($this->allowedAny( |
320
|
|
|
array( |
321
|
|
|
DNRoot::ALLOW_PROD_SNAPSHOT, |
322
|
|
|
DNRoot::ALLOW_NON_PROD_SNAPSHOT |
323
|
|
|
), |
324
|
|
|
$member |
325
|
|
|
)) { |
326
|
|
|
return true; |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
return (bool)$this->Environments()->filterByCallback(function($env) use($member) { |
330
|
|
|
return $env->canDownloadArchive($member); |
331
|
|
|
})->Count(); |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
/** |
335
|
|
|
* This is a permission check for the front-end only. |
336
|
|
|
* |
337
|
|
|
* Only admins can create environments for now. Also, we need to check the value |
338
|
|
|
* of AllowedEnvironmentType which dictates which backend to use to render the form. |
339
|
|
|
* |
340
|
|
|
* @param Member|null $member |
341
|
|
|
* |
342
|
|
|
* @return bool |
343
|
|
|
*/ |
344
|
|
|
public function canCreateEnvironments($member = null) { |
345
|
|
|
$envType = $this->AllowedEnvironmentType; |
|
|
|
|
346
|
|
|
if($envType) { |
347
|
|
|
$env = Injector::inst()->get($envType); |
348
|
|
|
if($env instanceof EnvironmentCreateBackend) { |
349
|
|
|
return $this->allowed(DNRoot::ALLOW_CREATE_ENVIRONMENT, $member); |
350
|
|
|
} |
351
|
|
|
} |
352
|
|
|
return false; |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
/** |
356
|
|
|
* @return DataList |
357
|
|
|
*/ |
358
|
|
|
public function DataArchives() { |
359
|
|
|
$envIds = $this->Environments()->column('ID'); |
360
|
|
|
return DNDataArchive::get()->filter('EnvironmentID', $envIds); |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
/** |
364
|
|
|
* Return all archives which are "manual upload requests", |
365
|
|
|
* meaning they don't have a file attached to them (yet). |
366
|
|
|
* |
367
|
|
|
* @return DataList |
368
|
|
|
*/ |
369
|
|
|
public function PendingManualUploadDataArchives() { |
370
|
|
|
return $this->DataArchives()->filter('ArchiveFileID', null); |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
/** |
374
|
|
|
* Build an environment variable array to be used with this project. |
375
|
|
|
* |
376
|
|
|
* This is relevant if every project needs to use an individual SSH pubkey. |
377
|
|
|
* |
378
|
|
|
* Include this with all Gitonomy\Git\Repository, and |
379
|
|
|
* \Symfony\Component\Process\Processes. |
380
|
|
|
* |
381
|
|
|
* @return array |
382
|
|
|
*/ |
383
|
|
|
public function getProcessEnv() { |
384
|
|
|
if(file_exists($this->getPrivateKeyPath())) { |
385
|
|
|
// Key-pair is available, use it. |
386
|
|
|
$processEnv = array( |
387
|
|
|
'IDENT_KEY' => $this->getPrivateKeyPath(), |
388
|
|
|
'GIT_SSH' => BASE_PATH . "/deploynaut/git-deploy.sh" |
389
|
|
|
); |
390
|
|
|
} else { |
391
|
|
|
$processEnv = array(); |
392
|
|
|
} |
393
|
|
|
$this->extend('updateProcessEnv', $processEnv); |
394
|
|
|
|
395
|
|
|
return $processEnv; |
396
|
|
|
} |
397
|
|
|
|
398
|
|
|
/** |
399
|
|
|
* Get a string of people allowed to view this project |
400
|
|
|
* |
401
|
|
|
* @return string |
402
|
|
|
*/ |
403
|
|
|
public function getViewersList() { |
404
|
|
|
return implode(", ", $this->Viewers()->column("Title")); |
405
|
|
|
} |
406
|
|
|
|
407
|
|
|
/** |
408
|
|
|
* @return DNData |
409
|
|
|
*/ |
410
|
|
|
public function DNData() { |
411
|
|
|
return DNData::inst(); |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
/** |
415
|
|
|
* Provides a DNBuildList of builds found in this project. |
416
|
|
|
* |
417
|
|
|
* @return DNReferenceList |
418
|
|
|
*/ |
419
|
|
|
public function DNBuildList() { |
420
|
|
|
return DNReferenceList::create($this, $this->DNData()); |
421
|
|
|
} |
422
|
|
|
|
423
|
|
|
/** |
424
|
|
|
* Provides a list of the branches in this project. |
425
|
|
|
* |
426
|
|
|
* @return DNBranchList |
427
|
|
|
*/ |
428
|
|
|
public function DNBranchList() { |
429
|
|
|
if($this->CVSPath && !$this->repoExists()) { |
430
|
|
|
$this->cloneRepo(); |
431
|
|
|
} |
432
|
|
|
return DNBranchList::create($this, $this->DNData()); |
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
/** |
436
|
|
|
* Provides a list of the tags in this project. |
437
|
|
|
* |
438
|
|
|
* @return DNReferenceList |
439
|
|
|
*/ |
440
|
|
|
public function DNTagList() { |
441
|
|
|
if($this->CVSPath && !$this->repoExists()) { |
442
|
|
|
$this->cloneRepo(); |
443
|
|
|
} |
444
|
|
|
return DNReferenceList::create($this, $this->DNData(), null, null, true); |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
/** |
448
|
|
|
* @return false|Gitonomy\Git\Repository |
449
|
|
|
*/ |
450
|
|
|
public function getRepository() { |
451
|
|
|
if(!$this->repoExists()) { |
452
|
|
|
return false; |
453
|
|
|
} |
454
|
|
|
|
455
|
|
|
return new Gitonomy\Git\Repository($this->getLocalCVSPath()); |
456
|
|
|
} |
457
|
|
|
|
458
|
|
|
/** |
459
|
|
|
* Provides a list of environments found in this project. |
460
|
|
|
* CAUTION: filterByCallback will change this into an ArrayList! |
461
|
|
|
* |
462
|
|
|
* @return ArrayList |
463
|
|
|
*/ |
464
|
|
|
public function DNEnvironmentList() { |
465
|
|
|
|
466
|
|
|
if(!self::$_current_member_cache) { |
467
|
|
|
self::$_current_member_cache = Member::currentUser(); |
|
|
|
|
468
|
|
|
} |
469
|
|
|
|
470
|
|
|
if(self::$_current_member_cache === false) { |
471
|
|
|
return new ArrayList(); |
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
$currentMember = self::$_current_member_cache; |
475
|
|
|
return $this->Environments() |
476
|
|
|
->filterByCallBack(function($item) use ($currentMember) { |
477
|
|
|
return $item->canView($currentMember); |
478
|
|
|
}); |
479
|
|
|
} |
480
|
|
|
|
481
|
|
|
/** |
482
|
|
|
* @param string $usage |
483
|
|
|
* @return ArrayList |
484
|
|
|
*/ |
485
|
|
|
public function EnvironmentsByUsage($usage) { |
486
|
|
|
return $this->DNEnvironmentList()->filter('Usage', $usage); |
487
|
|
|
} |
488
|
|
|
|
489
|
|
|
/** |
490
|
|
|
* Returns a map of envrionment name to build name |
491
|
|
|
* |
492
|
|
|
* @return false|DNDeployment |
493
|
|
|
*/ |
494
|
|
|
public function currentBuilds() { |
495
|
|
|
if(!isset(self::$relation_cache['currentBuilds.'.$this->ID])) { |
496
|
|
|
$currentBuilds = array(); |
497
|
|
|
foreach($this->Environments() as $env) { |
498
|
|
|
$currentBuilds[$env->Name] = $env->CurrentBuild(); |
499
|
|
|
} |
500
|
|
|
self::$relation_cache['currentBuilds.'.$this->ID] = $currentBuilds; |
501
|
|
|
} |
502
|
|
|
return self::$relation_cache['currentBuilds.'.$this->ID]; |
503
|
|
|
} |
504
|
|
|
|
505
|
|
|
/** |
506
|
|
|
* @param string |
507
|
|
|
* @return string |
508
|
|
|
*/ |
509
|
|
|
public function Link($action = '') { |
510
|
|
|
return Controller::join_links("naut", "project", $this->Name, $action); |
511
|
|
|
} |
512
|
|
|
|
513
|
|
|
/** |
514
|
|
|
* @return string|null |
515
|
|
|
*/ |
516
|
|
|
public function CreateEnvironmentLink() { |
517
|
|
|
if($this->canCreateEnvironments()) { |
518
|
|
|
return $this->Link('createenv'); |
519
|
|
|
} |
520
|
|
|
return null; |
521
|
|
|
} |
522
|
|
|
|
523
|
|
|
/** |
524
|
|
|
* @return string |
525
|
|
|
*/ |
526
|
|
|
public function ToggleStarLink() { |
527
|
|
|
return $this->Link('/star'); |
528
|
|
|
} |
529
|
|
|
|
530
|
|
|
/** |
531
|
|
|
* @return bool |
532
|
|
|
*/ |
533
|
|
|
public function IsStarred() { |
534
|
|
|
$member = Member::currentUser(); |
535
|
|
|
if($member === null) { |
536
|
|
|
return false; |
537
|
|
|
} |
538
|
|
|
$favourited = $this->StarredBy()->filter('MemberID', $member->ID); |
|
|
|
|
539
|
|
|
if($favourited->count() == 0) { |
540
|
|
|
return false; |
541
|
|
|
} |
542
|
|
|
return true; |
543
|
|
|
} |
544
|
|
|
|
545
|
|
|
/** |
546
|
|
|
* @param string $action |
547
|
|
|
* @return string |
548
|
|
|
*/ |
549
|
|
|
public function APILink($action) { |
550
|
|
|
return Controller::join_links("naut", "api", $this->Name, $action); |
551
|
|
|
} |
552
|
|
|
|
553
|
|
|
/** |
554
|
|
|
* @return FieldList |
555
|
|
|
*/ |
556
|
|
|
public function getCMSFields() { |
557
|
|
|
$fields = parent::getCMSFields(); |
558
|
|
|
|
559
|
|
|
/** @var GridField $environments */ |
560
|
|
|
$environments = $fields->dataFieldByName("Environments"); |
561
|
|
|
|
562
|
|
|
$fields->fieldByName("Root")->removeByName("Viewers"); |
563
|
|
|
$fields->fieldByName("Root")->removeByName("Environments"); |
564
|
|
|
$fields->fieldByName("Root")->removeByName("LocalCVSPath"); |
565
|
|
|
|
566
|
|
|
$diskQuotaDesc = 'This is the maximum amount of disk space (in megabytes) that all environments within this ' |
567
|
|
|
. 'project can use for stored snapshots'; |
568
|
|
|
$fields->dataFieldByName('DiskQuotaMB')->setDescription($diskQuotaDesc); |
569
|
|
|
|
570
|
|
|
$projectNameDesc = 'Changing the name will <strong>reset</strong> the deploy configuration and avoid using non' |
571
|
|
|
. 'alphanumeric characters'; |
572
|
|
|
$fields->fieldByName('Root.Main.Name') |
573
|
|
|
->setTitle('Project name') |
574
|
|
|
->setDescription($projectNameDesc); |
575
|
|
|
|
576
|
|
|
$fields->fieldByName('Root.Main.CVSPath') |
577
|
|
|
->setTitle('Git repository') |
578
|
|
|
->setDescription('E.g. [email protected]:silverstripe/silverstripe-installer.git'); |
579
|
|
|
|
580
|
|
|
$workspaceField = new ReadonlyField('LocalWorkspace', 'Git workspace', $this->getLocalCVSPath()); |
581
|
|
|
$workspaceField->setDescription('This is where the GIT repository are located on this server'); |
582
|
|
|
$fields->insertAfter($workspaceField, 'CVSPath'); |
|
|
|
|
583
|
|
|
|
584
|
|
|
$readAccessGroups = ListboxField::create('Viewers', 'Project viewers', Group::get()->map()->toArray()) |
585
|
|
|
->setMultiple(true) |
586
|
|
|
->setDescription('These groups can view the project in the front-end.'); |
587
|
|
|
$fields->addFieldToTab("Root.Main", $readAccessGroups); |
588
|
|
|
|
589
|
|
|
$this->setCreateProjectFolderField($fields); |
590
|
|
|
$this->setEnvironmentFields($fields, $environments); |
591
|
|
|
|
592
|
|
|
$environmentTypes = ClassInfo::implementorsOf('EnvironmentCreateBackend'); |
593
|
|
|
$types = array(); |
594
|
|
|
foreach($environmentTypes as $type) { |
595
|
|
|
$types[$type] = $type; |
596
|
|
|
} |
597
|
|
|
|
598
|
|
|
$fields->addFieldsToTab('Root.Main', array( |
599
|
|
|
DropdownField::create( |
600
|
|
|
'AllowedEnvironmentType', |
601
|
|
|
'Allowed Environment Type', |
602
|
|
|
$types |
603
|
|
|
)->setDescription('This defined which form to show on the front end for ' |
604
|
|
|
. 'environment creation. This will not affect backend functionality.') |
605
|
|
|
->setEmptyString(' - None - '), |
606
|
|
|
TextField::create('Client', 'Client'), |
607
|
|
|
)); |
608
|
|
|
|
609
|
|
|
return $fields; |
610
|
|
|
} |
611
|
|
|
|
612
|
|
|
/** |
613
|
|
|
* If there isn't a capistrano env project folder, show options to create one |
614
|
|
|
* |
615
|
|
|
* @param FieldList $fields |
616
|
|
|
*/ |
617
|
|
|
public function setCreateProjectFolderField(&$fields) { |
618
|
|
|
// Check if the capistrano project folder exists |
619
|
|
|
if(!$this->Name) { |
620
|
|
|
return; |
621
|
|
|
} |
622
|
|
|
|
623
|
|
|
if($this->projectFolderExists()) { |
624
|
|
|
return; |
625
|
|
|
} |
626
|
|
|
|
627
|
|
|
$createFolderNotice = new LabelField('CreateEnvFolderNotice', 'Warning: No Capistrano project folder exists'); |
628
|
|
|
$createFolderNotice->addExtraClass('message warning'); |
629
|
|
|
$fields->insertBefore($createFolderNotice, 'Name'); |
|
|
|
|
630
|
|
|
$createFolderField = new CheckboxField('CreateEnvFolder', 'Create folder'); |
631
|
|
|
$createFolderField->setDescription('Would you like to create the capistrano project folder?'); |
632
|
|
|
$fields->insertAfter($createFolderField, 'CreateEnvFolderNotice'); |
633
|
|
|
} |
634
|
|
|
|
635
|
|
|
/** |
636
|
|
|
* @return boolean |
637
|
|
|
*/ |
638
|
|
|
public function projectFolderExists() { |
639
|
|
|
if(file_exists($this->DNData()->getEnvironmentDir().'/'.$this->Name)) { |
640
|
|
|
return true; |
641
|
|
|
} |
642
|
|
|
return false; |
643
|
|
|
} |
644
|
|
|
|
645
|
|
|
/** |
646
|
|
|
* @return bool |
647
|
|
|
*/ |
648
|
|
|
public function repoExists() { |
649
|
|
|
return file_exists(DEPLOYNAUT_LOCAL_VCS_PATH . '/' . $this->Name.'/HEAD'); |
650
|
|
|
} |
651
|
|
|
|
652
|
|
|
/** |
653
|
|
|
* Setup a asyncronous resque job to clone a git repository |
654
|
|
|
*/ |
655
|
|
|
public function cloneRepo() { |
656
|
|
|
Resque::enqueue('git', 'CloneGitRepo', array( |
657
|
|
|
'repo' => $this->CVSPath, |
658
|
|
|
'path' => $this->getLocalCVSPath(), |
659
|
|
|
'env' => $this->getProcessEnv() |
660
|
|
|
)); |
661
|
|
|
} |
662
|
|
|
|
663
|
|
|
/** |
664
|
|
|
* @return string |
665
|
|
|
*/ |
666
|
|
|
public function getLocalCVSPath() { |
667
|
|
|
return DEPLOYNAUT_LOCAL_VCS_PATH . '/' . $this->Name; |
668
|
|
|
} |
669
|
|
|
|
670
|
|
|
/** |
671
|
|
|
* Checks for missing folders folder and schedules a git clone if the necessary |
672
|
|
|
*/ |
673
|
|
|
public function onBeforeWrite() { |
674
|
|
|
parent::onBeforeWrite(); |
675
|
|
|
|
676
|
|
|
$this->checkProjectPath(); |
677
|
|
|
$this->checkCVSPath(); |
678
|
|
|
} |
679
|
|
|
|
680
|
|
|
/** |
681
|
|
|
* Ensure the path for this project has been created |
682
|
|
|
*/ |
683
|
|
|
protected function checkProjectPath() { |
684
|
|
|
// Create the project capistrano folder |
685
|
|
|
if($this->CreateEnvFolder && !file_exists($this->getProjectFolderPath())) { |
|
|
|
|
686
|
|
|
mkdir($this->DNData()->getEnvironmentDir().'/'.$this->Name); |
687
|
|
|
} |
688
|
|
|
} |
689
|
|
|
|
690
|
|
|
/** |
691
|
|
|
* Check if the CVSPath has been changed, and if so, ensure the repository has been updated |
692
|
|
|
*/ |
693
|
|
|
protected function checkCVSPath() { |
694
|
|
|
$changedFields = $this->getChangedFields(true, 2); |
695
|
|
|
if(!$this->CVSPath) { |
696
|
|
|
return; |
697
|
|
|
} |
698
|
|
|
if(isset($changedFields['CVSPath']) || isset($changedFields['Name'])) { |
699
|
|
|
$this->cloneRepo(); |
700
|
|
|
} |
701
|
|
|
} |
702
|
|
|
|
703
|
|
|
/** |
704
|
|
|
* Delete related environments and folders |
705
|
|
|
*/ |
706
|
|
|
public function onAfterDelete() { |
707
|
|
|
parent::onAfterDelete(); |
708
|
|
|
|
709
|
|
|
// Delete related environments |
710
|
|
|
foreach($this->Environments() as $env) { |
711
|
|
|
$env->delete(); |
712
|
|
|
} |
713
|
|
|
|
714
|
|
|
if(!file_exists($this->getProjectFolderPath())) { |
715
|
|
|
return; |
716
|
|
|
} |
717
|
|
|
// Create a basic new environment config from a template |
718
|
|
|
if(Config::inst()->get('DNEnvironment', 'allow_web_editing')) { |
719
|
|
|
FileSystem::removeFolder($this->getProjectFolderPath()); |
720
|
|
|
} |
721
|
|
|
} |
722
|
|
|
|
723
|
|
|
/** |
724
|
|
|
* Fetch the public key for this project. |
725
|
|
|
* |
726
|
|
|
* @return string|void |
727
|
|
|
*/ |
728
|
|
|
public function getPublicKey() { |
729
|
|
|
$key = $this->getPublicKeyPath(); |
730
|
|
|
|
731
|
|
|
if(file_exists($key)) { |
732
|
|
|
return file_get_contents($key); |
733
|
|
|
} |
734
|
|
|
} |
735
|
|
|
|
736
|
|
|
/** |
737
|
|
|
* This returns that path of the public key if a key directory is set. It doesn't check whether the file exists. |
738
|
|
|
* |
739
|
|
|
* @return string|null |
740
|
|
|
*/ |
741
|
|
|
public function getPublicKeyPath() { |
742
|
|
|
if($privateKey = $this->getPrivateKeyPath()) { |
743
|
|
|
return $privateKey . '.pub'; |
744
|
|
|
} |
745
|
|
|
return null; |
746
|
|
|
} |
747
|
|
|
|
748
|
|
|
/** |
749
|
|
|
* This returns that path of the private key if a key directory is set. It doesn't check whether the file exists. |
750
|
|
|
* |
751
|
|
|
* @return string|null |
752
|
|
|
*/ |
753
|
|
|
public function getPrivateKeyPath() { |
754
|
|
|
$keyDir = $this->getKeyDir(); |
755
|
|
|
if(!empty($keyDir)) { |
756
|
|
|
$filter = FileNameFilter::create(); |
757
|
|
|
$name = $filter->filter($this->Name); |
758
|
|
|
return $keyDir . '/' . $name; |
759
|
|
|
} |
760
|
|
|
return null; |
761
|
|
|
} |
762
|
|
|
|
763
|
|
|
/** |
764
|
|
|
* Returns the location of the projects key dir if one exists. |
765
|
|
|
* |
766
|
|
|
* @return string|null |
767
|
|
|
*/ |
768
|
|
|
public function getKeyDir() { |
769
|
|
|
$keyDir = $this->DNData()->getKeyDir(); |
770
|
|
|
if(!$keyDir) { |
771
|
|
|
return null; |
772
|
|
|
} |
773
|
|
|
|
774
|
|
|
$filter = FileNameFilter::create(); |
775
|
|
|
$name = $filter->filter($this->Name); |
776
|
|
|
|
777
|
|
|
return $this->DNData()->getKeyDir() . '/' . $name; |
778
|
|
|
} |
779
|
|
|
|
780
|
|
|
/** |
781
|
|
|
* Setup a gridfield for the environment configs |
782
|
|
|
* |
783
|
|
|
* @param FieldList $fields |
784
|
|
|
* @param GridField $environments |
785
|
|
|
*/ |
786
|
|
|
protected function setEnvironmentFields(&$fields, $environments) { |
787
|
|
|
if(!$environments) { |
788
|
|
|
return; |
789
|
|
|
} |
790
|
|
|
|
791
|
|
|
$environments->getConfig()->addComponent(new GridFieldAddNewMultiClass()); |
792
|
|
|
$environments->getConfig()->removeComponentsByType('GridFieldAddNewButton'); |
793
|
|
|
$environments->getConfig()->removeComponentsByType('GridFieldAddExistingAutocompleter'); |
794
|
|
|
$environments->getConfig()->removeComponentsByType('GridFieldDeleteAction'); |
795
|
|
|
$environments->getConfig()->removeComponentsByType('GridFieldPageCount'); |
796
|
|
|
if(Config::inst()->get('DNEnvironment', 'allow_web_editing')) { |
797
|
|
|
$addNewRelease = new GridFieldAddNewButton('toolbar-header-right'); |
798
|
|
|
$addNewRelease->setButtonName('Add'); |
799
|
|
|
$environments->getConfig()->addComponent($addNewRelease); |
800
|
|
|
} |
801
|
|
|
|
802
|
|
|
$fields->addFieldToTab("Root.Main", $environments); |
803
|
|
|
} |
804
|
|
|
|
805
|
|
|
/** |
806
|
|
|
* Provide current repository URL to the users. |
807
|
|
|
* |
808
|
|
|
* @return void|string |
809
|
|
|
*/ |
810
|
|
|
public function getRepositoryURL() { |
811
|
|
|
$showUrl = Config::inst()->get($this->class, 'show_repository_url'); |
812
|
|
|
if($showUrl) { |
813
|
|
|
return $this->CVSPath; |
814
|
|
|
} |
815
|
|
|
} |
816
|
|
|
|
817
|
|
|
/** |
818
|
|
|
* Whitelist configuration that describes how to convert a repository URL into a link |
819
|
|
|
* to a web user interface for that URL |
820
|
|
|
* |
821
|
|
|
* Consists of a hash of "full.lower.case.domain" => {configuration} key/value pairs |
822
|
|
|
* |
823
|
|
|
* {configuration} can either be boolean true to auto-detect both the host and the |
824
|
|
|
* name of the UI provider, or a nested array that overrides either one or both |
825
|
|
|
* of the auto-detected valyes |
826
|
|
|
* |
827
|
|
|
* @var array |
828
|
|
|
*/ |
829
|
|
|
static private $repository_interfaces = array( |
830
|
|
|
'github.com' => array( |
831
|
|
|
'icon' => 'deploynaut/img/github.png' |
832
|
|
|
), |
833
|
|
|
'bitbucket.org' => array( |
834
|
|
|
'commit' => 'commits' |
835
|
|
|
), |
836
|
|
|
'repo.or.cz' => array( |
837
|
|
|
'scheme' => 'http', |
838
|
|
|
'name' => 'repo.or.cz', |
839
|
|
|
'regex' => array('^(.*)$' => '/w$1') |
840
|
|
|
), |
841
|
|
|
|
842
|
|
|
/* Example for adding your own gitlab repository and override all auto-detected values (with their defaults) |
|
|
|
|
843
|
|
|
'gitlab.mysite.com' => array( |
844
|
|
|
'icon' => 'deploynaut/img/git.png', |
845
|
|
|
'host' => 'gitlab.mysite.com', |
846
|
|
|
'name' => 'Gitlab', |
847
|
|
|
'regex' => array('.git$' => ''), |
848
|
|
|
'commit' => "commit" |
849
|
|
|
), |
850
|
|
|
*/ |
851
|
|
|
); |
852
|
|
|
|
853
|
|
|
/** |
854
|
|
|
* Get a ViewableData structure describing the UI tool that lets the user view the repository code |
855
|
|
|
* |
856
|
|
|
* @return ArrayData |
857
|
|
|
*/ |
858
|
|
|
public function getRepositoryInterface() { |
859
|
|
|
$interfaces = $this->config()->repository_interfaces; |
860
|
|
|
|
861
|
|
|
/* Look for each whitelisted hostname */ |
862
|
|
|
foreach($interfaces as $host => $interface) { |
863
|
|
|
/* See if the CVS Path is for this hostname, followed by some junk (maybe a port), then the path */ |
864
|
|
|
if(preg_match('{^[^.]*' . $host . '(.*?)([/a-zA-Z].+)}', $this->CVSPath, $match)) { |
865
|
|
|
|
866
|
|
|
$path = $match[2]; |
867
|
|
|
|
868
|
|
|
$scheme = isset($interface['scheme']) ? $interface['scheme'] : 'https'; |
869
|
|
|
$host = isset($interface['host']) ? $interface['host'] : $host; |
870
|
|
|
$regex = isset($interface['regex']) ? $interface['regex'] : array('\.git$' => ''); |
871
|
|
|
|
872
|
|
|
$components = explode('.', $host); |
873
|
|
|
|
874
|
|
|
foreach($regex as $pattern => $replacement) { |
875
|
|
|
$path = preg_replace('/' . $pattern . '/', $replacement, $path); |
876
|
|
|
} |
877
|
|
|
|
878
|
|
|
$uxurl = Controller::join_links($scheme . '://', $host, $path); |
879
|
|
|
|
880
|
|
|
if(array_key_exists('commit', $interface) && $interface['commit'] == false) { |
881
|
|
|
$commiturl = false; |
882
|
|
|
} else { |
883
|
|
|
$commiturl = Controller::join_links( |
884
|
|
|
$uxurl, |
885
|
|
|
isset($interface['commit']) ? $interface['commit'] : 'commit' |
886
|
|
|
); |
887
|
|
|
} |
888
|
|
|
|
889
|
|
|
return new ArrayData(array( |
890
|
|
|
'Name' => isset($interface['name']) ? $interface['name'] : ucfirst($components[0]), |
891
|
|
|
'Icon' => isset($interface['icon']) ? $interface['icon'] : 'deploynaut/img/git.png', |
892
|
|
|
'URL' => $uxurl, |
893
|
|
|
'CommitURL' => $commiturl |
894
|
|
|
)); |
895
|
|
|
} |
896
|
|
|
} |
897
|
|
|
} |
898
|
|
|
|
899
|
|
|
/** |
900
|
|
|
* @return string |
901
|
|
|
*/ |
902
|
|
|
protected function getProjectFolderPath() { |
903
|
|
|
return $this->DNData()->getEnvironmentDir().'/'.$this->Name; |
904
|
|
|
} |
905
|
|
|
|
906
|
|
|
/** |
907
|
|
|
* Convenience wrapper for a single permission code. |
908
|
|
|
* |
909
|
|
|
* @param string $code |
910
|
|
|
* @return SS_List |
911
|
|
|
*/ |
912
|
|
|
public function whoIsAllowed($code) { |
913
|
|
|
return $this->whoIsAllowedAny(array($code)); |
914
|
|
|
} |
915
|
|
|
|
916
|
|
|
/** |
917
|
|
|
* List members who have $codes on this project. |
918
|
|
|
* Does not support Permission::DENY_PERMISSION malarky, same as Permission::get_groups_by_permission anyway... |
919
|
|
|
* |
920
|
|
|
* @param array|string $codes |
921
|
|
|
* @return SS_List |
922
|
|
|
*/ |
923
|
|
|
public function whoIsAllowedAny($codes) { |
924
|
|
|
if(!is_array($codes)) $codes = array($codes); |
925
|
|
|
|
926
|
|
|
$SQLa_codes = Convert::raw2sql($codes); |
927
|
|
|
$SQL_codes = join("','", $SQLa_codes); |
928
|
|
|
|
929
|
|
|
return DataObject::get('Member') |
930
|
|
|
->where("\"PermissionRoleCode\".\"Code\" IN ('$SQL_codes') OR \"Permission\".\"Code\" IN ('$SQL_codes')") |
931
|
|
|
->filter("DNProject_Viewers.DNProjectID", $this->ID) |
932
|
|
|
->leftJoin('Group_Members', "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"") |
933
|
|
|
->leftJoin('Group', "\"Group_Members\".\"GroupID\" = \"Group\".\"ID\"") |
934
|
|
|
->leftJoin('DNProject_Viewers', "\"DNProject_Viewers\".\"GroupID\" = \"Group\".\"ID\"") |
935
|
|
|
->leftJoin('Permission', "\"Permission\".\"GroupID\" = \"Group\".\"ID\"") |
936
|
|
|
->leftJoin('Group_Roles', "\"Group_Roles\".\"GroupID\" = \"Group\".\"ID\"") |
937
|
|
|
->leftJoin('PermissionRole', "\"Group_Roles\".\"PermissionRoleID\" = \"PermissionRole\".\"ID\"") |
938
|
|
|
->leftJoin('PermissionRoleCode', "\"PermissionRoleCode\".\"RoleID\" = \"PermissionRole\".\"ID\""); |
939
|
|
|
} |
940
|
|
|
|
941
|
|
|
/** |
942
|
|
|
* Convenience wrapper for a single permission code. |
943
|
|
|
* |
944
|
|
|
* @param string $code |
945
|
|
|
* @param Member|null $member |
946
|
|
|
* |
947
|
|
|
* @return bool |
948
|
|
|
*/ |
949
|
|
|
public function allowed($code, $member = null) { |
950
|
|
|
return $this->allowedAny(array($code), $member); |
951
|
|
|
} |
952
|
|
|
|
953
|
|
|
/** |
954
|
|
|
* Check if member has a permission code in this project. |
955
|
|
|
* |
956
|
|
|
* @param string $code |
|
|
|
|
957
|
|
|
* @param Member|null $member |
958
|
|
|
* |
959
|
|
|
* @return bool |
960
|
|
|
*/ |
961
|
|
|
public function allowedAny($codes, $member = null) { |
962
|
|
|
if (!$member) { |
963
|
|
|
$member = Member::currentUser(); |
964
|
|
|
} |
965
|
|
|
|
966
|
|
|
if(Permission::checkMember($member, 'ADMIN')) return true; |
967
|
|
|
|
968
|
|
|
$hits = $this->whoIsAllowedAny($codes)->filter('Member.ID', $member->ID)->count(); |
969
|
|
|
return ($hits>0 ? true : false); |
970
|
|
|
} |
971
|
|
|
|
972
|
|
|
/** |
973
|
|
|
* @return ValidationResult |
974
|
|
|
*/ |
975
|
|
|
protected function validate() { |
976
|
|
|
$validation = parent::validate(); |
977
|
|
|
if($validation->valid()) { |
978
|
|
|
if(empty($this->Name)) { |
979
|
|
|
return $validation->error('The project must have a name.'); |
980
|
|
|
} |
981
|
|
|
|
982
|
|
|
if(empty($this->CVSPath)) { |
983
|
|
|
return $validation->error('You must provide a repository URL.'); |
984
|
|
|
} |
985
|
|
|
|
986
|
|
|
$existing = DNProject::get()->filter('Name', $this->Name); |
987
|
|
|
if($this->ID) { |
988
|
|
|
$existing = $existing->exclude('ID', $this->ID); |
989
|
|
|
} |
990
|
|
|
if($existing->count() > 0) { |
991
|
|
|
return $validation->error('A stack already exists with that name.'); |
992
|
|
|
} |
993
|
|
|
} |
994
|
|
|
return $validation; |
995
|
|
|
} |
996
|
|
|
|
997
|
|
|
} |
998
|
|
|
|
999
|
|
|
|
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.