forked from brefphp/bref
-
Notifications
You must be signed in to change notification settings - Fork 0
/
bref
executable file
·212 lines (188 loc) · 7.65 KB
/
bref
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
#!/usr/bin/env php
<?php
declare(strict_types=1);
use GuzzleHttp\Psr7\Stream;
use Bref\Util\CommandRunner;
use Joli\JoliNotif\Notification;
use Joli\JoliNotif\NotifierFactory;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Yaml\Yaml;
const PHP_TARGET_VERSION = '7.2.5';
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
require_once __DIR__ . '/vendor/autoload.php';
} elseif (file_exists(__DIR__ . '/../autoload.php')) {
require_once __DIR__ . '/../autoload.php';
} else {
require_once __DIR__ . '/../../autoload.php';
}
$app = new Silly\Application('Deploy serverless PHP applications');
$app->command('init', function (SymfonyStyle $io) {
/*
* TODO We should check that dependencies are correctly installed
* - check that `serverless` is installed
* - check that it is configured with environment variables
* If not, print a link to Bref's documentation and exit with an error.
*/
$fs = new Filesystem;
/*
* TODO Ask for a project name and configure it in `serverless.yml`.
*/
if (!file_exists('serverless.yml')) {
$io->writeln('Creating serverless.yml');
$fs->copy(__DIR__ . '/template/serverless.yml', 'serverless.yml');
}
if (!file_exists('bref.php')) {
$io->writeln('Creating bref.php');
$fs->copy(__DIR__ . '/template/bref.php', 'bref.php');
}
$io->success([
'Project initialized and ready to deploy using `bref deploy`',
'If you are using git, you will need to commit the following files:',
'- bref.php',
'- serverless.yml',
// TODO Do this automatically
'You can add `/.bref/` to your .gitignore.',
]);
});
$app->command('deploy', function (SymfonyStyle $io) {
$fs = new Filesystem;
$commandRunner = new CommandRunner;
if (!$fs->exists('serverless.yml') || !$fs->exists('bref.php')) {
throw new Exception('The files `bref.php` and `serverless.yml` are required to deploy, run `bref init` to create them');
}
// Parse .bref.yml
$projectConfig = [];
if ($fs->exists('.bref.yml')) {
/*
* TODO validate the content of the config, for example we should
* error if there are unknown keys. Using the Symfony Config component
* for that could make sense.
*/
$projectConfig = Yaml::parse(file_get_contents('.bref.yml'));
}
$io->writeln('Building the project in the `.bref/output` directory');
/*
* TODO Mirror the directory instead of recreating it from scratch every time
* Blocked by https://github.com/symfony/symfony/pull/26399
* In the meantime we destroy `.bref/output` completely every time which
* is not efficient.
*/
$fs->remove('.bref/output');
$fs->mkdir('.bref/output');
$filesToCopy = new Finder;
$filesToCopy->in('.')
->depth(0)
->exclude('.bref') // avoid a recursive copy
->ignoreDotFiles(false);
foreach ($filesToCopy as $fileToCopy) {
if (is_file($fileToCopy->getPathname())) {
$fs->copy($fileToCopy->getPathname(), '.bref/output/' . $fileToCopy->getFilename());
} else {
$fs->mirror($fileToCopy->getPathname(), '.bref/output/' . $fileToCopy->getFilename(), null, [
'copy_on_windows' => true, // Force to copy symlink content
]);
}
}
// Cache PHP's binary in `.bref/bin/php` to avoid downloading it
// on every deploy.
/*
* TODO Allow choosing a PHP version instead of using directly the
* constant `PHP_TARGET_VERSION`. That could be done using the `.bref.yml`
* config file: there could be an option in that config, for example:
* php:
* version: 7.2.2
*/
if (!$fs->exists('.bref/bin/php/php-' . PHP_TARGET_VERSION . '.tar.gz')) {
$io->writeln('Downloading PHP in the `.bref/bin/` directory');
$fs->mkdir('.bref/bin/php');
$defaultUrl = 'https://s3.amazonaws.com/bref-php/bin/php-' . PHP_TARGET_VERSION . '.tar.gz';
/*
* TODO This option allows to customize the PHP binary used. It should be documented
* and probably moved to a dedicated option like:
* php:
* url: 'https://s3.amazonaws.com/...'
*/
$url = $projectConfig['php'] ?? $defaultUrl;
$commandRunner->run("curl -sSL $url -o .bref/bin/php/php-" . PHP_TARGET_VERSION . ".tar.gz");
}
$io->writeln('Installing the PHP binary');
$fs->mkdir('.bref/output/.bref/bin');
$commandRunner->run('tar -xzf .bref/bin/php/php-' . PHP_TARGET_VERSION . '.tar.gz -C .bref/output/.bref/bin');
$io->writeln('Installing `handler.js`');
$fs->copy(__DIR__ . '/template/handler.js', '.bref/output/handler.js');
$io->writeln('Installing composer dependencies');
$commandRunner->run('cd .bref/output && composer install --no-dev --classmap-authoritative --no-scripts');
/*
* TODO Edit the `serverless.yml` copy (in `.bref/output` to deploy these files:
* - bref.php
* - handler.js
* - .bref/**
*/
// Run build hooks defined in .bref.yml
$buildHooks = $projectConfig['hooks']['build'] ?? [];
foreach ($buildHooks as $buildHook) {
$io->writeln('Running ' . $buildHook);
$commandRunner->run('cd .bref/output && ' . $buildHook);
}
$io->writeln('Uploading the lambda');
$commandRunner->run('cd .bref/output && serverless deploy');
// Trigger a desktop notification
$notifier = NotifierFactory::create();
$notification = (new Notification)
->setTitle('Deployment success')
->setBody('Bref has deployed your application');
$notifier->send($notification);
});
/**
* Run a CLI command in the remote environment.
*/
$app->command('cli [arguments]*', function (array $arguments, SymfonyStyle $io) {
$commandRunner = new CommandRunner;
$serverlessInfo = $commandRunner->run('serverless info');
foreach (explode(PHP_EOL, $serverlessInfo) as $line) {
if (strpos($line, 'region: ') === 0) {
$region = substr($line, strlen('region: '));
}
if (strpos($line, 'stack: ') === 0) {
$functionName = substr($line, strlen('stack: ')) . '-main';
}
}
if (empty($region) || empty($functionName)) {
throw new Exception('Unable to parse the output of `serverless info`');
}
// We cannot use the `serverless` CLI command here because it
// mangles the lambda response (JSON data) with logs on stdout.
// Using the AWS SDK allows to collect those data separately and properly.
$lambda = new \Aws\Lambda\LambdaClient([
'version' => 'latest',
'region' => $region,
]);
$result = $lambda->invoke([
'FunctionName' => $functionName,
'LogType' => 'Tail',
'Payload' => json_encode([
'cli' => implode(' ', $arguments),
]),
]);
/** @var Stream $payload */
$payload = $result->get('Payload');
$payload = json_decode($payload->getContents(), true);
if (isset($payload['output'])) {
$io->writeln($payload['output']);
} else {
$io->error('The command did not return a valid response.');
$io->writeln('<info>Logs:</info>');
$io->write('<comment>' . base64_decode($result->get('LogResult')) . '</comment>');
$io->writeln('<info>Lambda result payload:</info>');
$io->writeln(json_encode($payload, JSON_PRETTY_PRINT));
return 1;
}
return (int) ($payload['exitCode'] ?? 1);
});
$app->command('info', function (SymfonyStyle $io) {
$commandRunner = new CommandRunner;
$io->write($commandRunner->run('serverless info'));
});
$app->run();