Skip to content

Commit

Permalink
Draft of CLI deployment
Browse files Browse the repository at this point in the history
  • Loading branch information
deleugpn committed Oct 31, 2024
1 parent 5f9ee3c commit aaa3696
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 8 deletions.
42 changes: 41 additions & 1 deletion src/BrefCloudClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,44 @@ public function addAwsAccount(mixed $teamId, string $accountName, string $roleAr
],
]);
}
}

public function uploadSourceCodeToS3(string $hash, string $path, $team): string
{
$signedUrl = $this->retrieveS3SignedUrlForCodeUpload($team, $hash);

if (isset($signedUrl['uploadUrl'], $signedUrl['uploadHeaders'])) {
HttpClient::create()->request(
'PUT',
$signedUrl['uploadUrl'],
[
'headers' => $signedUrl['uploadHeaders'],
'body' => file_get_contents($path),
]
);
}

// @TODO: return this ready-to-go from the deployments/packages API
[$_, $s3Path] = explode('.amazonaws.com/', $signedUrl['downloadUrl']);

// @TODO: the API needs to be compliant with multiple regions and return which bucket was used
return "s3://bref-cloud-staging-storage-kpss5zgu7abr/$s3Path";
}

/**
* @param string $hash
* @return array{uploadUrl: string, uploadHeaders: array<string, string>, downloadUrl: string}
*/
private function retrieveS3SignedUrlForCodeUpload(string $team, string $hash): array
{
$response = $this->client->request('POST', '/api/v1/deployments/packages', [
'body' => [
'hash' => $hash,
'team' => $team,
],
]);

return $response->toArray();
}


}
55 changes: 49 additions & 6 deletions src/Commands/Deploy.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,30 @@ class Deploy extends Command
{
protected function configure(): void
{
ini_set('memory_limit', '512M');

$this
->setName('deploy')
->setDescription('Deploy the application')
->addOption('env', 'e', InputOption::VALUE_REQUIRED, 'The environment to deploy to', 'dev')
->addOption('directory', 'd', InputOption::VALUE_OPTIONAL, 'The directory to deploy', getcwd())
->addOption('force', null, InputOption::VALUE_NONE, 'Force the deployment');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
IO::writeln([Styles::brefHeader(), '']);

$brefCloud = new BrefCloudClient;

/** @var string $environment */
$environment = $input->getOption('env');
$config = Config::loadConfig();

/** @var string $dir */
$dir = $input->getOption('directory');

$config = Config::loadConfig($brefCloud, $dir);

$appName = $config['name'];

IO::writeln([
Expand All @@ -47,13 +57,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int

IO::spin('deploying');

// Upload artifacts
// ...

// Retrieve the current git ref and commit message to serve as a label for the deployment
[$gitRef, $gitMessage] = $this->getGitDetails();

$brefCloud = new BrefCloudClient;
try {
$deployment = $brefCloud->startDeployment($environment, $config, $gitRef, $gitMessage);
} catch (ClientExceptionInterface $e) {
Expand All @@ -70,7 +76,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int
IO::writeln(['', "Environment $appName/$environment does not exist and will be created."]);
$awsAccountName = $this->selectAwsAccount($body['selectAwsAccount']);
IO::spin('deploying');
$deployment = $brefCloud->startDeployment($environment, $config, $gitRef, $gitMessage, $awsAccountName);

// @TODO: DEFINITELY do not like this :|
try {
$deployment = $brefCloud->startDeployment($environment, $config, $gitRef, $gitMessage, $awsAccountName);
} catch (ClientExceptionInterface $e) {
$response = $e->getResponse();
if ($response->getStatusCode() === 400) {
$body = $response->toArray(false);
if (($body['code'] ?? '') === 'no_region_for_environment') {
$region = $this->selectAwsRegion();
IO::spin('deploying');
$config['region'] = $region;
$deployment = $brefCloud->startDeployment($environment, $config, $gitRef, $gitMessage, $awsAccountName, $region);
}
}
}
} else {
IO::spinError();
throw $e;
Expand Down Expand Up @@ -141,6 +162,28 @@ private function selectAwsAccount(array $selectAwsAccount): string
return $awsAccountName;
}


private function selectAwsRegion(): string
{
$region = IO::ask(new ChoiceQuestion(
'Please select the AWS region to deploy to:',
[
'us-east-1',
'us-east-2',
'eu-west-1',
'eu-west-2',
'eu-west-3',
// @TODO: regions
]
));

if (! is_string($region)) {
throw new Exception('No AWS Region selected');
}

return $region;
}

/**
* @return array{string, string}
* @throws BufferException
Expand Down
84 changes: 83 additions & 1 deletion src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,46 @@

use Exception;
use Symfony\Component\Yaml\Yaml;
use ZipArchive;

class Config
{
/**
* @return array{name: string, team: string, type: string}
* @throws Exception
*/
public static function loadConfig(): array
public static function loadConfig(BrefCloudClient $client, string $directory): array
{
if (is_file($directory . 'bref.php')) {
$config = require $directory . 'bref.php';

// @TODO: find a way to bring the Bref\Cloud\Laravel as a dependency to the CLI.
if (! $config instanceof \Bref\Cloud\Laravel) {
throw new Exception('The "bref.php" file must return an instance of \Bref\Cloud\Laravel');
}

// @TODO I don't think this is the best place to put this code, but
// we need access to the entire Laravel object before it gets serialized
// so that we can process it properly.
// The important attributes here are:
// - $path
// - $exclude
// - S3 source code URL
// - @TODO: assets?
[$hash, $path] = self::zipProjectContents($directory, $config);

$s3Path = $client->uploadSourceCodeToS3($hash, $path, $config->team);

$php = $config->path($s3Path)->serialize();

return [
'name' => $config->name,
'team' => $config->team,
'type' => 'laravel',
'php' => $php,
];
}

if (is_file('serverless.yml')) {
$serverlessConfig = self::readYamlFile('serverless.yml');
if (empty($serverlessConfig['service']) || ! is_string($serverlessConfig['service']) || str_contains($serverlessConfig['service'], '$')) {
Expand Down Expand Up @@ -58,4 +89,55 @@ private static function readYamlFile(string $fileName): array
throw new Exception("Cannot parse \"$fileName\": " . $e->getMessage(), 0, $e);
}
}

private static function zipProjectContents(string $directory, \Bref\Cloud\Laravel $config)
{
if (! is_dir($directory . '.bref/')) {
mkdir($directory . '.bref/');
}

$archive = $directory . '.bref/project.zip';

$zip = new ZipArchive;

// @TODO should we generate unique names for each time we go through here to not
// override existing zip files?
$zip->open($archive, ZipArchive::CREATE | ZipArchive::OVERWRITE);

// @TODO: we need to take $config->path into consideration. For now only the entire
// current path is being zipped.
self::addFolderContentsToZipArchive($zip, $directory);

$zip->close();

return [hash_file('sha256', $archive), $archive];
}

private static function addFolderContentsToZipArchive(ZipArchive $zip, $rootDirectory, $subfolder = ''): void
{
$contents = scandir($rootDirectory . $subfolder);

foreach ($contents as $content) {
if (in_array($content, ['.', '..', '.bref', '.git', '.idea'])) {
continue;
}

$relativePath = $subfolder . $content;

$absolutePath = $rootDirectory . $relativePath;

// @TODO: work out exclude logic that has to consider wildcard caracters
// such as `node_modules/**` or `tests/**/*.php`


if (is_dir($absolutePath)) {
self::addFolderContentsToZipArchive($zip, $rootDirectory, $relativePath . DIRECTORY_SEPARATOR);
} elseif (is_file($absolutePath)) {
$zip->addFile($absolutePath, $relativePath);
} else {
throw new Exception('Invalid path: ' . $absolutePath);
}
}
}

}

0 comments on commit aaa3696

Please sign in to comment.