1
|
|
|
<?php |
2
|
|
|
use \Symfony\Component\Process\Process; |
3
|
|
|
|
4
|
|
|
/** |
5
|
|
|
* God controller for the deploynaut interface |
6
|
|
|
* |
7
|
|
|
* @package deploynaut |
8
|
|
|
* @subpackage control |
9
|
|
|
*/ |
10
|
|
|
class DNRoot extends Controller implements PermissionProvider, TemplateGlobalProvider { |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* @const string - action type for actions that perform deployments |
14
|
|
|
*/ |
15
|
|
|
const ACTION_DEPLOY = 'deploy'; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* @const string - action type for actions that manipulate snapshots |
19
|
|
|
*/ |
20
|
|
|
const ACTION_SNAPSHOT = 'snapshot'; |
21
|
|
|
|
22
|
|
|
const ACTION_ENVIRONMENTS = 'createenv'; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* @var string |
26
|
|
|
*/ |
27
|
|
|
private $actionType = self::ACTION_DEPLOY; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* Bypass pipeline permission code |
31
|
|
|
*/ |
32
|
|
|
const DEPLOYNAUT_BYPASS_PIPELINE = 'DEPLOYNAUT_BYPASS_PIPELINE'; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* Allow dryrun of pipelines |
36
|
|
|
*/ |
37
|
|
|
const DEPLOYNAUT_DRYRUN_PIPELINE = 'DEPLOYNAUT_DRYRUN_PIPELINE'; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Allow advanced options on deployments |
41
|
|
|
*/ |
42
|
|
|
const DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS = 'DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS'; |
43
|
|
|
|
44
|
|
|
const ALLOW_PROD_DEPLOYMENT = 'ALLOW_PROD_DEPLOYMENT'; |
45
|
|
|
const ALLOW_NON_PROD_DEPLOYMENT = 'ALLOW_NON_PROD_DEPLOYMENT'; |
46
|
|
|
const ALLOW_PROD_SNAPSHOT = 'ALLOW_PROD_SNAPSHOT'; |
47
|
|
|
const ALLOW_NON_PROD_SNAPSHOT = 'ALLOW_NON_PROD_SNAPSHOT'; |
48
|
|
|
const ALLOW_CREATE_ENVIRONMENT = 'ALLOW_CREATE_ENVIRONMENT'; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @var array |
52
|
|
|
*/ |
53
|
|
|
private static $allowed_actions = array( |
54
|
|
|
'projects', |
55
|
|
|
'nav', |
56
|
|
|
'update', |
57
|
|
|
'project', |
58
|
|
|
'toggleprojectstar', |
59
|
|
|
'branch', |
60
|
|
|
'environment', |
61
|
|
|
'abortpipeline', |
62
|
|
|
'pipeline', |
63
|
|
|
'pipelinelog', |
64
|
|
|
'metrics', |
65
|
|
|
'createenvlog', |
66
|
|
|
'createenv', |
67
|
|
|
'getDeployForm', |
68
|
|
|
'doDeploy', |
69
|
|
|
'deploy', |
70
|
|
|
'deploylog', |
71
|
|
|
'getDataTransferForm', |
72
|
|
|
'transfer', |
73
|
|
|
'transferlog', |
74
|
|
|
'snapshots', |
75
|
|
|
'createsnapshot', |
76
|
|
|
'snapshotslog', |
77
|
|
|
'uploadsnapshot', |
78
|
|
|
'getCreateEnvironmentForm', |
79
|
|
|
'getUploadSnapshotForm', |
80
|
|
|
'getPostSnapshotForm', |
81
|
|
|
'getDataTransferRestoreForm', |
82
|
|
|
'getDeleteForm', |
83
|
|
|
'getMoveForm', |
84
|
|
|
'restoresnapshot', |
85
|
|
|
'deletesnapshot', |
86
|
|
|
'movesnapshot', |
87
|
|
|
'postsnapshotsuccess', |
88
|
|
|
'gitRevisions', |
89
|
|
|
'deploySummary', |
90
|
|
|
'startDeploy', |
91
|
|
|
'createproject', |
92
|
|
|
'CreateProjectForm', |
93
|
|
|
); |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* URL handlers pretending that we have a deep URL structure. |
97
|
|
|
*/ |
98
|
|
|
private static $url_handlers = array( |
99
|
|
|
'project/$Project/environment/$Environment/DeployForm' => 'getDeployForm', |
100
|
|
|
'project/$Project/createsnapshot/DataTransferForm' => 'getDataTransferForm', |
101
|
|
|
'project/$Project/DataTransferForm' => 'getDataTransferForm', |
102
|
|
|
'project/$Project/DataTransferRestoreForm' => 'getDataTransferRestoreForm', |
103
|
|
|
'project/$Project/DeleteForm' => 'getDeleteForm', |
104
|
|
|
'project/$Project/MoveForm' => 'getMoveForm', |
105
|
|
|
'project/$Project/UploadSnapshotForm' => 'getUploadSnapshotForm', |
106
|
|
|
'project/$Project/PostSnapshotForm' => 'getPostSnapshotForm', |
107
|
|
|
'project/$Project/environment/$Environment/metrics' => 'metrics', |
108
|
|
|
'project/$Project/environment/$Environment/pipeline/$Identifier//$Action/$ID/$OtherID' => 'pipeline', |
109
|
|
|
'project/$Project/environment/$Environment/deploy_summary' => 'deploySummary', |
110
|
|
|
'project/$Project/environment/$Environment/git_revisions' => 'gitRevisions', |
111
|
|
|
'project/$Project/environment/$Environment/start-deploy' => 'startDeploy', |
112
|
|
|
'project/$Project/environment/$Environment/deploy/$Identifier/log' => 'deploylog', |
113
|
|
|
'project/$Project/environment/$Environment/deploy/$Identifier' => 'deploy', |
114
|
|
|
'project/$Project/transfer/$Identifier/log' => 'transferlog', |
115
|
|
|
'project/$Project/transfer/$Identifier' => 'transfer', |
116
|
|
|
'project/$Project/environment/$Environment' => 'environment', |
117
|
|
|
'project/$Project/createenv/$Identifier/log' => 'createenvlog', |
118
|
|
|
'project/$Project/createenv/$Identifier' => 'createenv', |
119
|
|
|
'project/$Project/CreateEnvironmentForm' => 'getCreateEnvironmentForm', |
120
|
|
|
'project/$Project/branch' => 'branch', |
121
|
|
|
'project/$Project/build/$Build' => 'build', |
122
|
|
|
'project/$Project/restoresnapshot/$DataArchiveID' => 'restoresnapshot', |
123
|
|
|
'project/$Project/deletesnapshot/$DataArchiveID' => 'deletesnapshot', |
124
|
|
|
'project/$Project/movesnapshot/$DataArchiveID' => 'movesnapshot', |
125
|
|
|
'project/$Project/update' => 'update', |
126
|
|
|
'project/$Project/snapshots' => 'snapshots', |
127
|
|
|
'project/$Project/createsnapshot' => 'createsnapshot', |
128
|
|
|
'project/$Project/uploadsnapshot' => 'uploadsnapshot', |
129
|
|
|
'project/$Project/snapshotslog' => 'snapshotslog', |
130
|
|
|
'project/$Project/postsnapshotsuccess/$DataArchiveID' => 'postsnapshotsuccess', |
131
|
|
|
'project/$Project/star' => 'toggleprojectstar', |
132
|
|
|
'project/$Project' => 'project', |
133
|
|
|
'projects' => 'projects', |
134
|
|
|
); |
135
|
|
|
|
136
|
|
|
/** |
137
|
|
|
* @var array |
138
|
|
|
*/ |
139
|
|
|
protected static $_project_cache = array(); |
140
|
|
|
|
141
|
|
|
/** |
142
|
|
|
* @var array |
143
|
|
|
*/ |
144
|
|
|
private static $support_links = array(); |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* @var array |
148
|
|
|
*/ |
149
|
|
|
private static $platform_specific_strings = array(); |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* @var array |
153
|
|
|
*/ |
154
|
|
|
private static $action_types = array( |
155
|
|
|
self::ACTION_DEPLOY, |
156
|
|
|
self::ACTION_SNAPSHOT |
157
|
|
|
); |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* @var DNData |
161
|
|
|
*/ |
162
|
|
|
protected $data; |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* Include requirements that deploynaut needs, such as javascript. |
166
|
|
|
*/ |
167
|
|
|
public static function include_requirements() { |
168
|
|
|
|
169
|
|
|
// JS should always go to the bottom, otherwise there's the risk that Requirements |
170
|
|
|
// puts them halfway through the page to the nearest <script> tag. We don't want that. |
171
|
|
|
Requirements::set_force_js_to_bottom(true); |
172
|
|
|
|
173
|
|
|
Requirements::combine_files( |
174
|
|
|
'deploynaut.js', |
175
|
|
|
array( |
176
|
|
|
'deploynaut/javascript/jquery.js', |
177
|
|
|
'deploynaut/javascript/bootstrap.js', |
178
|
|
|
'deploynaut/javascript/q.js', |
179
|
|
|
'deploynaut/javascript/tablefilter.js', |
180
|
|
|
'deploynaut/javascript/deploynaut.js', |
181
|
|
|
'deploynaut/javascript/react-with-addons.js', |
182
|
|
|
'deploynaut/javascript/bootstrap.file-input.js', |
183
|
|
|
'deploynaut/thirdparty/select2/dist/js/select2.min.js', |
184
|
|
|
'deploynaut/javascript/material.js', |
185
|
|
|
) |
186
|
|
|
); |
187
|
|
|
|
188
|
|
|
if (\Director::isDev()) { |
189
|
|
|
\Requirements::javascript('deploynaut/static/bundle-debug.js'); |
190
|
|
|
} else { |
191
|
|
|
\Requirements::javascript('deploynaut/static/bundle.js'); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
Requirements::css('deploynaut/static/style.css'); |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* Check for feature flags: |
199
|
|
|
* - FLAG_SNAPSHOTS_ENABLED: set to true to enable globally |
200
|
|
|
* - FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS: set to semicolon-separated list of email addresses of allowed users. |
201
|
|
|
* |
202
|
|
|
* @return boolean |
203
|
|
|
*/ |
204
|
|
|
public static function FlagSnapshotsEnabled() { |
205
|
|
|
if(defined('FLAG_SNAPSHOTS_ENABLED') && FLAG_SNAPSHOTS_ENABLED) { |
206
|
|
|
return true; |
207
|
|
|
} |
208
|
|
|
if(defined('FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS') && FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS) { |
209
|
|
|
$allowedMembers = explode(';', FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS); |
210
|
|
|
$member = Member::currentUser(); |
211
|
|
|
if($allowedMembers && $member && in_array($member->Email, $allowedMembers)) { |
212
|
|
|
return true; |
213
|
|
|
} |
214
|
|
|
} |
215
|
|
|
return false; |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* @return ArrayList |
220
|
|
|
*/ |
221
|
|
|
public static function get_support_links() { |
222
|
|
|
$supportLinks = self::config()->support_links; |
223
|
|
|
if($supportLinks) { |
224
|
|
|
return new ArrayList($supportLinks); |
225
|
|
|
} |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
/** |
229
|
|
|
* @return array |
230
|
|
|
*/ |
231
|
|
|
public static function get_template_global_variables() { |
232
|
|
|
return array( |
233
|
|
|
'RedisUnavailable' => 'RedisUnavailable', |
234
|
|
|
'RedisWorkersCount' => 'RedisWorkersCount', |
235
|
|
|
'SidebarLinks' => 'SidebarLinks', |
236
|
|
|
"SupportLinks" => 'get_support_links' |
237
|
|
|
); |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
/** |
241
|
|
|
*/ |
242
|
|
|
public function init() { |
243
|
|
|
parent::init(); |
244
|
|
|
|
245
|
|
|
if(!Member::currentUser() && !Session::get('AutoLoginHash')) { |
246
|
|
|
return Security::permissionFailure(); |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
// Block framework jquery |
250
|
|
|
Requirements::block(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js'); |
251
|
|
|
|
252
|
|
|
self::include_requirements(); |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
/** |
256
|
|
|
* @return string |
257
|
|
|
*/ |
258
|
|
|
public function Link() { |
259
|
|
|
return "naut/"; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* Actions |
264
|
|
|
* |
265
|
|
|
* @param SS_HTTPRequest $request |
266
|
|
|
* @return \SS_HTTPResponse |
267
|
|
|
*/ |
268
|
|
|
public function index(SS_HTTPRequest $request) { |
269
|
|
|
return $this->redirect($this->Link() . 'projects/'); |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* Action |
274
|
|
|
* |
275
|
|
|
* @param SS_HTTPRequest $request |
276
|
|
|
* @return string - HTML |
277
|
|
|
*/ |
278
|
|
|
public function projects(SS_HTTPRequest $request) { |
279
|
|
|
// Performs canView permission check by limiting visible projects in DNProjectsList() call. |
280
|
|
|
return $this->customise(array( |
281
|
|
|
'Title' => 'Projects', |
282
|
|
|
))->render(); |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* @param SS_HTTPRequest $request |
287
|
|
|
* @return HTMLText |
288
|
|
|
*/ |
289
|
|
|
public function nav(SS_HTTPRequest $request) { |
290
|
|
|
return $this->renderWith('Nav'); |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* Return a link to the navigation template used for AJAX requests. |
295
|
|
|
* @return string |
296
|
|
|
*/ |
297
|
|
|
public function NavLink() { |
298
|
|
|
return Controller::join_links(Director::absoluteBaseURL(), 'naut', 'nav'); |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
/** |
302
|
|
|
* Action |
303
|
|
|
* |
304
|
|
|
* @param SS_HTTPRequest $request |
305
|
|
|
* @return SS_HTTPResponse - HTML |
306
|
|
|
*/ |
307
|
|
|
public function snapshots(SS_HTTPRequest $request) { |
308
|
|
|
$this->setCurrentActionType(self::ACTION_SNAPSHOT); |
309
|
|
|
return $this->getCustomisedViewSection('SnapshotsSection', 'Data Snapshots'); |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
/** |
313
|
|
|
* Action |
314
|
|
|
* |
315
|
|
|
* @param SS_HTTPRequest $request |
316
|
|
|
* @return string - HTML |
317
|
|
|
*/ |
318
|
|
View Code Duplication |
public function createsnapshot(SS_HTTPRequest $request) { |
|
|
|
|
319
|
|
|
$this->setCurrentActionType(self::ACTION_SNAPSHOT); |
320
|
|
|
|
321
|
|
|
// Performs canView permission check by limiting visible projects |
322
|
|
|
$project = $this->getCurrentProject(); |
323
|
|
|
if(!$project) { |
324
|
|
|
return $this->project404Response(); |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
if(!$project->canBackup()) { |
328
|
|
|
return new SS_HTTPResponse("Not allowed to create snapshots on any environments", 401); |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
return $this->customise(array( |
332
|
|
|
'Title' => 'Create Data Snapshot', |
333
|
|
|
'SnapshotsSection' => 1, |
334
|
|
|
'DataTransferForm' => $this->getDataTransferForm($request) |
335
|
|
|
))->render(); |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
/** |
339
|
|
|
* Action |
340
|
|
|
* |
341
|
|
|
* @param SS_HTTPRequest $request |
342
|
|
|
* @return string - HTML |
343
|
|
|
*/ |
344
|
|
View Code Duplication |
public function uploadsnapshot(SS_HTTPRequest $request) { |
|
|
|
|
345
|
|
|
$this->setCurrentActionType(self::ACTION_SNAPSHOT); |
346
|
|
|
|
347
|
|
|
// Performs canView permission check by limiting visible projects |
348
|
|
|
$project = $this->getCurrentProject(); |
349
|
|
|
if(!$project) { |
350
|
|
|
return $this->project404Response(); |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
if(!$project->canUploadArchive()) { |
354
|
|
|
return new SS_HTTPResponse("Not allowed to upload", 401); |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
return $this->customise(array( |
358
|
|
|
'SnapshotsSection' => 1, |
359
|
|
|
'UploadSnapshotForm' => $this->getUploadSnapshotForm($request), |
360
|
|
|
'PostSnapshotForm' => $this->getPostSnapshotForm($request) |
361
|
|
|
))->render(); |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
/** |
365
|
|
|
* Return the upload limit for snapshot uploads |
366
|
|
|
* @return string |
367
|
|
|
*/ |
368
|
|
|
public function UploadLimit() { |
369
|
|
|
return File::format_size(min( |
370
|
|
|
File::ini2bytes(ini_get('upload_max_filesize')), |
371
|
|
|
File::ini2bytes(ini_get('post_max_size')) |
372
|
|
|
)); |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
/** |
376
|
|
|
* Construct the upload form. |
377
|
|
|
* |
378
|
|
|
* @param SS_HTTPRequest $request |
379
|
|
|
* @return Form |
380
|
|
|
*/ |
381
|
|
|
public function getUploadSnapshotForm(SS_HTTPRequest $request) { |
382
|
|
|
// Performs canView permission check by limiting visible projects |
383
|
|
|
$project = $this->getCurrentProject(); |
384
|
|
|
if(!$project) { |
385
|
|
|
return $this->project404Response(); |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
if(!$project->canUploadArchive()) { |
389
|
|
|
return new SS_HTTPResponse("Not allowed to upload", 401); |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
// Framing an environment as a "group of people with download access" |
393
|
|
|
// makes more sense to the user here, while still allowing us to enforce |
394
|
|
|
// environment specific restrictions on downloading the file later on. |
395
|
|
|
$envs = $project->DNEnvironmentList()->filterByCallback(function($item) { |
396
|
|
|
return $item->canUploadArchive(); |
397
|
|
|
}); |
398
|
|
|
$envsMap = array(); |
399
|
|
|
foreach($envs as $env) { |
400
|
|
|
$envsMap[$env->ID] = $env->Name; |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
$maxSize = min(File::ini2bytes(ini_get('upload_max_filesize')), File::ini2bytes(ini_get('post_max_size'))); |
404
|
|
|
$fileField = DataArchiveFileField::create('ArchiveFile', 'File'); |
405
|
|
|
$fileField->getValidator()->setAllowedExtensions(array('sspak')); |
406
|
|
|
$fileField->getValidator()->setAllowedMaxFileSize(array('*' => $maxSize)); |
407
|
|
|
|
408
|
|
|
$form = Form::create( |
409
|
|
|
$this, |
410
|
|
|
'UploadSnapshotForm', |
411
|
|
|
FieldList::create( |
412
|
|
|
$fileField, |
413
|
|
|
DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()), |
414
|
|
|
DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap) |
415
|
|
|
->setEmptyString('Select an environment') |
416
|
|
|
), |
417
|
|
|
FieldList::create( |
418
|
|
|
FormAction::create('doUploadSnapshot', 'Upload File') |
419
|
|
|
->addExtraClass('btn') |
420
|
|
|
), |
421
|
|
|
RequiredFields::create('ArchiveFile') |
422
|
|
|
); |
423
|
|
|
|
424
|
|
|
$form->disableSecurityToken(); |
425
|
|
|
$form->addExtraClass('fields-wide'); |
426
|
|
|
// Tweak the action so it plays well with our fake URL structure. |
427
|
|
|
$form->setFormAction($project->Link() . '/UploadSnapshotForm'); |
428
|
|
|
|
429
|
|
|
return $form; |
430
|
|
|
} |
431
|
|
|
|
432
|
|
|
/** |
433
|
|
|
* @param array $data |
434
|
|
|
* @param Form $form |
435
|
|
|
* |
436
|
|
|
* @return bool|HTMLText|SS_HTTPResponse |
437
|
|
|
*/ |
438
|
|
|
public function doUploadSnapshot($data, Form $form) { |
439
|
|
|
$this->setCurrentActionType(self::ACTION_SNAPSHOT); |
440
|
|
|
|
441
|
|
|
// Performs canView permission check by limiting visible projects |
442
|
|
|
$project = $this->getCurrentProject(); |
443
|
|
|
if(!$project) { |
444
|
|
|
return $this->project404Response(); |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
$validEnvs = $project->DNEnvironmentList() |
448
|
|
|
->filterByCallback(function($item) { |
449
|
|
|
return $item->canUploadArchive(); |
450
|
|
|
}); |
451
|
|
|
|
452
|
|
|
// Validate $data['EnvironmentID'] by checking against $validEnvs. |
453
|
|
|
$environment = $validEnvs->find('ID', $data['EnvironmentID']); |
454
|
|
|
if(!$environment) { |
455
|
|
|
throw new LogicException('Invalid environment'); |
456
|
|
|
} |
457
|
|
|
|
458
|
|
|
$this->validateSnapshotMode($data['Mode']); |
459
|
|
|
|
460
|
|
|
$dataArchive = DNDataArchive::create(array( |
461
|
|
|
'AuthorID' => Member::currentUserID(), |
462
|
|
|
'EnvironmentID' => $data['EnvironmentID'], |
463
|
|
|
'IsManualUpload' => true, |
464
|
|
|
)); |
465
|
|
|
// needs an ID and transfer to determine upload path |
466
|
|
|
$dataArchive->write(); |
467
|
|
|
$dataTransfer = DNDataTransfer::create(array( |
468
|
|
|
'AuthorID' => Member::currentUserID(), |
469
|
|
|
'Mode' => $data['Mode'], |
470
|
|
|
'Origin' => 'ManualUpload', |
471
|
|
|
'EnvironmentID' => $data['EnvironmentID'] |
472
|
|
|
)); |
473
|
|
|
$dataTransfer->write(); |
474
|
|
|
$dataArchive->DataTransfers()->add($dataTransfer); |
475
|
|
|
$form->saveInto($dataArchive); |
476
|
|
|
$dataArchive->write(); |
477
|
|
|
$workingDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID; |
478
|
|
|
|
479
|
|
|
$cleanupFn = function() use($workingDir, $dataTransfer, $dataArchive) { |
480
|
|
|
$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir))); |
481
|
|
|
$process->run(); |
482
|
|
|
$dataTransfer->delete(); |
483
|
|
|
$dataArchive->delete(); |
484
|
|
|
}; |
485
|
|
|
|
486
|
|
|
// extract the sspak contents so we can inspect them |
487
|
|
|
try { |
488
|
|
|
$dataArchive->extractArchive($workingDir); |
489
|
|
|
} catch(Exception $e) { |
490
|
|
|
$cleanupFn(); |
491
|
|
|
$form->sessionMessage( |
492
|
|
|
'There was a problem trying to open your snapshot for processing. Please try uploading again', |
493
|
|
|
'bad' |
494
|
|
|
); |
495
|
|
|
return $this->redirectBack(); |
496
|
|
|
} |
497
|
|
|
|
498
|
|
|
// validate that the sspak contents match the declared contents |
499
|
|
|
$result = $dataArchive->validateArchiveContents(); |
500
|
|
|
if(!$result->valid()) { |
501
|
|
|
$cleanupFn(); |
502
|
|
|
$form->sessionMessage($result->message(), 'bad'); |
503
|
|
|
return $this->redirectBack(); |
504
|
|
|
} |
505
|
|
|
|
506
|
|
|
// fix file permissions of extracted sspak files then re-build the sspak |
507
|
|
|
try { |
508
|
|
|
$dataArchive->fixArchivePermissions($workingDir); |
509
|
|
|
$dataArchive->setArchiveFromFiles($workingDir); |
510
|
|
|
} catch(Exception $e) { |
511
|
|
|
$cleanupFn(); |
512
|
|
|
$form->sessionMessage( |
513
|
|
|
'There was a problem processing your snapshot. Please try uploading again', |
514
|
|
|
'bad' |
515
|
|
|
); |
516
|
|
|
return $this->redirectBack(); |
517
|
|
|
} |
518
|
|
|
|
519
|
|
|
// cleanup any extracted sspak contents lying around |
520
|
|
|
$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir))); |
521
|
|
|
$process->run(); |
522
|
|
|
|
523
|
|
|
return $this->customise(array( |
524
|
|
|
'Project' => $project, |
525
|
|
|
'CurrentProject' => $project, |
526
|
|
|
'SnapshotsSection' => 1, |
527
|
|
|
'DataArchive' => $dataArchive, |
528
|
|
|
'DataTransferRestoreForm' => $this->getDataTransferRestoreForm($this->request, $dataArchive), |
529
|
|
|
'BackURL' => $project->Link('snapshots') |
530
|
|
|
))->renderWith(array('DNRoot_uploadsnapshot', 'DNRoot')); |
531
|
|
|
} |
532
|
|
|
|
533
|
|
|
/** |
534
|
|
|
* @param SS_HTTPRequest $request |
535
|
|
|
* @return Form |
536
|
|
|
*/ |
537
|
|
|
public function getPostSnapshotForm(SS_HTTPRequest $request) { |
538
|
|
|
// Performs canView permission check by limiting visible projects |
539
|
|
|
$project = $this->getCurrentProject(); |
540
|
|
|
if(!$project) { |
541
|
|
|
return $this->project404Response(); |
542
|
|
|
} |
543
|
|
|
|
544
|
|
|
if(!$project->canUploadArchive()) { |
545
|
|
|
return new SS_HTTPResponse("Not allowed to upload", 401); |
546
|
|
|
} |
547
|
|
|
|
548
|
|
|
// Framing an environment as a "group of people with download access" |
549
|
|
|
// makes more sense to the user here, while still allowing us to enforce |
550
|
|
|
// environment specific restrictions on downloading the file later on. |
551
|
|
|
$envs = $project->DNEnvironmentList()->filterByCallback(function($item) { |
552
|
|
|
return $item->canUploadArchive(); |
553
|
|
|
}); |
554
|
|
|
$envsMap = array(); |
555
|
|
|
foreach($envs as $env) { |
556
|
|
|
$envsMap[$env->ID] = $env->Name; |
557
|
|
|
} |
558
|
|
|
|
559
|
|
|
$form = Form::create( |
560
|
|
|
$this, |
561
|
|
|
'PostSnapshotForm', |
562
|
|
|
FieldList::create( |
563
|
|
|
DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()), |
564
|
|
|
DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap) |
565
|
|
|
->setEmptyString('Select an environment') |
566
|
|
|
), |
567
|
|
|
FieldList::create( |
568
|
|
|
FormAction::create('doPostSnapshot', 'Submit request') |
569
|
|
|
->addExtraClass('btn') |
570
|
|
|
), |
571
|
|
|
RequiredFields::create('File') |
572
|
|
|
); |
573
|
|
|
|
574
|
|
|
$form->disableSecurityToken(); |
575
|
|
|
$form->addExtraClass('fields-wide'); |
576
|
|
|
// Tweak the action so it plays well with our fake URL structure. |
577
|
|
|
$form->setFormAction($project->Link() . '/PostSnapshotForm'); |
578
|
|
|
|
579
|
|
|
return $form; |
580
|
|
|
} |
581
|
|
|
|
582
|
|
|
/** |
583
|
|
|
* @param array $data |
584
|
|
|
* @param Form $form |
585
|
|
|
* |
586
|
|
|
* @return SS_HTTPResponse |
587
|
|
|
*/ |
588
|
|
|
public function doPostSnapshot($data, $form) { |
589
|
|
|
$this->setCurrentActionType(self::ACTION_SNAPSHOT); |
590
|
|
|
|
591
|
|
|
$project = $this->getCurrentProject(); |
592
|
|
|
if(!$project) { |
593
|
|
|
return $this->project404Response(); |
594
|
|
|
} |
595
|
|
|
|
596
|
|
|
$validEnvs = $project->DNEnvironmentList()->filterByCallback(function($item) { |
597
|
|
|
return $item->canUploadArchive(); |
598
|
|
|
}); |
599
|
|
|
|
600
|
|
|
// Validate $data['EnvironmentID'] by checking against $validEnvs. |
601
|
|
|
$environment = $validEnvs->find('ID', $data['EnvironmentID']); |
602
|
|
|
if(!$environment) { |
603
|
|
|
throw new LogicException('Invalid environment'); |
604
|
|
|
} |
605
|
|
|
|
606
|
|
|
$dataArchive = DNDataArchive::create(array( |
607
|
|
|
'UploadToken' => DNDataArchive::generate_upload_token(), |
608
|
|
|
)); |
609
|
|
|
$form->saveInto($dataArchive); |
610
|
|
|
$dataArchive->write(); |
611
|
|
|
|
612
|
|
|
return $this->redirect(Controller::join_links( |
613
|
|
|
$project->Link(), |
614
|
|
|
'postsnapshotsuccess', |
615
|
|
|
$dataArchive->ID |
616
|
|
|
)); |
617
|
|
|
} |
618
|
|
|
|
619
|
|
|
/** |
620
|
|
|
* Action |
621
|
|
|
* |
622
|
|
|
* @param SS_HTTPRequest $request |
623
|
|
|
* @return SS_HTTPResponse - HTML |
624
|
|
|
*/ |
625
|
|
|
public function snapshotslog(SS_HTTPRequest $request) { |
626
|
|
|
$this->setCurrentActionType(self::ACTION_SNAPSHOT); |
627
|
|
|
return $this->getCustomisedViewSection('SnapshotsSection', 'Data Snapshots Log'); |
628
|
|
|
} |
629
|
|
|
|
630
|
|
|
/** |
631
|
|
|
* @param SS_HTTPRequest $request |
632
|
|
|
* @return SS_HTTPResponse|string |
633
|
|
|
* @throws SS_HTTPResponse_Exception |
634
|
|
|
*/ |
635
|
|
|
public function postsnapshotsuccess(SS_HTTPRequest $request) { |
636
|
|
|
$this->setCurrentActionType(self::ACTION_SNAPSHOT); |
637
|
|
|
|
638
|
|
|
// Performs canView permission check by limiting visible projects |
639
|
|
|
$project = $this->getCurrentProject(); |
640
|
|
|
if(!$project) { |
641
|
|
|
return $this->project404Response(); |
642
|
|
|
} |
643
|
|
|
|
644
|
|
|
if(!$project->canUploadArchive()) { |
645
|
|
|
return new SS_HTTPResponse("Not allowed to upload", 401); |
646
|
|
|
} |
647
|
|
|
|
648
|
|
|
$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID')); |
649
|
|
|
if(!$dataArchive) { |
650
|
|
|
return new SS_HTTPResponse("Archive not found.", 404); |
651
|
|
|
} |
652
|
|
|
|
653
|
|
|
if(!$dataArchive->canRestore()) { |
654
|
|
|
throw new SS_HTTPResponse_Exception('Not allowed to restore archive', 403); |
655
|
|
|
} |
656
|
|
|
|
657
|
|
|
return $this->render(array( |
658
|
|
|
'Title' => 'How to send us your Data Snapshot by post', |
659
|
|
|
'DataArchive' => $dataArchive, |
660
|
|
|
'Address' => Config::inst()->get('Deploynaut', 'snapshot_post_address'), |
661
|
|
|
'BackURL' => $project->Link(), |
662
|
|
|
)); |
663
|
|
|
} |
664
|
|
|
|
665
|
|
|
/** |
666
|
|
|
* @param SS_HTTPRequest $request |
667
|
|
|
* @return \SS_HTTPResponse |
668
|
|
|
*/ |
669
|
|
|
public function project(SS_HTTPRequest $request) { |
670
|
|
|
return $this->getCustomisedViewSection('ProjectOverview'); |
671
|
|
|
} |
672
|
|
|
|
673
|
|
|
/** |
674
|
|
|
* This action will star / unstar a project for the current member |
675
|
|
|
* |
676
|
|
|
* @param SS_HTTPRequest $request |
677
|
|
|
* |
678
|
|
|
* @return SS_HTTPResponse |
679
|
|
|
*/ |
680
|
|
|
public function toggleprojectstar(SS_HTTPRequest $request) { |
681
|
|
|
$project = $this->getCurrentProject(); |
682
|
|
|
if(!$project) { |
683
|
|
|
return $this->project404Response(); |
684
|
|
|
} |
685
|
|
|
|
686
|
|
|
$member = Member::currentUser(); |
687
|
|
|
if($member === null) { |
688
|
|
|
return $this->project404Response(); |
689
|
|
|
} |
690
|
|
|
$favProject = $member->StarredProjects() |
691
|
|
|
->filter('DNProjectID', $project->ID) |
692
|
|
|
->first(); |
693
|
|
|
|
694
|
|
|
if($favProject) { |
695
|
|
|
$member->StarredProjects()->remove($favProject); |
696
|
|
|
} else { |
697
|
|
|
$member->StarredProjects()->add($project); |
698
|
|
|
} |
699
|
|
|
return $this->redirectBack(); |
700
|
|
|
} |
701
|
|
|
|
702
|
|
|
/** |
703
|
|
|
* @param SS_HTTPRequest $request |
704
|
|
|
* @return \SS_HTTPResponse |
705
|
|
|
*/ |
706
|
|
|
public function branch(SS_HTTPRequest $request) { |
707
|
|
|
$project = $this->getCurrentProject(); |
708
|
|
|
if(!$project) { |
709
|
|
|
return $this->project404Response(); |
710
|
|
|
} |
711
|
|
|
|
712
|
|
|
$branchName = $request->getVar('name'); |
713
|
|
|
$branch = $project->DNBranchList()->byName($branchName); |
714
|
|
|
if(!$branch) { |
715
|
|
|
return new SS_HTTPResponse("Branch '" . Convert::raw2xml($branchName) . "' not found.", 404); |
716
|
|
|
} |
717
|
|
|
|
718
|
|
|
return $this->render(array( |
719
|
|
|
'CurrentBranch' => $branch, |
720
|
|
|
)); |
721
|
|
|
} |
722
|
|
|
|
723
|
|
|
/** |
724
|
|
|
* @param SS_HTTPRequest $request |
725
|
|
|
* @return \SS_HTTPResponse |
726
|
|
|
*/ |
727
|
|
|
public function environment(SS_HTTPRequest $request) { |
728
|
|
|
// Performs canView permission check by limiting visible projects |
729
|
|
|
$project = $this->getCurrentProject(); |
730
|
|
|
if(!$project) { |
731
|
|
|
return $this->project404Response(); |
732
|
|
|
} |
733
|
|
|
|
734
|
|
|
// Performs canView permission check by limiting visible projects |
735
|
|
|
$env = $this->getCurrentEnvironment($project); |
736
|
|
|
if(!$env) { |
737
|
|
|
return $this->environment404Response(); |
738
|
|
|
} |
739
|
|
|
|
740
|
|
|
return $this->render(array( |
741
|
|
|
'DNEnvironmentList' => $this->getCurrentProject()->DNEnvironmentList(), |
742
|
|
|
'FlagSnapshotsEnabled' => $this->FlagSnapshotsEnabled(), |
743
|
|
|
)); |
744
|
|
|
} |
745
|
|
|
|
746
|
|
|
|
747
|
|
|
/** |
748
|
|
|
* Initiate a pipeline dry run |
749
|
|
|
* |
750
|
|
|
* @param array $data |
751
|
|
|
* @param DeployForm $form |
752
|
|
|
* |
753
|
|
|
* @return SS_HTTPResponse |
754
|
|
|
*/ |
755
|
|
|
public function doDryRun($data, DeployForm $form) { |
756
|
|
|
return $this->beginPipeline($data, $form, true); |
757
|
|
|
} |
758
|
|
|
|
759
|
|
|
/** |
760
|
|
|
* Initiate a pipeline |
761
|
|
|
* |
762
|
|
|
* @param array $data |
763
|
|
|
* @param DeployForm $form |
764
|
|
|
* @return \SS_HTTPResponse |
765
|
|
|
*/ |
766
|
|
|
public function startPipeline($data, $form) { |
767
|
|
|
return $this->beginPipeline($data, $form); |
768
|
|
|
} |
769
|
|
|
|
770
|
|
|
/** |
771
|
|
|
* Start a pipeline |
772
|
|
|
* |
773
|
|
|
* @param array $data |
774
|
|
|
* @param DeployForm $form |
775
|
|
|
* @param bool $isDryRun |
776
|
|
|
* @return \SS_HTTPResponse |
777
|
|
|
*/ |
778
|
|
|
protected function beginPipeline($data, DeployForm $form, $isDryRun = false) { |
779
|
|
|
$buildName = $form->getSelectedBuild($data); |
780
|
|
|
|
781
|
|
|
// Performs canView permission check by limiting visible projects |
782
|
|
|
$project = $this->getCurrentProject(); |
783
|
|
|
if(!$project) { |
784
|
|
|
return $this->project404Response(); |
785
|
|
|
} |
786
|
|
|
|
787
|
|
|
// Performs canView permission check by limiting visible projects |
788
|
|
|
$environment = $this->getCurrentEnvironment($project); |
789
|
|
|
if(!$environment) { |
790
|
|
|
return $this->environment404Response(); |
791
|
|
|
} |
792
|
|
|
|
793
|
|
|
if(!$environment->DryRunEnabled && $isDryRun) { |
794
|
|
|
return new SS_HTTPResponse("Dry-run for pipelines is not enabled for this environment", 404); |
795
|
|
|
} |
796
|
|
|
|
797
|
|
|
// Initiate the pipeline |
798
|
|
|
$sha = $project->DNBuildList()->byName($buildName); |
799
|
|
|
$pipeline = Pipeline::create(); |
800
|
|
|
$pipeline->DryRun = $isDryRun; |
801
|
|
|
$pipeline->EnvironmentID = $environment->ID; |
802
|
|
|
$pipeline->AuthorID = Member::currentUserID(); |
803
|
|
|
$pipeline->SHA = $sha->FullName(); |
804
|
|
|
// Record build at time of execution |
805
|
|
|
if($currentBuild = $environment->CurrentBuild()) { |
806
|
|
|
$pipeline->PreviousDeploymentID = $currentBuild->ID; |
807
|
|
|
} |
808
|
|
|
$pipeline->start(); // start() will call write(), so no need to do it here as well. |
809
|
|
|
return $this->redirect($environment->Link()); |
810
|
|
|
} |
811
|
|
|
|
812
|
|
|
/** |
813
|
|
|
* @param SS_HTTPRequest $request |
814
|
|
|
* |
815
|
|
|
* @return SS_HTTPResponse |
816
|
|
|
* @throws SS_HTTPResponse_Exception |
817
|
|
|
*/ |
818
|
|
|
public function pipeline(SS_HTTPRequest $request) { |
819
|
|
|
$params = $request->params(); |
820
|
|
|
$pipeline = Pipeline::get()->byID($params['Identifier']); |
821
|
|
|
|
822
|
|
|
if(!$pipeline || !$pipeline->ID || !$pipeline->Environment()) { |
823
|
|
|
throw new SS_HTTPResponse_Exception('Pipeline not found', 404); |
824
|
|
|
} |
825
|
|
|
if(!$pipeline->Environment()->canView()) { |
826
|
|
|
return Security::permissionFailure(); |
827
|
|
|
} |
828
|
|
|
|
829
|
|
|
$environment = $pipeline->Environment(); |
830
|
|
|
$project = $pipeline->Environment()->Project(); |
831
|
|
|
|
832
|
|
|
if($environment->Name != $params['Environment']) { |
833
|
|
|
throw new LogicException("Environment in URL doesn't match this pipeline"); |
834
|
|
|
} |
835
|
|
|
if($project->Name != $params['Project']) { |
836
|
|
|
throw new LogicException("Project in URL doesn't match this pipeline"); |
837
|
|
|
} |
838
|
|
|
|
839
|
|
|
// Delegate to sub-requesthandler |
840
|
|
|
return PipelineController::create($this, $pipeline); |
841
|
|
|
} |
842
|
|
|
|
843
|
|
|
/** |
844
|
|
|
* Shows the creation log. |
845
|
|
|
* |
846
|
|
|
* @param SS_HTTPRequest $request |
847
|
|
|
* @return string |
848
|
|
|
*/ |
849
|
|
|
public function createenv(SS_HTTPRequest $request) { |
850
|
|
|
$params = $request->params(); |
851
|
|
|
if($params['Identifier']) { |
852
|
|
|
$record = DNCreateEnvironment::get()->byId($params['Identifier']); |
853
|
|
|
|
854
|
|
|
if(!$record || !$record->ID) { |
855
|
|
|
throw new SS_HTTPResponse_Exception('Create environment not found', 404); |
856
|
|
|
} |
857
|
|
|
if(!$record->canView()) { |
858
|
|
|
return Security::permissionFailure(); |
859
|
|
|
} |
860
|
|
|
|
861
|
|
|
$project = $this->getCurrentProject(); |
862
|
|
|
if(!$project) { |
863
|
|
|
return $this->project404Response(); |
864
|
|
|
} |
865
|
|
|
|
866
|
|
|
if($project->Name != $params['Project']) { |
867
|
|
|
throw new LogicException("Project in URL doesn't match this creation"); |
868
|
|
|
} |
869
|
|
|
|
870
|
|
|
return $this->render(array( |
871
|
|
|
'CreateEnvironment' => $record, |
872
|
|
|
)); |
873
|
|
|
} |
874
|
|
|
return $this->render(); |
875
|
|
|
} |
876
|
|
|
|
877
|
|
|
|
878
|
|
View Code Duplication |
public function createenvlog(SS_HTTPRequest $request) { |
|
|
|
|
879
|
|
|
$this->setCurrentActionType(self::ACTION_SNAPSHOT); |
880
|
|
|
|
881
|
|
|
$params = $request->params(); |
882
|
|
|
$env = DNCreateEnvironment::get()->byId($params['Identifier']); |
883
|
|
|
|
884
|
|
|
if(!$env || !$env->ID) { |
885
|
|
|
throw new SS_HTTPResponse_Exception('Log not found', 404); |
886
|
|
|
} |
887
|
|
|
if(!$env->canView()) { |
888
|
|
|
return Security::permissionFailure(); |
889
|
|
|
} |
890
|
|
|
|
891
|
|
|
$project = $env->Project(); |
892
|
|
|
|
893
|
|
|
if($project->Name != $params['Project']) { |
894
|
|
|
throw new LogicException("Project in URL doesn't match this deploy"); |
895
|
|
|
} |
896
|
|
|
|
897
|
|
|
$log = $env->log(); |
898
|
|
|
if($log->exists()) { |
899
|
|
|
$content = $log->content(); |
900
|
|
|
} else { |
901
|
|
|
$content = 'Waiting for action to start'; |
902
|
|
|
} |
903
|
|
|
|
904
|
|
|
return $this->sendResponse($env->ResqueStatus(), $content); |
905
|
|
|
} |
906
|
|
|
|
907
|
|
|
/** |
908
|
|
|
* @param SS_HTTPRequest $request |
909
|
|
|
* @return Form |
910
|
|
|
*/ |
911
|
|
|
public function getCreateEnvironmentForm(SS_HTTPRequest $request) { |
912
|
|
|
$this->setCurrentActionType(self::ACTION_ENVIRONMENTS); |
913
|
|
|
|
914
|
|
|
$project = $this->getCurrentProject(); |
915
|
|
|
if(!$project) { |
916
|
|
|
return $this->project404Response(); |
917
|
|
|
} |
918
|
|
|
|
919
|
|
|
$envType = $project->AllowedEnvironmentType; |
|
|
|
|
920
|
|
|
if(!$envType || !class_exists($envType)) { |
921
|
|
|
return null; |
922
|
|
|
} |
923
|
|
|
|
924
|
|
|
$backend = Injector::inst()->get($envType); |
925
|
|
|
if(!($backend instanceof EnvironmentCreateBackend)) { |
926
|
|
|
// Only allow this for supported backends. |
927
|
|
|
return null; |
928
|
|
|
} |
929
|
|
|
|
930
|
|
|
$fields = $backend->getCreateEnvironmentFields($project); |
931
|
|
|
if(!$fields) return null; |
932
|
|
|
|
933
|
|
|
if(!$project->canCreateEnvironments()) { |
934
|
|
|
return new SS_HTTPResponse('Not allowed to create environments for this project', 401); |
935
|
|
|
} |
936
|
|
|
|
937
|
|
|
$form = Form::create( |
938
|
|
|
$this, |
939
|
|
|
'CreateEnvironmentForm', |
940
|
|
|
$fields, |
941
|
|
|
FieldList::create( |
942
|
|
|
FormAction::create('doCreateEnvironment', 'Create') |
943
|
|
|
->addExtraClass('btn') |
944
|
|
|
), |
945
|
|
|
$backend->getCreateEnvironmentValidator() |
946
|
|
|
); |
947
|
|
|
|
948
|
|
|
// Tweak the action so it plays well with our fake URL structure. |
949
|
|
|
$form->setFormAction($project->Link() . '/CreateEnvironmentForm'); |
950
|
|
|
|
951
|
|
|
return $form; |
952
|
|
|
} |
953
|
|
|
|
954
|
|
|
/** |
955
|
|
|
* @param array $data |
956
|
|
|
* @param Form $form |
957
|
|
|
* |
958
|
|
|
* @return bool|HTMLText|SS_HTTPResponse |
959
|
|
|
*/ |
960
|
|
|
public function doCreateEnvironment($data, Form $form) { |
|
|
|
|
961
|
|
|
$this->setCurrentActionType(self::ACTION_ENVIRONMENTS); |
962
|
|
|
|
963
|
|
|
$project = $this->getCurrentProject(); |
964
|
|
|
if(!$project) { |
965
|
|
|
return $this->project404Response(); |
966
|
|
|
} |
967
|
|
|
|
968
|
|
|
if(!$project->canCreateEnvironments()) { |
969
|
|
|
return new SS_HTTPResponse('Not allowed to create environments for this project', 401); |
970
|
|
|
} |
971
|
|
|
|
972
|
|
|
// Set the environment type so we know what we're creating. |
973
|
|
|
$data['EnvironmentType'] = $project->AllowedEnvironmentType; |
|
|
|
|
974
|
|
|
|
975
|
|
|
$job = DNCreateEnvironment::create(); |
976
|
|
|
|
977
|
|
|
$job->Data = serialize($data); |
|
|
|
|
978
|
|
|
$job->ProjectID = $project->ID; |
|
|
|
|
979
|
|
|
$job->write(); |
980
|
|
|
$job->start(); |
981
|
|
|
|
982
|
|
|
return $this->redirect($project->Link('createenv') . '/' . $job->ID); |
983
|
|
|
} |
984
|
|
|
|
985
|
|
|
/** |
986
|
|
|
* |
987
|
|
|
* @param SS_HTTPRequest $request |
988
|
|
|
* @return \SS_HTTPResponse |
989
|
|
|
*/ |
990
|
|
|
public function metrics(SS_HTTPRequest $request) { |
991
|
|
|
// Performs canView permission check by limiting visible projects |
992
|
|
|
$project = $this->getCurrentProject(); |
993
|
|
|
if(!$project) { |
994
|
|
|
return $this->project404Response(); |
995
|
|
|
} |
996
|
|
|
|
997
|
|
|
// Performs canView permission check by limiting visible projects |
998
|
|
|
$env = $this->getCurrentEnvironment($project); |
999
|
|
|
if(!$env) { |
1000
|
|
|
return $this->environment404Response(); |
1001
|
|
|
} |
1002
|
|
|
|
1003
|
|
|
return $this->render(); |
1004
|
|
|
} |
1005
|
|
|
|
1006
|
|
|
/** |
1007
|
|
|
* Get the DNData object. |
1008
|
|
|
* |
1009
|
|
|
* @return DNData |
1010
|
|
|
*/ |
1011
|
|
|
public function DNData() { |
1012
|
|
|
return DNData::inst(); |
1013
|
|
|
} |
1014
|
|
|
|
1015
|
|
|
/** |
1016
|
|
|
* Provide a list of all projects. |
1017
|
|
|
* |
1018
|
|
|
* @return SS_List |
1019
|
|
|
*/ |
1020
|
|
|
public function DNProjectList() { |
1021
|
|
|
$memberId = Member::currentUserID(); |
1022
|
|
|
if(!$memberId) { |
1023
|
|
|
return new ArrayList(); |
1024
|
|
|
} |
1025
|
|
|
|
1026
|
|
|
if(Permission::check('ADMIN')) { |
1027
|
|
|
return DNProject::get(); |
1028
|
|
|
} |
1029
|
|
|
|
1030
|
|
|
return Member::get()->filter('ID', $memberId) |
1031
|
|
|
->relation('Groups') |
1032
|
|
|
->relation('Projects'); |
1033
|
|
|
} |
1034
|
|
|
|
1035
|
|
|
/** |
1036
|
|
|
* @return ArrayList |
1037
|
|
|
*/ |
1038
|
|
|
public function getPlatformSpecificStrings() { |
1039
|
|
|
$strings = $this->config()->platform_specific_strings; |
1040
|
|
|
if ($strings) { |
1041
|
|
|
return new ArrayList($strings); |
1042
|
|
|
} |
1043
|
|
|
} |
1044
|
|
|
|
1045
|
|
|
/** |
1046
|
|
|
* Provide a list of all starred projects for the currently logged in member |
1047
|
|
|
* |
1048
|
|
|
* @return SS_List |
1049
|
|
|
*/ |
1050
|
|
|
public function getStarredProjects() { |
1051
|
|
|
$member = Member::currentUser(); |
1052
|
|
|
if($member === null) { |
1053
|
|
|
return new ArrayList(); |
1054
|
|
|
} |
1055
|
|
|
|
1056
|
|
|
$favProjects = $member->StarredProjects(); |
1057
|
|
|
|
1058
|
|
|
$list = new ArrayList(); |
1059
|
|
|
foreach($favProjects as $project) { |
1060
|
|
|
if($project->canView($member)) { |
1061
|
|
|
$list->add($project); |
1062
|
|
|
} |
1063
|
|
|
} |
1064
|
|
|
return $list; |
1065
|
|
|
} |
1066
|
|
|
|
1067
|
|
|
/** |
1068
|
|
|
* Returns top level navigation of projects. |
1069
|
|
|
* |
1070
|
|
|
* @param int $limit |
1071
|
|
|
* |
1072
|
|
|
* @return ArrayList |
1073
|
|
|
*/ |
1074
|
|
|
public function Navigation($limit = 5) { |
1075
|
|
|
$navigation = new ArrayList(); |
1076
|
|
|
|
1077
|
|
|
$currentProject = $this->getCurrentProject(); |
1078
|
|
|
|
1079
|
|
|
$projects = $this->getStarredProjects(); |
1080
|
|
|
if($projects->count() < 1) { |
1081
|
|
|
$projects = $this->DNProjectList(); |
1082
|
|
|
} else { |
1083
|
|
|
$limit = -1; |
1084
|
|
|
} |
1085
|
|
|
|
1086
|
|
|
if($projects->count() > 0) { |
1087
|
|
|
$activeProject = false; |
1088
|
|
|
|
1089
|
|
|
if($limit > 0) { |
1090
|
|
|
$limitedProjects = $projects->limit($limit); |
1091
|
|
|
} else { |
1092
|
|
|
$limitedProjects = $projects; |
1093
|
|
|
} |
1094
|
|
|
|
1095
|
|
|
foreach($limitedProjects as $project) { |
1096
|
|
|
$isActive = $currentProject && $currentProject->ID == $project->ID; |
1097
|
|
|
if($isActive) { |
1098
|
|
|
$activeProject = true; |
1099
|
|
|
} |
1100
|
|
|
|
1101
|
|
|
$navigation->push(array( |
1102
|
|
|
'Project' => $project, |
1103
|
|
|
'IsActive' => $currentProject && $currentProject->ID == $project->ID, |
1104
|
|
|
)); |
1105
|
|
|
} |
1106
|
|
|
|
1107
|
|
|
// Ensure the current project is in the list |
1108
|
|
|
if(!$activeProject && $currentProject) { |
1109
|
|
|
$navigation->unshift(array( |
1110
|
|
|
'Project' => $currentProject, |
1111
|
|
|
'IsActive' => true, |
1112
|
|
|
)); |
1113
|
|
|
if($limit > 0 && $navigation->count() > $limit) { |
1114
|
|
|
$navigation->pop(); |
1115
|
|
|
} |
1116
|
|
|
} |
1117
|
|
|
} |
1118
|
|
|
|
1119
|
|
|
return $navigation; |
1120
|
|
|
} |
1121
|
|
|
|
1122
|
|
|
/** |
1123
|
|
|
* Construct the deployment form |
1124
|
|
|
* |
1125
|
|
|
* @return Form |
1126
|
|
|
*/ |
1127
|
|
|
public function getDeployForm($request = null) { |
1128
|
|
|
|
1129
|
|
|
// Performs canView permission check by limiting visible projects |
1130
|
|
|
$project = $this->getCurrentProject(); |
1131
|
|
|
if(!$project) { |
1132
|
|
|
return $this->project404Response(); |
1133
|
|
|
} |
1134
|
|
|
|
1135
|
|
|
// Performs canView permission check by limiting visible projects |
1136
|
|
|
$environment = $this->getCurrentEnvironment($project); |
1137
|
|
|
if(!$environment) { |
1138
|
|
|
return $this->environment404Response(); |
1139
|
|
|
} |
1140
|
|
|
|
1141
|
|
|
if(!$environment->canDeploy()) { |
1142
|
|
|
return new SS_HTTPResponse("Not allowed to deploy", 401); |
1143
|
|
|
} |
1144
|
|
|
|
1145
|
|
|
// Generate the form |
1146
|
|
|
$form = new DeployForm($this, 'DeployForm', $environment, $project); |
1147
|
|
|
|
1148
|
|
|
// If this is an ajax request we don't want to submit the form - we just want to retrieve the markup. |
1149
|
|
|
if( |
1150
|
|
|
$request && |
1151
|
|
|
!$request->requestVar('action_showDeploySummary') && |
1152
|
|
|
$this->getRequest()->isAjax() && |
1153
|
|
|
$this->getRequest()->isGET() |
1154
|
|
|
) { |
1155
|
|
|
// We can just use the URL we're accessing |
1156
|
|
|
$form->setFormAction($this->getRequest()->getURL()); |
1157
|
|
|
|
1158
|
|
|
$body = json_encode(array('Content' => $form->forAjaxTemplate()->forTemplate())); |
1159
|
|
|
$this->getResponse()->addHeader('Content-Type', 'application/json'); |
1160
|
|
|
$this->getResponse()->setBody($body); |
1161
|
|
|
return $body; |
1162
|
|
|
} |
1163
|
|
|
|
1164
|
|
|
$form->setFormAction($this->getRequest()->getURL() . '/DeployForm'); |
1165
|
|
|
return $form; |
1166
|
|
|
} |
1167
|
|
|
|
1168
|
|
|
/** |
1169
|
|
|
* @param SS_HTTPRequest $request |
1170
|
|
|
* |
1171
|
|
|
* @return SS_HTTPResponse|string |
1172
|
|
|
*/ |
1173
|
|
|
public function gitRevisions(SS_HTTPRequest $request) { |
1174
|
|
|
|
1175
|
|
|
// Performs canView permission check by limiting visible projects |
1176
|
|
|
$project = $this->getCurrentProject(); |
1177
|
|
|
if(!$project) { |
1178
|
|
|
return $this->project404Response(); |
1179
|
|
|
} |
1180
|
|
|
|
1181
|
|
|
// Performs canView permission check by limiting visible projects |
1182
|
|
|
$env = $this->getCurrentEnvironment($project); |
1183
|
|
|
if(!$env) { |
1184
|
|
|
return $this->environment404Response(); |
1185
|
|
|
} |
1186
|
|
|
|
1187
|
|
|
// For now only permit advanced options on one environment type, because we hacked the "full-deploy" |
1188
|
|
|
// checkbox in. Other environments such as the fast or capistrano one wouldn't know what to do with it. |
1189
|
|
|
if(get_class($env) === 'RainforestEnvironment') { |
1190
|
|
|
$advanced = Permission::check('DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS') ? 'true' : 'false'; |
1191
|
|
|
} else { |
1192
|
|
|
$advanced = 'false'; |
1193
|
|
|
} |
1194
|
|
|
|
1195
|
|
|
$tabs = array(); |
1196
|
|
|
$id = 0; |
1197
|
|
|
$data = array( |
1198
|
|
|
'id' => ++$id, |
1199
|
|
|
'name' => 'Deploy the latest version of a branch', |
1200
|
|
|
'field_type' => 'dropdown', |
1201
|
|
|
'field_label' => 'Choose a branch', |
1202
|
|
|
'field_id' => 'branch', |
1203
|
|
|
'field_data' => array(), |
1204
|
|
|
'advanced_opts' => $advanced |
1205
|
|
|
); |
1206
|
|
|
foreach($project->DNBranchList() as $branch) { |
1207
|
|
|
$sha = $branch->SHA(); |
1208
|
|
|
$name = $branch->Name(); |
1209
|
|
|
$branchValue = sprintf("%s (%s, %s old)", |
1210
|
|
|
$name, |
1211
|
|
|
substr($sha, 0, 8), |
1212
|
|
|
$branch->LastUpdated()->TimeDiff() |
1213
|
|
|
); |
1214
|
|
|
$data['field_data'][] = array( |
1215
|
|
|
'id' => $sha, |
1216
|
|
|
'text' => $branchValue |
1217
|
|
|
); |
1218
|
|
|
} |
1219
|
|
|
$tabs[] = $data; |
1220
|
|
|
|
1221
|
|
|
$data = array( |
1222
|
|
|
'id' => ++$id, |
1223
|
|
|
'name' => 'Deploy a tagged release', |
1224
|
|
|
'field_type' => 'dropdown', |
1225
|
|
|
'field_label' => 'Choose a tag', |
1226
|
|
|
'field_id' => 'tag', |
1227
|
|
|
'field_data' => array(), |
1228
|
|
|
'advanced_opts' => $advanced |
1229
|
|
|
); |
1230
|
|
|
|
1231
|
|
|
foreach($project->DNTagList()->setLimit(null) as $tag) { |
1232
|
|
|
$name = $tag->Name(); |
1233
|
|
|
$data['field_data'][] = array( |
1234
|
|
|
'id' => $tag->SHA(), |
1235
|
|
|
'text' => sprintf("%s", $name) |
1236
|
|
|
); |
1237
|
|
|
} |
1238
|
|
|
|
1239
|
|
|
// show newest tags first. |
1240
|
|
|
$data['field_data'] = array_reverse($data['field_data']); |
1241
|
|
|
|
1242
|
|
|
$tabs[] = $data; |
1243
|
|
|
|
1244
|
|
|
// Past deployments |
1245
|
|
|
$data = array( |
1246
|
|
|
'id' => ++$id, |
1247
|
|
|
'name' => 'Redeploy a release that was previously deployed (to any environment)', |
1248
|
|
|
'field_type' => 'dropdown', |
1249
|
|
|
'field_label' => 'Choose a previously deployed release', |
1250
|
|
|
'field_id' => 'release', |
1251
|
|
|
'field_data' => array(), |
1252
|
|
|
'advanced_opts' => $advanced |
1253
|
|
|
); |
1254
|
|
|
// We are aiming at the format: |
1255
|
|
|
// [{text: 'optgroup text', children: [{id: '<sha>', text: '<inner text>'}]}] |
|
|
|
|
1256
|
|
|
$redeploy = array(); |
1257
|
|
|
foreach($project->DNEnvironmentList() as $dnEnvironment) { |
1258
|
|
|
$envName = $dnEnvironment->Name; |
1259
|
|
|
$perEnvDeploys = array(); |
1260
|
|
|
|
1261
|
|
|
foreach($dnEnvironment->DeployHistory() as $deploy) { |
1262
|
|
|
$sha = $deploy->SHA; |
1263
|
|
|
|
1264
|
|
|
// Check if exists to make sure the newest deployment date is used. |
1265
|
|
|
if(!isset($perEnvDeploys[$sha])) { |
1266
|
|
|
$pastValue = sprintf("%s (deployed %s)", |
1267
|
|
|
substr($sha, 0, 8), |
1268
|
|
|
$deploy->obj('LastEdited')->Ago() |
1269
|
|
|
); |
1270
|
|
|
$perEnvDeploys[$sha] = array( |
1271
|
|
|
'id' => $sha, |
1272
|
|
|
'text' => $pastValue |
1273
|
|
|
); |
1274
|
|
|
} |
1275
|
|
|
} |
1276
|
|
|
|
1277
|
|
|
if(!empty($perEnvDeploys)) { |
1278
|
|
|
$redeploy[$envName] = array_values($perEnvDeploys); |
1279
|
|
|
} |
1280
|
|
|
} |
1281
|
|
|
// Convert the array to the frontend format (i.e. keyed to regular array) |
1282
|
|
|
foreach($redeploy as $env => $descr) { |
1283
|
|
|
$data['field_data'][] = array('text'=>$env, 'children'=>$descr); |
1284
|
|
|
} |
1285
|
|
|
$tabs[] = $data; |
1286
|
|
|
|
1287
|
|
|
$data = array( |
1288
|
|
|
'id' => ++$id, |
1289
|
|
|
'name' => 'Deploy a specific SHA', |
1290
|
|
|
'field_type' => 'textfield', |
1291
|
|
|
'field_label' => 'Choose a SHA', |
1292
|
|
|
'field_id' => 'SHA', |
1293
|
|
|
'field_data' => array(), |
1294
|
|
|
'advanced_opts' => $advanced |
1295
|
|
|
); |
1296
|
|
|
$tabs[] = $data; |
1297
|
|
|
|
1298
|
|
|
// get the last time git fetch was run |
1299
|
|
|
$lastFetched = 'never'; |
1300
|
|
|
$fetch = DNGitFetch::get() |
1301
|
|
|
->filter('ProjectID', $project->ID) |
1302
|
|
|
->sort('LastEdited', 'DESC') |
1303
|
|
|
->first(); |
1304
|
|
|
if($fetch) { |
1305
|
|
|
$lastFetched = $fetch->dbObject('LastEdited')->Ago(); |
1306
|
|
|
} |
1307
|
|
|
|
1308
|
|
|
$data = array( |
1309
|
|
|
'Tabs' => $tabs, |
1310
|
|
|
'last_fetched' => $lastFetched |
1311
|
|
|
); |
1312
|
|
|
|
1313
|
|
|
return json_encode($data, JSON_PRETTY_PRINT); |
1314
|
|
|
} |
1315
|
|
|
|
1316
|
|
|
/** |
1317
|
|
|
* Check and regenerate a global CSRF token |
1318
|
|
|
* |
1319
|
|
|
* @param SS_HTTPRequest $request |
1320
|
|
|
* @param bool $resetToken |
1321
|
|
|
* |
1322
|
|
|
* @return bool |
1323
|
|
|
*/ |
1324
|
|
|
protected function checkCsrfToken(SS_HTTPRequest $request, $resetToken = true) { |
1325
|
|
|
$token = SecurityToken::inst(); |
1326
|
|
|
|
1327
|
|
|
// Ensure the submitted token has a value |
1328
|
|
|
$submittedToken = $request->postVar('SecurityID'); |
1329
|
|
|
if(!$submittedToken) { |
1330
|
|
|
return false; |
1331
|
|
|
} |
1332
|
|
|
|
1333
|
|
|
// Do the actual check. |
1334
|
|
|
$check = $token->check($submittedToken); |
1335
|
|
|
|
1336
|
|
|
// Reset the token after we've checked the existing token |
1337
|
|
|
if($resetToken) { |
1338
|
|
|
$token->reset(); |
1339
|
|
|
} |
1340
|
|
|
|
1341
|
|
|
// Return whether the token was correct or not |
1342
|
|
|
return $check; |
1343
|
|
|
} |
1344
|
|
|
|
1345
|
|
|
/** |
1346
|
|
|
* @param SS_HTTPRequest $request |
1347
|
|
|
* |
1348
|
|
|
* @return string |
1349
|
|
|
*/ |
1350
|
|
|
public function deploySummary(SS_HTTPRequest $request) { |
1351
|
|
|
|
1352
|
|
|
// Performs canView permission check by limiting visible projects |
1353
|
|
|
$project = $this->getCurrentProject(); |
1354
|
|
|
if(!$project) { |
1355
|
|
|
return $this->project404Response(); |
1356
|
|
|
} |
1357
|
|
|
|
1358
|
|
|
// Performs canView permission check by limiting visible projects |
1359
|
|
|
$environment = $this->getCurrentEnvironment($project); |
1360
|
|
|
if(!$environment) { |
1361
|
|
|
return $this->environment404Response(); |
1362
|
|
|
} |
1363
|
|
|
|
1364
|
|
|
// Plan the deployment. |
1365
|
|
|
$strategy = $environment->Backend()->planDeploy( |
1366
|
|
|
$environment, |
1367
|
|
|
$request->requestVars() |
|
|
|
|
1368
|
|
|
); |
1369
|
|
|
$data = $strategy->toArray(); |
1370
|
|
|
|
1371
|
|
|
// Add in a URL for comparing from->to code changes. Ensure that we have |
1372
|
|
|
// two proper 40 character SHAs, otherwise we can't show the compare link. |
1373
|
|
|
if( |
1374
|
|
|
!empty($data['changes']['Code version']['from']) |
1375
|
|
|
&& strlen($data['changes']['Code version']['from']) == '40' |
1376
|
|
|
&& !empty($data['changes']['Code version']['to']) |
1377
|
|
|
&& strlen($data['changes']['Code version']['to']) == '40' |
1378
|
|
|
) { |
1379
|
|
|
$interface = $project->getRepositoryInterface(); |
1380
|
|
|
$compareurl = sprintf( |
1381
|
|
|
'%s/compare/%s...%s', |
1382
|
|
|
$interface->URL, |
1383
|
|
|
$data['changes']['Code version']['from'], |
1384
|
|
|
$data['changes']['Code version']['to'] |
1385
|
|
|
); |
1386
|
|
|
$data['changes']['Code version']['compareUrl'] = $compareurl; |
1387
|
|
|
} |
1388
|
|
|
|
1389
|
|
|
// Append json to response |
1390
|
|
|
$token = SecurityToken::inst(); |
1391
|
|
|
$data['SecurityID'] = $token->getValue(); |
1392
|
|
|
|
1393
|
|
|
return json_encode($data); |
1394
|
|
|
} |
1395
|
|
|
|
1396
|
|
|
/** |
1397
|
|
|
* Deployment form submission handler. |
1398
|
|
|
* |
1399
|
|
|
* Initiate a DNDeployment record and redirect to it for status polling |
1400
|
|
|
* |
1401
|
|
|
* @param SS_HTTPRequest $request |
1402
|
|
|
* |
1403
|
|
|
* @return SS_HTTPResponse |
1404
|
|
|
* @throws ValidationException |
1405
|
|
|
* @throws null |
1406
|
|
|
*/ |
1407
|
|
|
public function startDeploy(SS_HTTPRequest $request) { |
1408
|
|
|
|
1409
|
|
|
// Ensure the CSRF Token is correct |
1410
|
|
|
if(!$this->checkCsrfToken($request)) { |
1411
|
|
|
// CSRF token didn't match |
1412
|
|
|
return $this->httpError(400, 'Bad Request'); |
1413
|
|
|
} |
1414
|
|
|
|
1415
|
|
|
// Performs canView permission check by limiting visible projects |
1416
|
|
|
$project = $this->getCurrentProject(); |
1417
|
|
|
if(!$project) { |
1418
|
|
|
return $this->project404Response(); |
1419
|
|
|
} |
1420
|
|
|
|
1421
|
|
|
// Performs canView permission check by limiting visible projects |
1422
|
|
|
$environment = $this->getCurrentEnvironment($project); |
1423
|
|
|
if(!$environment) { |
1424
|
|
|
return $this->environment404Response(); |
1425
|
|
|
} |
1426
|
|
|
|
1427
|
|
|
// Initiate the deployment |
1428
|
|
|
// The extension point should pass in: Project, Environment, SelectRelease, buildName |
1429
|
|
|
$this->extend('doDeploy', $project, $environment, $buildName, $data); |
1430
|
|
|
|
1431
|
|
|
// Start the deployment based on the approved strategy. |
1432
|
|
|
$strategy = new DeploymentStrategy($environment); |
1433
|
|
|
$strategy->fromArray($request->requestVar('strategy')); |
1434
|
|
|
$deployment = $strategy->createDeployment(); |
1435
|
|
|
$deployment->start(); |
1436
|
|
|
|
1437
|
|
|
return json_encode(array( |
1438
|
|
|
'url' => Director::absoluteBaseURL() . $deployment->Link() |
1439
|
|
|
), JSON_PRETTY_PRINT); |
1440
|
|
|
} |
1441
|
|
|
|
1442
|
|
|
/** |
1443
|
|
|
* Action - Do the actual deploy |
1444
|
|
|
* |
1445
|
|
|
* @param SS_HTTPRequest $request |
1446
|
|
|
* |
1447
|
|
|
* @return SS_HTTPResponse|string |
1448
|
|
|
* @throws SS_HTTPResponse_Exception |
1449
|
|
|
*/ |
1450
|
|
|
public function deploy(SS_HTTPRequest $request) { |
1451
|
|
|
$params = $request->params(); |
1452
|
|
|
$deployment = DNDeployment::get()->byId($params['Identifier']); |
1453
|
|
|
|
1454
|
|
|
if(!$deployment || !$deployment->ID) { |
1455
|
|
|
throw new SS_HTTPResponse_Exception('Deployment not found', 404); |
1456
|
|
|
} |
1457
|
|
|
if(!$deployment->canView()) { |
1458
|
|
|
return Security::permissionFailure(); |
1459
|
|
|
} |
1460
|
|
|
|
1461
|
|
|
$environment = $deployment->Environment(); |
1462
|
|
|
$project = $environment->Project(); |
1463
|
|
|
|
1464
|
|
|
if($environment->Name != $params['Environment']) { |
1465
|
|
|
throw new LogicException("Environment in URL doesn't match this deploy"); |
1466
|
|
|
} |
1467
|
|
|
if($project->Name != $params['Project']) { |
1468
|
|
|
throw new LogicException("Project in URL doesn't match this deploy"); |
1469
|
|
|
} |
1470
|
|
|
|
1471
|
|
|
return $this->render(array( |
1472
|
|
|
'Deployment' => $deployment, |
1473
|
|
|
)); |
1474
|
|
|
} |
1475
|
|
|
|
1476
|
|
|
|
1477
|
|
|
/** |
1478
|
|
|
* Action - Get the latest deploy log |
1479
|
|
|
* |
1480
|
|
|
* @param SS_HTTPRequest $request |
1481
|
|
|
* |
1482
|
|
|
* @return string |
1483
|
|
|
* @throws SS_HTTPResponse_Exception |
1484
|
|
|
*/ |
1485
|
|
|
public function deploylog(SS_HTTPRequest $request) { |
1486
|
|
|
$params = $request->params(); |
1487
|
|
|
$deployment = DNDeployment::get()->byId($params['Identifier']); |
1488
|
|
|
|
1489
|
|
|
if(!$deployment || !$deployment->ID) { |
1490
|
|
|
throw new SS_HTTPResponse_Exception('Deployment not found', 404); |
1491
|
|
|
} |
1492
|
|
|
if(!$deployment->canView()) { |
1493
|
|
|
return Security::permissionFailure(); |
1494
|
|
|
} |
1495
|
|
|
|
1496
|
|
|
$environment = $deployment->Environment(); |
1497
|
|
|
$project = $environment->Project(); |
1498
|
|
|
|
1499
|
|
|
if($environment->Name != $params['Environment']) { |
1500
|
|
|
throw new LogicException("Environment in URL doesn't match this deploy"); |
1501
|
|
|
} |
1502
|
|
|
if($project->Name != $params['Project']) { |
1503
|
|
|
throw new LogicException("Project in URL doesn't match this deploy"); |
1504
|
|
|
} |
1505
|
|
|
|
1506
|
|
|
$log = $deployment->log(); |
1507
|
|
|
if($log->exists()) { |
1508
|
|
|
$content = $log->content(); |
1509
|
|
|
} else { |
1510
|
|
|
$content = 'Waiting for action to start'; |
1511
|
|
|
} |
1512
|
|
|
|
1513
|
|
|
return $this->sendResponse($deployment->ResqueStatus(), $content); |
1514
|
|
|
} |
1515
|
|
|
|
1516
|
|
|
/** |
1517
|
|
|
* @param SS_HTTPRequest|null $request |
1518
|
|
|
* |
1519
|
|
|
* @return Form |
1520
|
|
|
*/ |
1521
|
|
|
public function getDataTransferForm(SS_HTTPRequest $request = null) { |
1522
|
|
|
// Performs canView permission check by limiting visible projects |
1523
|
|
|
$envs = $this->getCurrentProject()->DNEnvironmentList()->filterByCallback(function($item) { |
1524
|
|
|
return $item->canBackup(); |
1525
|
|
|
}); |
1526
|
|
|
|
1527
|
|
|
if(!$envs) { |
1528
|
|
|
return $this->environment404Response(); |
1529
|
|
|
} |
1530
|
|
|
|
1531
|
|
|
$form = Form::create( |
1532
|
|
|
$this, |
1533
|
|
|
'DataTransferForm', |
1534
|
|
|
FieldList::create( |
1535
|
|
|
HiddenField::create('Direction', null, 'get'), |
1536
|
|
|
DropdownField::create('EnvironmentID', 'Environment', $envs->map()) |
1537
|
|
|
->setEmptyString('Select an environment'), |
1538
|
|
|
DropdownField::create('Mode', 'Transfer', DNDataArchive::get_mode_map()) |
1539
|
|
|
), |
1540
|
|
|
FieldList::create( |
1541
|
|
|
FormAction::create('doDataTransfer', 'Create') |
1542
|
|
|
->addExtraClass('btn') |
1543
|
|
|
) |
1544
|
|
|
); |
1545
|
|
|
$form->setFormAction($this->getRequest()->getURL() . '/DataTransferForm'); |
1546
|
|
|
|
1547
|
|
|
return $form; |
1548
|
|
|
} |
1549
|
|
|
|
1550
|
|
|
/** |
1551
|
|
|
* @param array $data |
1552
|
|
|
* @param Form $form |
1553
|
|
|
* |
1554
|
|
|
* @return SS_HTTPResponse |
1555
|
|
|
* @throws SS_HTTPResponse_Exception |
1556
|
|
|
*/ |
1557
|
|
|
public function doDataTransfer($data, Form $form) { |
|
|
|
|
1558
|
|
|
$this->setCurrentActionType(self::ACTION_SNAPSHOT); |
1559
|
|
|
|
1560
|
|
|
// Performs canView permission check by limiting visible projects |
1561
|
|
|
$project = $this->getCurrentProject(); |
1562
|
|
|
if(!$project) { |
1563
|
|
|
return $this->project404Response(); |
1564
|
|
|
} |
1565
|
|
|
|
1566
|
|
|
$dataArchive = null; |
1567
|
|
|
|
1568
|
|
|
// Validate direction. |
1569
|
|
|
if($data['Direction'] == 'get') { |
1570
|
|
|
$validEnvs = $this->getCurrentProject()->DNEnvironmentList() |
1571
|
|
|
->filterByCallback(function($item) { |
1572
|
|
|
return $item->canBackup(); |
1573
|
|
|
}); |
1574
|
|
|
} else if($data['Direction'] == 'push') { |
1575
|
|
|
$validEnvs = $this->getCurrentProject()->DNEnvironmentList() |
1576
|
|
|
->filterByCallback(function($item) { |
1577
|
|
|
return $item->canRestore(); |
1578
|
|
|
}); |
1579
|
|
|
} else { |
1580
|
|
|
throw new LogicException('Invalid direction'); |
1581
|
|
|
} |
1582
|
|
|
|
1583
|
|
|
// Validate $data['EnvironmentID'] by checking against $validEnvs. |
1584
|
|
|
$environment = $validEnvs->find('ID', $data['EnvironmentID']); |
1585
|
|
|
if(!$environment) { |
1586
|
|
|
throw new LogicException('Invalid environment'); |
1587
|
|
|
} |
1588
|
|
|
|
1589
|
|
|
$this->validateSnapshotMode($data['Mode']); |
1590
|
|
|
|
1591
|
|
|
|
1592
|
|
|
// Only 'push' direction is allowed an association with an existing archive. |
1593
|
|
|
if( |
1594
|
|
|
$data['Direction'] == 'push' |
1595
|
|
|
&& isset($data['DataArchiveID']) |
1596
|
|
|
&& is_numeric($data['DataArchiveID']) |
1597
|
|
|
) { |
1598
|
|
|
$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']); |
1599
|
|
|
if(!$dataArchive) { |
1600
|
|
|
throw new LogicException('Invalid data archive'); |
1601
|
|
|
} |
1602
|
|
|
|
1603
|
|
|
if(!$dataArchive->canDownload()) { |
1604
|
|
|
throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403); |
1605
|
|
|
} |
1606
|
|
|
} |
1607
|
|
|
|
1608
|
|
|
$transfer = DNDataTransfer::create(); |
1609
|
|
|
$transfer->EnvironmentID = $environment->ID; |
1610
|
|
|
$transfer->Direction = $data['Direction']; |
1611
|
|
|
$transfer->Mode = $data['Mode']; |
1612
|
|
|
$transfer->DataArchiveID = $dataArchive ? $dataArchive->ID : null; |
1613
|
|
|
if($data['Direction'] == 'push') { |
1614
|
|
|
$transfer->setBackupBeforePush(!empty($data['BackupBeforePush'])); |
1615
|
|
|
} |
1616
|
|
|
$transfer->write(); |
1617
|
|
|
$transfer->start(); |
1618
|
|
|
|
1619
|
|
|
return $this->redirect($transfer->Link()); |
1620
|
|
|
} |
1621
|
|
|
|
1622
|
|
|
/** |
1623
|
|
|
* View into the log for a {@link DNDataTransfer}. |
1624
|
|
|
* |
1625
|
|
|
* @param SS_HTTPRequest $request |
1626
|
|
|
* |
1627
|
|
|
* @return SS_HTTPResponse|string |
1628
|
|
|
* @throws SS_HTTPResponse_Exception |
1629
|
|
|
*/ |
1630
|
|
|
public function transfer(SS_HTTPRequest $request) { |
1631
|
|
|
$this->setCurrentActionType(self::ACTION_SNAPSHOT); |
1632
|
|
|
|
1633
|
|
|
$params = $request->params(); |
1634
|
|
|
$transfer = DNDataTransfer::get()->byId($params['Identifier']); |
1635
|
|
|
|
1636
|
|
|
if(!$transfer || !$transfer->ID) { |
1637
|
|
|
throw new SS_HTTPResponse_Exception('Transfer not found', 404); |
1638
|
|
|
} |
1639
|
|
|
if(!$transfer->canView()) { |
1640
|
|
|
return Security::permissionFailure(); |
1641
|
|
|
} |
1642
|
|
|
|
1643
|
|
|
$environment = $transfer->Environment(); |
1644
|
|
|
$project = $environment->Project(); |
1645
|
|
|
|
1646
|
|
|
if($project->Name != $params['Project']) { |
1647
|
|
|
throw new LogicException("Project in URL doesn't match this deploy"); |
1648
|
|
|
} |
1649
|
|
|
|
1650
|
|
|
return $this->render(array( |
1651
|
|
|
'CurrentTransfer' => $transfer, |
1652
|
|
|
'SnapshotsSection' => 1, |
1653
|
|
|
)); |
1654
|
|
|
} |
1655
|
|
|
|
1656
|
|
|
/** |
1657
|
|
|
* Action - Get the latest deploy log |
1658
|
|
|
* |
1659
|
|
|
* @param SS_HTTPRequest $request |
1660
|
|
|
* |
1661
|
|
|
* @return string |
1662
|
|
|
* @throws SS_HTTPResponse_Exception |
1663
|
|
|
*/ |
1664
|
|
View Code Duplication |
public function transferlog(SS_HTTPRequest $request) { |
|
|
|
|
1665
|
|
|
$this->setCurrentActionType(self::ACTION_SNAPSHOT); |
1666
|
|
|
|
1667
|
|
|
$params = $request->params(); |
1668
|
|
|
$transfer = DNDataTransfer::get()->byId($params['Identifier']); |
1669
|
|
|
|
1670
|
|
|
if(!$transfer || !$transfer->ID) { |
1671
|
|
|
throw new SS_HTTPResponse_Exception('Transfer not found', 404); |
1672
|
|
|
} |
1673
|
|
|
if(!$transfer->canView()) { |
1674
|
|
|
return Security::permissionFailure(); |
1675
|
|
|
} |
1676
|
|
|
|
1677
|
|
|
$environment = $transfer->Environment(); |
1678
|
|
|
$project = $environment->Project(); |
1679
|
|
|
|
1680
|
|
|
if($project->Name != $params['Project']) { |
1681
|
|
|
throw new LogicException("Project in URL doesn't match this deploy"); |
1682
|
|
|
} |
1683
|
|
|
|
1684
|
|
|
$log = $transfer->log(); |
1685
|
|
|
if($log->exists()) { |
1686
|
|
|
$content = $log->content(); |
1687
|
|
|
} else { |
1688
|
|
|
$content = 'Waiting for action to start'; |
1689
|
|
|
} |
1690
|
|
|
|
1691
|
|
|
return $this->sendResponse($transfer->ResqueStatus(), $content); |
1692
|
|
|
} |
1693
|
|
|
|
1694
|
|
|
/** |
1695
|
|
|
* Note: Submits to the same action as {@link getDataTransferForm()}, |
1696
|
|
|
* but with a Direction=push and an archive reference. |
1697
|
|
|
* |
1698
|
|
|
* @param SS_HTTPRequest $request |
1699
|
|
|
* @param DNDataArchive|null $dataArchive Only set when method is called manually in {@link restore()}, |
1700
|
|
|
* otherwise the state is inferred from the request data. |
1701
|
|
|
* @return Form |
1702
|
|
|
*/ |
1703
|
|
|
public function getDataTransferRestoreForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) { |
1704
|
|
|
$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID')); |
1705
|
|
|
|
1706
|
|
|
// Performs canView permission check by limiting visible projects |
1707
|
|
|
$project = $this->getCurrentProject(); |
1708
|
|
|
$envs = $project->DNEnvironmentList()->filterByCallback(function($item) { |
1709
|
|
|
return $item->canRestore(); |
1710
|
|
|
}); |
1711
|
|
|
|
1712
|
|
|
if(!$envs) { |
1713
|
|
|
return $this->environment404Response(); |
1714
|
|
|
} |
1715
|
|
|
|
1716
|
|
|
$modesMap = array(); |
1717
|
|
|
if(in_array($dataArchive->Mode, array('all'))) { |
1718
|
|
|
$modesMap['all'] = 'Database and Assets'; |
1719
|
|
|
}; |
1720
|
|
|
if(in_array($dataArchive->Mode, array('all', 'db'))) { |
1721
|
|
|
$modesMap['db'] = 'Database only'; |
1722
|
|
|
}; |
1723
|
|
|
if(in_array($dataArchive->Mode, array('all', 'assets'))) { |
1724
|
|
|
$modesMap['assets'] = 'Assets only'; |
1725
|
|
|
}; |
1726
|
|
|
|
1727
|
|
|
$alertMessage = '<div class="alert alert-warning"><strong>Warning:</strong> ' |
1728
|
|
|
. 'This restore will overwrite the data on the chosen environment below</div>'; |
1729
|
|
|
|
1730
|
|
|
$form = Form::create( |
1731
|
|
|
$this, |
1732
|
|
|
'DataTransferRestoreForm', |
1733
|
|
|
FieldList::create( |
1734
|
|
|
HiddenField::create('DataArchiveID', null, $dataArchive->ID), |
1735
|
|
|
HiddenField::create('Direction', null, 'push'), |
1736
|
|
|
LiteralField::create('Warning', $alertMessage), |
1737
|
|
|
DropdownField::create('EnvironmentID', 'Environment', $envs->map()) |
1738
|
|
|
->setEmptyString('Select an environment'), |
1739
|
|
|
DropdownField::create('Mode', 'Transfer', $modesMap), |
1740
|
|
|
CheckboxField::create('BackupBeforePush', 'Backup existing data', '1') |
1741
|
|
|
), |
1742
|
|
|
FieldList::create( |
1743
|
|
|
FormAction::create('doDataTransfer', 'Restore Data') |
1744
|
|
|
->addExtraClass('btn') |
1745
|
|
|
) |
1746
|
|
|
); |
1747
|
|
|
$form->setFormAction($project->Link() . '/DataTransferRestoreForm'); |
1748
|
|
|
|
1749
|
|
|
return $form; |
1750
|
|
|
} |
1751
|
|
|
|
1752
|
|
|
/** |
1753
|
|
|
* View a form to restore a specific {@link DataArchive}. |
1754
|
|
|
* Permission checks are handled in {@link DataArchives()}. |
1755
|
|
|
* Submissions are handled through {@link doDataTransfer()}, same as backup operations. |
1756
|
|
|
* |
1757
|
|
|
* @param SS_HTTPRequest $request |
1758
|
|
|
* |
1759
|
|
|
* @return HTMLText |
1760
|
|
|
* @throws SS_HTTPResponse_Exception |
1761
|
|
|
*/ |
1762
|
|
View Code Duplication |
public function restoresnapshot(SS_HTTPRequest $request) { |
|
|
|
|
1763
|
|
|
$this->setCurrentActionType(self::ACTION_SNAPSHOT); |
1764
|
|
|
|
1765
|
|
|
/** @var DNDataArchive $dataArchive */ |
1766
|
|
|
$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID')); |
1767
|
|
|
|
1768
|
|
|
if(!$dataArchive) { |
1769
|
|
|
throw new SS_HTTPResponse_Exception('Archive not found', 404); |
1770
|
|
|
} |
1771
|
|
|
|
1772
|
|
|
// We check for canDownload because that implies access to the data. |
1773
|
|
|
// canRestore is later checked on the actual restore action per environment. |
1774
|
|
|
if(!$dataArchive->canDownload()) { |
1775
|
|
|
throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403); |
1776
|
|
|
} |
1777
|
|
|
|
1778
|
|
|
$form = $this->getDataTransferRestoreForm($this->request, $dataArchive); |
1779
|
|
|
|
1780
|
|
|
// View currently only available via ajax |
1781
|
|
|
return $form->forTemplate(); |
1782
|
|
|
} |
1783
|
|
|
|
1784
|
|
|
/** |
1785
|
|
|
* View a form to delete a specific {@link DataArchive}. |
1786
|
|
|
* Permission checks are handled in {@link DataArchives()}. |
1787
|
|
|
* Submissions are handled through {@link doDelete()}. |
1788
|
|
|
* |
1789
|
|
|
* @param SS_HTTPRequest $request |
1790
|
|
|
* |
1791
|
|
|
* @return HTMLText |
1792
|
|
|
* @throws SS_HTTPResponse_Exception |
1793
|
|
|
*/ |
1794
|
|
View Code Duplication |
public function deletesnapshot(SS_HTTPRequest $request) { |
|
|
|
|
1795
|
|
|
$this->setCurrentActionType(self::ACTION_SNAPSHOT); |
1796
|
|
|
|
1797
|
|
|
/** @var DNDataArchive $dataArchive */ |
1798
|
|
|
$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID')); |
1799
|
|
|
|
1800
|
|
|
if(!$dataArchive) { |
1801
|
|
|
throw new SS_HTTPResponse_Exception('Archive not found', 404); |
1802
|
|
|
} |
1803
|
|
|
|
1804
|
|
|
if(!$dataArchive->canDelete()) { |
1805
|
|
|
throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403); |
1806
|
|
|
} |
1807
|
|
|
|
1808
|
|
|
$form = $this->getDeleteForm($this->request, $dataArchive); |
1809
|
|
|
|
1810
|
|
|
// View currently only available via ajax |
1811
|
|
|
return $form->forTemplate(); |
1812
|
|
|
} |
1813
|
|
|
|
1814
|
|
|
/** |
1815
|
|
|
* @param SS_HTTPRequest $request |
1816
|
|
|
* @param DNDataArchive|null $dataArchive Only set when method is called manually, otherwise the state is inferred |
1817
|
|
|
* from the request data. |
1818
|
|
|
* @return Form |
1819
|
|
|
*/ |
1820
|
|
|
public function getDeleteForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) { |
1821
|
|
|
$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID')); |
1822
|
|
|
|
1823
|
|
|
// Performs canView permission check by limiting visible projects |
1824
|
|
|
$project = $this->getCurrentProject(); |
1825
|
|
|
if(!$project) { |
1826
|
|
|
return $this->project404Response(); |
1827
|
|
|
} |
1828
|
|
|
|
1829
|
|
|
$snapshotDeleteWarning = '<div class="alert alert-warning">' |
1830
|
|
|
. 'Are you sure you want to permanently delete this snapshot from this archive area?' |
1831
|
|
|
. '</div>'; |
1832
|
|
|
|
1833
|
|
|
$form = Form::create( |
1834
|
|
|
$this, |
1835
|
|
|
'DeleteForm', |
1836
|
|
|
FieldList::create( |
1837
|
|
|
HiddenField::create('DataArchiveID', null, $dataArchive->ID), |
1838
|
|
|
LiteralField::create('Warning', $snapshotDeleteWarning) |
1839
|
|
|
), |
1840
|
|
|
FieldList::create( |
1841
|
|
|
FormAction::create('doDelete', 'Delete') |
1842
|
|
|
->addExtraClass('btn') |
1843
|
|
|
) |
1844
|
|
|
); |
1845
|
|
|
$form->setFormAction($project->Link() . '/DeleteForm'); |
1846
|
|
|
|
1847
|
|
|
return $form; |
1848
|
|
|
} |
1849
|
|
|
|
1850
|
|
|
/** |
1851
|
|
|
* @param array $data |
1852
|
|
|
* @param Form $form |
1853
|
|
|
* |
1854
|
|
|
* @return bool|SS_HTTPResponse |
1855
|
|
|
* @throws SS_HTTPResponse_Exception |
1856
|
|
|
*/ |
1857
|
|
|
public function doDelete($data, Form $form) { |
|
|
|
|
1858
|
|
|
$this->setCurrentActionType(self::ACTION_SNAPSHOT); |
1859
|
|
|
|
1860
|
|
|
// Performs canView permission check by limiting visible projects |
1861
|
|
|
$project = $this->getCurrentProject(); |
1862
|
|
|
if(!$project) { |
1863
|
|
|
return $this->project404Response(); |
1864
|
|
|
} |
1865
|
|
|
|
1866
|
|
|
$dataArchive = null; |
1867
|
|
|
|
1868
|
|
|
if( |
1869
|
|
|
isset($data['DataArchiveID']) |
1870
|
|
|
&& is_numeric($data['DataArchiveID']) |
1871
|
|
|
) { |
1872
|
|
|
$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']); |
1873
|
|
|
} |
1874
|
|
|
|
1875
|
|
|
if(!$dataArchive) { |
1876
|
|
|
throw new LogicException('Invalid data archive'); |
1877
|
|
|
} |
1878
|
|
|
|
1879
|
|
|
if(!$dataArchive->canDelete()) { |
1880
|
|
|
throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403); |
1881
|
|
|
} |
1882
|
|
|
|
1883
|
|
|
$dataArchive->delete(); |
1884
|
|
|
|
1885
|
|
|
return $this->redirectBack(); |
1886
|
|
|
} |
1887
|
|
|
|
1888
|
|
|
/** |
1889
|
|
|
* View a form to move a specific {@link DataArchive}. |
1890
|
|
|
* |
1891
|
|
|
* @param SS_HTTPRequest $request |
1892
|
|
|
* |
1893
|
|
|
* @return HTMLText |
1894
|
|
|
* @throws SS_HTTPResponse_Exception |
1895
|
|
|
*/ |
1896
|
|
View Code Duplication |
public function movesnapshot(SS_HTTPRequest $request) { |
|
|
|
|
1897
|
|
|
$this->setCurrentActionType(self::ACTION_SNAPSHOT); |
1898
|
|
|
|
1899
|
|
|
/** @var DNDataArchive $dataArchive */ |
1900
|
|
|
$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID')); |
1901
|
|
|
|
1902
|
|
|
if(!$dataArchive) { |
1903
|
|
|
throw new SS_HTTPResponse_Exception('Archive not found', 404); |
1904
|
|
|
} |
1905
|
|
|
|
1906
|
|
|
// We check for canDownload because that implies access to the data. |
1907
|
|
|
if(!$dataArchive->canDownload()) { |
1908
|
|
|
throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403); |
1909
|
|
|
} |
1910
|
|
|
|
1911
|
|
|
$form = $this->getMoveForm($this->request, $dataArchive); |
1912
|
|
|
|
1913
|
|
|
// View currently only available via ajax |
1914
|
|
|
return $form->forTemplate(); |
|
|
|
|
1915
|
|
|
} |
1916
|
|
|
|
1917
|
|
|
/** |
1918
|
|
|
* Build snapshot move form. |
1919
|
|
|
* |
1920
|
|
|
* @param SS_HTTPRequest $request |
1921
|
|
|
* @param DNDataArchive|null $dataArchive |
1922
|
|
|
* |
1923
|
|
|
* @return Form|SS_HTTPResponse |
1924
|
|
|
*/ |
1925
|
|
|
public function getMoveForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) { |
1926
|
|
|
$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID')); |
1927
|
|
|
|
1928
|
|
|
$envs = $dataArchive->validTargetEnvironments(); |
1929
|
|
|
if(!$envs) { |
1930
|
|
|
return $this->environment404Response(); |
1931
|
|
|
} |
1932
|
|
|
|
1933
|
|
|
$warningMessage = '<div class="alert alert-warning"><strong>Warning:</strong> This will make the snapshot ' |
1934
|
|
|
. 'available to people with access to the target environment.<br>By pressing "Change ownership" you ' |
1935
|
|
|
. 'confirm that you have considered data confidentiality regulations.</div>'; |
1936
|
|
|
|
1937
|
|
|
$form = Form::create( |
1938
|
|
|
$this, |
1939
|
|
|
'MoveForm', |
1940
|
|
|
FieldList::create( |
1941
|
|
|
HiddenField::create('DataArchiveID', null, $dataArchive->ID), |
1942
|
|
|
LiteralField::create('Warning', $warningMessage), |
1943
|
|
|
DropdownField::create('EnvironmentID', 'Environment', $envs->map()) |
1944
|
|
|
->setEmptyString('Select an environment') |
1945
|
|
|
), |
1946
|
|
|
FieldList::create( |
1947
|
|
|
FormAction::create('doMove', 'Change ownership') |
1948
|
|
|
->addExtraClass('btn') |
1949
|
|
|
) |
1950
|
|
|
); |
1951
|
|
|
$form->setFormAction($this->getCurrentProject()->Link() . '/MoveForm'); |
1952
|
|
|
|
1953
|
|
|
return $form; |
1954
|
|
|
} |
1955
|
|
|
|
1956
|
|
|
/** |
1957
|
|
|
* @param array $data |
1958
|
|
|
* @param Form $form |
1959
|
|
|
* |
1960
|
|
|
* @return bool|SS_HTTPResponse |
1961
|
|
|
* @throws SS_HTTPResponse_Exception |
1962
|
|
|
* @throws ValidationException |
1963
|
|
|
* @throws null |
1964
|
|
|
*/ |
1965
|
|
|
public function doMove($data, Form $form) { |
|
|
|
|
1966
|
|
|
$this->setCurrentActionType(self::ACTION_SNAPSHOT); |
1967
|
|
|
|
1968
|
|
|
// Performs canView permission check by limiting visible projects |
1969
|
|
|
$project = $this->getCurrentProject(); |
1970
|
|
|
if(!$project) { |
1971
|
|
|
return $this->project404Response(); |
1972
|
|
|
} |
1973
|
|
|
|
1974
|
|
|
/** @var DNDataArchive $dataArchive */ |
1975
|
|
|
$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']); |
1976
|
|
|
if(!$dataArchive) { |
1977
|
|
|
throw new LogicException('Invalid data archive'); |
1978
|
|
|
} |
1979
|
|
|
|
1980
|
|
|
// We check for canDownload because that implies access to the data. |
1981
|
|
|
if(!$dataArchive->canDownload()) { |
1982
|
|
|
throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403); |
1983
|
|
|
} |
1984
|
|
|
|
1985
|
|
|
// Validate $data['EnvironmentID'] by checking against $validEnvs. |
1986
|
|
|
$validEnvs = $dataArchive->validTargetEnvironments(); |
1987
|
|
|
$environment = $validEnvs->find('ID', $data['EnvironmentID']); |
1988
|
|
|
if(!$environment) { |
1989
|
|
|
throw new LogicException('Invalid environment'); |
1990
|
|
|
} |
1991
|
|
|
|
1992
|
|
|
$dataArchive->EnvironmentID = $environment->ID; |
1993
|
|
|
$dataArchive->write(); |
1994
|
|
|
|
1995
|
|
|
return $this->redirectBack(); |
1996
|
|
|
} |
1997
|
|
|
|
1998
|
|
|
/** |
1999
|
|
|
* Returns an error message if redis is unavailable |
2000
|
|
|
* |
2001
|
|
|
* @return string |
2002
|
|
|
*/ |
2003
|
|
|
public static function RedisUnavailable() { |
2004
|
|
|
try { |
2005
|
|
|
Resque::queues(); |
2006
|
|
|
} catch(Exception $e) { |
2007
|
|
|
return $e->getMessage(); |
2008
|
|
|
} |
2009
|
|
|
return ''; |
2010
|
|
|
} |
2011
|
|
|
|
2012
|
|
|
/** |
2013
|
|
|
* Returns the number of connected Redis workers |
2014
|
|
|
* |
2015
|
|
|
* @return int |
2016
|
|
|
*/ |
2017
|
|
|
public static function RedisWorkersCount() { |
2018
|
|
|
return count(Resque_Worker::all()); |
2019
|
|
|
} |
2020
|
|
|
|
2021
|
|
|
/** |
2022
|
|
|
* @return array |
2023
|
|
|
*/ |
2024
|
|
|
public function providePermissions() { |
2025
|
|
|
return array( |
2026
|
|
|
self::DEPLOYNAUT_BYPASS_PIPELINE => array( |
2027
|
|
|
'name' => "Bypass Pipeline", |
2028
|
|
|
'category' => "Deploynaut", |
2029
|
|
|
'help' => "Enables users to directly initiate deployments, bypassing any pipeline", |
2030
|
|
|
), |
2031
|
|
|
self::DEPLOYNAUT_DRYRUN_PIPELINE => array( |
2032
|
|
|
'name' => 'Dry-run Pipeline', |
2033
|
|
|
'category' => 'Deploynaut', |
2034
|
|
|
'help' => 'Enable dry-run execution of pipelines for testing' |
2035
|
|
|
), |
2036
|
|
|
self::DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS => array( |
2037
|
|
|
'name' => "Access to advanced deploy options", |
2038
|
|
|
'category' => "Deploynaut", |
2039
|
|
|
), |
2040
|
|
|
|
2041
|
|
|
// Permissions that are intended to be added to the roles. |
2042
|
|
|
self::ALLOW_PROD_DEPLOYMENT => array( |
2043
|
|
|
'name' => "Ability to deploy to production environments", |
2044
|
|
|
'category' => "Deploynaut", |
2045
|
|
|
), |
2046
|
|
|
self::ALLOW_NON_PROD_DEPLOYMENT => array( |
2047
|
|
|
'name' => "Ability to deploy to non-production environments", |
2048
|
|
|
'category' => "Deploynaut", |
2049
|
|
|
), |
2050
|
|
|
self::ALLOW_PROD_SNAPSHOT => array( |
2051
|
|
|
'name' => "Ability to make production snapshots", |
2052
|
|
|
'category' => "Deploynaut", |
2053
|
|
|
), |
2054
|
|
|
self::ALLOW_NON_PROD_SNAPSHOT => array( |
2055
|
|
|
'name' => "Ability to make non-production snapshots", |
2056
|
|
|
'category' => "Deploynaut", |
2057
|
|
|
), |
2058
|
|
|
self::ALLOW_CREATE_ENVIRONMENT => array( |
2059
|
|
|
'name' => "Ability to create environments", |
2060
|
|
|
'category' => "Deploynaut", |
2061
|
|
|
), |
2062
|
|
|
); |
2063
|
|
|
} |
2064
|
|
|
|
2065
|
|
|
/** |
2066
|
|
|
* @return DNProject|null |
2067
|
|
|
*/ |
2068
|
|
|
public function getCurrentProject() { |
2069
|
|
|
$projectName = trim($this->getRequest()->param('Project')); |
2070
|
|
|
if(!$projectName) { |
2071
|
|
|
return null; |
2072
|
|
|
} |
2073
|
|
|
if(empty(self::$_project_cache[$projectName])) { |
2074
|
|
|
self::$_project_cache[$projectName] = $this->DNProjectList()->filter('Name', $projectName)->First(); |
2075
|
|
|
} |
2076
|
|
|
return self::$_project_cache[$projectName]; |
2077
|
|
|
} |
2078
|
|
|
|
2079
|
|
|
/** |
2080
|
|
|
* @param DNProject|null $project |
2081
|
|
|
* @return DNEnvironment|null |
2082
|
|
|
*/ |
2083
|
|
|
public function getCurrentEnvironment(DNProject $project = null) { |
2084
|
|
|
if($this->getRequest()->param('Environment') === null) { |
2085
|
|
|
return null; |
2086
|
|
|
} |
2087
|
|
|
if($project === null) { |
2088
|
|
|
$project = $this->getCurrentProject(); |
2089
|
|
|
} |
2090
|
|
|
// project can still be null |
2091
|
|
|
if($project === null) { |
2092
|
|
|
return null; |
2093
|
|
|
} |
2094
|
|
|
return $project->DNEnvironmentList()->filter('Name', $this->getRequest()->param('Environment'))->First(); |
2095
|
|
|
} |
2096
|
|
|
|
2097
|
|
|
/** |
2098
|
|
|
* This will return a const that indicates the class of action currently being performed |
2099
|
|
|
* |
2100
|
|
|
* Until DNRoot is de-godded, it does a bunch of different actions all in the same class. |
2101
|
|
|
* So we just have each action handler calll setCurrentActionType to define what sort of |
2102
|
|
|
* action it is. |
2103
|
|
|
* |
2104
|
|
|
* @return string - one of the consts from self::$action_types |
2105
|
|
|
*/ |
2106
|
|
|
public function getCurrentActionType() { |
2107
|
|
|
return $this->actionType; |
2108
|
|
|
} |
2109
|
|
|
|
2110
|
|
|
/** |
2111
|
|
|
* Sets the current action type |
2112
|
|
|
* |
2113
|
|
|
* @param string $actionType string - one of the consts from self::$action_types |
2114
|
|
|
*/ |
2115
|
|
|
public function setCurrentActionType($actionType) { |
2116
|
|
|
$this->actionType = $actionType; |
2117
|
|
|
} |
2118
|
|
|
|
2119
|
|
|
/** |
2120
|
|
|
* Helper method to allow templates to know whether they should show the 'Archive List' include or not. |
2121
|
|
|
* The actual permissions are set on a per-environment level, so we need to find out if this $member can upload to |
2122
|
|
|
* or download from *any* {@link DNEnvironment} that (s)he has access to. |
2123
|
|
|
* |
2124
|
|
|
* TODO To be replaced with a method that just returns the list of archives this {@link Member} has access to. |
2125
|
|
|
* |
2126
|
|
|
* @param Member|null $member The {@link Member} to check (or null to check the currently logged in Member) |
2127
|
|
|
* @return boolean|null true if $member has access to upload or download to at least one {@link DNEnvironment}. |
2128
|
|
|
*/ |
2129
|
|
|
public function CanViewArchives(Member $member = null) { |
2130
|
|
|
if($member === null) { |
2131
|
|
|
$member = Member::currentUser(); |
2132
|
|
|
} |
2133
|
|
|
|
2134
|
|
|
if(Permission::checkMember($member, 'ADMIN')) { |
2135
|
|
|
return true; |
2136
|
|
|
} |
2137
|
|
|
|
2138
|
|
|
$allProjects = $this->DNProjectList(); |
2139
|
|
|
if(!$allProjects) { |
2140
|
|
|
return false; |
2141
|
|
|
} |
2142
|
|
|
|
2143
|
|
|
foreach($allProjects as $project) { |
2144
|
|
|
if($project->Environments()) { |
2145
|
|
|
foreach($project->Environments() as $environment) { |
2146
|
|
|
if( |
2147
|
|
|
$environment->canRestore($member) || |
2148
|
|
|
$environment->canBackup($member) || |
2149
|
|
|
$environment->canUploadArchive($member) || |
2150
|
|
|
$environment->canDownloadArchive($member) |
2151
|
|
|
) { |
2152
|
|
|
// We can return early as we only need to know that we can access one environment |
2153
|
|
|
return true; |
2154
|
|
|
} |
2155
|
|
|
} |
2156
|
|
|
} |
2157
|
|
|
} |
2158
|
|
|
} |
2159
|
|
|
|
2160
|
|
|
/** |
2161
|
|
|
* Returns a list of attempted environment creations. |
2162
|
|
|
* |
2163
|
|
|
* @return PaginatedList |
2164
|
|
|
*/ |
2165
|
|
|
public function CreateEnvironmentList() { |
2166
|
|
|
$project = $this->getCurrentProject(); |
2167
|
|
|
if($project) { |
2168
|
|
|
return new PaginatedList($project->CreateEnvironments()->sort("Created DESC"), $this->request); |
|
|
|
|
2169
|
|
|
} |
2170
|
|
|
return new PaginatedList(new ArrayList(), $this->request); |
|
|
|
|
2171
|
|
|
} |
2172
|
|
|
|
2173
|
|
|
/** |
2174
|
|
|
* Returns a list of all archive files that can be accessed by the currently logged-in {@link Member} |
2175
|
|
|
* |
2176
|
|
|
* @return PaginatedList |
2177
|
|
|
*/ |
2178
|
|
|
public function CompleteDataArchives() { |
2179
|
|
|
$project = $this->getCurrentProject(); |
2180
|
|
|
$archives = new ArrayList(); |
2181
|
|
|
|
2182
|
|
|
$archiveList = $project->Environments()->relation("DataArchives"); |
2183
|
|
|
if($archiveList->count() > 0) { |
2184
|
|
|
foreach($archiveList as $archive) { |
2185
|
|
|
if($archive->canView() && !$archive->isPending()) { |
2186
|
|
|
$archives->push($archive); |
2187
|
|
|
} |
2188
|
|
|
} |
2189
|
|
|
} |
2190
|
|
|
return new PaginatedList($archives->sort("Created", "DESC"), $this->request); |
|
|
|
|
2191
|
|
|
} |
2192
|
|
|
|
2193
|
|
|
/** |
2194
|
|
|
* @return PaginatedList The list of "pending" data archives which are waiting for a file |
2195
|
|
|
* to be delivered offline by post, and manually uploaded into the system. |
2196
|
|
|
*/ |
2197
|
|
|
public function PendingDataArchives() { |
2198
|
|
|
$project = $this->getCurrentProject(); |
2199
|
|
|
$archives = new ArrayList(); |
2200
|
|
|
foreach($project->DNEnvironmentList() as $env) { |
2201
|
|
|
foreach($env->DataArchives() as $archive) { |
2202
|
|
|
if($archive->canView() && $archive->isPending()) { |
2203
|
|
|
$archives->push($archive); |
2204
|
|
|
} |
2205
|
|
|
} |
2206
|
|
|
} |
2207
|
|
|
return new PaginatedList($archives->sort("Created", "DESC"), $this->request); |
|
|
|
|
2208
|
|
|
} |
2209
|
|
|
|
2210
|
|
|
/** |
2211
|
|
|
* @return PaginatedList |
2212
|
|
|
*/ |
2213
|
|
|
public function DataTransferLogs() { |
2214
|
|
|
$project = $this->getCurrentProject(); |
2215
|
|
|
|
2216
|
|
|
$transfers = DNDataTransfer::get()->filterByCallback(function($record) use($project) { |
2217
|
|
|
return |
2218
|
|
|
$record->Environment()->Project()->ID == $project->ID && // Ensure only the current Project is shown |
2219
|
|
|
( |
2220
|
|
|
$record->Environment()->canRestore() || // Ensure member can perform an action on the transfers env |
2221
|
|
|
$record->Environment()->canBackup() || |
2222
|
|
|
$record->Environment()->canUploadArchive() || |
2223
|
|
|
$record->Environment()->canDownloadArchive() |
2224
|
|
|
); |
2225
|
|
|
}); |
2226
|
|
|
|
2227
|
|
|
return new PaginatedList($transfers->sort("Created", "DESC"), $this->request); |
|
|
|
|
2228
|
|
|
} |
2229
|
|
|
|
2230
|
|
|
/** |
2231
|
|
|
* @return null|PaginatedList |
2232
|
|
|
*/ |
2233
|
|
|
public function DeployHistory() { |
2234
|
|
|
if($env = $this->getCurrentEnvironment()) { |
2235
|
|
|
$history = $env->DeployHistory(); |
2236
|
|
|
if($history->count() > 0) { |
2237
|
|
|
$pagination = new PaginatedList($history, $this->getRequest()); |
|
|
|
|
2238
|
|
|
$pagination->setPageLength(8); |
2239
|
|
|
return $pagination; |
2240
|
|
|
} |
2241
|
|
|
} |
2242
|
|
|
return null; |
2243
|
|
|
} |
2244
|
|
|
|
2245
|
|
|
/** |
2246
|
|
|
* @return SS_HTTPResponse |
2247
|
|
|
*/ |
2248
|
|
|
protected function project404Response() { |
2249
|
|
|
return new SS_HTTPResponse( |
2250
|
|
|
"Project '" . Convert::raw2xml($this->getRequest()->param('Project')) . "' not found.", |
2251
|
|
|
404 |
2252
|
|
|
); |
2253
|
|
|
} |
2254
|
|
|
|
2255
|
|
|
/** |
2256
|
|
|
* @return SS_HTTPResponse |
2257
|
|
|
*/ |
2258
|
|
|
protected function environment404Response() { |
2259
|
|
|
$envName = Convert::raw2xml($this->getRequest()->param('Environment')); |
2260
|
|
|
return new SS_HTTPResponse("Environment '" . $envName . "' not found.", 404); |
2261
|
|
|
} |
2262
|
|
|
|
2263
|
|
|
/** |
2264
|
|
|
* @param string $status |
2265
|
|
|
* @param string $content |
2266
|
|
|
* |
2267
|
|
|
* @return string |
2268
|
|
|
*/ |
2269
|
|
|
protected function sendResponse($status, $content) { |
2270
|
|
|
// strip excessive newlines |
2271
|
|
|
$content = preg_replace('/(?:(?:\r\n|\r|\n)\s*){2}/s', "\n", $content); |
2272
|
|
|
|
2273
|
|
|
$sendJSON = (strpos($this->getRequest()->getHeader('Accept'), 'application/json') !== false) |
2274
|
|
|
|| $this->getRequest()->getExtension() == 'json'; |
2275
|
|
|
|
2276
|
|
|
if(!$sendJSON) { |
2277
|
|
|
$this->response->addHeader("Content-type", "text/plain"); |
2278
|
|
|
return $content; |
2279
|
|
|
} |
2280
|
|
|
$this->response->addHeader("Content-type", "application/json"); |
2281
|
|
|
return json_encode(array( |
2282
|
|
|
'status' => $status, |
2283
|
|
|
'content' => $content, |
2284
|
|
|
)); |
2285
|
|
|
} |
2286
|
|
|
|
2287
|
|
|
/** |
2288
|
|
|
* Validate the snapshot mode |
2289
|
|
|
* |
2290
|
|
|
* @param string $mode |
2291
|
|
|
*/ |
2292
|
|
|
protected function validateSnapshotMode($mode) { |
2293
|
|
|
if(!in_array($mode, array('all', 'assets', 'db'))) { |
2294
|
|
|
throw new LogicException('Invalid mode'); |
2295
|
|
|
} |
2296
|
|
|
} |
2297
|
|
|
|
2298
|
|
|
/** |
2299
|
|
|
* @param string $sectionName |
2300
|
|
|
* @param string $title |
2301
|
|
|
* |
2302
|
|
|
* @return SS_HTTPResponse |
2303
|
|
|
*/ |
2304
|
|
|
protected function getCustomisedViewSection($sectionName, $title = '') { |
2305
|
|
|
// Performs canView permission check by limiting visible projects |
2306
|
|
|
$project = $this->getCurrentProject(); |
2307
|
|
|
if(!$project) { |
2308
|
|
|
return $this->project404Response(); |
2309
|
|
|
} |
2310
|
|
|
$data = array( |
2311
|
|
|
$sectionName => 1, |
2312
|
|
|
); |
2313
|
|
|
|
2314
|
|
|
if($this !== '') { |
2315
|
|
|
$data['Title'] = $title; |
2316
|
|
|
} |
2317
|
|
|
|
2318
|
|
|
return $this->customise($data)->render(); |
2319
|
|
|
} |
2320
|
|
|
|
2321
|
|
|
/** |
2322
|
|
|
* Get items for the ambient menu that should be accessible from all pages. |
2323
|
|
|
* |
2324
|
|
|
* @return ArrayList |
2325
|
|
|
*/ |
2326
|
|
|
public function AmbientMenu() { |
2327
|
|
|
$list = new ArrayList(); |
2328
|
|
|
|
2329
|
|
|
if (Member::currentUserID()) { |
2330
|
|
|
$list->push(new ArrayData(array( |
2331
|
|
|
'Classes' => 'logout', |
2332
|
|
|
'FaIcon' => 'sign-out', |
2333
|
|
|
'Link' => 'Security/logout', |
2334
|
|
|
'Title' => 'Log out', |
2335
|
|
|
'IsCurrent' => false, |
2336
|
|
|
'IsSection' => false |
2337
|
|
|
))); |
2338
|
|
|
} |
2339
|
|
|
|
2340
|
|
|
$this->extend('updateAmbientMenu', $list); |
2341
|
|
|
return $list; |
2342
|
|
|
} |
2343
|
|
|
|
2344
|
|
|
/** |
2345
|
|
|
* Create project action. |
2346
|
|
|
* |
2347
|
|
|
* @return SS_HTTPResponse |
2348
|
|
|
*/ |
2349
|
|
|
public function createproject(SS_HTTPRequest $request) { |
2350
|
|
|
if($this->canCreateProjects()) { |
2351
|
|
|
return $this->render(['CurrentTitle' => 'Create Stack']); |
2352
|
|
|
} |
2353
|
|
|
return $this->httpError(403); |
2354
|
|
|
} |
2355
|
|
|
|
2356
|
|
|
/** |
2357
|
|
|
* Checks whether the user can create a project. |
2358
|
|
|
* |
2359
|
|
|
* @return bool |
2360
|
|
|
*/ |
2361
|
|
|
protected function canCreateProjects($member = null) { |
2362
|
|
|
if(!$member) $member = Member::currentUser(); |
2363
|
|
|
if(!$member) return false; |
2364
|
|
|
|
2365
|
|
|
return Permission::checkMember($member, 'ADMIN'); |
2366
|
|
|
} |
2367
|
|
|
|
2368
|
|
|
/** |
2369
|
|
|
* @return Form |
2370
|
|
|
*/ |
2371
|
|
|
public function CreateProjectForm() { |
2372
|
|
|
$form = Form::create( |
2373
|
|
|
$this, |
2374
|
|
|
__FUNCTION__, |
2375
|
|
|
$this->getCreateProjectFormFields(), |
2376
|
|
|
$this->getCreateProjectFormActions(), |
2377
|
|
|
new RequiredFields('Name', 'CVSPath') |
2378
|
|
|
); |
2379
|
|
|
$this->extend('updateCreateProjectForm', $form); |
2380
|
|
|
return $form; |
2381
|
|
|
} |
2382
|
|
|
|
2383
|
|
|
/** |
2384
|
|
|
* @return FieldList |
2385
|
|
|
*/ |
2386
|
|
|
protected function getCreateProjectFormFields() { |
2387
|
|
|
$fields = FieldList::create(); |
2388
|
|
|
$fields->merge([ |
2389
|
|
|
TextField::create('Name', 'Title'), |
2390
|
|
|
TextField::create('CVSPath', 'Git URL'), |
2391
|
|
|
TextField::create('Client', 'Client'), |
2392
|
|
|
]); |
2393
|
|
|
$this->extend('updateCreateProjectFormFields', $fields); |
2394
|
|
|
return $fields; |
2395
|
|
|
} |
2396
|
|
|
|
2397
|
|
|
/** |
2398
|
|
|
* @return FieldList |
2399
|
|
|
*/ |
2400
|
|
|
protected function getCreateProjectFormActions() { |
2401
|
|
|
$fields = FieldList::create( |
2402
|
|
|
FormAction::create('doCreateProject', 'Create Project') |
2403
|
|
|
); |
2404
|
|
|
$this->extend('updateCreateProjectFormActions', $fields); |
2405
|
|
|
return $fields; |
2406
|
|
|
} |
2407
|
|
|
|
2408
|
|
|
/** |
2409
|
|
|
* Does the actual project creation. |
2410
|
|
|
* |
2411
|
|
|
* @param $data array |
2412
|
|
|
* @param $form Form |
2413
|
|
|
* |
2414
|
|
|
* @return SS_HTTPResponse |
2415
|
|
|
*/ |
2416
|
|
|
public function doCreateProject($data, $form) { |
2417
|
|
|
$form->loadDataFrom($data); |
2418
|
|
|
$project = DNProject::create(); |
2419
|
|
|
|
2420
|
|
|
$form->saveInto($project); |
2421
|
|
|
$this->extend('onBeforeCreateProject', $project, $data, $form); |
2422
|
|
|
try { |
2423
|
|
|
if($project->write() > 0) { |
2424
|
|
|
$this->extend('onAfterCreateProject', $project, $data, $form); |
2425
|
|
|
|
2426
|
|
|
// If an extension hasn't redirected us, we'll redirect to the project. |
2427
|
|
|
if(!$this->redirectedTo()) { |
2428
|
|
|
return $this->redirect($project->Link()); |
2429
|
|
|
} else { |
2430
|
|
|
return $this->response; |
2431
|
|
|
} |
2432
|
|
|
} else { |
2433
|
|
|
$form->sessionMessage('Unable to write the stack to the database.', 'bad'); |
2434
|
|
|
} |
2435
|
|
|
} catch (ValidationException $e) { |
2436
|
|
|
$form->sessionMessage($e->getMessage(), 'bad'); |
2437
|
|
|
} |
2438
|
|
|
return $this->redirectBack(); |
2439
|
|
|
} |
2440
|
|
|
|
2441
|
|
|
} |
2442
|
|
|
|
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.