1
|
|
|
<?php |
2
|
|
|
namespace Kunstmaan\Skylab\Command; |
3
|
|
|
|
4
|
|
|
use CL\Slack\Model\Attachment; |
5
|
|
|
use CL\Slack\Model\AttachmentField; |
6
|
|
|
use CL\Slack\Payload\ChatDeletePayload; |
7
|
|
|
use CL\Slack\Payload\ChatPostMessagePayload; |
8
|
|
|
use CL\Slack\Payload\ChatPostMessagePayloadResponse; |
9
|
|
|
use CL\Slack\Transport\ApiClient; |
10
|
|
|
use Symfony\Component\Console\Input\InputArgument; |
11
|
|
|
use Symfony\Component\Console\Input\InputOption; |
12
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
13
|
|
|
use Symfony\Component\Process\Process; |
14
|
|
|
use Symfony\Component\Yaml\Exception\ParseException; |
15
|
|
|
use Symfony\Component\Yaml\Parser; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* ExecuteCommand |
19
|
|
|
*/ |
20
|
|
|
class ExecuteCommand extends AbstractCommand |
21
|
|
|
{ |
22
|
|
|
|
23
|
|
|
private $ts = null; |
24
|
|
|
private $channel = null; |
25
|
|
|
/** @var ApiClient */ |
26
|
|
|
private $slackApiClient = null; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* Configures the current command. |
30
|
|
|
*/ |
31
|
|
|
protected function configure() |
32
|
|
|
{ |
33
|
|
|
$this |
34
|
|
|
->addDefaults() |
35
|
|
|
->setName('execute') |
36
|
|
|
->setDescription('Executes a Skylab YAML file') |
37
|
|
|
->addArgument('file', InputArgument::REQUIRED, 'The full path to the YAML file') |
38
|
|
|
->addArgument('deploy-environment', InputArgument::OPTIONAL, 'The environment to deploy to') |
39
|
|
|
->addOption("--skip-tests", null, InputOption::VALUE_NONE, 'If set, the test steps will be skipped') |
40
|
|
|
->addOption("--skip-deploy", null, InputOption::VALUE_NONE, 'If set, the deploy steps will be skipped') |
41
|
|
|
->addOption("--debug-yml", null, InputOption::VALUE_NONE, 'If set, the resulting yml will be shown without executing it') |
42
|
|
|
->setHelp(<<<EOT |
43
|
|
|
The <info>execute</info> command will execute a Skylab YAML file, used for testing and deploying via Jenkins |
44
|
|
|
|
45
|
|
|
<info>php skylab.phar execute /opt/skylab/templates/execute/deploy.yml</info> |
46
|
|
|
EOT |
47
|
|
|
); |
48
|
|
|
} |
49
|
|
|
|
50
|
|
|
protected function runStep($step, $yaml, $deployEnv, $successStep = null) |
51
|
|
|
{ |
52
|
|
|
if (isset($yaml[$step])) { |
53
|
|
|
$this->dialogProvider->logStep($step); |
54
|
|
|
|
55
|
|
|
foreach ($yaml[$step] as $list) { |
56
|
|
|
foreach ($list as $source) { |
57
|
|
|
foreach ($source as $command) { |
58
|
|
|
try { |
59
|
|
|
$result = $this->processProvider->executeCommand($command, false, function ($type, $buffer) { |
60
|
|
|
if (Process::ERR === $type) { |
61
|
|
|
$this->dialogProvider->logOutput($buffer, true); |
62
|
|
|
} else { |
63
|
|
|
if ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) { |
64
|
|
|
$this->dialogProvider->logOutput($buffer, false); |
65
|
|
|
} |
66
|
|
|
} |
67
|
|
|
}, $yaml["env"]); |
68
|
|
|
if ($result === false) { |
69
|
|
|
$this->notifySlack("Error while running the " . $step . " phase, check the console log in Jenkins", $yaml["deploy_matrix"][$deployEnv]["project"], $deployEnv, getenv("slack_user"), array(), "#CC0000", true); |
70
|
|
|
$this->dialogProvider->logError($step . " failed!", false); |
71
|
|
|
} |
72
|
|
|
} catch (\Exception $ex) { |
73
|
|
|
$this->notifySlack("Error while running the " . $step . " phase with error: " . $ex->getMessage(), $yaml["deploy_matrix"][$deployEnv]["project"], $deployEnv, getenv("slack_user"), array(), "#CC0000", true); |
74
|
|
|
$extra = array(); |
75
|
|
|
$tags = array(); |
76
|
|
|
$this->dialogProvider->logException($ex, $tags, $extra); |
77
|
|
|
} |
78
|
|
|
} |
79
|
|
|
} |
80
|
|
|
} |
81
|
|
|
if (!is_null($successStep)) { |
82
|
|
|
$this->runStep($successStep, $yaml, $deployEnv); |
83
|
|
|
} |
84
|
|
|
} else { |
85
|
|
|
return; |
86
|
|
|
} |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
protected function notifySlack($message, $project, $env, $user, $resolverArray, $color = "#FFCC00", $update = false) |
90
|
|
|
{ |
91
|
|
|
if ($update) { |
92
|
|
|
$payload = new ChatDeletePayload(); |
93
|
|
|
$payload->setSlackTimestamp($this->ts); |
94
|
|
|
$payload->setChannelId($this->channel); |
95
|
|
|
$response = $this->slackApiClient->send($payload); |
96
|
|
|
|
97
|
|
View Code Duplication |
if ($response->isOk()) { |
|
|
|
|
98
|
|
|
if ($response instanceof ChatPostMessagePayloadResponse) { |
99
|
|
|
/** @var ChatPostMessagePayloadResponse $response */ |
100
|
|
|
$this->ts = $response->getSlackTimestamp(); |
101
|
|
|
} |
102
|
|
|
} else { |
103
|
|
|
// something went wrong, but what? |
104
|
|
|
// simple error (Slack's error message) |
105
|
|
|
echo $response->getError(); |
106
|
|
|
// explained error (Slack's explanation of the error, according to the documentation) |
107
|
|
|
echo $response->getErrorExplanation(); |
108
|
|
|
exit(1); |
|
|
|
|
109
|
|
|
} |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
$payload = new ChatPostMessagePayload(); |
113
|
|
|
$payload->setChannel("#" . getenv("slack_channel")); |
114
|
|
|
$payload->setIconUrl("https://www.dropbox.com/s/ivrj3wcze7cwh54/masterjenkins.png?dl=1"); |
115
|
|
|
$payload->setUsername("Master Jenkins"); |
116
|
|
|
|
117
|
|
|
$attachment = new Attachment(); |
118
|
|
|
$attachment->setColor($color); |
119
|
|
|
$attachment->setFallback("[".$project."#" . getenv("BUILD_NUMBER"). " - branch *" . getenv("GIT_BRANCH") . "* to *". $env . "* by _" . $user . "_] " . $message); |
120
|
|
|
$attachment->setText("[".$project."#" . getenv("BUILD_NUMBER"). " - branch *" . getenv("GIT_BRANCH") . "* to *". $env . "* by _" . $user ."_] " . $message . "\n<" . getenv("BUILD_URL") . "console|Jenkins Console> - <".getenv("BUILD_URL")."changes|Changes>" . (isset($resolverArray["shared_package_target"]) && file_exists($resolverArray["shared_package_target"])?" - <" . $resolverArray["shared_package_url"] . "|Download>":"")); |
121
|
|
|
$payload->addAttachment($attachment); |
122
|
|
|
|
123
|
|
|
$response = $this->slackApiClient->send($payload); |
124
|
|
|
|
125
|
|
View Code Duplication |
if ($response->isOk()) { |
|
|
|
|
126
|
|
|
if ($response instanceof ChatPostMessagePayloadResponse) { |
127
|
|
|
/** @var ChatPostMessagePayloadResponse $response */ |
128
|
|
|
$this->ts = $response->getSlackTimestamp(); |
129
|
|
|
$this->channel = $response->getChannelId(); |
130
|
|
|
} |
131
|
|
|
} else { |
132
|
|
|
// something went wrong, but what? |
133
|
|
|
// simple error (Slack's error message) |
134
|
|
|
echo $response->getError(); |
135
|
|
|
// explained error (Slack's explanation of the error, according to the documentation) |
136
|
|
|
echo $response->getErrorExplanation(); |
137
|
|
|
exit(1); |
|
|
|
|
138
|
|
|
} |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
protected function doExecute() |
142
|
|
|
{ |
143
|
|
|
if (isset($this->app["config"]["slack_api_key"])) { |
144
|
|
|
$this->slackApiClient = new ApiClient($this->app["config"]["slack_api_key"]); |
145
|
|
|
} else { |
146
|
|
|
$this->slackApiClient = new ApiClient("fake key"); |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
$deployEnv = $this->input->getArgument('deploy-environment'); |
150
|
|
|
list($yaml, $resolverArray) = $this->parseYaml(); |
151
|
|
|
|
152
|
|
|
if ($this->input->getOption('debug-yml')) { |
153
|
|
|
print_r($yaml); |
154
|
|
|
print_r($resolverArray); |
155
|
|
|
exit(0); |
|
|
|
|
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
if (is_null($deployEnv)) { |
159
|
|
|
$this->dialogProvider->logError("You cannot run a deploy step without an environment", true); |
160
|
|
|
} |
161
|
|
|
if (isset($yaml["deploy_matrix"][$deployEnv])) { |
162
|
|
|
|
163
|
|
|
//build |
164
|
|
|
$this->notifySlack("Build started", $yaml["deploy_matrix"][$deployEnv]["project"], $deployEnv, getenv("slack_user"), $resolverArray); |
165
|
|
|
$this->runStep("before_build", $yaml, $deployEnv); |
166
|
|
|
$this->runStep("build", $yaml, $deployEnv, "after_build_success"); |
167
|
|
|
$this->notifySlack("Build successful", $yaml["deploy_matrix"][$deployEnv]["project"], $deployEnv, getenv("slack_user"), $resolverArray, "#FFCC00", true); |
168
|
|
|
|
169
|
|
|
// test |
170
|
|
View Code Duplication |
if (!$this->input->getOption('skip-tests')) { |
|
|
|
|
171
|
|
|
$this->notifySlack("Tests started", $yaml["deploy_matrix"][$deployEnv]["project"], $deployEnv, getenv("slack_user"), $resolverArray, "#FFCC00", true); |
172
|
|
|
$this->runStep("before_test", $yaml, $deployEnv); |
173
|
|
|
$this->runStep("test", $yaml, $deployEnv, "after_test_success"); |
174
|
|
|
$this->notifySlack("Tests successful", $yaml["deploy_matrix"][$deployEnv]["project"], $deployEnv, getenv("slack_user"), $resolverArray, ($this->input->getOption('skip-deploy')?"#7CD197":"#FFCC00"), true); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
// deploy |
178
|
|
View Code Duplication |
if (!$this->input->getOption('skip-deploy')) { |
|
|
|
|
179
|
|
|
$this->notifySlack("Deploy started", $yaml["deploy_matrix"][$deployEnv]["project"], $deployEnv, getenv("slack_user"), $resolverArray, "#FFCC00", true); |
180
|
|
|
$this->runStep("before_deploy", $yaml, $deployEnv); |
181
|
|
|
$this->runStep("deploy", $yaml, $deployEnv, "after_deploy_success"); |
182
|
|
|
$this->notifySlack("Deploy successful", $yaml["deploy_matrix"][$deployEnv]["project"], $deployEnv, getenv("slack_user"), $resolverArray, "#7CD197", true); |
183
|
|
|
} else { |
184
|
|
|
$this->dialogProvider->logNotice("Deploy is skipped"); |
185
|
|
|
} |
186
|
|
|
} else { |
187
|
|
|
$this->dialogProvider->logError("The deploy environment " . $deployEnv . " does not exist", true); |
188
|
|
|
} |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
/** |
192
|
|
|
* @return array |
|
|
|
|
193
|
|
|
* @throws \Exception |
194
|
|
|
*/ |
195
|
|
|
protected function parseYaml() |
196
|
|
|
{ |
197
|
|
|
try { |
198
|
|
|
$mergedYaml = $this->buildMergedYaml(); |
199
|
|
|
$resolverArray = $this->buildResolverArray($mergedYaml); |
200
|
|
|
$resolvedYaml = $this->resolveYaml($mergedYaml, $resolverArray); |
201
|
|
|
$mergedYaml["env"]["SHELL"] = "/bin/bash"; |
202
|
|
|
return array($resolvedYaml, $resolverArray); |
203
|
|
|
} catch (\Exception $ex){ |
204
|
|
|
$deployEnv = $this->input->getArgument('deploy-environment'); |
205
|
|
|
$this->notifySlack("Error while parding the YAML files, reason: " . $ex->getMessage(), "unknown", $deployEnv, getenv("slack_user"), array(), "#CC0000", true); |
206
|
|
|
throw $ex; |
207
|
|
|
} |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* @param $mergedYaml |
212
|
|
|
* @return array |
213
|
|
|
*/ |
214
|
|
|
protected function buildResolverArray($mergedYaml) |
215
|
|
|
{ |
216
|
|
|
$resolverArray = array_merge($this->app["config"], $mergedYaml["env"]); |
217
|
|
|
$resolverArray["base_dir"] = BASE_DIR; |
218
|
|
|
$deployEnv = $this->input->getArgument('deploy-environment'); |
219
|
|
|
if (!empty($deployEnv) && isset($mergedYaml["deploy_matrix"][$deployEnv])) { |
220
|
|
|
$resolverArray = $this->collectDeploySettings($mergedYaml["deploy_matrix"][$deployEnv], "deploy", $resolverArray); |
221
|
|
|
} |
222
|
|
|
if (isset($mergedYaml["database_source"])) { |
223
|
|
|
$dbSource = $mergedYaml["database_source"]; |
224
|
|
|
if (isset($mergedYaml["deploy_matrix"][$dbSource])) { |
225
|
|
|
$resolverArray = $this->collectDeploySettings($mergedYaml["deploy_matrix"][$dbSource], "dbsource", $resolverArray); |
226
|
|
|
} |
227
|
|
|
$resolverArray["fetch_mysql"] = "yes"; |
228
|
|
|
} else { |
229
|
|
|
$resolverArray["fetch_mysql"] = "no"; |
230
|
|
|
} |
231
|
|
|
$parametersFile = dirname(dirname($this->input->getArgument('file'))) . "/app/config/parameters.yml"; |
232
|
|
|
if (file_exists($parametersFile) && strpos(file_get_contents($parametersFile), 'database_host') !== false) { |
233
|
|
|
$parametersYaml = $this->loadYaml($parametersFile); |
234
|
|
|
$resolverArray = array_merge($parametersYaml["parameters"], $resolverArray); |
235
|
|
|
$resolverArray["run_mysql"] = (getenv('NO_MYSQL')?"no":"yes"); |
236
|
|
|
} else { |
237
|
|
|
$resolverArray["run_mysql"] = "no"; |
238
|
|
|
} |
239
|
|
|
$resolverArray["webserver_engine"] = $this->app["config"]["webserver"]["engine"]; |
240
|
|
|
$resolverArray["mysql_root_password"] = $this->app["config"]["mysql"]["password"]; |
241
|
|
|
$resolverArray["buildtag"] = $deployEnv . "-" . $this->getRevision(); |
242
|
|
|
$resolverArray["home"] = getenv("HOME"); |
243
|
|
|
$resolverArray["job_name"] = getenv("JOB_NAME"); |
244
|
|
|
$resolverArray["build_package_target"] = $resolverArray["home"] . "/builds/".$resolverArray["job_name"]."-".$resolverArray["buildtag"].".tar.gz"; |
245
|
|
|
$resolverArray["shared_package_folder"] = "/home/projects/build/data/shared/web/uploads/"; |
246
|
|
|
$resolverArray["shared_package_target"] = "/home/projects/build/data/shared/web/uploads/".$resolverArray["job_name"]."-".$resolverArray["deploy_timestamp"] . "-" . $resolverArray["buildtag"].".tar.gz"; |
247
|
|
|
$resolverArray["shared_package_url"] = "http://build.kunstmaan.be/uploads/".$resolverArray["job_name"]."-".$resolverArray["deploy_timestamp"] . "-" . $resolverArray["buildtag"].".tar.gz"; |
248
|
|
|
return $resolverArray; |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* @param $mergedYaml |
253
|
|
|
* @param $resolverArray |
254
|
|
|
* @return mixed |
255
|
|
|
*/ |
256
|
|
|
protected function resolveYaml($mergedYaml, $resolverArray) |
257
|
|
|
{ |
258
|
|
|
array_walk_recursive($mergedYaml, function (&$item, $key, $resolver) { |
259
|
|
|
$item = preg_replace_callback('/%%|%([^%\s]+)%/', function ($match) use ($key, $resolver) { |
260
|
|
|
|
261
|
|
|
// skip %% |
262
|
|
|
if (!isset($match[1])) { |
263
|
|
|
return '%%'; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
$key = $match[1]; |
|
|
|
|
267
|
|
|
|
268
|
|
|
if (isset($resolver[$key])) { |
269
|
|
|
return $resolver[$key]; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
return $match[0]; |
273
|
|
|
}, $item); |
274
|
|
|
}, $resolverArray); |
275
|
|
|
|
276
|
|
|
return $mergedYaml; |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
/** |
280
|
|
|
* @return array |
281
|
|
|
*/ |
282
|
|
|
protected function buildMergedYaml() |
283
|
|
|
{ |
284
|
|
|
$ymlPath = $this->input->getArgument('file'); |
285
|
|
|
$parsedYaml = $this->loadYaml($ymlPath); |
286
|
|
|
$mergedYaml = $this->handleTemplateYaml($parsedYaml); |
287
|
|
|
$mergedYaml = $this->handleResources($mergedYaml); |
288
|
|
|
return $mergedYaml; |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
protected function handleResources($mergedYaml) |
292
|
|
|
{ |
293
|
|
|
|
294
|
|
|
array_walk_recursive($mergedYaml, function (&$item, $key, &$mergedYaml) { |
|
|
|
|
295
|
|
|
if ($key === "resource") { |
296
|
|
|
$resourceYaml = $this->loadYaml(BASE_DIR . "/templates/execute/" . $item); |
297
|
|
|
$item = $resourceYaml["steps"]; |
298
|
|
|
} |
299
|
|
|
}, $mergedYaml); |
300
|
|
|
|
301
|
|
|
return $mergedYaml; |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
/** |
305
|
|
|
* @param $ymlPath |
306
|
|
|
* @return array |
|
|
|
|
307
|
|
|
*/ |
308
|
|
|
protected function loadYaml($ymlPath) |
309
|
|
|
{ |
310
|
|
|
$yaml = new Parser(); |
311
|
|
|
try { |
312
|
|
|
$parsedYaml = $yaml->parse(file_get_contents($ymlPath)); |
313
|
|
|
return $parsedYaml; |
314
|
|
|
} catch (ParseException $e) { |
315
|
|
|
$this->dialogProvider->logError(sprintf("Unable to parse the YAML string: %s", $e->getMessage()), false); |
316
|
|
|
} |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
protected function handleTemplateYaml($parsedYaml) |
320
|
|
|
{ |
321
|
|
|
if (isset($parsedYaml["template"])) { |
322
|
|
|
$templateYaml = $this->loadYaml(BASE_DIR . "/templates/execute/" . $parsedYaml["template"] . ".yml"); |
323
|
|
|
$mergedYaml = array_merge($templateYaml, $parsedYaml); |
324
|
|
|
} else { |
325
|
|
|
$mergedYaml = $parsedYaml; |
326
|
|
|
} |
327
|
|
|
$mergedYaml["env"] = array_merge((isset($templateYaml["env"]) ? $templateYaml["env"] : array()), (isset($parsedYaml["env"]) ? $parsedYaml["env"] : array())); |
328
|
|
|
return $mergedYaml; |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
protected function collectDeploySettings($deploySettings, $prefix, $resolverArray) |
332
|
|
|
{ |
333
|
|
|
$resolverArray[$prefix . "_server"] = $deploySettings["server"]; |
334
|
|
View Code Duplication |
if (isset($deploySettings["port"])) { |
|
|
|
|
335
|
|
|
$resolverArray[$prefix . "_port"] = $deploySettings["port"]; |
336
|
|
|
} else { |
337
|
|
|
$resolverArray[$prefix . "_port"] = 22; |
338
|
|
|
} |
339
|
|
|
$resolverArray[$prefix . "_project"] = $deploySettings["project"]; |
340
|
|
View Code Duplication |
if (isset($deploySettings["app_path"])) { |
|
|
|
|
341
|
|
|
$resolverArray[$prefix . "_app_path"] = $deploySettings["app_path"]; |
342
|
|
|
} else { |
343
|
|
|
$resolverArray[$prefix . "_app_path"] = "/ROOT"; |
344
|
|
|
} |
345
|
|
View Code Duplication |
if (isset($deploySettings["symfony_env"])) { |
|
|
|
|
346
|
|
|
$resolverArray[$prefix . "_symfony_env"] = $deploySettings["symfony_env"]; |
347
|
|
|
} else { |
348
|
|
|
$resolverArray[$prefix . "_symfony_env"] = "prod"; |
349
|
|
|
} |
350
|
|
|
$resolverArray[$prefix . "_timestamp"] = time(); |
351
|
|
|
return $resolverArray; |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
/** |
355
|
|
|
* @return mixed |
|
|
|
|
356
|
|
|
*/ |
357
|
|
|
protected function getRevision() |
358
|
|
|
{ |
359
|
|
|
return $this->processProvider->executeCommand('git log --pretty=format:"%h" -1'); |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
/** |
363
|
|
|
* @return mixed |
|
|
|
|
364
|
|
|
*/ |
365
|
|
|
protected function getBranch() |
366
|
|
|
{ |
367
|
|
|
return $this->processProvider->executeCommand('git rev-parse --abbrev-ref HEAD'); |
368
|
|
|
} |
369
|
|
|
} |
370
|
|
|
|
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.