diff --git a/README.md b/README.md index 04a2e27..ad94aec 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,9 @@ For example, if we were wanting to use Node on Lambda to generate an og:image fo ```php namespace App\Sidecar; -use Hammerstone\Sidecar\LambdaFunction; +use Hammerstone\Sidecar\ServerlessFunction; -class OgImage extends LambdaFunction +class OgImage extends ServerlessFunction { public function handler() { diff --git a/config/sidecar.php b/config/sidecar.php index 23ce2a3..283534b 100644 --- a/config/sidecar.php +++ b/config/sidecar.php @@ -73,4 +73,38 @@ * See CreateExecutionRole::policy for the IAM policy. */ 'execution_role' => env('SIDECAR_EXECUTION_ROLE'), + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * If you're not using Vercel, you can skip this whole section. You can * + * edit this directly or use `php artisan sidecar:configure --vercel`. * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + + /* + * Your Vercel API token. If you're not using Vercel to deploy + * your functions, you won't need this. You can generate + * a token at https://vercel.com/account/tokens. + */ + 'vercel_token' => env('SIDECAR_VERCEL_TOKEN'), + + /* + * This is a random string used to generate unique, determinative + * domain names on Vercel. It is not used for security in any + * way. It should be 16 random alphanumeric characters. + */ + 'vercel_domain_seed' => env('SIDECAR_VERCEL_DOMAIN_SEED'), + + /* + * This is the secret token that Sidecar uses to sign outgoing function + * invocations. The same secret will be used to validate the requests + * on Vercel. You need to redeploy your functions if this changes! + */ + 'vercel_signing_secret' => env('SIDECAR_VERCEL_SIGNING_SECRET'), + + /* + * If you are a part of a team, you can deploy your functions into + * that team specifically by supplying the team ID here. + */ + 'vercel_team' => env('SIDECAR_VERCEL_TEAM'), ]; diff --git a/docs/functions/handlers-and-packages.md b/docs/functions/handlers-and-packages.md index f5a5154..d05f85b 100644 --- a/docs/functions/handlers-and-packages.md +++ b/docs/functions/handlers-and-packages.md @@ -7,12 +7,12 @@ Every Lambda function requires at least two things: - the name of handler function - the file or files needed to execute that handler function -Because these two things are required, these are the two abstract methods that you must implement in your Sidecar function class. +Because these two things are required, these are the two abstract methods that you must implement in your Sidecar function class. ```php -use Hammerstone\Sidecar\LambdaFunction; +use Hammerstone\Sidecar\ServerlessFunction; -class ExampleFunction extends LambdaFunction +class ExampleFunction extends ServerlessFunction { public function handler() { @@ -407,10 +407,10 @@ To use a container image with Sidecar you must first build a Lambda compatible d Once the container has been added to the registry update the Sidecar function's handler method to return the `Package::CONTAINER_HANDLER` constant. Finally, update the function's `package` method to return the ECR Image URI as shown below. ```php -use Hammerstone\Sidecar\LambdaFunction; +use Hammerstone\Sidecar\ServerlessFunction; use Hammerstone\Sidecar\Package; -class ExampleFunction extends LambdaFunction +class ExampleFunction extends ServerlessFunction { public function handler() { diff --git a/docs/overview.md b/docs/overview.md index 8045608..728ca7b 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -34,12 +34,13 @@ Every Sidecar Function requires two things: For example, if you want to use Node on Lambda to generate an og:image for all of your blog posts, you would first set up a simple class in PHP called e.g. `OgImage`. App\Sidecar\OgImage.php {.filename} + ```php namespace App\Sidecar; -use Hammerstone\Sidecar\LambdaFunction; +use Hammerstone\Sidecar\ServerlessFunction; -class OgImage extends LambdaFunction +class OgImage extends ServerlessFunction { public function handler() { diff --git a/src/Clients/LambdaClient.php b/src/Clients/LambdaClient.php index d92d31f..94467a5 100644 --- a/src/Clients/LambdaClient.php +++ b/src/Clients/LambdaClient.php @@ -9,22 +9,19 @@ use Aws\Lambda\LambdaClient as BaseClient; use Aws\Result; use Exception; -use Hammerstone\Sidecar\LambdaFunction; +use Hammerstone\Sidecar\Contracts\FaasClient; +use Hammerstone\Sidecar\ServerlessFunction; use Hammerstone\Sidecar\Sidecar; use Illuminate\Support\Arr; use Illuminate\Support\Str; -class LambdaClient extends BaseClient +class LambdaClient extends BaseClient implements FaasClient { - const CREATED = 1; - const UPDATED = 2; - const NOOP = 3; - /** - * @param LambdaFunction $function + * @param ServerlessFunction $function * @return string */ - public function getLatestVersion(LambdaFunction $function) + public function getLatestVersion(ServerlessFunction $function) { return last($this->getVersions($function))['Version']; } @@ -32,11 +29,11 @@ public function getLatestVersion(LambdaFunction $function) /** * Test whether the latest deployed version is the one that is aliased. * - * @param LambdaFunction $function + * @param ServerlessFunction $function * @param $alias * @return bool */ - public function latestVersionHasAlias(LambdaFunction $function, $alias) + public function latestVersionHasAlias(ServerlessFunction $function, $alias) { $version = $this->getLatestVersion($function); @@ -46,11 +43,11 @@ public function latestVersionHasAlias(LambdaFunction $function, $alias) } /** - * @param LambdaFunction $function + * @param ServerlessFunction $function * @param null|string $marker * @return \Aws\Result */ - public function getVersions(LambdaFunction $function, $marker = null) + public function getVersions(ServerlessFunction $function, $marker = null) { $result = $this->listVersionsByFunction([ 'FunctionName' => $function->nameWithPrefix(), @@ -68,12 +65,12 @@ public function getVersions(LambdaFunction $function, $marker = null) } /** - * @param LambdaFunction $function + * @param ServerlessFunction $function * @param string $alias * @param string|null $version * @return int */ - public function aliasVersion(LambdaFunction $function, $alias, $version = null) + public function aliasVersion(ServerlessFunction $function, $alias, $version = null) { $version = $version ?? $this->getLatestVersion($function); @@ -81,7 +78,7 @@ public function aliasVersion(LambdaFunction $function, $alias, $version = null) // The alias already exists and it's the version we were trying to alias anyway. if ($aliased && $version === Arr::get($aliased, 'FunctionVersion')) { - return self::NOOP; + return FaasClient::NOOP; } $args = [ @@ -93,20 +90,20 @@ public function aliasVersion(LambdaFunction $function, $alias, $version = null) if ($aliased) { $this->updateAlias($args); - return self::UPDATED; + return FaasClient::UPDATED; } $this->createAlias($args); - return self::CREATED; + return FaasClient::CREATED; } /** - * @param LambdaFunction $function + * @param ServerlessFunction $function * @param $name * @return \Aws\Result|false */ - public function getAliasWithoutException(LambdaFunction $function, $name) + public function getAliasWithoutException(ServerlessFunction $function, $name) { try { return $this->getAlias([ @@ -123,12 +120,22 @@ public function getAliasWithoutException(LambdaFunction $function, $name) } /** - * @param LambdaFunction $function + * @param ServerlessFunction $function + * + * @throws \Hammerstone\Sidecar\Exceptions\SidecarException + */ + public function createNewFunction(ServerlessFunction $function) + { + $this->createFunction($function->toDeploymentArray()); + } + + /** + * @param ServerlessFunction $function * @return int * * @throws Exception */ - public function updateExistingFunction(LambdaFunction $function) + public function updateExistingFunction(ServerlessFunction $function) { $config = $function->toDeploymentArray(); @@ -138,7 +145,7 @@ public function updateExistingFunction(LambdaFunction $function) // See if the function already exists with these *exact* parameters. if ($this->functionExists($function, $checksum)) { - return self::NOOP; + return FaasClient::NOOP; } // Add the checksum to the description, so we can look for it next time. @@ -167,7 +174,7 @@ public function updateExistingFunction(LambdaFunction $function) $this->updateFunctionCode($code); } - public function updateFunctionVariables(LambdaFunction $function) + public function updateFunctionVariables(ServerlessFunction $function) { $variables = $function->variables(); @@ -219,9 +226,9 @@ public function updateFunctionVariables(LambdaFunction $function) * @link https://github.com/hammerstonedev/sidecar/issues/32 * @link https://github.com/aws/aws-sdk-php/blob/master/src/data/lambda/2015-03-31/waiters-2.json * - * @param LambdaFunction $function + * @param ServerlessFunction $function */ - public function waitUntilFunctionUpdated(LambdaFunction $function) + public function waitUntilFunctionUpdated(ServerlessFunction $function) { $this->waitUntil('FunctionUpdated', [ 'FunctionName' => $function->nameWithPrefix(), @@ -231,10 +238,10 @@ public function waitUntilFunctionUpdated(LambdaFunction $function) /** * Delete a particular version of a function. * - * @param LambdaFunction $function + * @param ServerlessFunction $function * @param string $version */ - public function deleteFunctionVersion(LambdaFunction $function, $version) + public function deleteFunctionVersion(ServerlessFunction $function, $version) { $this->deleteFunction([ 'FunctionName' => $function->nameWithPrefix(), @@ -243,11 +250,11 @@ public function deleteFunctionVersion(LambdaFunction $function, $version) } /** - * @param LambdaFunction $function + * @param ServerlessFunction $function * @param null $checksum * @return bool */ - public function functionExists(LambdaFunction $function, $checksum = null) + public function functionExists(ServerlessFunction $function, $checksum = null) { try { $response = $this->getFunction([ diff --git a/src/Commands/Configurators/ConfigureVercel.php b/src/Commands/Configurators/ConfigureVercel.php new file mode 100644 index 0000000..abef83c --- /dev/null +++ b/src/Commands/Configurators/ConfigureVercel.php @@ -0,0 +1,69 @@ + + */ + +namespace Hammerstone\Sidecar\Commands\Configurators; + +use Hammerstone\Sidecar\Vercel\Client; +use Illuminate\Support\Str; + +trait ConfigureVercel +{ + public function configureVercel() + { + $this->line(str_repeat('-', $this->width)); + $this->text('This interactive command will set up your Sidecar credentials for Vercel.'); + $this->line(''); + $this->text("The first thing you'll need is a Vercel token, which you can generate here:"); + $this->text('https://vercel.com/account/tokens'); + $this->line(str_repeat('-', $this->width)); + $this->line(''); + + $token = $this->secret('Paste your Vercel token, or press enter to skip'); + $team = $token ? $this->selectTeam($token) : ''; + + $this->line(' '); + $this->info('Done! Here are your environment variables:'); + $this->line('SIDECAR_VERCEL_TOKEN=' . $token); + $this->line('SIDECAR_VERCEL_TEAM=' . $team); + $this->line('SIDECAR_VERCEL_DOMAIN_SEED=' . Str::random(16)); + $this->line('SIDECAR_VERCEL_SIGNING_SECRET=' . Str::random(40)); + $this->line(' '); + $this->info('They will work in any environment.'); + } + + protected function selectTeam($token) + { + $vercel = new Client([ + 'base_uri' => 'https://api.vercel.com', + 'allow_redirects' => true, + 'headers' => [ + 'Authorization' => 'Bearer ' . $token, + ] + ]); + + $teams = collect($vercel->listTeams()['teams'])->mapWithKeys(function ($team) { + return [$team['id'] => $team['name']]; + }); + + if (!count($teams)) { + return; + } + + $teams['personal'] = 'Personal Account'; + + $team = $this->choice( + 'You are a part of one or more teams. Where would you like your functions deployed?', + $teams->values()->toArray() + ); + + $id = $teams->flip()[$team]; + + if ($id === 'personal') { + return; + } + + return $id; + } +} diff --git a/src/Commands/Configure.php b/src/Commands/Configure.php index e2593ee..7a6097c 100644 --- a/src/Commands/Configure.php +++ b/src/Commands/Configure.php @@ -11,24 +11,27 @@ use Hammerstone\Sidecar\Commands\Actions\CreateExecutionRole; use Hammerstone\Sidecar\Commands\Actions\DestroyAdminKeys; use Hammerstone\Sidecar\Commands\Actions\DetermineRegion; +use Hammerstone\Sidecar\Commands\Configurators\ConfigureVercel; use Illuminate\Console\Command; use Throwable; class Configure extends Command { + use ConfigureVercel; + /** * The name and signature of the console command. * * @var string */ - protected $signature = 'sidecar:configure'; + protected $signature = 'sidecar:configure {--vercel}'; /** * The console command description. * * @var string */ - protected $description = 'Interactively configure your Sidecar AWS environment variables.'; + protected $description = 'Interactively configure your Sidecar environment variables.'; /** * @var string @@ -56,6 +59,12 @@ class Configure extends Command */ public function handle() { + if ($this->option('vercel')) { + return $this->configureVercel(); + } + + // @TODO Factor AWS configuration out to a trait. + $this->askForAdminCredentials(); $this->region = $this->action(DetermineRegion::class)->invoke(); diff --git a/src/Concerns/ExecutionMethods.php b/src/Concerns/ExecutionMethods.php new file mode 100644 index 0000000..30ba8f2 --- /dev/null +++ b/src/Concerns/ExecutionMethods.php @@ -0,0 +1,74 @@ + + */ + +namespace Hammerstone\Sidecar\Concerns; + +use Hammerstone\Sidecar\Results\PendingResult; +use Hammerstone\Sidecar\Results\SettledResult; +use Hammerstone\Sidecar\Sidecar; + +trait ExecutionMethods +{ + /** + * Execute the current function and return the response. + * + * @param array $payload + * @param bool $async + * @return SettledResult|PendingResult + */ + public static function execute($payload = [], $async = false, $invocationType = 'RequestResponse') + { + return Sidecar::execute(static::class, $payload, $async, $invocationType); + } + + /** + * Execute the current function and return the response. + * + * @param array $payload + * @return PendingResult + */ + public static function executeAsync($payload = []) + { + return static::execute($payload, $async = true); + } + + /** + * Execute the current function and return the response. + * + * @param $payloads + * @param bool $async + * @return array + * + * @throws \Throwable + */ + public static function executeMany($payloads, $async = false) + { + return Sidecar::executeMany(static::class, $payloads, $async); + } + + /** + * Execute the current function and return the response. + * + * @param $payloads + * @return array + * + * @throws \Throwable + */ + public static function executeManyAsync($payloads) + { + return static::executeMany($payloads, $async = true); + } + + /** + * Execute the current function asynchronously as an event. This is "fire-and-forget" style. + * + * @param array $payload + * @return PendingResult + */ + public static function executeAsEvent($payload = []) + { + return static::execute($payload, $async = false, $invocationType = 'Event'); + } +} diff --git a/src/Concerns/HandlesLogging.php b/src/Concerns/HandlesLogging.php index 5912732..40cf6b1 100644 --- a/src/Concerns/HandlesLogging.php +++ b/src/Concerns/HandlesLogging.php @@ -45,6 +45,13 @@ public function addCommandLogger(Command $command) }); } + public function addPhpUnitLogger() + { + $this->addLogger(function ($message, $level = 'info') { + fwrite(STDERR, "\n" . $message); + }); + } + /** * @param $message */ diff --git a/src/Contracts/FaasClient.php b/src/Contracts/FaasClient.php new file mode 100644 index 0000000..6f8a7ab --- /dev/null +++ b/src/Contracts/FaasClient.php @@ -0,0 +1,78 @@ + + */ + +namespace Hammerstone\Sidecar\Contracts; + +use Hammerstone\Sidecar\ServerlessFunction; + +interface FaasClient +{ + const CREATED = 1; + const UPDATED = 2; + const NOOP = 3; + + /** + * @param ServerlessFunction $function + * @param null $checksum + * @return bool + */ + public function functionExists(ServerlessFunction $function, $checksum = null); + + public function createNewFunction(ServerlessFunction $function); + + /** + * @param ServerlessFunction $function + * @return int + * + * @throws Exception + */ + public function updateExistingFunction(ServerlessFunction $function); + + public function updateFunctionVariables(ServerlessFunction $function); + + public function waitUntilFunctionUpdated(ServerlessFunction $function); + + /** + * Test whether the latest deployed version is the one that is aliased. + * + * @param ServerlessFunction $function + * @param $alias + * @return bool + */ + public function latestVersionHasAlias(ServerlessFunction $function, $alias); + + /** + * @param ServerlessFunction $function + * @return string + */ + public function getLatestVersion(ServerlessFunction $function); + + /** + * @param ServerlessFunction $function + * @param string $alias + * @param string|null $version + * @return int + */ + public function aliasVersion(ServerlessFunction $function, $alias, $version = null); + + /** + * @param ServerlessFunction $function + * @param null|string $marker + * @return \Aws\Result + */ + public function getVersions(ServerlessFunction $function, $marker = null); + + /** + * Delete a particular version of a function. + * + * @param ServerlessFunction $function + * @param string $version + */ + public function deleteFunctionVersion(ServerlessFunction $function, $version); + + public function invoke(array $args = []); + + public function invokeAsync(array $args = []); +} diff --git a/src/Deployment.php b/src/Deployment.php index aed639b..5666b25 100644 --- a/src/Deployment.php +++ b/src/Deployment.php @@ -7,11 +7,13 @@ use Exception; use Hammerstone\Sidecar\Clients\LambdaClient; +use Hammerstone\Sidecar\Contracts\FaasClient; use Hammerstone\Sidecar\Events\AfterFunctionsActivated; use Hammerstone\Sidecar\Events\AfterFunctionsDeployed; use Hammerstone\Sidecar\Events\BeforeFunctionsActivated; use Hammerstone\Sidecar\Events\BeforeFunctionsDeployed; use Hammerstone\Sidecar\Exceptions\NoFunctionsRegisteredException; +use Hammerstone\Sidecar\Vercel\Client; class Deployment { @@ -21,9 +23,9 @@ class Deployment protected $functions; /** - * @var LambdaClient + * @var FaasClient */ - protected $lambda; + protected $client; /** * @param $functions @@ -43,7 +45,8 @@ public static function make($functions = null) */ public function __construct($functions = null) { - $this->lambda = app(LambdaClient::class); +// $this->client = app(LambdaClient::class); + $this->client = app(Client::class); $this->functions = Sidecar::instantiatedFunctions($functions); @@ -65,7 +68,8 @@ public function deploy() event(new BeforeFunctionsDeployed($this->functions)); foreach ($this->functions as $function) { - Sidecar::log('Deploying ' . get_class($function) . ' to Lambda as `' . $function->nameWithPrefix() . '`.'); + // @TODO insert platform name + Sidecar::log('Deploying ' . get_class($function) . ' as `' . $function->nameWithPrefix() . '`.'); Sidecar::sublog(function () use ($function) { $this->deploySingle($function); @@ -101,11 +105,11 @@ public function activate($prewarm = false) } /** - * @param LambdaFunction $function + * @param ServerlessFunction $function * * @throws Exception */ - protected function deploySingle(LambdaFunction $function) + protected function deploySingle(ServerlessFunction $function) { Sidecar::log('Environment: ' . Sidecar::getEnvironment()); Sidecar::log('Package Type: ' . $function->packageType()); @@ -115,7 +119,7 @@ protected function deploySingle(LambdaFunction $function) $function->beforeDeployment(); - $this->lambda->functionExists($function) + $this->client->functionExists($function) ? $this->updateExistingFunction($function) : $this->createNewFunction($function); @@ -123,10 +127,10 @@ protected function deploySingle(LambdaFunction $function) } /** - * @param LambdaFunction $function + * @param ServerlessFunction $function * @param bool $prewarm */ - protected function activateSingle(LambdaFunction $function, $prewarm) + protected function activateSingle(ServerlessFunction $function, $prewarm) { $function->beforeActivation(); @@ -142,27 +146,27 @@ protected function activateSingle(LambdaFunction $function, $prewarm) } /** - * @param LambdaFunction $function + * @param ServerlessFunction $function * * @throws Exception */ - protected function createNewFunction(LambdaFunction $function) + protected function createNewFunction(ServerlessFunction $function) { Sidecar::log('Creating new lambda function.'); - $this->lambda->createFunction($function->toDeploymentArray()); + $this->client->createNewFunction($function); } /** - * @param LambdaFunction $function + * @param ServerlessFunction $function * * @throws Exception */ - protected function updateExistingFunction(LambdaFunction $function) + protected function updateExistingFunction(ServerlessFunction $function) { Sidecar::log('Function already exists, potentially updating code and configuration.'); - if ($this->lambda->updateExistingFunction($function) === LambdaClient::NOOP) { + if ($this->client->updateExistingFunction($function) === FaasClient::NOOP) { Sidecar::log('Function code and configuration are unchanged. Not updating anything.'); } else { Sidecar::log('Function code and configuration updated.'); @@ -170,35 +174,35 @@ protected function updateExistingFunction(LambdaFunction $function) } /** - * Add environment variables to the Lambda function, if they are provided. + * Add environment variables to the function, if they are provided. * - * @param LambdaFunction $function + * @param ServerlessFunction $function */ - protected function setEnvironmentVariables(LambdaFunction $function) + protected function setEnvironmentVariables(ServerlessFunction $function) { if (!is_array($function->variables())) { return Sidecar::log('Environment variables not managed by Sidecar. Skipping.'); } - $this->lambda->updateFunctionVariables($function); + $this->client->updateFunctionVariables($function); } /** * Send warming requests to the latest version. * - * @param LambdaFunction $function + * @param ServerlessFunction $function */ - protected function warmLatestVersion(LambdaFunction $function) + protected function warmLatestVersion(ServerlessFunction $function) { - $this->lambda->waitUntilFunctionUpdated($function); + $this->client->waitUntilFunctionUpdated($function); - if ($this->lambda->latestVersionHasAlias($function, 'active')) { + if ($this->client->latestVersionHasAlias($function, 'active')) { Sidecar::log('Active version unchanged, no need to warm.'); return; } - $version = $this->lambda->getLatestVersion($function); + $version = $this->client->getLatestVersion($function); Sidecar::log("Warming Version $version of {$function->nameWithPrefix()}..."); @@ -217,17 +221,17 @@ protected function warmLatestVersion(LambdaFunction $function) /** * Alias the latest version of a function as the "active" one. * - * @param LambdaFunction $function + * @param ServerlessFunction $function */ - protected function aliasLatestVersion(LambdaFunction $function) + protected function aliasLatestVersion(ServerlessFunction $function) { - $version = $this->lambda->getLatestVersion($function); - $result = $this->lambda->aliasVersion($function, 'active', $version); + $version = $this->client->getLatestVersion($function); + $result = $this->client->aliasVersion($function, 'active', $version); $messages = [ - LambdaClient::CREATED => "Creating alias for Version $version of {$function->nameWithPrefix()}.", - LambdaClient::UPDATED => "Activating Version $version of {$function->nameWithPrefix()}.", - LambdaClient::NOOP => "Version $version of {$function->nameWithPrefix()} is already active.", + FaasClient::CREATED => "Creating alias for Version $version of {$function->nameWithPrefix()}.", + FaasClient::UPDATED => "Activating Version $version of {$function->nameWithPrefix()}.", + FaasClient::NOOP => "Version $version of {$function->nameWithPrefix()} is already active.", ]; Sidecar::log($messages[$result]); @@ -236,11 +240,11 @@ protected function aliasLatestVersion(LambdaFunction $function) /** * Remove old, outdated versions of a function. * - * @param LambdaFunction $function + * @param ServerlessFunction $function */ - protected function sweep(LambdaFunction $function) + protected function sweep(ServerlessFunction $function) { - $versions = $this->lambda->getVersions($function); + $versions = $this->client->getVersions($function); $keep = 20; @@ -263,7 +267,7 @@ protected function sweep(LambdaFunction $function) $version = $version['Version']; Sidecar::log("Removing outdated version $version."); - $this->lambda->deleteFunctionVersion($function, $version); + $this->client->deleteFunctionVersion($function, $version); } } } diff --git a/src/Events/AfterFunctionExecuted.php b/src/Events/AfterFunctionExecuted.php index 7b39982..987f6b0 100644 --- a/src/Events/AfterFunctionExecuted.php +++ b/src/Events/AfterFunctionExecuted.php @@ -5,14 +5,14 @@ namespace Hammerstone\Sidecar\Events; -use Hammerstone\Sidecar\LambdaFunction; use Hammerstone\Sidecar\Results\PendingResult; use Hammerstone\Sidecar\Results\SettledResult; +use Hammerstone\Sidecar\ServerlessFunction; class AfterFunctionExecuted { /** - * @var LambdaFunction + * @var ServerlessFunction */ public $function; diff --git a/src/Events/BeforeFunctionExecuted.php b/src/Events/BeforeFunctionExecuted.php index 122b380..09a6dec 100644 --- a/src/Events/BeforeFunctionExecuted.php +++ b/src/Events/BeforeFunctionExecuted.php @@ -5,12 +5,12 @@ namespace Hammerstone\Sidecar\Events; -use Hammerstone\Sidecar\LambdaFunction; +use Hammerstone\Sidecar\ServerlessFunction; class BeforeFunctionExecuted { /** - * @var LambdaFunction + * @var ServerlessFunction */ public $function; diff --git a/src/Exceptions/FunctionNotFoundException.php b/src/Exceptions/FunctionNotFoundException.php index 008dd44..a5f8e0c 100644 --- a/src/Exceptions/FunctionNotFoundException.php +++ b/src/Exceptions/FunctionNotFoundException.php @@ -5,12 +5,12 @@ namespace Hammerstone\Sidecar\Exceptions; -use Hammerstone\Sidecar\LambdaFunction; +use Hammerstone\Sidecar\ServerlessFunction; use Hammerstone\Sidecar\Sidecar; class FunctionNotFoundException extends SidecarException { - public static function make(LambdaFunction $function) + public static function make(ServerlessFunction $function) { $env = Sidecar::getEnvironment(); diff --git a/src/LambdaFunction.php b/src/LambdaFunction.php index 948e9ba..176379c 100644 --- a/src/LambdaFunction.php +++ b/src/LambdaFunction.php @@ -5,395 +5,11 @@ namespace Hammerstone\Sidecar; -use Aws\Result; -use GuzzleHttp\Promise\PromiseInterface; -use Hammerstone\Sidecar\Exceptions\SidecarException; -use Hammerstone\Sidecar\Results\PendingResult; -use Hammerstone\Sidecar\Results\SettledResult; -use Illuminate\Support\Arr; -use Illuminate\Support\Str; - -abstract class LambdaFunction +/** + * @see ServerlessFunction + * @deprecated Use ServerlessFunction instead. + */ +abstract class LambdaFunction extends ServerlessFunction { - /** - * Execute the current function and return the response. - * - * @param array $payload - * @param bool $async - * @return SettledResult|PendingResult - */ - public static function execute($payload = [], $async = false, $invocationType = 'RequestResponse') - { - return Sidecar::execute(static::class, $payload, $async, $invocationType); - } - - /** - * Execute the current function and return the response. - * - * @param array $payload - * @return PendingResult - */ - public static function executeAsync($payload = []) - { - return static::execute($payload, $async = true); - } - - /** - * Execute the current function and return the response. - * - * @param $payloads - * @param bool $async - * @return array - * - * @throws \Throwable - */ - public static function executeMany($payloads, $async = false) - { - return Sidecar::executeMany(static::class, $payloads, $async); - } - - /** - * Execute the current function and return the response. - * - * @param $payloads - * @return array - * - * @throws \Throwable - */ - public static function executeManyAsync($payloads) - { - return static::executeMany($payloads, $async = true); - } - - /** - * Execute the current function asynchronously as an event. This is "fire-and-forget" style. - * - * @param array $payload - * @return PendingResult - */ - public static function executeAsEvent($payload = []) - { - return static::execute($payload, $async = false, $invocationType = 'Event'); - } - - /** - * Deploy this function only. - * - * @param bool $activate - */ - public static function deploy($activate = true) - { - $deployment = Deployment::make(static::class)->deploy(); - - if ($activate) { - $deployment->activate(); - } - } - - /** - * Used by Lambda to uniquely identify a function. - * - * @return string - */ - public function name() - { - return Str::replaceFirst('App-', '', str_replace('\\', '-', static::class)); - } - - /** - * Used by Sidecar to differentiate between apps and environments. - * - * @return string - */ - public function prefix() - { - return 'SC-' . config('app.name') . '-' . Sidecar::getEnvironment() . '-'; - } - - /** - * Function name, including a prefix to differentiate between apps. - * - * @return string - */ - public function nameWithPrefix() - { - $prefix = $this->prefix(); - - // Names can only be 64 characters long. - $name = $prefix . substr($this->name(), -(64 - strlen($prefix))); - - return str_replace(' ', '-', $name); - } - - /** - * Not used by Sidecar at all, use as you see fit. - * - * @return string - */ - public function description() - { - return sprintf('%s [%s]: Sidecar function `%s`.', ...[ - config('app.name'), - config('app.env'), - static::class, - ]); - } - - /** - * A warming configuration that can help mitigate against - * the Lambda "Cold Boot" problem. - * - * @return WarmingConfig - */ - public function warmingConfig() - { - return new WarmingConfig; - } - - /** - * The default representation of this function as an HTTP response. - * - * @param $request - * @param SettledResult $result - * @return \Illuminate\Http\Response - * - * @throws \Exception - */ - public function toResponse($request, SettledResult $result) - { - $result->throw(); - - return response($result->body()); - } - - /** - * @param Result|PromiseInterface $raw - * @return SettledResult|PendingResult - * - * @throws SidecarException - */ - public function toResult($raw) - { - if ($raw instanceof Result) { - return $this->toSettledResult($raw); - } - - if ($raw instanceof PromiseInterface) { - return $this->toPendingResult($raw); - } - - throw new SidecarException('Unable to determine Result for class ' . json_encode(get_class($raw))); - } - - /** - * @param Result $raw - * @return SettledResult - */ - public function toSettledResult(Result $raw) - { - return new SettledResult($raw, $this); - } - - /** - * @param PromiseInterface $raw - * @return PendingResult - */ - public function toPendingResult(PromiseInterface $raw) - { - return new PendingResult($raw, $this); - } - - /** - * The runtime environment for the Lambda function. - * - * @see https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html - * - * @return string - */ - public function runtime() - { - return Runtime::NODEJS_14; - } - - /** - * The type of deployment package. Set to Image for container - * image and set Zip for .zip file archive. - * - * @see https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-lambda-2015-03-31.html#createfunction - * - * @return string - */ - public function packageType() - { - return $this->handler() === Package::CONTAINER_HANDLER ? 'Image' : 'Zip'; - } - - /** - * An array full of ARN strings. Totally optional. - * - * @return array - */ - public function layers() - { - return [ - // 'arn:aws:lambda:us-east-1:XXX:layer:XXX:1', - ]; - } - - /** - * A key/value array of environment variables that will be injected into the - * environment of the Lambda function. If Sidecar manages your environment - * variables, it will overwrite all variables that you set through the - * AWS UI. Return false to disable. - * - * @return bool|array - */ - public function variables() - { - // By default, Sidecar does not manage your environment variables. - return false; - } - - /** - * The function within your code that Lambda calls to begin execution. - * For Node.js, it is the `module-name.export` value in your function. - * - * For example, if your file is named "image.js" and in that file you have - * an "exports.generate" function, your handler would be "image.generate". - * - * If your file lived in a folder called "lambda", you can just prepend the - * path to your handler, leaving you with e.g. "lambda/image.generate". - * - * @see https://hammerstone.dev/sidecar/docs/main/functions/handlers-and-packages - * @see https://docs.aws.amazon.com/aws-sdk-php/v2/api/class-Aws.Lambda.LambdaClient.html#_createFunction - * - * @return string - */ - abstract public function handler(); - - /** - * All the directories and files needed to run your function. - * - * @return Package - */ - abstract public function package(); - - /** - * The amount of memory, in MB, your Lambda function is given. Lambda uses this - * memory size to infer the amount of CPU and memory allocated to your function. - * Your function use-case determines your CPU and memory requirements. - * - * @return int - */ - public function memory() - { - return config('sidecar.memory'); - } - - /** - * The function execution time, in MS, at which Lambda should terminate the function. - * Because the execution time has cost implications, we recommend you set this - * value based on your expected execution time. - * - * @return int - */ - public function timeout() - { - return config('sidecar.timeout'); - } - - public function preparePayload($payload) - { - return $payload; - } - - public function beforeDeployment() - { - // - } - - public function afterDeployment() - { - // - } - - public function beforeActivation() - { - // - } - - public function afterActivation() - { - // - } - - public function beforeExecution($payload) - { - // - } - - public function afterExecution($payload, $result) - { - // - } - - /** - * @return array - */ - public function normalizedHandler() - { - $handler = $this->handler(); - - // Allow an at-sign to separate the file and function. This - // matches the Laravel ecosystem better: `image@handler` - // and `image.handler` will work the exact same way. - if (is_string($handler) && Str::contains($handler, '@')) { - $handler = Str::replace('@', '.', $handler); - } - - return $handler; - } - - /** - * @return Package - */ - public function makePackage() - { - $package = $this->package(); - - return is_array($package) ? Package::make($package) : $package; - } - - /** - * @return array - * - * @throws SidecarException - */ - public function toDeploymentArray() - { - $config = [ - 'FunctionName' => $this->nameWithPrefix(), - 'Runtime' => $this->runtime(), - 'Role' => config('sidecar.execution_role'), - 'Handler' => $this->normalizedHandler(), - 'Code' => $this->packageType() === 'Zip' - ? $this->makePackage()->deploymentConfiguration() - : $this->package(), - 'Description' => $this->description(), - 'Timeout' => (int)$this->timeout(), - 'MemorySize' => (int)$this->memory(), - 'Layers' => $this->layers(), - 'Publish' => true, - 'PackageType' => $this->packageType(), - ]; - - // For container image packages, we need to remove the Runtime - // and Handler since the container handles both of those - // things inherently. - if ($this->packageType() === 'Image') { - $config = Arr::except($config, ['Runtime', 'Handler']); - } - - return $config; - } + // } diff --git a/src/Manager.php b/src/Manager.php index 81070c6..fe7474f 100644 --- a/src/Manager.php +++ b/src/Manager.php @@ -7,7 +7,6 @@ use Aws\Lambda\Exception\LambdaException; use Closure; -use Hammerstone\Sidecar\Clients\LambdaClient; use Hammerstone\Sidecar\Concerns\HandlesLogging; use Hammerstone\Sidecar\Concerns\ManagesEnvironments; use Hammerstone\Sidecar\Events\AfterFunctionExecuted; @@ -15,6 +14,7 @@ use Hammerstone\Sidecar\Exceptions\FunctionNotFoundException; use Hammerstone\Sidecar\Results\PendingResult; use Hammerstone\Sidecar\Results\SettledResult; +use Hammerstone\Sidecar\Vercel\Client; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Arr; use Illuminate\Support\Traits\Macroable; @@ -55,7 +55,7 @@ public function overrideExecutionVersion($version, $callback = null) } /** - * @param string|LambdaFunction $function + * @param string|ServerlessFunction $function * @param array $payload * @param bool $async * @return PendingResult|SettledResult @@ -83,7 +83,7 @@ public function execute($function, $payload = [], $async = false, $invocationTyp $function->beforeExecution($payload); try { - $result = app(LambdaClient::class)->{$method}([ + $result = app(Client::class)->{$method}([ // Function name plus our alias name. 'FunctionName' => $function->nameWithPrefix() . ':' . $this->executionVersion, @@ -208,7 +208,7 @@ public function instantiatedFunctions($functions = null) */ public function warm($functions = null) { - array_map(function (LambdaFunction $function) { + array_map(function (ServerlessFunction $function) { $this->warmSingle($function); }, $this->instantiatedFunctions($functions)); } @@ -216,14 +216,14 @@ public function warm($functions = null) /** * Warm a single function, with the option to override the version. * - * @param LambdaFunction $function + * @param ServerlessFunction $function * @param bool $async * @param string $version * @return array * * @throws Throwable */ - public function warmSingle(LambdaFunction $function, $async = true, $version = 'active') + public function warmSingle(ServerlessFunction $function, $async = true, $version = 'active') { $config = $function->warmingConfig(); diff --git a/src/Package.php b/src/Package.php index be7d7ad..4be83a3 100644 --- a/src/Package.php +++ b/src/Package.php @@ -330,17 +330,53 @@ public function upload() // Stream the zip directly to S3, without it ever touching // a disk anywhere. Important because we might not have // a writeable local disk! + $this->zipInto($path); + + return $filename; + } + + /** + * @return string + * + * @throws \ZipStream\Exception + */ + public function createZip($path = null) + { + Sidecar::log('Packaging files for deployment.'); + + $path = $path ?? tempnam(sys_get_temp_dir(), 'sc-'); + + $this->zipInto($path); + + return $path; + } + + /** + * Create a zip file in a given stream. + * + * @param $path + * @return array + * + * @throws \ZipStream\Exception\FileNotFoundException + * @throws \ZipStream\Exception\FileNotReadableException + * @throws \ZipStream\Exception\OverflowException + */ + public function zipInto($path) + { $stream = fopen($path, 'w'); - $options = new Archive; - $options->setEnableZip64(false); - $options->setOutputStream($stream); + $fileOptions = new FileOptions; + $archiveOptions = new Archive; + $archiveOptions->setEnableZip64(false); + $archiveOptions->setOutputStream($stream); - $zip = new ZipStream($name = null, $options); + $zip = new ZipStream($name = null, $archiveOptions); // Set the time to now so that hashes are // stable during testing. - $options = tap(new FileOptions)->setTime(Carbon::now()); + if (app()->environment('testing')) { + $fileOptions->setTime(Carbon::now()); + } foreach ($this->files() as $file) { // Add the base path so that ZipStream can @@ -350,32 +386,30 @@ public function upload() // Remove the base path so that everything inside // the zip is relative to the project root. $zip->addFileFromPath( - $this->removeBasePath($file), $file, $options + $this->removeBasePath($file), $file, $fileOptions ); } foreach ($this->exactIncludes as $source => $destination) { $zip->addFileFromPath( - $destination, $source, $options + $destination, $source, $fileOptions ); } foreach ($this->stringContents as $destination => $stringContent) { $zip->addFile( - $destination, $stringContent, $options + $destination, $stringContent, $fileOptions ); } $zip->finish(); - $size = fstat($stream)['size'] / 1024 / 1024; - $size = round($size, 2); + $stat = fstat($stream); + $stat['size_mb'] = round($stat['size'] / 1024 / 1024, 2); fclose($stream); - Sidecar::log("Zip file created at $path. ({$size}MB)"); - - return $filename; + Sidecar::log("Zip file created at $path. ({$stat['size_mb']}MB)"); } /** diff --git a/src/Platform.php b/src/Platform.php new file mode 100644 index 0000000..4832858 --- /dev/null +++ b/src/Platform.php @@ -0,0 +1,18 @@ + + */ + +namespace Hammerstone\Sidecar; + +class Platform +{ + const LAMBDA = 'lambda'; + const VERCEL = 'vercel'; + + // const AZURE = 'azure'; + // const GCF = 'gcf'; + // const NETLIFY = 'netlify'; + // const CLOUDFLARE = 'cloudflare'; + // const DIGITAL_OCEAN = 'digital_ocean'; +} diff --git a/src/Providers/SidecarServiceProvider.php b/src/Providers/SidecarServiceProvider.php index 78adbbb..62691d9 100644 --- a/src/Providers/SidecarServiceProvider.php +++ b/src/Providers/SidecarServiceProvider.php @@ -13,6 +13,7 @@ use Hammerstone\Sidecar\Commands\Install; use Hammerstone\Sidecar\Commands\Warm; use Hammerstone\Sidecar\Manager; +use Hammerstone\Sidecar\Vercel\Client as VercelClient; use Illuminate\Support\ServiceProvider; class SidecarServiceProvider extends ServiceProvider @@ -30,6 +31,21 @@ public function register() $this->app->singleton(CloudWatchLogsClient::class, function () { return new CloudWatchLogsClient($this->getAwsClientConfiguration()); }); + + $this->app->singleton(VercelClient::class, function () { + return new VercelClient($this->getVercelConfiguration()); + }); + } + + protected function getVercelConfiguration() + { + return [ + 'base_uri' => 'https://api.vercel.com', + 'allow_redirects' => true, + 'headers' => [ + 'Authorization' => 'Bearer ' . config('sidecar.vercel_token'), + ] + ]; } protected function getAwsClientConfiguration() diff --git a/src/Region.php b/src/Region.php index 17f2e78..d6bc6af 100644 --- a/src/Region.php +++ b/src/Region.php @@ -5,6 +5,8 @@ namespace Hammerstone\Sidecar; +use ReflectionClass; + class Region { const US_EAST_1 = 'us-east-1'; // US East (N. Virginia) @@ -33,6 +35,6 @@ class Region public static function all() { - return (new \ReflectionClass(static::class))->getConstants(); + return (new ReflectionClass(static::class))->getConstants(); } } diff --git a/src/Results/PendingResult.php b/src/Results/PendingResult.php index 3b87d92..4262f4b 100644 --- a/src/Results/PendingResult.php +++ b/src/Results/PendingResult.php @@ -6,7 +6,7 @@ namespace Hammerstone\Sidecar\Results; use GuzzleHttp\Promise\PromiseInterface; -use Hammerstone\Sidecar\LambdaFunction; +use Hammerstone\Sidecar\ServerlessFunction; use Illuminate\Contracts\Support\Responsable; class PendingResult implements Responsable, ResultContract @@ -22,15 +22,15 @@ class PendingResult implements Responsable, ResultContract protected $raw; /** - * @var LambdaFunction + * @var ServerlessFunction */ protected $function; /** * @param PromiseInterface $raw - * @param LambdaFunction $function + * @param ServerlessFunction $function */ - public function __construct($raw, LambdaFunction $function) + public function __construct($raw, ServerlessFunction $function) { $this->raw = $raw; $this->function = $function; diff --git a/src/Results/ResultContract.php b/src/Results/ResultContract.php index 2594af7..c82d53d 100644 --- a/src/Results/ResultContract.php +++ b/src/Results/ResultContract.php @@ -5,15 +5,15 @@ namespace Hammerstone\Sidecar\Results; -use Hammerstone\Sidecar\LambdaFunction; +use Hammerstone\Sidecar\ServerlessFunction; interface ResultContract { /** * @param $raw - * @param LambdaFunction $function + * @param ServerlessFunction $function */ - public function __construct($raw, LambdaFunction $function); + public function __construct($raw, ServerlessFunction $function); /** * @param \Illuminate\Http\Request $request diff --git a/src/Results/SettledResult.php b/src/Results/SettledResult.php index 2b69cfb..5055cf5 100644 --- a/src/Results/SettledResult.php +++ b/src/Results/SettledResult.php @@ -9,7 +9,7 @@ use Carbon\Carbon; use Exception; use Hammerstone\Sidecar\Exceptions\LambdaExecutionException; -use Hammerstone\Sidecar\LambdaFunction; +use Hammerstone\Sidecar\ServerlessFunction; use Illuminate\Contracts\Support\Responsable; use Illuminate\Support\Arr; use Illuminate\Support\Str; @@ -22,7 +22,7 @@ class SettledResult implements Responsable, ResultContract protected $raw; /** - * @var LambdaFunction + * @var ServerlessFunction */ protected $function; @@ -41,7 +41,7 @@ class SettledResult implements Responsable, ResultContract */ protected $logs = []; - public function __construct($raw, LambdaFunction $function) + public function __construct($raw, ServerlessFunction $function) { $this->raw = $raw; $this->function = $function; diff --git a/src/ServerlessFunction.php b/src/ServerlessFunction.php new file mode 100644 index 0000000..f5b6730 --- /dev/null +++ b/src/ServerlessFunction.php @@ -0,0 +1,346 @@ + + */ + +namespace Hammerstone\Sidecar; + +use Aws\Result; +use GuzzleHttp\Promise\PromiseInterface; +use Hammerstone\Sidecar\Concerns\ExecutionMethods; +use Hammerstone\Sidecar\Exceptions\SidecarException; +use Hammerstone\Sidecar\Results\PendingResult; +use Hammerstone\Sidecar\Results\SettledResult; +use Illuminate\Support\Arr; +use Illuminate\Support\Str; + +abstract class ServerlessFunction +{ + use ExecutionMethods; + + /** + * Deploy this function only. + * + * @param bool $activate + */ + public static function deploy($activate = true) + { + $deployment = Deployment::make(static::class)->deploy(); + + if ($activate) { + $deployment->activate(); + } + } + + public static function activate() + { + Deployment::make(static::class)->activate(); + } + + /** + * Used by Lambda to uniquely identify a function. + * + * @return string + */ + public function name() + { + return Str::replaceFirst('App-', '', str_replace('\\', '-', static::class)); + } + + /** + * Used by Sidecar to differentiate between apps and environments. + * + * @return string + */ + public function prefix() + { + return config('sidecar.function_prefix', 'SC-' . config('app.name')) . '-' . Sidecar::getEnvironment() . '-'; + } + + /** + * Function name, including a prefix to differentiate between apps. + * + * @return string + */ + public function nameWithPrefix() + { + $prefix = $this->prefix(); + + // Names can only be 64 characters long. + $name = $prefix . substr($this->name(), -(64 - strlen($prefix))); + + return str_replace(' ', '-', $name); + } + + /** + * Not used by Sidecar at all, use as you see fit. + * + * @return string + */ + public function description() + { + return sprintf('%s [%s]: Sidecar function `%s`.', ...[ + config('app.name'), + config('app.env'), + static::class, + ]); + } + + /** + * A warming configuration that can help mitigate against + * the Lambda "Cold Boot" problem. + * + * @return WarmingConfig + */ + public function warmingConfig() + { + return new WarmingConfig; + } + + /** + * The default representation of this function as an HTTP response. + * + * @param $request + * @param SettledResult $result + * @return \Illuminate\Http\Response + * + * @throws \Exception + */ + public function toResponse($request, SettledResult $result) + { + $result->throw(); + + return response($result->body()); + } + + /** + * @param Result|PromiseInterface $raw + * @return SettledResult|PendingResult + * + * @throws SidecarException + */ + public function toResult($raw) + { + if ($raw instanceof Result) { + return $this->toSettledResult($raw); + } + + if ($raw instanceof PromiseInterface) { + return $this->toPendingResult($raw); + } + + throw new SidecarException('Unable to determine Result for class ' . json_encode(get_class($raw))); + } + + /** + * @param Result $raw + * @return SettledResult + */ + public function toSettledResult(Result $raw) + { + return new SettledResult($raw, $this); + } + + /** + * @param PromiseInterface $raw + * @return PendingResult + */ + public function toPendingResult(PromiseInterface $raw) + { + return new PendingResult($raw, $this); + } + + /** + * The runtime environment for the Lambda function. + * + * @see https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html + * + * @return string + */ + public function runtime() + { + return Runtime::NODEJS_14; + } + + /** + * The type of deployment package. Set to Image for container + * image and set Zip for .zip file archive. + * + * @see https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-lambda-2015-03-31.html#createfunction + * + * @return string + */ + public function packageType() + { + return $this->handler() === Package::CONTAINER_HANDLER ? 'Image' : 'Zip'; + } + + /** + * An array full of ARN strings. Totally optional. + * + * @return array + */ + public function layers() + { + return [ + // 'arn:aws:lambda:us-east-1:XXX:layer:XXX:1', + ]; + } + + /** + * A key/value array of environment variables that will be injected into the + * environment of the Lambda function. If Sidecar manages your environment + * variables, it will overwrite all variables that you set through the + * AWS UI. Return false to disable. + * + * @return bool|array + */ + public function variables() + { + // By default, Sidecar does not manage your environment variables. + return false; + } + + /** + * The function within your code that Lambda calls to begin execution. + * For Node.js, it is the `module-name.export` value in your function. + * + * For example, if your file is named "image.js" and in that file you have + * an "exports.generate" function, your handler would be "image.generate". + * + * If your file lived in a folder called "lambda", you can just prepend the + * path to your handler, leaving you with e.g. "lambda/image.generate". + * + * @see https://hammerstone.dev/sidecar/docs/main/functions/handlers-and-packages + * @see https://docs.aws.amazon.com/aws-sdk-php/v2/api/class-Aws.Lambda.LambdaClient.html#_createFunction + * + * @return string + */ + abstract public function handler(); + + /** + * All the directories and files needed to run your function. + * + * @return Package + */ + abstract public function package(); + + /** + * The amount of memory, in MB, your Lambda function is given. Lambda uses this + * memory size to infer the amount of CPU and memory allocated to your function. + * Your function use-case determines your CPU and memory requirements. + * + * @return int + */ + public function memory() + { + return config('sidecar.memory'); + } + + /** + * The function execution time, in MS, at which Lambda should terminate the function. + * Because the execution time has cost implications, we recommend you set this + * value based on your expected execution time. + * + * @return int + */ + public function timeout() + { + return config('sidecar.timeout'); + } + + public function preparePayload($payload) + { + return $payload; + } + + public function beforeDeployment() + { + // + } + + public function afterDeployment() + { + // + } + + public function beforeActivation() + { + // + } + + public function afterActivation() + { + // + } + + public function beforeExecution($payload) + { + // + } + + public function afterExecution($payload, $result) + { + // + } + + /** + * @return array + */ + public function normalizedHandler() + { + $handler = $this->handler(); + + // Allow an at-sign to separate the file and function. This + // matches the Laravel ecosystem better: `image@handler` + // and `image.handler` will work the exact same way. + if (is_string($handler) && Str::contains($handler, '@')) { + $handler = Str::replace('@', '.', $handler); + } + + return $handler; + } + + /** + * @return Package + */ + public function makePackage() + { + $package = $this->package(); + + return is_array($package) ? Package::make($package) : $package; + } + + /** + * @return array + * + * @throws SidecarException + */ + public function toDeploymentArray() + { + $config = [ + 'FunctionName' => $this->nameWithPrefix(), + 'Runtime' => $this->runtime(), + 'Role' => config('sidecar.execution_role'), + 'Handler' => $this->normalizedHandler(), + 'Code' => $this->packageType() === 'Zip' + ? $this->makePackage()->deploymentConfiguration() + : $this->package(), + 'Description' => $this->description(), + 'Timeout' => (int) $this->timeout(), + 'MemorySize' => (int) $this->memory(), + 'Layers' => $this->layers(), + 'Publish' => true, + 'PackageType' => $this->packageType(), + ]; + + // For container image packages, we need to remove the Runtime + // and Handler since the container handles both of those + // things inherently. + if ($this->packageType() === 'Image') { + $config = Arr::except($config, ['Runtime', 'Handler']); + } + + return $config; + } +} diff --git a/src/Vercel/Client.php b/src/Vercel/Client.php new file mode 100644 index 0000000..213918d --- /dev/null +++ b/src/Vercel/Client.php @@ -0,0 +1,328 @@ + + */ + +namespace Hammerstone\Sidecar\Vercel; + +use GuzzleHttp\Client as Guzzle; +use Hammerstone\Sidecar\Contracts\FaasClient; +use Hammerstone\Sidecar\Exceptions\ConfigurationException; +use Hammerstone\Sidecar\ServerlessFunction; +use Hammerstone\Sidecar\Sidecar; +use Hammerstone\Sidecar\Vercel\Concerns\Primitives; +use Illuminate\Support\Arr; + +class Client implements FaasClient +{ + use Primitives; + + /** + * @var Guzzle + */ + protected $client; + + /** + * @var string + */ + protected $seed; + + /** + * @var string + */ + protected $secret; + + /** + * @var string|null + */ + protected $teamId; + + public function __construct($config) + { + $this->client = new Guzzle($config); + $this->seed = config('sidecar.vercel_domain_seed'); + $this->secret = config('sidecar.vercel_signing_secret'); + $this->teamId = config('sidecar.vercel_team'); + } + + protected function validateConfiguration() + { + if (!$this->seed || !$this->secret) { + throw new ConfigurationException('The Vercel seed and secret must be set.'); + } + } + + public function getLatestVersion(ServerlessFunction $function) + { + $versions = $this->getVersions($function); + + return end($versions)['Version']; + } + + public function aliasVersion(ServerlessFunction $function, $alias, $version = null) + { + $deploymentId = $version; + + // These don't ever get used by humans, so they + // don't need to be decipherable. + $domain = $this->domainForFunction($function, $alias); + $existing = $this->getDeploymentAliases($deploymentId); + + foreach ($existing['aliases'] as $assigned) { + if ($assigned['alias'] === "$domain.vercel.app") { + return FaasClient::NOOP; + } + } + + $this->setDeploymentAlias($version, $domain); + + return FaasClient::UPDATED; + } + + public function updateFunctionVariables(ServerlessFunction $function) + { + // @TODO this is duped from Lambda client + $variables = $function->variables(); + + // Makes the checksum hash more stable. + ksort($variables); + + // Add a checksum so that we can easily see later if anything has changed. + $variables['SIDECAR_CHECKSUM'] = substr(md5(json_encode($variables)), 0, 8); + + // @TODO + $projectId = 1; + + // @TODO see if anything has changed + $this->setProjectEnv($projectId, $variables); + } + + public function createNewFunction(ServerlessFunction $function) + { + $response = $this->createProject([ + 'name' => strtolower($function->nameWithPrefix()), + ]); + + if (!array_key_exists('id', $response)) { + throw new \Exception('Project not created on Vercel.'); + } + + return $this->deployProject($function); + } + + public function deleteFunction(ServerlessFunction $function) + { + $this->deleteProject($function->nameWithPrefix()); + } + + public function functionExists(ServerlessFunction $function, $checksum = null) + { + // @TODO checksum? + return $this->projectExists($function->nameWithPrefix()); + } + + public function updateExistingFunction(ServerlessFunction $function) + { + $this->deployProject($function); + } + + protected function deployProject(ServerlessFunction $function) + { + if ($function->packageType() === 'Image') { + throw new \Exception('Cannot deploy containers to Vercel. You must use a zip file.'); + } + + if ($timeout = $function->timeout() > 60) { + $timeout = 60; + + Sidecar::log('A Vercel function is limited to 60 seconds.'); + } + + $response = $this->createDeployment([ + // Project name? + 'name' => $function->nameWithPrefix(), + // Arbitrary KV Pairs + 'meta' => (object) [ + // + ], + 'projectSettings' => [ + 'sourceFilesOutsideRootDirectory' => true + ], + 'source' => 'cli', + 'version' => 2, + 'functions' => [ + // Universal shim entrypoint from our scaffolding. + 'api/index.js' => [ + 'memory' => $function->memory(), + 'maxDuration' => $timeout, + ] + ], + 'routes' => [ + [ + // Route everything to our entrypoint + 'src' => '/(.*)', + 'dest' => (new Scaffolding($function))->entry() + ] + ], + 'files' => $this->uploadPackage($function)->toArray() + ]); + + while (true) { + $state = $this->getDeployment($response['id'])['readyState']; + + if ($state === 'QUEUED') { + Sidecar::log('Queued at Vercel...'); + } elseif ($state === 'BUILDING') { + Sidecar::log('Building on Vercel...'); + } elseif ($state === 'READY') { + Sidecar::log('Deployed to Vercel!'); + break; + } else { + throw new \Exception('Unknown Vercel state: ' . json_encode($state)); + } + + sleep(3); + } + } + + public function domainForFunction(ServerlessFunction $function, $alias) + { + return $this->domainForFunctionName($function->nameWithPrefix(), $alias); + } + + public function domainForFunctionName($name, $alias) + { + $this->validateConfiguration(); + + return strtolower(implode('-', [ + // Obscure the name, but make it still determinative. + substr(md5($name), 0, 16), + // Domain seed, can be public. + $this->seed, + $alias + ])); + } + + public function invoke($args = []) + { + return $this->doInvocation($args, $async = false); + } + + public function invokeAsync($args = []) + { + return $this->doInvocation($args, $async = true); + } + + public function executionUrl($function, $minutes = 60 * 4) + { + $domain = $this->domainForFunction($function, 'active'); + $timestamp = now()->addMinutes($minutes)->timestamp; + $digest = sha1($this->secret . $timestamp); + + return "https://$domain.vercel.app/tok-$digest-$timestamp/execute"; + } + + protected function doInvocation($args, $async) + { + $this->validateConfiguration(); + + $function = explode(':', $args['FunctionName']); + $domain = $this->domainForFunctionName($function[0], $function[1]); + + $url = "https://$domain.vercel.app/execute"; + + $method = $async ? 'postAsync' : 'post'; + + $response = (new Guzzle)->{$method}($url, [ + 'body' => $args['Payload'], + 'headers' => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer ' . $this->secret + ], + 'query' => [ + 'scevent' => ($args['InvocationType'] === 'Event') + ], + ]); + + $response = $response->getBody()->getContents(); + + return json_decode($response); + } + + public function getVersions(ServerlessFunction $function, $marker = null) + { + // @TODO Marker? + + $project = $this->getProject($function->nameWithPrefix()); + $response = $this->listDeployments($project['id']); + + $deployments = array_reverse($response['deployments']); + + return array_map(function ($deployment) { + return [ + 'Version' => $deployment['uid'] + ]; + }, $deployments); + } + + public function deleteFunctionVersion(ServerlessFunction $function, $version) + { + $project = $this->getProject($function->nameWithPrefix()); + + $keep = array_map(function ($config) { + return $config['id']; + }, $project['targets']); + + $keep = array_values($keep); + + if (in_array($version, $keep)) { + Sidecar::log("Not deleting version $keep, as it is currently active."); + + return; + } + + $this->client->delete("/v13/deployments/$version"); + } + + public function waitUntilFunctionUpdated(ServerlessFunction $function) + { + // There is no concept of waiting with Vercel. + // Once it's deployed, it's live. + } + + public function latestVersionHasAlias(ServerlessFunction $function, $alias) + { + // TODO: Implement latestVersionHasAlias() method. + } + + public function uploadPackage(ServerlessFunction $function) + { + Sidecar::log('Uploading files to Vercel.'); + + $files = (new Scaffolding($function))->files(); + + // Put the developer's code in a zip and write it to a tmp file. + $tmpzip = $function->makePackage()->createZip(); + + $files->push([ + // Developer's code is zipped into this hardcoded path. It is + // similarly hardcoded in the build step of package.json. + 'file' => '/package/package.zip', + 'stream' => fopen($tmpzip, 'r'), + 'sha' => sha1_file($tmpzip), + 'size' => filesize($tmpzip), + ]); + + $files->each(function ($file) { + $this->uploadFile($file); + }); + + @unlink($tmpzip); + + return $files->map(function ($file) { + Arr::forget($file, 'stream'); + + return $file; + }); + } +} diff --git a/src/Vercel/Concerns/Primitives.php b/src/Vercel/Concerns/Primitives.php new file mode 100644 index 0000000..90661b5 --- /dev/null +++ b/src/Vercel/Concerns/Primitives.php @@ -0,0 +1,181 @@ + + */ + +namespace Hammerstone\Sidecar\Vercel\Concerns; + +use GuzzleHttp\Exception\RequestException; + +trait Primitives +{ + protected function get($uri, array $options = []) + { + return $this->request('get', $uri, $options); + } + + protected function post($uri, array $options = []) + { + return $this->request('post', $uri, $options); + } + + protected function request($method, $uri, array $options = []) + { + // https://vercel.com/docs/rest-api#introduction/api-basics/authentication/accessing-resources-owned-by-a-team + if ($this->teamId) { + $options = array_merge_recursive($options, [ + 'query' => [ + 'teamId' => $this->teamId + ] + ]); + } + + $response = $this->client->{$method}($uri, $options); + + return json_decode($response->getBody()->getContents(), JSON_OBJECT_AS_ARRAY); + } + + /* + |-------------------------------------------------------------------------- + | Teams + |-------------------------------------------------------------------------- + */ + public function listTeams() + { + return $this->get('/v2/teams'); + } + + /* + |-------------------------------------------------------------------------- + | Projects + |-------------------------------------------------------------------------- + */ + public function getProject($idOrName) + { + return $this->get("/v8/projects/$idOrName"); + } + + public function createProject($json) + { + return $this->post('/v8/projects', [ + 'json' => $json, + ]); + } + + public function deleteProject($idOrName) + { + return $this->request('DELETE', "/v8/projects/$idOrName"); + } + + public function projectExists($idOrName) + { + try { + $this->getProject($idOrName); + } catch (RequestException $e) { + if ($e->getCode() === 404) { + return false; + } + + throw $e; + } + + return true; + } + + public function setProjectEnv($id, $variables) + { + $mapped = []; + + foreach ($variables as $key => $value) { + $mapped[] = [ + 'type' => 'encrypted', + 'key' => $key, + 'value' => $value, + 'target' => [ + 'production', + 'preview', + 'development' + ] + ]; + } + + return $this->post("/v7/projects/$id/env", [ + 'json' => $mapped, + ]); + } + + /* + |-------------------------------------------------------------------------- + | Files + |-------------------------------------------------------------------------- + */ + public function uploadFile($file) + { + return $this->post('/v2/files', [ + 'body' => $file['stream'], + 'headers' => [ + 'Content-Length' => $file['size'], + 'x-now-size' => $file['size'], + 'x-now-digest' => $file['sha'], + ] + ]); + } + + /* + |-------------------------------------------------------------------------- + | Deployments + |-------------------------------------------------------------------------- + */ + public function getDeployment($id) + { + return $this->get("/v13/deployments/$id"); + } + + public function createDeployment($json) + { + return $this->post('/v13/deployments', [ + 'json' => $json, + ]); + } + + public function listDeployments($projectId = null) + { + return $this->get('/v6/deployments', [ + 'query' => $projectId ? [ + 'projectId' => $projectId, + 'limit' => 100, + ] : [ + // + ] + ]); + } + + /* + |-------------------------------------------------------------------------- + | Deployment Aliases + |-------------------------------------------------------------------------- + */ + public function setDeploymentAlias($id, $alias) + { + return $this->post("/v2/deployments/$id/aliases", [ + 'json' => [ + 'alias' => $alias, + ], + ]); + } + + public function getDeploymentAliases($id) + { + return $this->get("/v2/deployments/$id/aliases"); + } + + /* + |-------------------------------------------------------------------------- + | Deployment Builds + |-------------------------------------------------------------------------- + */ + public function getDeploymentBuilds($id) + { + return $this->get("/v11/deployments/$id/builds"); + } +} diff --git a/src/Vercel/Scaffolding.php b/src/Vercel/Scaffolding.php new file mode 100644 index 0000000..cc385b7 --- /dev/null +++ b/src/Vercel/Scaffolding.php @@ -0,0 +1,128 @@ + + */ + +namespace Hammerstone\Sidecar\Vercel; + +use Exception; +use Hammerstone\Sidecar\Exceptions\ConfigurationException; +use Hammerstone\Sidecar\Finder; +use Hammerstone\Sidecar\Runtime; +use Hammerstone\Sidecar\ServerlessFunction; +use Illuminate\Support\Arr; +use Illuminate\Support\Str; + +class Scaffolding +{ + /** + * @var ServerlessFunction + */ + protected $function; + + /** + * @var array + */ + protected $configuration; + + /** + * @param ServerlessFunction $function + * + * @throws Exception + */ + public function __construct(ServerlessFunction $function) + { + $this->function = $function; + $this->configuration = $this->configuration(); + } + + public function entry() + { + return $this->configuration['entry']; + } + + public function files() + { + $scaffolding = $this->directory(); + + return Finder::create($scaffolding)->selected() + ->map(function ($file) use ($scaffolding) { + $replacements = $this->replacements(); + + // Swap all of the placeholders out in the scaffolding files. + $contents = str_replace( + array_keys($replacements), + array_values($replacements), + file_get_contents($file) + ); + + // Remove the base path. + $name = Str::replace($scaffolding, '', $file); + + return [ + 'file' => $name, + 'stream' => $contents, + 'size' => strlen($contents), + 'sha' => sha1($contents), + ]; + }); + } + + public function directory() + { + $directory = __DIR__ . DIRECTORY_SEPARATOR . 'Scaffolding' . DIRECTORY_SEPARATOR . $this->configuration['directory']; + + if (!is_dir($directory)) { + throw new Exception('Unable to find Vercel Scaffolding.'); + } + + return $directory; + } + + protected function replacements() + { + $handler = $this->function->normalizedHandler(); + + if (!$handler) { + throw new ConfigurationException('Handler not set.'); + } + + $handler = explode('.', $handler); + + return [ + 'sc_replace__handler_file' => $handler[0], + 'sc_replace__handler_function' => $handler[1], + 'sc_replace__middleware_token' => config('sidecar.vercel_signing_secret'), + 'sc_replace__runtime_version' => $this->configuration['runtime_version'] + ]; + } + + protected function configuration() + { + $config = Arr::get($this->configurations(), $this->function->runtime()); + + if (!$config) { + throw new Exception("Unable to find Vercel scaffolding for the `{$this->function->runtime()}` runtime."); + } + + return $config; + } + + protected function configurations() + { + return [ + Runtime::NODEJS_14 => $this->js('14.x'), + Runtime::NODEJS_12 => $this->js('12.x'), + Runtime::NODEJS_10 => $this->js('10.x'), + ]; + } + + protected function js($runtime) + { + return [ + 'entry' => 'api/index.js', + 'directory' => 'js', + 'runtime_version' => $runtime, + ]; + } +} diff --git a/src/Vercel/Scaffolding/js/_middleware.js b/src/Vercel/Scaffolding/js/_middleware.js new file mode 100644 index 0000000..6d23c2c --- /dev/null +++ b/src/Vercel/Scaffolding/js/_middleware.js @@ -0,0 +1,53 @@ +// https://jameshfisher.com/2017/10/30/web-cryptography-api-hello-world/ +async function sha1(str) { + const buf = await crypto.subtle.digest('SHA-1', new TextEncoder('utf-8').encode(str)); + return Array.prototype.map.call(new Uint8Array(buf), x => (('00' + x.toString(16)).slice(-2))).join(''); +} + +function token(headers) { + return (headers['authorization'] || '').split(' ')[1] || ''; +} + +function passesSimpleAuth(headers) { + return token(headers) === 'sc_replace__middleware_token'; +} + +async function passesDigestAuth(request) { + const headers = request.headers; + + let auth = token(headers); + + // Get it from the path if it's not in the header. + if (!auth) { + auth = request.url.split('/tok-')[1] || ''; + auth = auth.split('/')[0] || '' + } + + const digest = auth.split('-')[0]; + const timestamp = auth.split('-')[1]; + + if (!digest || !timestamp) { + return false; + } + + if (parseInt(timestamp) < (Math.floor(Date.now() / 1000))) { + return false; + } + + return digest === await sha1('sc_replace__middleware_token' + timestamp); +} + +export default async function (request) { + if (!passesSimpleAuth(request.headers) && !await passesDigestAuth(request)) { + return new Response('Unauthorized', { + status: 401 + }) + } + + // return new Response(null, { + // headers: { + // 'x-middleware-rewrite': 'https://google.com/', + // }, + // }) + // let path = request.url.split('.vercel.app/')[1]; +} diff --git a/src/Vercel/Scaffolding/js/api/index.js b/src/Vercel/Scaffolding/js/api/index.js new file mode 100644 index 0000000..d6b61eb --- /dev/null +++ b/src/Vercel/Scaffolding/js/api/index.js @@ -0,0 +1,66 @@ +import {sc_replace__handler_function} from '../package/sc_replace__handler_file'; + +let logs = []; + +shim('log'); +shim('info'); +shim('warn'); + +export default async function (request, response) { + if (!request.url.includes('/execute')) { + return response.status(404).send(); + } + + logs = []; + let result = execute(request); + + // This is the signal from the execution side that + // they are not interested in the response, and to + // return as quickly as possible. + if (request.query.scevent == 1) { + // Don't wait, send an empty "received" response. + return response.status(202).send(); + } + + result = await result; + + // If the developer indicates that the raw response should + // be returned, we'll do that. This helps in the cases + // where the handler generates an image and is being + // invoked from the frontend. + if (request.query.scraw == 1) { + return response.status(200).send(result); + } + + response.setHeader('Cache-Control', 's-maxage=60'); + + response.status(200).json({ + logs: logs, + result: JSON.stringify(result) + }); +} + +function execute(request) { + const payload = request.body; + // const payload = (request.method === 'POST') ? request.body : request.query; + + try { + return sc_replace__handler_function(payload); + } catch (e) { + return e; + } +} + +function shim(method) { + let original = console[method]; + + console[method] = function (...args) { + original(...args); + + logs.push({ + time: (Math.floor(Date.now() / 1000)), + level: method, + body: args.map(arg => (typeof arg === 'string') ? arg : JSON.stringify(arg)) + }) + } +} \ No newline at end of file diff --git a/src/Vercel/Scaffolding/js/package.json b/src/Vercel/Scaffolding/js/package.json new file mode 100644 index 0000000..d672a38 --- /dev/null +++ b/src/Vercel/Scaffolding/js/package.json @@ -0,0 +1,12 @@ +{ + "_comment": "You should not manage this file directly, it is managed by Sidecar.", + "name": "sidecar-managed", + "version": "0.0.1", + "scripts": { + "_build": "# The developer's code is zipped up into a package.zip file and uploaded to Vercel. On Vercel, the `build` command is called, which unzips the developer's code and puts it in place.", + "build": "unzip -d package package/package.zip && rm package/package.zip" + }, + "engines": { + "node": "sc_replace__runtime_version" + } +} diff --git a/tests/BaseTest.php b/tests/BaseTest.php new file mode 100644 index 0000000..6ae9cba --- /dev/null +++ b/tests/BaseTest.php @@ -0,0 +1,29 @@ + + */ + +namespace Hammerstone\Sidecar\Tests; + +use Hammerstone\Sidecar\Providers\SidecarServiceProvider; +use Orchestra\Testbench\TestCase; + +abstract class BaseTest extends TestCase +{ + public $loadEnvironmentVariables = true; + + protected function resolveApplication() + { + // Since we have live Vercel and AWS keys for integration tests, we can't + // use PHPUnit's environment handling as those would be exposed in the + // git repository. We use an ignored .env file for our local tests. + return parent::resolveApplication()->useEnvironmentPath(dirname(__DIR__)); + } + + protected function getPackageProviders($app) + { + return [ + SidecarServiceProvider::class + ]; + } +} diff --git a/tests/Integration/Vercel/FirstTest.php b/tests/Integration/Vercel/FirstTest.php new file mode 100644 index 0000000..312edfc --- /dev/null +++ b/tests/Integration/Vercel/FirstTest.php @@ -0,0 +1,43 @@ + + */ + +namespace Hammerstone\Sidecar\Tests\Integration\Vercel; + +use Hammerstone\Sidecar\Sidecar; +use Hammerstone\Sidecar\Tests\BaseTest; +use Hammerstone\Sidecar\Tests\Integration\Vercel\Support\BasicVercelFunction; +use Hammerstone\Sidecar\Vercel\Client; +use Illuminate\Support\Facades\Http; + +class FirstTest extends BaseTest +{ + /** @test */ + public function it_can_deploys() + { + Sidecar::addPhpUnitLogger(); + +// $vercel = app(Client::class); +// $function = new BasicVercelFunction; +// +// if ($vercel->functionExists($function)) { +// $vercel->deleteFunction($function); +// } +// +// $this->assertFalse($vercel->functionExists($function)); +// +// BasicVercelFunction::deploy($activate = true); +// +// $this->assertTrue($vercel->functionExists($function)); +// +// $url = $vercel->executionUrl(new BasicVercelFunction); +// $response = Http::get($url)->json(); +// +// $this->assertEquals('"Hello world!"', $response['result']); + + $response = BasicVercelFunction::execute(); + + dd($response); + } +} diff --git a/tests/Integration/Vercel/Support/BasicVercelFunction.php b/tests/Integration/Vercel/Support/BasicVercelFunction.php new file mode 100644 index 0000000..bb6d6c1 --- /dev/null +++ b/tests/Integration/Vercel/Support/BasicVercelFunction.php @@ -0,0 +1,31 @@ + + */ + +namespace Hammerstone\Sidecar\Tests\Integration\Vercel\Support; + +use Hammerstone\Sidecar\Package; +use Hammerstone\Sidecar\ServerlessFunction; + +class BasicVercelFunction extends ServerlessFunction +{ + public function name() + { + return 'Vercel-Test'; + } + + public function handler() + { + return 'helloworld.handler'; + } + + public function package() + { + return Package::make() + ->setBasePath(__DIR__ . DIRECTORY_SEPARATOR . 'Files') + ->include([ + 'helloworld.js' + ]); + } +} diff --git a/tests/Integration/Vercel/Support/Files/helloworld.js b/tests/Integration/Vercel/Support/Files/helloworld.js new file mode 100644 index 0000000..cc2dbf7 --- /dev/null +++ b/tests/Integration/Vercel/Support/Files/helloworld.js @@ -0,0 +1,3 @@ +exports.handler = async function (event) { + return 'Hello world!'; +} diff --git a/tests/Unit/BaseTest.php b/tests/Unit/BaseTest.php deleted file mode 100644 index 49a6295..0000000 --- a/tests/Unit/BaseTest.php +++ /dev/null @@ -1,19 +0,0 @@ - - */ - -namespace Hammerstone\Sidecar\Tests\Unit; - -use Hammerstone\Sidecar\Providers\SidecarServiceProvider; -use Orchestra\Testbench\TestCase; - -abstract class BaseTest extends TestCase -{ - protected function getPackageProviders($app) - { - return [ - SidecarServiceProvider::class - ]; - } -} diff --git a/tests/Unit/Commands/ActivateTest.php b/tests/Unit/Commands/ActivateTest.php index dc8eeea..9d145cc 100644 --- a/tests/Unit/Commands/ActivateTest.php +++ b/tests/Unit/Commands/ActivateTest.php @@ -12,6 +12,7 @@ use Hammerstone\Sidecar\Events\BeforeFunctionsActivated; use Hammerstone\Sidecar\Events\BeforeFunctionsDeployed; use Hammerstone\Sidecar\Sidecar; +use Hammerstone\Sidecar\Tests\BaseTest; use Hammerstone\Sidecar\Tests\Unit\Support\DeploymentTestFunction; use Illuminate\Support\Facades\Event; diff --git a/tests/Unit/Commands/ConfigureTest.php b/tests/Unit/Commands/ConfigureTest.php index a01bc0a..a445a9a 100644 --- a/tests/Unit/Commands/ConfigureTest.php +++ b/tests/Unit/Commands/ConfigureTest.php @@ -10,6 +10,7 @@ use Aws\Iam\IamClient; use Aws\S3\Exception\S3Exception; use Aws\S3\S3Client; +use Hammerstone\Sidecar\Tests\BaseTest; use Illuminate\Support\Carbon; use Mockery; diff --git a/tests/Unit/DeploymentTest.php b/tests/Unit/DeploymentTest.php index 84b2452..039bdb1 100644 --- a/tests/Unit/DeploymentTest.php +++ b/tests/Unit/DeploymentTest.php @@ -13,6 +13,7 @@ use Hammerstone\Sidecar\Events\BeforeFunctionsActivated; use Hammerstone\Sidecar\Events\BeforeFunctionsDeployed; use Hammerstone\Sidecar\Exceptions\NoFunctionsRegisteredException; +use Hammerstone\Sidecar\Tests\BaseTest; use Hammerstone\Sidecar\Tests\Unit\Support\DeploymentTestFunction; use Hammerstone\Sidecar\Tests\Unit\Support\DeploymentTestFunctionWithVariables; use Illuminate\Support\Facades\Event; diff --git a/tests/Unit/EnvironmentMismatchTest.php b/tests/Unit/EnvironmentMismatchTest.php index 3fa52b3..1785a9a 100644 --- a/tests/Unit/EnvironmentMismatchTest.php +++ b/tests/Unit/EnvironmentMismatchTest.php @@ -8,6 +8,7 @@ use Aws\Lambda\Exception\LambdaException; use Hammerstone\Sidecar\Clients\LambdaClient; use Hammerstone\Sidecar\Exceptions\FunctionNotFoundException; +use Hammerstone\Sidecar\Tests\BaseTest; use Hammerstone\Sidecar\Tests\Unit\Support\EmptyTestFunction; use Illuminate\Support\Facades\Event; use Mockery; diff --git a/tests/Unit/EnvironmentTest.php b/tests/Unit/EnvironmentTest.php index eb22ecc..df7b1d2 100644 --- a/tests/Unit/EnvironmentTest.php +++ b/tests/Unit/EnvironmentTest.php @@ -6,6 +6,7 @@ namespace Hammerstone\Sidecar\Tests\Unit; use Hammerstone\Sidecar\Sidecar; +use Hammerstone\Sidecar\Tests\BaseTest; class EnvironmentTest extends BaseTest { diff --git a/tests/Unit/ExecuteMultipleTest.php b/tests/Unit/ExecuteMultipleTest.php index cfe88a6..97c349e 100644 --- a/tests/Unit/ExecuteMultipleTest.php +++ b/tests/Unit/ExecuteMultipleTest.php @@ -12,6 +12,7 @@ use Hammerstone\Sidecar\Events\BeforeFunctionExecuted; use Hammerstone\Sidecar\Results\PendingResult; use Hammerstone\Sidecar\Sidecar; +use Hammerstone\Sidecar\Tests\BaseTest; use Hammerstone\Sidecar\Tests\Unit\Support\EmptyTestFunction; use Illuminate\Support\Facades\Event; use Mockery; diff --git a/tests/Unit/ExecuteTest.php b/tests/Unit/ExecuteTest.php index 9de9c03..1410bd8 100644 --- a/tests/Unit/ExecuteTest.php +++ b/tests/Unit/ExecuteTest.php @@ -10,6 +10,7 @@ use Hammerstone\Sidecar\Events\AfterFunctionExecuted; use Hammerstone\Sidecar\Events\BeforeFunctionExecuted; use Hammerstone\Sidecar\Sidecar; +use Hammerstone\Sidecar\Tests\BaseTest; use Hammerstone\Sidecar\Tests\Unit\Support\EmptyTestFunction; use Illuminate\Support\Facades\Event; diff --git a/tests/Unit/FunctionTest.php b/tests/Unit/FunctionTest.php index 3d626eb..f7f0e5b 100644 --- a/tests/Unit/FunctionTest.php +++ b/tests/Unit/FunctionTest.php @@ -5,6 +5,7 @@ namespace Hammerstone\Sidecar\Tests\Unit; +use Hammerstone\Sidecar\Tests\BaseTest; use Hammerstone\Sidecar\Tests\Unit\Support\EmptyTestFunction; class FunctionTest extends BaseTest diff --git a/tests/Unit/LambdaClientTest.php b/tests/Unit/LambdaClientTest.php index 893b5d4..590be6a 100644 --- a/tests/Unit/LambdaClientTest.php +++ b/tests/Unit/LambdaClientTest.php @@ -7,6 +7,7 @@ use Aws\Lambda\Exception\LambdaException; use Hammerstone\Sidecar\Clients\LambdaClient; +use Hammerstone\Sidecar\Tests\BaseTest; use Hammerstone\Sidecar\Tests\Unit\Support\DeploymentTestFunction; use Hammerstone\Sidecar\Tests\Unit\Support\DeploymentTestFunctionWithImage; use Hammerstone\Sidecar\Tests\Unit\Support\EmptyTestFunction; diff --git a/tests/Unit/PackageTest.php b/tests/Unit/PackageTest.php index 163449e..ff2b9d4 100644 --- a/tests/Unit/PackageTest.php +++ b/tests/Unit/PackageTest.php @@ -6,6 +6,7 @@ namespace Hammerstone\Sidecar\Tests\Unit; use Hammerstone\Sidecar\Package; +use Hammerstone\Sidecar\Tests\BaseTest; use Hammerstone\Sidecar\Tests\Unit\Support\FakeStreamWrapper; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Storage; diff --git a/tests/Unit/Support/DeploymentTestFunction.php b/tests/Unit/Support/DeploymentTestFunction.php index c92008f..8033065 100644 --- a/tests/Unit/Support/DeploymentTestFunction.php +++ b/tests/Unit/Support/DeploymentTestFunction.php @@ -5,10 +5,10 @@ namespace Hammerstone\Sidecar\Tests\Unit\Support; -use Hammerstone\Sidecar\LambdaFunction; +use Hammerstone\Sidecar\ServerlessFunction; use Hammerstone\Sidecar\WarmingConfig; -class DeploymentTestFunction extends LambdaFunction +class DeploymentTestFunction extends ServerlessFunction { public function handler() { diff --git a/tests/Unit/Support/DeploymentTestFunctionWithImage.php b/tests/Unit/Support/DeploymentTestFunctionWithImage.php index 7c3136e..e7ef4a8 100644 --- a/tests/Unit/Support/DeploymentTestFunctionWithImage.php +++ b/tests/Unit/Support/DeploymentTestFunctionWithImage.php @@ -5,10 +5,10 @@ namespace Hammerstone\Sidecar\Tests\Unit\Support; -use Hammerstone\Sidecar\LambdaFunction; use Hammerstone\Sidecar\Package; +use Hammerstone\Sidecar\ServerlessFunction; -class DeploymentTestFunctionWithImage extends LambdaFunction +class DeploymentTestFunctionWithImage extends ServerlessFunction { public function handler() { diff --git a/tests/Unit/Support/EmptyTestFunction.php b/tests/Unit/Support/EmptyTestFunction.php index 215b334..9a039c9 100644 --- a/tests/Unit/Support/EmptyTestFunction.php +++ b/tests/Unit/Support/EmptyTestFunction.php @@ -5,9 +5,9 @@ namespace Hammerstone\Sidecar\Tests\Unit\Support; -use Hammerstone\Sidecar\LambdaFunction; +use Hammerstone\Sidecar\ServerlessFunction; -class EmptyTestFunction extends LambdaFunction +class EmptyTestFunction extends ServerlessFunction { public function handler() {