apx-aws-lambda 0.1.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. apx_aws_lambda-0.3.0/PKG-INFO +93 -0
  2. apx_aws_lambda-0.3.0/README.md +75 -0
  3. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/apx_aws_lambda/lambda_.py +96 -10
  4. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/apx_aws_lambda/overlays/cloud/aws/lambda/{{cookiecutter.project_slug}}/_lang/python/lambda_handler.py +2 -1
  5. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/apx_aws_lambda/plugin.py +3 -3
  6. apx_aws_lambda-0.3.0/apx_aws_lambda/shell.py +66 -0
  7. apx_aws_lambda-0.3.0/apx_aws_lambda.egg-info/PKG-INFO +93 -0
  8. apx_aws_lambda-0.3.0/apx_aws_lambda.egg-info/requires.txt +8 -0
  9. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/pyproject.toml +2 -2
  10. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/tests/test_targets.py +51 -1
  11. apx_aws_lambda-0.1.0/PKG-INFO +0 -56
  12. apx_aws_lambda-0.1.0/README.md +0 -40
  13. apx_aws_lambda-0.1.0/apx_aws_lambda/shell.py +0 -46
  14. apx_aws_lambda-0.1.0/apx_aws_lambda.egg-info/PKG-INFO +0 -56
  15. apx_aws_lambda-0.1.0/apx_aws_lambda.egg-info/requires.txt +0 -6
  16. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/LICENSE +0 -0
  17. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/apx_aws_lambda/__init__.py +0 -0
  18. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/apx_aws_lambda/cli.py +0 -0
  19. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/apx_aws_lambda/overlays/cloud/aws/lambda/cookiecutter.json +0 -0
  20. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/apx_aws_lambda/overlays/cloud/aws/lambda/hooks/post_gen_project.py +0 -0
  21. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/apx_aws_lambda/overlays/cloud/aws/lambda/{{cookiecutter.project_slug}}/.github/workflows/deploy.yml +0 -0
  22. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/apx_aws_lambda/overlays/cloud/aws/lambda/{{cookiecutter.project_slug}}/DEPLOY.md +0 -0
  23. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/apx_aws_lambda/overlays/cloud/aws/lambda/{{cookiecutter.project_slug}}/_lang/python/Makefile +0 -0
  24. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/apx_aws_lambda/overlays/cloud/aws/lambda/{{cookiecutter.project_slug}}/requirements/policy.json +0 -0
  25. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/apx_aws_lambda/overlays/cloud/aws/lambda/{{cookiecutter.project_slug}}/requirements/trust.json +0 -0
  26. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/apx_aws_lambda/overlays/cloud/aws/lambda/{{cookiecutter.project_slug}}/samconfig.toml +0 -0
  27. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/apx_aws_lambda/overlays/cloud/aws/lambda/{{cookiecutter.project_slug}}/template.yaml +0 -0
  28. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/apx_aws_lambda/overlays/index.json +0 -0
  29. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/apx_aws_lambda/tools.py +0 -0
  30. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/apx_aws_lambda.egg-info/SOURCES.txt +0 -0
  31. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/apx_aws_lambda.egg-info/dependency_links.txt +0 -0
  32. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/apx_aws_lambda.egg-info/entry_points.txt +0 -0
  33. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/apx_aws_lambda.egg-info/top_level.txt +0 -0
  34. {apx_aws_lambda-0.1.0 → apx_aws_lambda-0.3.0}/setup.cfg +0 -0
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: apx-aws-lambda
3
+ Version: 0.3.0
4
+ Summary: AWS Lambda for Action Platform: the aws/lambda deploy target (SAM) with its overlay, and tools to read stacks and functions.
5
+ Author-email: Action Platform <cloud@actionplatform.io>
6
+ License-Expression: Apache-2.0
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: action-platform>=0.17
11
+ Requires-Dist: awscli>=1.34
12
+ Requires-Dist: aws-sam-cli>=1.120
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=8; extra == "dev"
15
+ Requires-Dist: ruff>=0.6; extra == "dev"
16
+ Requires-Dist: action-platform[mcp]; extra == "dev"
17
+ Dynamic: license-file
18
+
19
+ # apx-aws-lambda
20
+
21
+ AWS Lambda for [Action Platform](https://github.com/actionplatform/action-platform): the `aws/lambda` deploy target (SAM) with its overlay, read-only tools and commands. `apx-` is the prefix every Action Platform extension carries.
22
+
23
+ ```bash
24
+ action-platform plugin install aws-lambda
25
+ action-platform cloud set aws/lambda # overlay files come from this plugin
26
+ action-platform deploy --dry-run # preflight: sam validate, credentials
27
+ action-platform deploy # sam build + sam deploy --config-env <stage>
28
+ action-platform diagnose
29
+ ```
30
+
31
+ | | |
32
+ |---|---|
33
+ | Overlay | `template.yaml`, `samconfig.toml`, `lambda_handler.py`, `Makefile`, `requirements/` (IAM), `.github/workflows/deploy.yml` |
34
+ | Deploy | `sam build` + `sam deploy --config-env <stage>` (`dev` → `default`, `prod` → `prod`) |
35
+ | Rollback | CloudFormation `rollback-stack` — previous stack state |
36
+ | Diagnose | stack status and the HTTP API url |
37
+ | Destroy | `sam delete` |
38
+
39
+ `[deploy]` in platform.toml:
40
+
41
+ ```toml
42
+ [deploy]
43
+ target = "aws/lambda"
44
+ region = "us-east-1" # optional; samconfig.toml / AWS_REGION otherwise
45
+ ```
46
+
47
+ ## Credentials without keys
48
+
49
+ Set `role_arn` and nothing else: the target assumes that role with a short-lived OIDC token the platform signs (`sts assume-role-with-web-identity`). No access key on the platform, on your machine or in the repository; what the app may do is the role's policy.
50
+
51
+ ```toml
52
+ [deploy]
53
+ target = "aws/lambda"
54
+ role_arn = "arn:aws:iam::123456789012:role/shop-deploy"
55
+ ```
56
+
57
+ Once per AWS account, register the platform as an identity provider — its discovery document is `https://<platform>/.well-known/openid-configuration`:
58
+
59
+ ```bash
60
+ aws iam create-open-id-connect-provider --url https://platform.example.com --client-id-list sts.amazonaws.com
61
+ ```
62
+
63
+ Then a role per app (or per project) whose trust policy names the platform and the app; `requirements/policy.json` from the overlay is its permission policy:
64
+
65
+ ```json
66
+ {
67
+ "Version": "2012-10-17",
68
+ "Statement": [{
69
+ "Effect": "Allow",
70
+ "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/platform.example.com" },
71
+ "Action": "sts:AssumeRoleWithWebIdentity",
72
+ "Condition": {
73
+ "StringEquals": { "platform.example.com:aud": "sts.amazonaws.com" },
74
+ "StringLike": { "platform.example.com:sub": "org:acme:project:shop:app:orders" }
75
+ }
76
+ }]
77
+ }
78
+ ```
79
+
80
+ `sub` is `org:<org>:project:<project>:app:<app>` for a deploy the platform runs; `org:<org>` plus an `actor` claim for `action-platform deploy` from a logged-in machine (`StringLike` with `org:acme:*` covers both). Without `role_arn` the AWS CLI's own chain applies: `aws sso login`, a profile, an instance role — still no key in a file when you use SSO.
81
+
82
+ Tools (`action-platform mcp`): `aws_lambda_stacks`, `aws_lambda_functions`. Commands: `action-platform aws-lambda stacks|functions`. Deploying itself goes through the core's `deploy` / `rollback` / `diagnose`, which drive the target.
83
+
84
+ Needs: `aws` and `sam` — on PATH when present, otherwise the `awscli` and `aws-sam-cli` packages the plugin depends on run as `python -m`, which is how the hosted platform deploys without the CLIs in its image; `AWS_REGION` (or `region` under `[deploy]`), and either `role_arn` (OIDC, no keys) or the AWS CLI's own credentials. Talks to `*.amazonaws.com` only.
85
+
86
+ ## Development
87
+
88
+ ```bash
89
+ pip install -e ".[dev]"
90
+ pytest
91
+ ```
92
+
93
+ The tests fake `aws` and `sam`; nothing reaches AWS.
@@ -0,0 +1,75 @@
1
+ # apx-aws-lambda
2
+
3
+ AWS Lambda for [Action Platform](https://github.com/actionplatform/action-platform): the `aws/lambda` deploy target (SAM) with its overlay, read-only tools and commands. `apx-` is the prefix every Action Platform extension carries.
4
+
5
+ ```bash
6
+ action-platform plugin install aws-lambda
7
+ action-platform cloud set aws/lambda # overlay files come from this plugin
8
+ action-platform deploy --dry-run # preflight: sam validate, credentials
9
+ action-platform deploy # sam build + sam deploy --config-env <stage>
10
+ action-platform diagnose
11
+ ```
12
+
13
+ | | |
14
+ |---|---|
15
+ | Overlay | `template.yaml`, `samconfig.toml`, `lambda_handler.py`, `Makefile`, `requirements/` (IAM), `.github/workflows/deploy.yml` |
16
+ | Deploy | `sam build` + `sam deploy --config-env <stage>` (`dev` → `default`, `prod` → `prod`) |
17
+ | Rollback | CloudFormation `rollback-stack` — previous stack state |
18
+ | Diagnose | stack status and the HTTP API url |
19
+ | Destroy | `sam delete` |
20
+
21
+ `[deploy]` in platform.toml:
22
+
23
+ ```toml
24
+ [deploy]
25
+ target = "aws/lambda"
26
+ region = "us-east-1" # optional; samconfig.toml / AWS_REGION otherwise
27
+ ```
28
+
29
+ ## Credentials without keys
30
+
31
+ Set `role_arn` and nothing else: the target assumes that role with a short-lived OIDC token the platform signs (`sts assume-role-with-web-identity`). No access key on the platform, on your machine or in the repository; what the app may do is the role's policy.
32
+
33
+ ```toml
34
+ [deploy]
35
+ target = "aws/lambda"
36
+ role_arn = "arn:aws:iam::123456789012:role/shop-deploy"
37
+ ```
38
+
39
+ Once per AWS account, register the platform as an identity provider — its discovery document is `https://<platform>/.well-known/openid-configuration`:
40
+
41
+ ```bash
42
+ aws iam create-open-id-connect-provider --url https://platform.example.com --client-id-list sts.amazonaws.com
43
+ ```
44
+
45
+ Then a role per app (or per project) whose trust policy names the platform and the app; `requirements/policy.json` from the overlay is its permission policy:
46
+
47
+ ```json
48
+ {
49
+ "Version": "2012-10-17",
50
+ "Statement": [{
51
+ "Effect": "Allow",
52
+ "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/platform.example.com" },
53
+ "Action": "sts:AssumeRoleWithWebIdentity",
54
+ "Condition": {
55
+ "StringEquals": { "platform.example.com:aud": "sts.amazonaws.com" },
56
+ "StringLike": { "platform.example.com:sub": "org:acme:project:shop:app:orders" }
57
+ }
58
+ }]
59
+ }
60
+ ```
61
+
62
+ `sub` is `org:<org>:project:<project>:app:<app>` for a deploy the platform runs; `org:<org>` plus an `actor` claim for `action-platform deploy` from a logged-in machine (`StringLike` with `org:acme:*` covers both). Without `role_arn` the AWS CLI's own chain applies: `aws sso login`, a profile, an instance role — still no key in a file when you use SSO.
63
+
64
+ Tools (`action-platform mcp`): `aws_lambda_stacks`, `aws_lambda_functions`. Commands: `action-platform aws-lambda stacks|functions`. Deploying itself goes through the core's `deploy` / `rollback` / `diagnose`, which drive the target.
65
+
66
+ Needs: `aws` and `sam` — on PATH when present, otherwise the `awscli` and `aws-sam-cli` packages the plugin depends on run as `python -m`, which is how the hosted platform deploys without the CLIs in its image; `AWS_REGION` (or `region` under `[deploy]`), and either `role_arn` (OIDC, no keys) or the AWS CLI's own credentials. Talks to `*.amazonaws.com` only.
67
+
68
+ ## Development
69
+
70
+ ```bash
71
+ pip install -e ".[dev]"
72
+ pytest
73
+ ```
74
+
75
+ The tests fake `aws` and `sam`; nothing reaches AWS.
@@ -1,4 +1,6 @@
1
- """`aws/lambda`: a SAM stack per stage. Deploy = `sam build` + `sam deploy --config-env <stage>`; rollback = CloudFormation rollback to the previous stack state; diagnose = stack status and the HTTP API url."""
1
+ """`aws/lambda`: a SAM stack per stage. Deploy = `sam build` + `sam deploy --config-env <stage>`; rollback = CloudFormation rollback to the previous stack state; diagnose = stack status and the HTTP API url.
2
+
3
+ Credentials: with `role_arn` under `[deploy]`, the target assumes that role with a short-lived OIDC token the platform signs (`sts assume-role-with-web-identity`) — no access key anywhere. Without it, the AWS CLI's own chain applies (profile, SSO, instance role)."""
2
4
 
3
5
  from __future__ import annotations
4
6
 
@@ -7,7 +9,8 @@ import tomllib
7
9
 
8
10
  from action_platform.abc import DeployTarget
9
11
  from action_platform.core.context import Context, DeployResult, Diagnosis
10
- from action_platform.core.exception import DeployError
12
+ from action_platform.core.exception import ActionPlatformError, DeployError
13
+ from action_platform.remote.client import Remote
11
14
 
12
15
  from apx_aws_lambda import shell
13
16
 
@@ -16,7 +19,12 @@ class LambdaTarget(DeployTarget):
16
19
  name = "aws/lambda"
17
20
 
18
21
  def __init__(
19
- self, region: str | None = None, config: str = "samconfig.toml", **_: object
22
+ self,
23
+ region: str | None = None,
24
+ config: str = "samconfig.toml",
25
+ role_arn: str | None = None,
26
+ session_name: str = "action-platform",
27
+ **_: object,
20
28
  ) -> None:
21
29
  self.region = (
22
30
  region
@@ -24,6 +32,22 @@ class LambdaTarget(DeployTarget):
24
32
  or os.environ.get("AWS_DEFAULT_REGION")
25
33
  )
26
34
  self.config = config
35
+ self.role_arn = role_arn or os.environ.get("AWS_ROLE_ARN")
36
+ self.session_name = session_name
37
+ self._env: dict[str, str] | None = None
38
+
39
+ def env(self, ctx: Context) -> dict[str, str] | None:
40
+ """The environment `aws` and `sam` run with: temporary credentials from `role_arn` when set, the caller's own otherwise."""
41
+ if not self.role_arn:
42
+ return None
43
+
44
+ if self._env is None:
45
+ self._env = {
46
+ **os.environ,
47
+ **assume_role(ctx, self.role_arn, self.session_name, self.region),
48
+ }
49
+
50
+ return self._env
27
51
 
28
52
  def _stage(self, ctx: Context) -> str:
29
53
  return "prod" if ctx.stage == "prod" else "default"
@@ -72,19 +96,30 @@ class LambdaTarget(DeployTarget):
72
96
  )
73
97
 
74
98
  self._stack(ctx)
75
- shell.aws("sts", "get-caller-identity", region=self._region(ctx))
76
- shell.run([shell.require("sam", ""), "validate", "--lint"], cwd=ctx.repo_root)
99
+ shell.aws(
100
+ "sts", "get-caller-identity", region=self._region(ctx), env=self.env(ctx)
101
+ )
102
+ shell.run(
103
+ [*shell.require("sam", ""), "validate", "--lint"],
104
+ cwd=ctx.repo_root,
105
+ env=self.env(ctx),
106
+ )
77
107
 
78
108
  def deploy(self, ctx: Context) -> DeployResult:
79
109
  sam = shell.require("sam", "pip install aws-sam-cli")
80
110
  stage = self._stage(ctx)
81
- shell.run([sam, "build"], cwd=ctx.repo_root)
82
- args = [sam, "deploy", "--no-confirm-changeset", "--no-fail-on-empty-changeset"]
111
+ shell.run([*sam, "build"], cwd=ctx.repo_root, env=self.env(ctx))
112
+ args = [
113
+ *sam,
114
+ "deploy",
115
+ "--no-confirm-changeset",
116
+ "--no-fail-on-empty-changeset",
117
+ ]
83
118
 
84
119
  if stage != "default":
85
120
  args += ["--config-env", stage]
86
121
 
87
- shell.run(args, cwd=ctx.repo_root)
122
+ shell.run(args, cwd=ctx.repo_root, env=self.env(ctx))
88
123
  url = self._url(ctx)
89
124
 
90
125
  return DeployResult(
@@ -104,6 +139,7 @@ class LambdaTarget(DeployTarget):
104
139
  "--stack-name",
105
140
  self._stack(ctx),
106
141
  region=self._region(ctx),
142
+ env=self.env(ctx),
107
143
  )
108
144
 
109
145
  def diagnose(self, ctx: Context) -> Diagnosis:
@@ -116,6 +152,7 @@ class LambdaTarget(DeployTarget):
116
152
  "--stack-name",
117
153
  stack,
118
154
  region=self._region(ctx),
155
+ env=self.env(ctx),
119
156
  )
