Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
260 changes: 11 additions & 249 deletions bref
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@
<?php
declare(strict_types=1);

use Bref\Console\Command\Cli;
use Bref\Console\Command\Dashboard;
use Bref\Console\Command\Init;
use Bref\Console\Command\Layers;
use Bref\Console\Command\Local;
use Bref\Console\LoadingAnimation;
use Bref\Console\OpenUrl;
use Bref\Lambda\InvocationFailed;
use Bref\Lambda\SimpleLambdaClient;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\Process;
use Symfony\Component\Console\Application;

if (file_exists(__DIR__ . '/vendor/autoload.php')) {
require_once __DIR__ . '/vendor/autoload.php';
Expand All @@ -22,246 +19,11 @@ if (file_exists(__DIR__ . '/vendor/autoload.php')) {
require_once __DIR__ . '/../../autoload.php';
}

$app = new Silly\Application('Deploy serverless PHP applications');

$app->command('init', function (SymfonyStyle $io) {
$exeFinder = new ExecutableFinder();
if (! $exeFinder->find('serverless')) {
$io->error(
'The `serverless` command is not installed.' . PHP_EOL .
'Please follow the instructions at https://bref.sh/docs/installation.html'
);

return 1;
}

if (file_exists('serverless.yml') || file_exists('index.php')) {
$io->error('The directory already contains a `serverless.yml` and/or `index.php` file.');

return 1;
}

$choice = $io->choice(
'What kind of lambda do you want to create? (you will be able to add more functions later by editing `serverless.yml`)',
[
'Web application',
'Event-driven function',
],
'Web application',
);
$templateDirectory = [
'Web application' => 'http',
'Event-driven function' => 'function',
][$choice];

$fs = new Filesystem;
$rootPath = __DIR__ . "/template/$templateDirectory";

$io->writeln("Creating index.php");
$fs->copy("$rootPath/index.php", 'index.php');

$io->writeln("Creating serverless.yml");

$template = file_get_contents("$rootPath/serverless.yml");

$template = str_replace('PHP_VERSION', PHP_MAJOR_VERSION . PHP_MINOR_VERSION, $template);

file_put_contents('serverless.yml', $template);

$filesToGitAdd = ['index.php', 'serverless.yml'];

/*
* We check if this is a git repository to automatically add files to git.
*/
if ((new Process(['git', 'rev-parse', '--is-inside-work-tree']))->run() === 0) {
foreach ($filesToGitAdd as $file) {
(new Process(['git', 'add', $file]))->run();
}
$io->success([
'Project initialized and ready to test or deploy.',
'The files created were automatically added to git.',
]);
} else {
$io->success('Project initialized and ready to test or deploy.');
}

return 0;
});

/**
* Run a CLI command in the remote environment.
*/
$app->command('cli function [--region=] [--profile=] [arguments]*', function (string $function, ?string $region, ?string $profile, array $arguments, SymfonyStyle $io) {
$lambda = new SimpleLambdaClient(
$region ?: getenv('AWS_DEFAULT_REGION') ?: 'us-east-1',
$profile ?: getenv('AWS_PROFILE') ?: 'default',
15 * 60 // maximum duration on Lambda
);

// Because arguments may contain spaces, and are going to be executed remotely
// as a separate process, we need to escape all arguments.
$arguments = array_map(static function (string $arg): string {
return escapeshellarg($arg);
}, $arguments);

try {
$result = $lambda->invoke($function, json_encode(implode(' ', $arguments)));
} catch (InvocationFailed $e) {
$io->getErrorStyle()->writeln('<info>' . $e->getInvocationLogs() . '</info>');
$io->error($e->getMessage());
return 1;
}

$payload = $result->getPayload();
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>' . $result->getLogs() . '</comment>');
$io->writeln('<info>Lambda result payload:</info>');
$io->writeln(json_encode($payload, JSON_PRETTY_PRINT));
return 1;
}

return (int) ($payload['exitCode'] ?? 1);
});

/**
* Invoke a function locally
*/
$app->command(Local::SIGNATURE, new Local);

