Cronjobs   F
last analyzed

Complexity

Total Complexity 62

Size/Duplication

Total Lines 604
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 196
c 0
b 0
f 0
dl 0
loc 604
rs 3.44
wmc 62

25 Methods

Rating   Name   Duplication   Size   Complexity  
A getCommand() 0 30 2
A enableJenkins() 0 6 1
A getHeaders() 0 9 2
A indexJobsByName() 0 27 6
A getCronjobs() 0 13 2
A callJenkins() 0 25 2
A getExistingJobs() 0 16 3
A buildExceptionMessage() 0 9 2
A getJenkinsCsrfHeader() 0 3 1
A getJenkinsApiResponse() 0 19 2
A createJobDefinitions() 0 26 4
A checkRoles() 0 6 3
A getPublisherString() 0 11 3
A extendJobCommand() 0 25 6
A getJobConfigPath() 0 3 1
A prepareJobXml() 0 39 2
A buildCsrfProtectionErrorMessage() 0 7 1
A generateCronjobs() 0 15 2
A getSchedule() 0 14 5
A getJenkinsUrl() 0 3 1
A disableJenkins() 0 6 1
A updateOrDelete() 0 28 5
A getDaysToKeep() 0 7 3
A getJobsDir() 0 3 1
A __construct() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Cronjobs often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Cronjobs, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Copyright © 2016-present Spryker Systems GmbH. All rights reserved.
5
 * Use of this software requires acceptance of the Evaluation License Agreement. See LICENSE file.
6
 */
7
8
namespace Spryker\Zed\Setup\Business\Model;
9
10
use ErrorException;
11
use Spryker\Zed\Setup\SetupConfig;
12
13
/**
14
 * @deprecated Use Scheduler module instead. Will be removed without replacement.
15
 */