120
157
  except DeployError as e:
121
158
  return Diagnosis(
@@ -145,16 +182,65 @@ class LambdaTarget(DeployTarget):
145
182
 
146
183
  def delete(self, ctx: Context) -> None:
147
184
  sam = shell.require("sam", "pip install aws-sam-cli")
148
- args = [sam, "delete", "--no-prompts", "--stack-name", self._stack(ctx)]
185
+ args = [*sam, "delete", "--no-prompts", "--stack-name", self._stack(ctx)]
149
186
  region = self._region(ctx)
150
187
 
151
188
  if region:
152
189
  args += ["--region", region]
153
190
 
154
- shell.run(args, cwd=ctx.repo_root)
191
+ shell.run(args, cwd=ctx.repo_root, env=self.env(ctx))
155
192
 
156
193
  def _url(self, ctx: Context) -> str | None:
157
194
  try:
158
195
  return self.diagnose(ctx).url
159
196
  except DeployError:
160
197
  return None
198
+
199
+
200
+ def assume_role(
201
+ ctx: Context, role_arn: str, session_name: str, region: str | None
202
+ ) -> dict[str, str]:
203
+ """Temporary credentials for `role_arn` from an OIDC token: the platform's for a deploy it runs, the platform the CLI is logged in to otherwise."""
204
+ token = ctx.identity_token("sts.amazonaws.com")
205
+
206
+ if token is None:
207
+ try:
208
+ token = Remote.from_credentials().identity_token("sts.amazonaws.com")[
209
+ "token"
210
+ ]
211
+ except ActionPlatformError as e:
212
+ raise DeployError(
213
+ f"role_arn is set but nothing can sign an identity token here: {e}. "
214
+ "Deploy from the platform, or log in with `action-platform login`."
215
+ ) from e
216
+
217
+ data = shell.aws(
218
+ "sts",
219
+ "assume-role-with-web-identity",
220
+ "--role-arn",
221
+ role_arn,
222
+ "--role-session-name",
223
+ session_name,
224
+ "--web-identity-token",
225
+ token,
226
+ "--duration-seconds",
227
+ "3600",
228
+ region=region,
229
+ env={
230
+ k: v
231
+ for k, v in os.environ.items()
232
+ if not k.startswith("AWS_ACCESS") and k != "AWS_SESSION_TOKEN"
233
+ },
234
+ )
235
+ creds = data.get("Credentials") or {}
236
+
237
+ if not creds.get("AccessKeyId"):
238
+ raise DeployError(
239
+ f"assume-role-with-web-identity gave no credentials for {role_arn}"
240
+ )
241
+
242
+ return {
243
+ "AWS_ACCESS_KEY_ID": creds["AccessKeyId"],
244
+ "AWS_SECRET_ACCESS_KEY": creds["SecretAccessKey"],
245
+ "AWS_SESSION_TOKEN": creds["SessionToken"],
246
+ }
@@ -7,9 +7,10 @@ milliseconds. Stateless transports are the app's own concern (see create_app).
7
7
 
8
8
  import os
9
9
 
10
- from app import create_app
11
10
  from mangum import Mangum
12
11
 
12
+ from app import create_app
13
+
13
14
 
14
15
  def lambda_handler(event, context):
15
16
  adapter = Mangum(
@@ -15,10 +15,10 @@ class AwsLambdaPlugin(Plugin):
15
15
  description = (
16
16
  "Deploy to AWS Lambda with SAM; read CloudFormation stacks and functions"
17
17
  )
18
- min_core = "0.16"
18
+ min_core = "0.17"
19
19
  needs = [
20
- "tool: aws (AWS CLI v2), sam (AWS SAM CLI)",
21
- "env: AWS_PROFILE or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, AWS_REGION",
20
+ "tool: aws, sam (bundled as Python packages when not on PATH)",
21
+ "env: AWS_REGION; credentials from role_arn (OIDC, no keys) or the AWS CLI chain (SSO, profile, instance role)",
22
22
  "net: *.amazonaws.com",
23
23
  ]
24
24
 
@@ -0,0 +1,66 @@
1
+ """Running `aws` and `sam` — the two CLIs the targets drive — and reading their JSON."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import json
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from action_platform.core.exception import DeployError
14
+
15
+ MODULES = {"aws": "awscli", "sam": "samcli"}
16
+
17
+
18
+ def require(tool: str, hint: str) -> list[str]:
19
+ """How to run `tool`: the binary on PATH, else the Python module the plugin depends on (`python -m awscli` / `python -m samcli`) — what makes the hosted platform work without the CLIs in its image."""
20
+ path = shutil.which(tool)
21
+
22
+ if path:
23
+ return [path]
24
+
25
+ module = MODULES.get(tool)
26
+
27
+ if module and importlib.util.find_spec(module) is not None:
28
+ return [sys.executable, "-m", module]
29
+
30
+ raise DeployError(f"{tool} is not installed: {hint}")
31
+
32
+
33
+ def run(
34
+ args: list[str], cwd: Path | None = None, env: dict[str, str] | None = None
35
+ ) -> str:
36
+ result = subprocess.run(
37
+ args, cwd=cwd, env=env, capture_output=True, text=True, check=False
38
+ )
39
+
40
+ if result.returncode != 0:
41
+ raise DeployError(
42
+ f"{' '.join(args[:2])} failed: {(result.stderr or result.stdout).strip()[-2000:]}"
43
+ )
44
+
45
+ return result.stdout
46
+
47
+
48
+ def aws(
49
+ *args: str,
50
+ region: str | None = None,
51
+ cwd: Path | None = None,
52
+ env: dict[str, str] | None = None,
53
+ ) -> Any:
54
+ command = [
55
+ *require("aws", "https://aws.amazon.com/cli/"),
56
+ *args,
57
+ "--output",
58
+ "json",
59
+ ]
60
+
61
+ if region:
62
+ command += ["--region", region]
63
+
64
+ out = run(command, cwd=cwd, env=env)
65
+
66
+ return json.loads(out) if out.strip() else {}
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: apx-aws-lambda
3
+ Version: 0.3.0
4
+ Summary: AWS Lambda for Action Platform: the aws/lambda deploy target (SAM) with its overlay, and tools to read stacks and functions.
5
+ Author-email: Action Platform <cloud@actionplatform.io>
6
+ License-Expression: Apache-2.0
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: action-platform>=0.17
11
+ Requires-Dist: awscli>=1.34
12
+ Requires-Dist: aws-sam-cli>=1.120
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=8; extra == "dev"
15
+ Requires-Dist: ruff>=0.6; extra == "dev"
16
+ Requires-Dist: action-platform[mcp]; extra == "dev"
17
+ Dynamic: license-file
18
+
19
+ # apx-aws-lambda
20
+
21
+ AWS Lambda for [Action Platform](https://github.com/actionplatform/action-platform): the `aws/lambda` deploy target (SAM) with its overlay, read-only tools and commands. `apx-` is the prefix every Action Platform extension carries.
22
+
23
+ ```bash
24
+ action-platform plugin install aws-lambda
25
+ action-platform cloud set aws/lambda # overlay files come from this plugin
26
+ action-platform deploy --dry-run # preflight: sam validate, credentials
27
+ action-platform deploy # sam build + sam deploy --config-env <stage>
28
+ action-platform diagnose
29
+ ```
30
+
31
+ | | |
32
+ |---|---|
33
+ | Overlay | `template.yaml`, `samconfig.toml`, `lambda_handler.py`, `Makefile`, `requirements/` (IAM), `.github/workflows/deploy.yml` |
34
+ | Deploy | `sam build` + `sam deploy --config-env <stage>` (`dev` → `default`, `prod` → `prod`) |
35
+ | Rollback | CloudFormation `rollback-stack` — previous stack state |
36
+ | Diagnose | stack status and the HTTP API url |
37
+ | Destroy | `sam delete` |
38
+
39
+ `[deploy]` in platform.toml:
40
+
41
+ ```toml
42
+ [deploy]
43
+ target = "aws/lambda"
44
+ region = "us-east-1" # optional; samconfig.toml / AWS_REGION otherwise
45
+ ```
46
+
47
+ ## Credentials without keys
48
+
49
+ Set `role_arn` and nothing else: the target assumes that role with a short-lived OIDC token the platform signs (`sts assume-role-with-web-identity`). No access key on the platform, on your machine or in the repository; what the app may do is the role's policy.
50
+
51
+ ```toml
52
+ [deploy]
53
+ target = "aws/lambda"
54
+ role_arn = "arn:aws:iam::123456789012:role/shop-deploy"
55
+ ```
56
+
57
+ Once per AWS account, register the platform as an identity provider — its discovery document is `https://<platform>/.well-known/openid-configuration`:
58
+
59
+ ```bash
60
+ aws iam create-open-id-connect-provider --url https://platform.example.com --client-id-list sts.amazonaws.com
61
+ ```
62
+
63
+ Then a role per app (or per project) whose trust policy names the platform and the app; `requirements/policy.json` from the overlay is its permission policy:
64
+
65
+ ```json
66
+ {
67
+ "Version": "2012-10-17",
68
+ "Statement": [{
69
+ "Effect": "Allow",
70
+ "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/platform.example.com" },
71
+ "Action": "sts:AssumeRoleWithWebIdentity",
72
+ "Condition": {
73
+ "StringEquals": { "platform.example.com:aud": "sts.amazonaws.com" },
74
+ "StringLike": { "platform.example.com:sub": "org:acme:project:shop:app:orders" }
75
+ }
76
+ }]
77
+ }
78
+ ```
79
+
80
+ `sub` is `org:<org>:project:<project>:app:<app>` for a deploy the platform runs; `org:<org>` plus an `actor` claim for `action-platform deploy` from a logged-in machine (`StringLike` with `org:acme:*` covers both). Without `role_arn` the AWS CLI's own chain applies: `aws sso login`, a profile, an instance role — still no key in a file when you use SSO.
81
+
82
+ Tools (`action-platform mcp`): `aws_lambda_stacks`, `aws_lambda_functions`. Commands: `action-platform aws-lambda stacks|functions`. Deploying itself goes through the core's `deploy` / `rollback` / `diagnose`, which drive the target.
83
+
84
+ Needs: `aws` and `sam` — on PATH when present, otherwise the `awscli` and `aws-sam-cli` packages the plugin depends on run as `python -m`, which is how the hosted platform deploys without the CLIs in its image; `AWS_REGION` (or `region` under `[deploy]`), and either `role_arn` (OIDC, no keys) or the AWS CLI's own credentials. Talks to `*.amazonaws.com` only.
85
+
86
+ ## Development
87
+
88
+ ```bash
89
+ pip install -e ".[dev]"
90
+ pytest
91
+ ```
92
+
93
+ The tests fake `aws` and `sam`; nothing reaches AWS.
@@ -0,0 +1,8 @@
1
+ action-platform>=0.17
2
+ awscli>=1.34
3
+ aws-sam-cli>=1.120
4
+
5
+ [dev]
6
+ pytest>=8
7
+ ruff>=0.6
8
+ action-platform[mcp]
@@ -1,12 +1,12 @@
1
1
  [project]
2
2
  name = "apx-aws-lambda"
3
- version = "0.1.0"
3
+ version = "0.3.0"
4
4
  description = "AWS Lambda for Action Platform: the aws/lambda deploy target (SAM) with its overlay, and tools to read stacks and functions."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
7
7
  requires-python = ">=3.11"
8
8
  authors = [{ name = "Action Platform", email = "cloud@actionplatform.io" }]
9
- dependencies = ["action-platform>=0.16"]
9
+ dependencies = ["action-platform>=0.17", "awscli>=1.34", "aws-sam-cli>=1.120"]
10
10
 
11
11
  [project.optional-dependencies]
12
12
  dev = ["pytest>=8", "ruff>=0.6", "action-platform[mcp]"]
@@ -43,7 +43,7 @@ class LambdaTargetTest(unittest.TestCase):
43
43
  return json.dumps(answers.get(key, {}))
44
44
 
45
45
  return mock.patch.multiple(
46
- shell, run=run, require=lambda tool, hint: f"/usr/bin/{tool}"
46
+ shell, run=run, require=lambda tool, hint: [f"/usr/bin/{tool}"]
47
47
  )
48
48
 
49
49
  def ctx(self, stage="dev"):
@@ -141,3 +141,53 @@ class PluginTest(unittest.TestCase):
141
141
  names = {t.name for t in asyncio.run(server.build().list_tools())}
142
142
 
143
143
  self.assertLessEqual({"aws_lambda_stacks", "aws_lambda_functions"}, names)
144
+
145
+
146
+ class AssumeRoleTest(unittest.TestCase):
147
+ def test_role_arn_turns_the_platform_token_into_temporary_credentials(self):
148
+ calls: list[list[str]] = []
149
+
150
+ def run(args, cwd=None, env=None):
151
+ calls.append([Path(args[0]).name, *args[1:]])
152
+ if args[1:3] == ["sts", "assume-role-with-web-identity"]:
153
+ return json.dumps(
154
+ {
155
+ "Credentials": {
156
+ "AccessKeyId": "AKIA",
157
+ "SecretAccessKey": "s",
158
+ "SessionToken": "t",
159
+ }
160
+ }
161
+ )
162
+ return json.dumps(
163
+ {"Stacks": [{"StackStatus": "UPDATE_COMPLETE", "Outputs": []}]}
164
+ )
165
+
166
+ ctx = Context(repo_root=Path("."), identity=lambda aud: f"jwt-for-{aud}")
167
+
168
+ with mock.patch.multiple(
169
+ shell, run=run, require=lambda t, h: [f"/usr/bin/{t}"]
170
+ ):
171
+ env = LambdaTarget(
172
+ role_arn="arn:aws:iam::1:role/deploy", region="us-east-1"
173
+ ).env(ctx)
174
+
175
+ self.assertEqual(env["AWS_ACCESS_KEY_ID"], "AKIA")
176
+ self.assertEqual(env["AWS_SESSION_TOKEN"], "t")
177
+ sts = next(
178
+ c for c in calls if c[1:3] == ["sts", "assume-role-with-web-identity"]
179
+ )
180
+ self.assertIn("jwt-for-sts.amazonaws.com", sts)
181
+ self.assertIn("arn:aws:iam::1:role/deploy", sts)
182
+
183
+ def test_without_an_issuer_or_a_login_it_says_so(self):
184
+ with (
185
+ mock.patch.object(shell, "require", lambda t, h: "/usr/bin/aws"),
186
+ mock.patch.dict("os.environ", {"AP_HOME": "/nonexistent"}, clear=False),
187
+ ):
188
+ with self.assertRaises(DeployError) as caught:
189
+ LambdaTarget(role_arn="arn:aws:iam::1:role/deploy").env(
190
+ Context(repo_root=Path("."))
191
+ )
192
+
193
+ self.assertIn("log in", str(caught.exception))
@@ -1,56 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: apx-aws-lambda
3
- Version: 0.1.0
4
- Summary: AWS Lambda for Action Platform: the aws/lambda deploy target (SAM) with its overlay, and tools to read stacks and functions.
5
- Author-email: Action Platform <cloud@actionplatform.io>
6
- License-Expression: Apache-2.0
7
- Requires-Python: >=3.11
8
- Description-Content-Type: text/markdown
9
- License-File: LICENSE
10
- Requires-Dist: action-platform>=0.16
11
- Provides-Extra: dev
12
- Requires-Dist: pytest>=8; extra == "dev"
13
- Requires-Dist: ruff>=0.6; extra == "dev"
14
- Requires-Dist: action-platform[mcp]; extra == "dev"
15
- Dynamic: license-file
16
-
17
- # apx-aws-lambda
18
-
19
- AWS Lambda for [Action Platform](https://github.com/actionplatform/action-platform): the `aws/lambda` deploy target (SAM) with its overlay, read-only tools and commands. `apx-` is the prefix every Action Platform extension carries.
20
-
21
- ```bash
22
- action-platform plugin install aws-lambda
23
- action-platform cloud set aws/lambda # overlay files come from this plugin
24
- action-platform deploy --dry-run # preflight: sam validate, credentials
25
- action-platform deploy # sam build + sam deploy --config-env <stage>
26
- action-platform diagnose
27
- ```
28
-
29
- | | |
30
- |---|---|
31
- | Overlay | `template.yaml`, `samconfig.toml`, `lambda_handler.py`, `Makefile`, `requirements/` (IAM), `.github/workflows/deploy.yml` |
32
- | Deploy | `sam build` + `sam deploy --config-env <stage>` (`dev` → `default`, `prod` → `prod`) |
33
- | Rollback | CloudFormation `rollback-stack` — previous stack state |
34
- | Diagnose | stack status and the HTTP API url |
35
- | Destroy | `sam delete` |
36
-
37
- `[deploy]` in platform.toml:
38
-
39
- ```toml
40
- [deploy]
41
- target = "aws/lambda"
42
- region = "us-east-1" # optional; samconfig.toml / AWS_REGION otherwise
43
- ```
44
-
45
- Tools (`action-platform mcp`): `aws_lambda_stacks`, `aws_lambda_functions`. Commands: `action-platform aws-lambda stacks|functions`. Deploying itself goes through the core's `deploy` / `rollback` / `diagnose`, which drive the target.
46
-
47
- Needs: AWS CLI v2 (`aws`), SAM CLI (`sam`), credentials in the environment (`AWS_PROFILE` or keys), `AWS_REGION`. Talks to `*.amazonaws.com` only.
48
-
49
- ## Development
50
-
51
- ```bash
52
- pip install -e ".[dev]"
53
- pytest
54
- ```
55
-
56
- The tests fake `aws` and `sam`; nothing reaches AWS.
@@ -1,40 +0,0 @@
1
- # apx-aws-lambda
2
-
3
- AWS Lambda for [Action Platform](https://github.com/actionplatform/action-platform): the `aws/lambda` deploy target (SAM) with its overlay, read-only tools and commands. `apx-` is the prefix every Action Platform extension carries.
4
-
5
- ```bash
6
- action-platform plugin install aws-lambda
7
- action-platform cloud set aws/lambda # overlay files come from this plugin
8
- action-platform deploy --dry-run # preflight: sam validate, credentials
9
- action-platform deploy # sam build + sam deploy --config-env <stage>
10
- action-platform diagnose
11
- ```
12
-
13
- | | |
14
- |---|---|
15
- | Overlay | `template.yaml`, `samconfig.toml`, `lambda_handler.py`, `Makefile`, `requirements/` (IAM), `.github/workflows/deploy.yml` |
16
- | Deploy | `sam build` + `sam deploy --config-env <stage>` (`dev` → `default`, `prod` → `prod`) |
17
- | Rollback | CloudFormation `rollback-stack` — previous stack state |
18
- | Diagnose | stack status and the HTTP API url |
19
- | Destroy | `sam delete` |
20
-
21
- `[deploy]` in platform.toml:
22
-
23
- ```toml
24
- [deploy]
25
- target = "aws/lambda"
26
- region = "us-east-1" # optional; samconfig.toml / AWS_REGION otherwise
27
- ```
28
-
29
- Tools (`action-platform mcp`): `aws_lambda_stacks`, `aws_lambda_functions`. Commands: `action-platform aws-lambda stacks|functions`. Deploying itself goes through the core's `deploy` / `rollback` / `diagnose`, which drive the target.
30
-
31
- Needs: AWS CLI v2 (`aws`), SAM CLI (`sam`), credentials in the environment (`AWS_PROFILE` or keys), `AWS_REGION`. Talks to `*.amazonaws.com` only.
32
-
33
- ## Development
34
-
35
- ```bash
36
- pip install -e ".[dev]"
37
- pytest
38
- ```
39
-
40
- The tests fake `aws` and `sam`; nothing reaches AWS.
@@ -1,46 +0,0 @@
1
- """Running `aws` and `sam` — the two CLIs the targets drive — and reading their JSON."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import shutil
7
- import subprocess
8
- from pathlib import Path
9
- from typing import Any
10
-
11
- from action_platform.core.exception import DeployError
12
-
13
-
14
- def require(tool: str, hint: str) -> str:
15
- path = shutil.which(tool)
16
-
17
- if not path:
18
- raise DeployError(f"{tool} is not installed: {hint}")
19
-
20
- return path
21
-
22
-
23
- def run(
24
- args: list[str], cwd: Path | None = None, env: dict[str, str] | None = None
25
- ) -> str:
26
- result = subprocess.run(
27
- args, cwd=cwd, env=env, capture_output=True, text=True, check=False
28
- )
29
-
30
- if result.returncode != 0:
31
- raise DeployError(
32
- f"{' '.join(args[:2])} failed: {(result.stderr or result.stdout).strip()[-2000:]}"
33
- )
34
-
35
- return result.stdout
36
-
37
-
38
- def aws(*args: str, region: str | None = None, cwd: Path | None = None) -> Any:
39
- command = [require("aws", "https://aws.amazon.com/cli/"), *args, "--output", "json"]
40
-
41
- if region:
42
- command += ["--region", region]
43
-
44
- out = run(command, cwd=cwd)
45
-
46
- return json.loads(out) if out.strip() else {}
@@ -1,56 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: apx-aws-lambda
3
- Version: 0.1.0
4
- Summary: AWS Lambda for Action Platform: the aws/lambda deploy target (SAM) with its overlay, and tools to read stacks and functions.
5
- Author-email: Action Platform <cloud@actionplatform.io>
6
- License-Expression: Apache-2.0
7
- Requires-Python: >=3.11
8
- Description-Content-Type: text/markdown
9
- License-File: LICENSE
10
- Requires-Dist: action-platform>=0.16
11
- Provides-Extra: dev
12
- Requires-Dist: pytest>=8; extra == "dev"
13
- Requires-Dist: ruff>=0.6; extra == "dev"
14
- Requires-Dist: action-platform[mcp]; extra == "dev"
15
- Dynamic: license-file
16
-
17
- # apx-aws-lambda
18
-
19
- AWS Lambda for [Action Platform](https://github.com/actionplatform/action-platform): the `aws/lambda` deploy target (SAM) with its overlay, read-only tools and commands. `apx-` is the prefix every Action Platform extension carries.
20
-
21
- ```bash
22
- action-platform plugin install aws-lambda
23
- action-platform cloud set aws/lambda # overlay files come from this plugin
24
- action-platform deploy --dry-run # preflight: sam validate, credentials
25
- action-platform deploy # sam build + sam deploy --config-env <stage>
26
- action-platform diagnose
27
- ```
28
-
29
- | | |
30
- |---|---|
31
- | Overlay | `template.yaml`, `samconfig.toml`, `lambda_handler.py`, `Makefile`, `requirements/` (IAM), `.github/workflows/deploy.yml` |
32
- | Deploy | `sam build` + `sam deploy --config-env <stage>` (`dev` → `default`, `prod` → `prod`) |
33
- | Rollback | CloudFormation `rollback-stack` — previous stack state |
34
- | Diagnose | stack status and the HTTP API url |
35
- | Destroy | `sam delete` |
36
-
37
- `[deploy]` in platform.toml:
38
-
39
- ```toml
40
- [deploy]
41
- target = "aws/lambda"
42
- region = "us-east-1" # optional; samconfig.toml / AWS_REGION otherwise
43
- ```
44
-
45
- Tools (`action-platform mcp`): `aws_lambda_stacks`, `aws_lambda_functions`. Commands: `action-platform aws-lambda stacks|functions`. Deploying itself goes through the core's `deploy` / `rollback` / `diagnose`, which drive the target.
46
-
47
- Needs: AWS CLI v2 (`aws`), SAM CLI (`sam`), credentials in the environment (`AWS_PROFILE` or keys), `AWS_REGION`. Talks to `*.amazonaws.com` only.
48
-
49
- ## Development
50
-
51
- ```bash
52
- pip install -e ".[dev]"
53
- pytest
54
- ```
55
-
56
- The tests fake `aws` and `sam`; nothing reaches AWS.
@@ -1,6 +0,0 @@
1
- action-platform>=0.16
2
-
3
- [dev]
4
- pytest>=8
5
- ruff>=0.6
6
- action-platform[mcp]
File without changes
File without changes