Skip to content

Commit bbd3d2a

Browse files
authored
Merge pull request #21 from xp-forge/feature/run-local
Implement `xp lambda run [Handler]` to run lambdas locally
2 parents cf29cc7 + e8666e2 commit bbd3d2a

File tree

5 files changed

+113
-14
lines changed

5 files changed

+113
-14
lines changed

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,25 @@ If you need to run any initialization code, you can do before returning the lamb
3838

3939
Development
4040
-----------
41-
To test your lambda locally, run the following:
41+
To run your lambda locally, use the following:
42+
43+
```bash
44+
$ xp lambda run Greet '{"name":"Timm"}'
45+
Hello Timm from PHP 8.2.7 via Greet @ test-local-1
46+
```
47+
48+
*This does not provide a complete lambda environment, and does not have any execution limits imposed on it!*
49+
50+
Integration testing
51+
-------------------
52+
To test your lambda inside a local containerized lambda environment, use the *test* command.
4253

4354
```bash
4455
$ xp lambda test Greet '{"name":"Timm"}'
4556
START RequestId: 9ff45cda-df9b-1b8c-c21b-5fe27c8f2d24 Version: $LATEST
4657
END RequestId: 9ff45cda-df9b-1b8c-c21b-5fe27c8f2d24
4758
REPORT RequestId: 9ff45cda-df9b-1b8c-c21b-5fe27c8f2d24 Init Duration: 922.19 ms...
48-
"Hello Timm from PHP 8.0.10 via test @ us-east-1"
59+
"Hello Timm from PHP 8.2.7 via test @ us-east-1"
4960
```
5061