16
class Cronjobs
17
{
18
    /**
19
     * @var string
20
     */
21
    public const ROLE_ADMIN = 'admin';
22
23
    /**
24
     * @var string
25
     */
26
    public const ROLE_REPORTING = 'reporting';
27
28
    /**
29
     * @var string
30
     */
31
    public const ROLE_EMPTY = 'empty';
32
33
    public const DEFAULT_ROLE = self::ROLE_ADMIN;
34
35
    /**
36
     * @var int
37
     */
38
    public const DEFAULT_AMOUNT_OF_DAYS_FOR_LOGFILE_ROTATION = 7;
39
40
    /**
41
     * @var string
42
     */
43
    public const JENKINS_API_JOBS_URL = 'api/json/jobs?pretty=true&tree=jobs[name]';
44
45
    /**
46
     * @var string
47
     */
48
    protected const JENKINS_URL_API_CSRF_TOKEN = 'crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)';
49
50
    /**
51
     * @var string
52
     */
53
    protected const JENKINS_CSRF_TOKEN_NAME = 'crumb';
54
55
    /**
56
     * @var string
57
     */
58
    protected const TEMPLATE_MESSAGE_ERROR_CURL = 'cURL error: %s  while calling Jenkins URL %s';
59
60
    /**
61
     * @var array
62
     */
63
    protected $allowedRoles = [
64
        self::ROLE_ADMIN,
65
        self::ROLE_REPORTING,
66
        self::ROLE_EMPTY,
67
    ];
68
69
    /**
70
     * @var \Spryker\Zed\Setup\SetupConfig
71
     */
72
    protected $config;
73
74
    /**
75
     * @param \Spryker\Zed\Setup\SetupConfig $config
76
     */
77
    public function __construct(SetupConfig $config)
78
    {
79
        $this->config = $config;
80
    }
81
82
    /**
83
     * @param array $roles
84
     *
85
     * @return string
86
     */
87
    public function generateCronjobs(array $roles)
88
    {
89
        if (!$roles) {
90
            $roles = [static::DEFAULT_ROLE];
91
        }
92
93
        $this->checkRoles($roles);
94
95
        $jobsByName = $this->getCronjobs($roles);
96
97
        $consoleOutput = '';
98
        $consoleOutput .= $this->updateOrDelete($jobsByName);
99
        $consoleOutput .= $this->createJobDefinitions($jobsByName);
100
101
        return $consoleOutput;
102
    }
103
104
    /**
105
     * @return string
106
     */
107
    public function disableJenkins()
108
    {
109
        $url = 'quietDown';
110
        $code = $this->callJenkins($url);
111
112
        return "Jenkins disabled (response: $code)\n";
113
    }
114
115
    /**
116
     * @return string
117
     */
118
    public function enableJenkins()
119
    {
120
        $url = 'cancelQuietDown';
121
        $code = $this->callJenkins($url);
122
123
        return "Jenkins enabled (response: $code)\n";
124
    }
125
126
    /**
127
     * @param array $roles
128
     *
129
     * @throws \ErrorException
130
     *
131
     * @return void
132
     */
133
    protected function checkRoles(array $roles)
134
    {
135
        foreach ($roles as $role) {
136
            if (!in_array($role, $this->allowedRoles)) {
137
                throw new ErrorException(
138
                    $role . ' is not in the list of allowed job roles! Cannot continue configuration of jenkins!',
139
                );
140
            }
141
        }
142
    }
143
144
    /**
145
     * @param array $roles
146
     *
147
     * @return array
148
     */
149
    protected function getCronjobs(array $roles)
150
    {
151
        $jobs = [];
152
153
        include_once $this->getJobConfigPath();
154
155
        if (count($jobs) === 0) {
156
            return [];
157
        }
158
159
        $jobs = $this->extendJobCommand($jobs);
160
161
        return $this->indexJobsByName($jobs, $roles);
162
    }
163
164
    /**
165
     * @param array $jobs
166
     * @param array $roles
167
     *
168
     * @return array
169
     */
170
    protected function indexJobsByName(array $jobs, array $roles)
171
    {
172
        $jobsByName = [];
173
174
        foreach ($jobs as $v) {
175
            if (array_key_exists('role', $v) && in_array($v['role'], $this->allowedRoles)) {
176
                $jobRole = $v['role'];
177
            } else {
178
                $jobRole = static::DEFAULT_ROLE;
179
            }
180
181
            // Enable jobs only for roles matching those specified via command line argument
182
            if (array_search($jobRole, $roles) === false) {
183
                continue;
184
            }
185
186
            foreach ($v['stores'] as $store) {
187
                $name = $store . '__' . $v['name'];
188
                $jobsByName[$name] = $v;
189
                $jobsByName[$name]['name'] = $name;
190
                $jobsByName[$name]['store'] = $store;
191
                $jobsByName[$name]['role'] = $jobRole;
192
                unset($jobsByName[$name]['stores']);
193
            }
194
        }
195
196
        return $jobsByName;
197
    }
198
199
    /**
200
     * @return array
201
     */
202
    protected function getExistingJobs()
203
    {
204
        $jobsNames = [];
205
206
        $jobs = $this->getJenkinsApiResponse(static::JENKINS_API_JOBS_URL);
207
        $jobs = json_decode($jobs, true);
208
209
        if (count($jobs['jobs']) === 0) {
210
            return $jobsNames;
211
        }
212
213
        foreach ($jobs['jobs'] as $job) {
214
            $jobsNames[] = $job['name'];
215
        }
216
217
        return $jobsNames;
218
    }
219
220
    /**
221
     * Loop over existing jobs: either update or delete job
222
     *
223
     * @param array $jobsByName
224
     *
225
     * @return string
226
     */
227
    protected function updateOrDelete(array $jobsByName)
228
    {
229
        $output = '';
230
        $existingJobs = $this->getExistingJobs();
231
232
        if (count($existingJobs) === 0) {
233
            return $output;
234
        }
235
236
        foreach ($existingJobs as $name) {
237
            if (!in_array($name, array_keys($jobsByName))) {
238
                // Job does not exist anymore - we have to delete it.
239
                $url = 'job/' . $name . '/doDelete';
240
                $code = $this->callJenkins($url);
241
                $output .= "DELETE  jenkins job: $url (http_response: $code)" . PHP_EOL;
242
            } else {
243
                // Job exists - let's update config.xml and remove it from array of jobs
244
                $xml = $this->prepareJobXml($jobsByName[$name]);
245
                $url = 'job/' . $name . '/config.xml';
246
                $code = $this->callJenkins($url, $xml);
247
248
                if ($code !== 200) {
249
                    $output .= "UPDATE jenkins job: $url (http_response: $code)" . PHP_EOL;
250
                }
251
            }
252
        }
253
254
        return $output;
255
    }
256
257
    /**
258
     * Create Jenkins jobs for provided list of jobs
259
     *
260
     * @param array $jobsByName
261
     *
262
     * @return string
263
     */
264
    protected function createJobDefinitions(array $jobsByName)
265
    {
266
        $output = '';
267
268
        $existingJobs = $this->getExistingJobs();
269
270
        foreach ($jobsByName as $k => $v) {
271
            // skip if job is in existingjobs
272
            if (in_array($k, $existingJobs)) {
273
                $output .= "SKIPPED jenkins job: $k (already exists)" . PHP_EOL;
274
275
                continue;
276
            }
277
278
            $url = 'createItem?name=' . $v['name'];
279
280
            $xml = $this->prepareJobXml($v);
281
            $code = $this->callJenkins($url, $xml);
282
283
            if ($code === 400) {
284
                $code = '400: already exists';
285
            }
286
            $output .= "CREATE jenkins job: $url (http_response: $code)" . PHP_EOL;
287
        }
288
289
        return $output;
290
    }
291
292
    /**
293
     * @param string $url
294
     * @param string $body
295
     *
296
     * @throws \ErrorException
297
     *
298
     * @return int
299
     */
300
    protected function callJenkins($url, $body = ''): int
301
    {
302
        $postUrl = $this->getJenkinsUrl($url);
303
304
        $ch = curl_init();
305
        curl_setopt($ch, CURLOPT_URL, $postUrl);
306
        curl_setopt($ch, CURLOPT_POST, 1);
307
        curl_setopt($ch, CURLOPT_HTTPHEADER, $this->getHeaders());
308
        curl_setopt($ch, CURLOPT_HEADER, true);
309
        curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
310
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
311
        curl_setopt($ch, CURLOPT_FAILONERROR, true);
312
313
        $curlResponse = curl_exec($ch);
314
315
        if ($curlResponse === false) {
316
            $exceptionMessage = $this->buildExceptionMessage(curl_error($ch), $postUrl);
317
318
            throw new ErrorException($exceptionMessage);
319
        }
320
321
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
322
        curl_close($ch);
323
324
        return (int)$httpCode;
325
    }
326
327
    /**
328
     * @param string $url
329
     *
330
     * @throws \ErrorException
331
     *
332
     * @return string
333
     */
334
    protected function getJenkinsApiResponse($url)
335
    {
336
        $getUrl = $this->getJenkinsUrl($url);
337
338
        $ch = curl_init();
339
        curl_setopt($ch, CURLOPT_URL, $getUrl);
340
        curl_setopt($ch, CURLOPT_HEADER, false);
341
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
342
        curl_setopt($ch, CURLOPT_FAILONERROR, true);
343
344
        $curlResponse = curl_exec($ch);
345
346
        if ($curlResponse === false) {
347
            throw new ErrorException('cURL error: ' . curl_error($ch) . ' while calling Jenkins URL ' . $getUrl);
348
        }
349
        curl_close($ch);
350
351
        /** @phpstan-var string */
352
        return $curlResponse;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $curlResponse also could return the type true which is incompatible with the documented return type string.
Loading history...
353
    }
354
355
    /**
356
     * Render Job description (as XML) for Jenkins API call
357
     *
358
     * @todo Move XML snippet to twig template
359
     *
360
     * @param array $job
361
     *
362
     * @return string
363
     */
364
    protected function prepareJobXml(array $job)
365
    {
366
        $disabled = ($job['enable'] === true) ? 'false' : 'true';
367
        $schedule = $this->getSchedule($job);
368
        $daysToKeep = $this->getDaysToKeep($job);
369
        $command = htmlspecialchars($job['command']);
370
        $store = $job['store'];
371
372
        $xml = "<?xml version='1.0' encoding='UTF-8'?>
373
<project>
374
  <actions/>
375
  <description></description>
376
  <logRotator>
377
    <daysToKeep>$daysToKeep</daysToKeep>
378
    <numToKeep>-1</numToKeep>
379
    <artifactDaysToKeep>$daysToKeep</artifactDaysToKeep>
380
    <artifactNumToKeep>-1</artifactNumToKeep>
381
  </logRotator>
382
  <keepDependencies>false</keepDependencies>
383
  <properties/>
384
  <scm class='hudson.scm.NullSCM'/>
385
  <canRoam>true</canRoam>
386
  <disabled>$disabled</disabled>
387
  <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
388
  <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
389
  <triggers class='vector'>$schedule</triggers>
390
  <concurrentBuild>false</concurrentBuild>
391
  <builders>
392
    <hudson.tasks.Shell>";
393
394
        $xml .= $this->getCommand($command, $store);
395
        $xml .= "
396
    </hudson.tasks.Shell>
397
  </builders>\n"
398
            . $this->getPublisherString($job) . "\n
399
  <buildWrappers/>
400
</project>\n";
401
402
        return $xml;
403
    }
404
405
   /**
406
    * Render partial for job description with publisher settings
407
    * it returns not empty XML entity if job has email notifications set.
408
    *
409
    * @param array $job
410
    *
411
    * @return string
412
    */
413
    protected function getPublisherString(array $job)
414
    {
415
        if (empty($job['notifications']) || !is_array($job['notifications'])) {
416
            return '<publishers/>';
417
        }
418
419
        $recipients = implode(' ', $job['notifications']);
420
421
        return "<publishers>
422
    <hudson.tasks.Mailer>
423
      <recipients>$recipients</recipients>
424
      <dontNotifyEveryUnstableBuild>false</dontNotifyEveryUnstableBuild>
425
      <sendToIndividuals>false</sendToIndividuals>
426
    </hudson.tasks.Mailer>
427
</publishers>";
428
    }
429
430
    /**
431
     * Gets a string with job schedule (how often run job). The schedule string is compatible
432
     * with cronjob schedule defininion (eg. 0 * * * * meaning: run once each hour at 00 minute).
433
     * If environment is development, return empty string - we execute cronjobs on development environment
434
     * only manually.
435
     *
436
     * @param array $job
437
     *
438
     * @return string
439
     */
440
    protected function getSchedule(array $job)
441
    {
442
        $schedule = ($job['schedule'] === '') ? '' : ' <hudson.triggers.TimerTrigger><spec>' . $job['schedule'] . '</spec></hudson.triggers.TimerTrigger>';
443
444
        if (array_key_exists('run_on_non_production', $job) && $job['run_on_non_production'] === true) {
445
            return $schedule;
446
        }
447
448
        if ($this->config->isSchedulerEnabled() === false) {
449
            // Non-production - don't run automatically via Jenkins
450
            return '';
451
        }
452
453
        return $schedule;
454
    }
455
456
    /**
457
     * Get number of days to keep job output history. Each run is a directory, so we definitely need to keep it clean.
458
     *
459
     * @param array $job
460
     *
461
     * @return int
462
     */
463
    protected function getDaysToKeep(array $job)
464
    {
465
        if (array_key_exists('logrotate_days', $job) && is_int($job['logrotate_days'])) {
466
            return $job['logrotate_days'];
467
        }
468
469
        return static::DEFAULT_AMOUNT_OF_DAYS_FOR_LOGFILE_ROTATION;
470
    }
471
472
    /**
473
     * @param string $command
474
     * @param string $store
475
     *
476
     * @return string
477
     */
478
    protected function getCommand($command, $store)
479
    {
480
        $commandTemplate = '<command>%s
481
export APPLICATION_ENV=%s
482
export APPLICATION_STORE=%s
483
cd %s
484
. %s
485
%s</command>';
486
487
        $cronjobsConfigPath = $this->config->getCronjobsConfigFilePath();
488
489
        $customBashCommand = '';
490
        $destination = APPLICATION_ROOT_DIR;
491
492
        if ($this->config->isDeployVarsEnabled()) {
493
            $checkDeployFolderExistsBashCommand = '[ -f ' . APPLICATION_ROOT_DIR . '/deploy/vars ]';
494
            $sourceBashCommand = '. ' . APPLICATION_ROOT_DIR . '/deploy/vars';
495
496
            $customBashCommand = $checkDeployFolderExistsBashCommand . ' ' . '&amp;&amp;' . ' ' . $sourceBashCommand;
497
            $destination = '$destination_release_dir';
498
        }
499
500
        return sprintf(
501
            $commandTemplate,
502
            $customBashCommand,
503
            APPLICATION_ENV,
504
            $store,
505
            $destination,
506
            $cronjobsConfigPath,
507
            $command,
508
        );
509
    }
510
511
    /**
512
     * @return string
513
     */
514
    protected function getJobConfigPath()
515
    {
516
        return $this->config->getCronjobsDefinitionFilePath();
517
    }
518
519
    /**
520
     * @param string $location
521
     *
522
     * @return string
523
     */
524
    protected function getJenkinsUrl($location)
525
    {
526
        return $this->config->getJenkinsUrl() . $location;
527
    }
528
529
    /**
530
     * @return string
531
     */
532
    protected function getJobsDir()
533
    {
534
        return $this->config->getJenkinsJobsDirectory();
535
    }
536
537
    /**
538
     * @return string
539
     */
540
    protected function getJenkinsCsrfHeader(): string
541
    {
542
        return $this->getJenkinsApiResponse(static::JENKINS_URL_API_CSRF_TOKEN);
543
    }
544
545
    /**
546
     * @param string $errorMessage
547
     * @param string $url
548
     *
549
     * @return string
550
     */
551
    protected function buildExceptionMessage(string $errorMessage, string $url): string
552
    {
553
        $curlErrorMessage = sprintf(static::TEMPLATE_MESSAGE_ERROR_CURL, $errorMessage, $url);
554
555
        if (strpos($curlErrorMessage, static::JENKINS_CSRF_TOKEN_NAME) !== false) {
556
            $curlErrorMessage = $this->buildCsrfProtectionErrorMessage($curlErrorMessage);
557
        }
558
559
        return $curlErrorMessage;
560
    }
561
562
    /**
563
     * @param string $errorMessage
564
     *
565
     * @return string
566
     */
567
    protected function buildCsrfProtectionErrorMessage(string $errorMessage): string
568
    {
569
        $csrfErrorMessage = 'Please add the following configuration to your config_* file to enable the CSRF protection for Jenkins.'
570
            . PHP_EOL
571
            . '$config[SetupConstants::JENKINS_CSRF_PROTECTION_ENABLED] = true;';
572
573
        return $errorMessage . PHP_EOL . $csrfErrorMessage;
574
    }
575
576
    /**
577
     * @return array<string>
578
     */
579
    protected function getHeaders(): array
580
    {
581
        $httpHeader = ['Content-Type: text/xml'];
582
583
        if ($this->config->isJenkinsCsrfProtectionEnabled()) {
584
            $httpHeader[] = $this->getJenkinsCsrfHeader();
585
        }
586
587
        return $httpHeader;
588
    }
589
590
    /**
591
     * @param array $jobs
592
     *
593
     * @return array
594
     */
595
    protected function extendJobCommand(array $jobs): array
596
    {
597
        foreach ($jobs as $i => $job) {
598
            if (empty($job['command'])) {
599
                continue;
600
            }
601
            $command = $job['command'];
602
            $commandExpl = explode(' ', $command);
603
            $requestParts = ['module' => '', 'controller' => '', 'action' => ''];
604
            foreach ($commandExpl as $part) {
605
                $segments = array_keys($requestParts);
606
                foreach ($segments as $segment) {
607
                    if (strpos($part, $segment . '=') !== false) {
608
                        $requestParts[$segment] = str_replace('--' . $segment . '=', '', $part);
609
                    }
610
                }
611
            }
612
613
            $jobs[$i]['request'] = '/' . $requestParts['module'] . '/' . $requestParts['controller']
614
                . '/' . $requestParts['action'];
615
616
            $jobs[$i]['id'] = null;
617
        }
618
619
        return $jobs;
620
    }
621
}
622