$app->command('dashboard [--host=] [--port=] [--profile=] [--stage=]', function (SymfonyStyle $io, string $host = 'localhost', int $port = 8000, string $profile = null, string $stage = null) {
$io->info('The Bref Dashboard is also available as an application: https://dashboard.bref.sh');
if ($host === 'localhost') {
$host = '127.0.0.1';
}
if ($profile === null) {
$profile = getenv('AWS_PROFILE') ?: 'default';
}

if (! file_exists('serverless.yml')) {
$io->error('No `serverless.yml` file found.');

return 1;
}

$exeFinder = new ExecutableFinder();
if (! $exeFinder->find('docker')) {
$io->error(
'The `docker` command is not installed.' . PHP_EOL .
'Please follow the instructions at https://docs.docker.com/install/'
);

return 1;
}

if (! $exeFinder->find('serverless')) {
$io->error(
'The `serverless` command is not installed.' . PHP_EOL .
'Please follow the instructions at https://bref.sh/docs/installation.html'
);

return 1;
}

$serverlessInfo = new Process(['serverless', 'info', '--stage', $stage, '--aws-profile', $profile]);
$serverlessInfo->start();
$animation = new LoadingAnimation($io);
do {
$animation->tick('Retrieving the stack');
usleep(100*1000);
} while ($serverlessInfo->isRunning());
$animation->clear();

if (!$serverlessInfo->isSuccessful()) {
$io->error('The command `serverless info` failed' . PHP_EOL . $serverlessInfo->getOutput());

return 1;
}

$serverlessInfoOutput = $serverlessInfo->getOutput();

$region = [];
preg_match('/region: ([a-z0-9-]*)/', $serverlessInfoOutput, $region);
$region = $region[1];

$stack = [];
preg_match('/stack: ([a-zA-Z0-9-]*)/', $serverlessInfoOutput, $stack);
$stack = $stack[1];

$io->writeln("Stack: <fg=yellow>$stack ($region)</>");

$dockerPull = new Process(['docker', 'pull', 'bref/dashboard']);
$dockerPull->setTimeout(null);
$dockerPull->start();
do {
$animation->tick('Retrieving the latest version of the dashboard');
usleep(100*1000);
} while ($dockerPull->isRunning());
$animation->clear();
if (! $dockerPull->isSuccessful()) {
$io->error([
'The command `docker pull bref/dashboard` failed',
$dockerPull->getErrorOutput(),
]);

return 1;
}

$process = new Process(['docker', 'run', '--rm', '-p', $host . ':' . $port.':8000', '-v', getenv('HOME').'/.aws:/root/.aws:ro', '--env', 'STACKNAME='.$stack, '--env', 'REGION='.$region, '--env', 'AWS_PROFILE='.$profile, 'bref/dashboard']);
$process->setTimeout(null);
$process->start();
do {
$animation->tick('Starting the dashboard');
usleep(100*1000);
$serverOutput = $process->getOutput() . $process->getErrorOutput();
$hasStarted = (strpos($serverOutput, 'Development Server') !== false);
} while ($process->isRunning() && !$hasStarted);
$animation->clear();
if (!$process->isRunning()) {
$io->error([
'The dashboard failed to start',
$process->getErrorOutput(),
]);

return 1;
}
$url = "http://$host:$port";
$io->writeln("Dashboard started: <fg=green;options=bold,underscore>$url</>");
OpenUrl::open($url);
$process->wait(function ($type, $buffer) {
if (Process::ERR === $type) {
echo 'ERR > '.$buffer;
} else {
echo 'OUT > '.$buffer;
}
});

return $process->getExitCode();
})->descriptions('Start the dashboard');

$app->command('layers region', function (string $region, SymfonyStyle $io) {
$layers = json_decode(file_get_contents(__DIR__ . '/layers.json'), true);
$io->title("Layers for the $region region");

$array = [];
foreach ($layers as $layer => $versions) {
$version = $versions[$region];
$array[] = [
$layer,
$version,
"arn:aws:lambda:$region:416566615250:layer:$layer:$version",
];
}
$io->table([
'Layer',
'Version',
'ARN',
], $array);

return 0;
})->descriptions('Displays the versions of the Bref layers');
$app = new Application('Deploy serverless PHP applications');
$app->add(new Init());
$app->add(new Cli());
$app->add(new Local());
$app->add(new Dashboard());
$app->add(new Layers());

$app->run();
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"php": ">=7.3.0",
"ext-curl": "*",
"ext-json": "*",
"mnapoli/silly": "^1.7",
"symfony/console": "^3.0|^4.0|^5.0|^6.0",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to support old and unmaintained versions? ^4.4|^5.0|^6.0 should be enough.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just copied the requirement from Silly, and then added ^6.0. I'd suggest to keep support for the old releases for now, and to think about it together with the other Symfony requirements in another issue/PR.

"symfony/filesystem": "^3.1|^4.0|^5.0|^6.0",
"symfony/process": "^4.2|^5.0|^6.0",
"psr/http-message": "^1.0",
Expand Down
60 changes: 39 additions & 21 deletions demo/console.php
Original file line number Diff line number Diff line change
@@ -1,27 +1,45 @@
<?php declare(strict_types=1);

use Silly\Application;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

require __DIR__.'/../vendor/autoload.php';

$silly = new Application;

$silly->command('hello [name]', function (string $name = 'World!', OutputInterface $output) {
$output->writeln('Hello ' . $name);
});
$silly->command('phpinfo', function (OutputInterface $output) {
ob_start();
phpinfo();
$phpinfo = ob_get_clean();
$output->write($phpinfo);
});
$silly->command('error', function (OutputInterface $output) {
$output->writeln('There was an error!');
return 1;
});
$silly->command('sleep', function () {
sleep(120);
});

$silly->run();
$app = new Application();

$app->register('hello')
->addArgument('name', InputArgument::OPTIONAL, '', 'World!')
->setCode(function (InputInterface $input, OutputInterface $output): int {
$output->writeln('Hello, ' . $input->getArgument('name'));
return Command::SUCCESS;
})
;

$app->register('phpinfo')
->setCode(function (InputInterface $input, OutputInterface $output): int {
ob_start();
phpinfo();
$phpinfo = ob_get_clean();
$output->write($phpinfo);
return Command::SUCCESS;
})
;

$app->register('error')
->setCode(function (InputInterface $input, OutputInterface $output): int {
$output->writeln('There was an error!');
return Command::FAILURE;
})
;

$app->register('sleep')
->setCode(function (InputInterface $input, OutputInterface $output): int {
sleep(120);
return Command::SUCCESS;
})
;

$app->run();
Loading