5162
*This functionality is provided by the [AWS Lambda base images for custom runtimes](https://gallery.ecr.aws/lambda/provided)*

src/main/php/com/amazon/aws/lambda/Environment.class.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ public function tempDir(): Path {
3838
return new Path(sys_get_temp_dir());
3939
}
4040

41+
/** Returns whether this is a local invocation */
42+
public function local(): bool {
43+
return isset($this->variables['AWS_LOCAL']);
44+
}
45+
4146
/**
4247
* Returns a given environment variable
4348
*
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php namespace xp\lambda;
2+
3+
use com\amazon\aws\lambda\{Environment, Context};
4+
use lang\{XPClass, Throwable};
5+
use util\UUID;
6+
use util\cmd\Console;
7+
8+
/**
9+
* Run lambdas locally
10+
*
11+
* @see https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html
12+
* @see https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html
13+
*/
14+
class RunLambda {
15+
const TRACE= 'Root=1-5bef4de7-ad49b0e87f6ef6c87fc2e700;Parent=9a9197af755a6419;Sampled=1';
16+
const REGION= 'test-local-1';
17+
18+
private $impl, $events;
19+
20+
/**
21+
* Creates a new `run` subcommand
22+
*
23+
* @param string $handler
24+
* @param string... $events
25+
* @throws lang.ClassLoadingException
26+
*/
27+
public function __construct($handler= 'Handler', ... $events) {
28+
$this->impl= XPClass::forName($handler);
29+
$this->events= $events ?: ['{}'];
30+
}
31+
32+
/** Runs this command */
33+
public function run(): int {
34+
$name= $this->impl->getSimpleName();
35+
$region= getenv('AWS_REGION') ?: self::REGION;
36+
$functionArn= "arn:aws:lambda:{$region}:123456789012:function:{$name}";
37+
$deadlineMs= (time() + 900) * 1000;
38+
$environment= $_ENV + ['AWS_LAMBDA_FUNCTION_NAME' => $name, 'AWS_REGION' => $region, 'AWS_LOCAL' => true];
39+
40+
try {
41+
$target= $this->impl->newInstance(new Environment(getcwd(), Console::$out, $environment))->target();
42+
$lambda= $target instanceof Lambda ? [$target, 'process'] : $target;
43+
} catch (Throwable $e) {
44+
Console::$err->writeLine($e);
45+
return 127;
46+
}
47+
48+
$status= 0;
49+
foreach ($this->events as $event) {
50+
$headers= [
51+
'Lambda-Runtime-Aws-Request-Id' => [UUID::randomUUID()->hashCode()],
52+
'Lambda-Runtime-Invoked-Function-Arn' => [$functionArn],
53+
'Lambda-Runtime-Trace-Id' => [self::TRACE],
54+
'Lambda-Runtime-Deadline-Ms' => [$deadlineMs],
55+
'Content-Length' => [strlen($event)],
56+
];
57+
58+
try {
59+
$result= $lambda(json_decode($event, true), new Context($headers, $environment));
60+
Console::$out->writeLine($result);
61+
} catch (Throwable $e) {
62+
Console::$err->writeLine($e);
63+
$status= 1;
64+
}
65+
}
66+
return $status;
67+
}
68+
}

src/main/php/xp/lambda/Runner.class.php

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,35 +8,39 @@
88
* XP AWS Lambda
99
* =============
1010
*
11-
* - Store runtime layer as `runtime-X.X.X.zip`, building if necessary:
11+
* - Run lambda locally:
1212
* ```sh
13-
* $ xp lambda runtime
13+
* $ xp lambda run Greet '{"name":"Test"}'
1414
* ```
15-
* - Rebuild runtime:
15+
* - Package single file in `function.zip` file for deployment:
1616
* ```sh
17-
* $ xp lambda runtime -b
17+
* $ xp lambda package Greet.class.php
1818
* ```
19-
* - Speficy runtime version, selecting newest PHP 8.0 release:
19+
* - Package INI file and source directory in `function.zip`:
2020
* ```sh
21-
* $ xp lambda runtime:8.0
21+
* $ xp lambda package task.ini src/main/php
2222
* ```
23-
* - Test lambda:
23+
* - Test lambda inside a containerized AWS environment:
2424
* ```sh
2525
* $ xp lambda test Greet '{"name":"Test"}'
2626
* ```
2727
* - Test lambda, pass environment variables:
2828
* ```sh
2929
* $ xp lambda test -e PROFILE=prod Audit
3030
* ```
31-
* - Package single file in `function.zip` file for deployment:
31+
* - Store runtime layer as `runtime-X.X.X.zip`, building if necessary:
3232
* ```sh
33-
* $ xp lambda package Greet.class.php
33+
* $ xp lambda runtime
3434
* ```
35-
* - Package INI file and source directory in `function.zip`:
35+
* - Rebuild runtime:
3636
* ```sh
37-
* $ xp lambda package task.ini src/main/php
37+
* $ xp lambda runtime -b
38+
* ```
39+
* - Speficy runtime version, selecting newest PHP 8.0 release:
40+
* ```sh
41+
* $ xp lambda runtime:8.0
3842
* ```
39-
* The `runtime` and `test` commands require Docker or Podman to be installed!
43+
* The `test` and `runtime` commands require Docker or Podman to be installed!
4044
* Packaging will always include the `vendor` directory automatically.
4145
*/
4246
class Runner {
@@ -66,6 +70,7 @@ private static function command(string $name, array $args): object {
6670
sscanf($name, "%[^:]:%[^\r]", $command, $version);
6771
switch ($command) {
6872
case 'package': return new PackageLambda(new Path('function.zip'), new Sources(new Path('.'), [...$args, 'vendor']));
73+
case 'run': return new RunLambda(...$args);
6974
case 'runtime': return new CreateRuntime(self::resolve($version), new Path('runtime-%s.zip'), in_array('-b', $args));
7075
case 'test': return new TestLambda(self::resolve($version), new Path('.'), $args);
7176
default: return new DisplayError('Unknown command "'.$args[0].'"');

src/test/php/com/amazon/aws/lambda/unittest/EnvironmentTest.class.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ public function non_existant_variable() {
4646
Assert::null((new Environment('.', null, []))->variable('TEST'));
4747
}
4848

49+
#[Test]
50+
public function not_local_by_default() {
51+
Assert::false((new Environment('.', null, []))->local());
52+
}
53+
54+
#[Test]
55+
public function local() {
56+
Assert::true((new Environment('.', null, ['AWS_LOCAL' => true]))->local());
57+
}
58+
4959
#[Test]
5060
public function credentials() {
5161
$env= [

0 commit comments

Comments
 (0)