magic-pocket-cli 0.2.0__py3-none-any.whl

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 (65) hide show
  1. magic_pocket_cli-0.2.0.dist-info/METADATA +14 -0
  2. magic_pocket_cli-0.2.0.dist-info/RECORD +65 -0
  3. magic_pocket_cli-0.2.0.dist-info/WHEEL +4 -0
  4. magic_pocket_cli-0.2.0.dist-info/entry_points.txt +2 -0
  5. pocket_cli/__init__.py +0 -0
  6. pocket_cli/cli/__init__.py +0 -0
  7. pocket_cli/cli/aws_auth.py +48 -0
  8. pocket_cli/cli/awscontainer_cli.py +328 -0
  9. pocket_cli/cli/cloudfront_cli.py +116 -0
  10. pocket_cli/cli/cloudfront_keys_cli.py +68 -0
  11. pocket_cli/cli/cloudfront_waf_cli.py +68 -0
  12. pocket_cli/cli/deploy_cli.py +274 -0
  13. pocket_cli/cli/destroy_cli.py +358 -0
  14. pocket_cli/cli/dsql_cli.py +60 -0
  15. pocket_cli/cli/main_cli.py +91 -0
  16. pocket_cli/cli/migrate_cli.py +148 -0
  17. pocket_cli/cli/neon_cli.py +97 -0
  18. pocket_cli/cli/permissions_cli.py +46 -0
  19. pocket_cli/cli/rds_cli.py +63 -0
  20. pocket_cli/cli/runtime_config_cli.py +185 -0
  21. pocket_cli/cli/s3_cli.py +69 -0
  22. pocket_cli/cli/status_cli.py +56 -0
  23. pocket_cli/cli/tidb_cli.py +73 -0
  24. pocket_cli/cli/vpc_cli.py +92 -0
  25. pocket_cli/cli/waf_cli.py +182 -0
  26. pocket_cli/django_cli.py +412 -0
  27. pocket_cli/mediator.py +220 -0
  28. pocket_cli/resources/__init__.py +0 -0
  29. pocket_cli/resources/aws/__init__.py +0 -0
  30. pocket_cli/resources/aws/builders/__init__.py +57 -0
  31. pocket_cli/resources/aws/builders/codebuild.py +363 -0
  32. pocket_cli/resources/aws/builders/depot.py +84 -0
  33. pocket_cli/resources/aws/builders/docker.py +34 -0
  34. pocket_cli/resources/aws/builders/dockerignore.py +44 -0
  35. pocket_cli/resources/aws/cloudformation.py +790 -0
  36. pocket_cli/resources/aws/ecr.py +145 -0
  37. pocket_cli/resources/aws/efs.py +138 -0
  38. pocket_cli/resources/aws/lambdahandler.py +182 -0
  39. pocket_cli/resources/aws/s3_utils.py +58 -0
  40. pocket_cli/resources/aws/state.py +74 -0
  41. pocket_cli/resources/awscontainer.py +265 -0
  42. pocket_cli/resources/cloudfront.py +491 -0
  43. pocket_cli/resources/cloudfront_acm.py +55 -0
  44. pocket_cli/resources/cloudfront_keys.py +81 -0
  45. pocket_cli/resources/cloudfront_waf.py +67 -0
  46. pocket_cli/resources/dsql.py +142 -0
  47. pocket_cli/resources/neon.py +353 -0
  48. pocket_cli/resources/rds.py +680 -0
  49. pocket_cli/resources/s3.py +307 -0
  50. pocket_cli/resources/tidb.py +298 -0
  51. pocket_cli/resources/upstash.py +152 -0
  52. pocket_cli/resources/vpc.py +67 -0
  53. pocket_cli/templates/cloudformation/awscontainer.yaml +516 -0
  54. pocket_cli/templates/cloudformation/cf_function_api_host.js +5 -0
  55. pocket_cli/templates/cloudformation/cf_function_spa_auth.js +28 -0
  56. pocket_cli/templates/cloudformation/cf_function_spa_fallback.js +8 -0
  57. pocket_cli/templates/cloudformation/cloudfront.yaml +309 -0
  58. pocket_cli/templates/cloudformation/cloudfront_acm.yaml +43 -0
  59. pocket_cli/templates/cloudformation/cloudfront_keys.yaml +32 -0
  60. pocket_cli/templates/cloudformation/cloudfront_waf.yaml +97 -0
  61. pocket_cli/templates/cloudformation/vpc.yaml +213 -0
  62. pocket_cli/templates/init/django-dotenv.env +3 -0
  63. pocket_cli/templates/init/django-settings.py +140 -0
  64. pocket_cli/templates/init/pocket.Dockerfile +26 -0
  65. pocket_cli/templates/init/pocket_simple.toml +31 -0
@@ -0,0 +1,60 @@
1
+ import click
2
+
3
+ from pocket.context import Context
4
+ from pocket.utils import echo
5
+ from pocket_cli.resources.dsql import Dsql
6
+
7
+
8
+ def _get_dsql_resource(stage: str) -> Dsql:
9
+ context = Context.from_toml(stage=stage)
10
+ if not context.dsql:
11
+ raise click.ClickException("dsql is not configured for this stage")
12
+ return Dsql(context.dsql)
13
+
14
+
15
+ @click.group()
16
+ def dsql():
17
+ pass
18
+
19
+
20
+ @dsql.command()
21
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
22
+ def status(stage):
23
+ """クラスター状態表示"""
24
+ r = _get_dsql_resource(stage)
25
+ echo.info("Tag Name: %s" % r.context.tag_name)
26
+ echo.info("Status: %s" % r.status)
27
+ if r.identifier:
28
+ echo.info("Identifier: %s" % r.identifier)
29
+
30
+
31
+ @dsql.command()
32
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
33
+ def endpoint(stage):
34
+ """接続情報表示"""
35
+ r = _get_dsql_resource(stage)
36
+ if not r.cluster:
37
+ echo.warning("Cluster not found")
38
+ return
39
+ _print_endpoint(r)
40
+
41
+
42
+ @dsql.command()
43
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
44
+ def destroy(stage):
45
+ """確認付き削除"""
46
+ r = _get_dsql_resource(stage)
47
+ if r.status == "NOEXIST":
48
+ echo.info("DSQL cluster does not exist.")
49
+ return
50
+ click.confirm(
51
+ "DSQL クラスター '%s' を削除しますか?" % r.context.tag_name,
52
+ abort=True,
53
+ )
54
+ r.delete()
55
+
56
+
57
+ def _print_endpoint(r: Dsql):
58
+ echo.success("Endpoint: %s" % r.endpoint)
59
+ echo.success("Region: %s" % r.context.region)
60
+ echo.success("Port: 5432")
@@ -0,0 +1,91 @@
1
+ import click
2
+
3
+ from pocket import __version__
4
+ from pocket_cli import django_cli
5
+ from pocket_cli.cli import (
6
+ awscontainer_cli,
7
+ cloudfront_cli,
8
+ cloudfront_keys_cli,
9
+ cloudfront_waf_cli,
10
+ deploy_cli,
11
+ destroy_cli,
12
+ dsql_cli,
13
+ migrate_cli,
14
+ neon_cli,
15
+ permissions_cli,
16
+ rds_cli,
17
+ runtime_config_cli,
18
+ s3_cli,
19
+ status_cli,
20
+ tidb_cli,
21
+ vpc_cli,
22
+ waf_cli,
23
+ )
24
+
25
+
26
+ class PocketCLI(click.Group):
27
+ def invoke(self, ctx):
28
+ try:
29
+ return super().invoke(ctx)
30
+ except ValueError as e:
31
+ click.echo(f"エラー: {e}", err=True)
32
+ ctx.exit(1)
33
+
34
+
35
+ @click.group(cls=PocketCLI)
36
+ def main():
37
+ pass
38
+
39
+
40
+ @main.command()
41
+ def version():
42
+ """Print the version number."""
43
+ click.echo(__version__)
44
+
45
+
46
+ @main.command()
47
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
48
+ def context(stage):
49
+ """Context を JSON で出力する(AWS API 呼び出しを伴う)。"""
50
+ from pocket.context import Context
51
+
52
+ ctx = Context.from_toml(stage=stage)
53
+ print(ctx.model_dump_json(indent=2))
54
+
55
+
56
+ @main.command()
57
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
58
+ def settings(stage):
59
+ """Settings を JSON で出力する(pocket.toml のみ、AWS 不要)。"""
60
+ from pocket.settings import Settings
61
+
62
+ s = Settings.from_toml(stage=stage)
63
+ print(s.model_dump_json(indent=2))
64
+
65
+
66
+ main.add_command(deploy_cli.deploy)
67
+ main.add_command(deploy_cli.promote)
68
+ main.add_command(destroy_cli.destroy)
69
+ main.add_command(status_cli.status)
70
+ main.add_command(django_cli.django)
71
+ main.add_command(runtime_config_cli.runtime_config)
72
+ main.add_command(migrate_cli.migrate)
73
+ main.add_command(permissions_cli.permissions)
74
+ main.add_command(waf_cli.waf)
75
+
76
+
77
+ @main.group()
78
+ def resource():
79
+ pass
80
+
81
+
82
+ resource.add_command(vpc_cli.vpc)
83
+ resource.add_command(awscontainer_cli.awscontainer)
84
+ resource.add_command(neon_cli.neon)
85
+ resource.add_command(tidb_cli.tidb)
86
+ resource.add_command(dsql_cli.dsql)
87
+ resource.add_command(rds_cli.rds)
88
+ resource.add_command(s3_cli.s3)
89
+ resource.add_command(cloudfront_cli.cloudfront)
90
+ resource.add_command(cloudfront_keys_cli.cloudfront_keys)
91
+ resource.add_command(cloudfront_waf_cli.cloudfront_waf)
@@ -0,0 +1,148 @@
1
+ """pocket migrate: スタックのテンプレートハッシュタグを一括付与する。
2
+
3
+ 旧バージョンでデプロイされたスタックに pocket:template_hash タグを付与し、
4
+ yaml_synced の判定をハッシュベースに移行する。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+
11
+ import click
12
+ import yaml
13
+
14
+ from pocket.context import Context
15
+ from pocket.utils import echo
16
+ from pocket_cli.cli.deploy_cli import get_resources
17
+
18
+
19
+ def _get_existing_stacks(context: Context) -> list:
20
+ """デプロイ済みの全スタックを収集する"""
21
+ stacks = []
22
+ for resource in get_resources(context):
23
+ if hasattr(resource, "stack") and resource.stack.exists:
24
+ stacks.append(resource.stack)
25
+ return stacks
26
+
27
+
28
+ def _compute_uploaded_hash(stack) -> str | None:
29
+ """uploaded template からハッシュを計算する"""
30
+ uploaded = stack.uploaded_template
31
+ if uploaded is None:
32
+ return None
33
+ if isinstance(uploaded, str):
34
+ return hashlib.sha256(uploaded.encode()).hexdigest()[:16]
35
+ return hashlib.sha256(
36
+ yaml.dump(dict(uploaded), sort_keys=True).encode()
37
+ ).hexdigest()[:16]
38
+
39
+
40
+ def _backfill_tags(stacks: list) -> None:
41
+ """タグがないスタックに uploaded template のハッシュでタグを付与する"""
42
+ for stack in stacks:
43
+ if stack._deployed_template_hash is not None:
44
+ continue
45
+ h = _compute_uploaded_hash(stack)
46
+ if h is None:
47
+ continue
48
+ echo.log("タグ付与 (uploaded hash): %s (%s)" % (stack.name, h))
49
+ stack.client.update_stack(
50
+ StackName=stack.name,
51
+ UsePreviousTemplate=True,
52
+ Capabilities=stack.capabilities,
53
+ Tags=stack.stack_tags + [{"Key": "pocket:template_hash", "Value": h}],
54
+ )
55
+ stack.wait_status("COMPLETED", timeout=300, interval=5)
56
+ stack.clear_status()
57
+ echo.success("完了: %s" % stack.name)
58
+
59
+
60
+ def _find_stacks_needing_update(stacks: list) -> list:
61
+ """ローカルテンプレートとハッシュが異なるスタックを返す"""
62
+ targets = []
63
+ for stack in stacks:
64
+ if stack._deployed_template_hash == stack._template_hash:
65
+ echo.info("%s: タグは最新です" % stack.name)
66
+ continue
67
+ targets.append(stack)
68
+ return targets
69
+
70
+
71
+ def _apply_local_tags(targets: list) -> None:
72
+ """ローカルテンプレートのハッシュでタグを更新する"""
73
+ for stack in targets:
74
+ echo.log("タグ更新 (local hash): %s" % stack.name)
75
+ stack.client.update_stack(
76
+ StackName=stack.name,
77
+ UsePreviousTemplate=True,
78
+ Capabilities=stack.capabilities,
79
+ Tags=stack._build_tags(),
80
+ )
81
+ stack.wait_status("COMPLETED", timeout=300, interval=5)
82
+ echo.success("完了: %s" % stack.name)
83
+
84
+
85
+ def _check_real_diffs(stacks: list) -> list[str]:
86
+ """非 ASCII 文字化け以外のテンプレート差分があるスタックを返す"""
87
+ needs_deploy = []
88
+ for stack in stacks:
89
+ diff = stack.yaml_diff
90
+ if diff == {}:
91
+ continue
92
+ if set(diff.keys()) == {"values_changed"}:
93
+ all_garbled = all(
94
+ isinstance(c.get("old_value"), str) and "??" in c["old_value"]
95
+ for c in diff["values_changed"].values()
96
+ )
97
+ if all_garbled:
98
+ continue
99
+ needs_deploy.append(stack.name)
100
+ return needs_deploy
101
+
102
+
103
+ @click.command()
104
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
105
+ @click.option(
106
+ "--yes", "-y", is_flag=True, default=False, help="確認プロンプトをスキップ"
107
+ )
108
+ def migrate(stage: str, yes: bool):
109
+ """スタックのテンプレートハッシュタグを一括付与する"""
110
+ from pocket_cli.cli.aws_auth import check_aws_credentials
111
+
112
+ check_aws_credentials()
113
+ context = Context.from_toml(stage=stage)
114
+ stacks = _get_existing_stacks(context)
115
+
116
+ if not stacks:
117
+ echo.warning("対象のスタックがありません。")
118
+ return
119
+
120
+ # 1. タグがないスタックに uploaded template のハッシュで仮タグを付与
121
+ _backfill_tags(stacks)
122
+
123
+ # 2. ローカルとの差分チェック(非 ASCII 文字化け以外の差分があれば中断)
124
+ needs_deploy = _check_real_diffs(stacks)
125
+ if needs_deploy:
126
+ echo.danger("以下のスタックにテンプレートの差分があります:")
127
+ for name in needs_deploy:
128
+ echo.info(" - %s" % name)
129
+ echo.danger(
130
+ "先に pocket deploy --stage=%s で最新バージョンを"
131
+ "デプロイしてから再実行してください。" % stage
132
+ )
133
+ raise SystemExit(1)
134
+
135
+ # 3. ローカルテンプレートのハッシュでタグを更新
136
+ targets = _find_stacks_needing_update(stacks)
137
+ if not targets:
138
+ echo.success("全スタックのタグが最新です。")
139
+ return
140
+
141
+ echo.info("以下のスタックのタグをローカルハッシュに更新します:")
142
+ for stack in targets:
143
+ echo.info(" - %s" % stack.name)
144
+ if not yes:
145
+ click.confirm("実行しますか?", abort=True)
146
+
147
+ _apply_local_tags(targets)
148
+ echo.success("全スタックのマイグレーションが完了しました。")
@@ -0,0 +1,97 @@
1
+ from pprint import pprint
2
+
3
+ import click
4
+
5
+ from pocket.context import Context
6
+ from pocket.utils import echo
7
+ from pocket_cli.resources.neon import Neon
8
+
9
+
10
+ @click.group()
11
+ def neon():
12
+ pass
13
+
14
+
15
+ def get_neon_resource(stage):
16
+ context = Context.from_toml(stage=stage)
17
+ if not context.neon:
18
+ echo.danger("neon is not configured for this stage")
19
+ raise Exception("neon is not configured for this stage")
20
+ return Neon(context=context.neon)
21
+
22
+
23
+ @neon.command()
24
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
25
+ def context(stage):
26
+ neon = get_neon_resource(stage)
27
+ pprint(neon.context.model_dump())
28
+
29
+
30
+ @neon.command()
31
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
32
+ def create(stage):
33
+ neon = get_neon_resource(stage)
34
+ neon.create()
35
+ echo.success("New branch was created")
36
+
37
+
38
+ @neon.command()
39
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
40
+ def reset_database(stage):
41
+ neon = get_neon_resource(stage)
42
+ neon.reset_database()
43
+ echo.success("Reset database")
44
+
45
+
46
+ @neon.command()
47
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
48
+ @click.option("--base-stage", default=None)
49
+ def branch_out(stage, base_stage):
50
+ neon = get_neon_resource(stage)
51
+ if neon.branch:
52
+ raise Exception("Branch already exists")
53
+ base_neon = get_neon_resource(base_stage)
54
+ if not base_neon.working:
55
+ raise Exception("Base stage is not working")
56
+ assert base_neon.branch
57
+ neon.create_branch(base_neon.branch)
58
+ echo.success("New branch was created")
59
+
60
+
61
+ @neon.command()
62
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
63
+ def delete(stage):
64
+ neon = get_neon_resource(stage)
65
+ neon.delete_branch()
66
+ echo.success("Branch was deleted successfully.")
67
+
68
+
69
+ @neon.command()
70
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
71
+ def status(stage):
72
+ neon = get_neon_resource(stage)
73
+ if neon.project:
74
+ echo.success("Project found")
75
+ else:
76
+ echo.warning("Project not found")
77
+ return
78
+ if neon.branch:
79
+ echo.success("Branch found")
80
+ else:
81
+ echo.warning("Branch not found")
82
+ return
83
+ if neon.database:
84
+ echo.success("Database found")
85
+ else:
86
+ echo.warning("Database not found")
87
+ return
88
+ if neon.endpoint:
89
+ echo.success("Endpoint found: %s" % neon.endpoint.host)
90
+ else:
91
+ echo.warning("Endpoint not found")
92
+ if neon.role:
93
+ echo.success("Role found: %s" % neon.context.role_name)
94
+ else:
95
+ echo.warning("Role not found")
96
+ if neon.role and neon.endpoint:
97
+ echo.success("Database url: %s" % neon.database_url)
@@ -0,0 +1,46 @@
1
+ """`pocket permissions` サブコマンド群。
2
+
3
+ `pocket.toml` の構成から必要な AWS IAM Action を算出して出力する。
4
+ 外部の IAM Role プロビジョニング処理が GitHub Actions デプロイ用 IAM Role を
5
+ 作る際の inline policy 生成に使用することを想定。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+
12
+ import click
13
+
14
+ from pocket.permissions import compute_actions
15
+ from pocket.settings import Settings
16
+
17
+
18
+ @click.group()
19
+ def permissions():
20
+ """IAM 権限関連のサブコマンド。"""
21
+
22
+
23
+ @permissions.command("list")
24
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
25
+ @click.option(
26
+ "--format",
27
+ "format_",
28
+ type=click.Choice(["text", "json"]),
29
+ default="text",
30
+ show_default=True,
31
+ help='出力形式。text: 1 行 1 Action / json: {"actions": [...]}',
32
+ )
33
+ def list_(stage: str, format_: str):
34
+ """pocket.toml から必要な AWS Action 一覧を出力する。
35
+
36
+ docs/permissions/aws.md のテーブルに基づき、`[cloudfront]` / `[rds]` /
37
+ `[ses]` などの設定有無に応じて必要 Action を組み立てる。粒度は
38
+ ワイルドカード中心 (`cloudformation:*` 等)。
39
+ """
40
+ settings = Settings.from_toml(stage=stage)
41
+ actions = compute_actions(settings)
42
+ if format_ == "json":
43
+ click.echo(json.dumps({"actions": actions}, indent=2))
44
+ else:
45
+ for action in actions:
46
+ click.echo(action)
@@ -0,0 +1,63 @@
1
+ import click
2
+
3
+ from pocket.context import Context
4
+ from pocket.utils import echo
5
+ from pocket_cli.resources.rds import Rds
6
+
7
+
8
+ def _get_rds_resource(stage: str) -> Rds:
9
+ context = Context.from_toml(stage=stage)
10
+ if not context.rds:
11
+ raise click.ClickException("rds is not configured for this stage")
12
+ return Rds(context.rds)
13
+
14
+
15
+ @click.group()
16
+ def rds():
17
+ pass
18
+
19
+
20
+ @rds.command()
21
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
22
+ def status(stage):
23
+ """クラスター状態表示"""
24
+ r = _get_rds_resource(stage)
25
+ echo.info("Cluster: %s" % r.context.cluster_identifier)
26
+ echo.info("Status: %s" % r.status)
27
+ if r.cluster:
28
+ echo.info("Engine: %s" % r.cluster.get("EngineVersion", ""))
29
+
30
+
31
+ @rds.command()
32
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
33
+ def endpoint(stage):
34
+ """接続情報表示"""
35
+ r = _get_rds_resource(stage)
36
+ if not r.cluster:
37
+ echo.warning("Cluster not found")
38
+ return
39
+ _print_endpoint(r)
40
+
41
+
42
+ @rds.command()
43
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
44
+ def destroy(stage):
45
+ """確認付き削除"""
46
+ r = _get_rds_resource(stage)
47
+ if r.status == "NOEXIST":
48
+ echo.info("RDS cluster does not exist.")
49
+ return
50
+ click.confirm(
51
+ "RDS Aurora クラスター '%s' を削除しますか?" % r.context.cluster_identifier,
52
+ abort=True,
53
+ )
54
+ r.delete()
55
+ echo.success("RDS Aurora cluster was destroyed. Final snapshot was created.")
56
+
57
+
58
+ def _print_endpoint(r: Rds):
59
+ assert r.cluster
60
+ echo.success("Endpoint: %s" % r.cluster.get("Endpoint", ""))
61
+ echo.success("Port: %s" % r.cluster.get("Port", ""))
62
+ echo.success("Database: %s" % r.context.database_name)
63
+ echo.success("Username: %s" % r.context.master_username)
@@ -0,0 +1,185 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import click
8
+
9
+ from pocket.utils import get_toml_path
10
+
11
+ if sys.version_info >= (3, 11):
12
+ import tomllib
13
+ else:
14
+ import tomli as tomllib
15
+
16
+ # awscontainer から除外するキー(ビルド時のみ必要)
17
+ _AWSCONTAINER_REMOVE_KEYS = {
18
+ "platform",
19
+ "build",
20
+ "permissions_boundary",
21
+ "use_vpc",
22
+ }
23
+
24
+ # awscontainer でダミー値に置き換えるキー(必須フィールドだが runtime では不要)
25
+ _AWSCONTAINER_DUMMY_VALUES = {
26
+ "dockerfile_path": "__runtime__",
27
+ }
28
+
29
+ # cloudfront の各エントリから除外するキー
30
+ _CLOUDFRONT_REMOVE_KEYS = {
31
+ "managed_assets",
32
+ "hosted_zone_id_override",
33
+ "redirect_from",
34
+ "signing_key",
35
+ "token_secret",
36
+ }
37
+
38
+ # route から除外するキー
39
+ _ROUTE_REMOVE_KEYS = {
40
+ "build",
41
+ "build_dir",
42
+ "require_token",
43
+ "login_path",
44
+ }
45
+
46
+ # awscontainer.django から除外するキー
47
+ _DJANGO_REMOVE_KEYS = {
48
+ "project_dir",
49
+ }
50
+
51
+ # トップレベルから除外するセクション
52
+ _TOPLEVEL_REMOVE_KEYS = {
53
+ "vpc",
54
+ }
55
+
56
+
57
+ def _remove_keys(d: dict, keys: set[str]) -> None:
58
+ for key in keys:
59
+ d.pop(key, None)
60
+
61
+
62
+ def _clean_cloudfront(cf: dict) -> None:
63
+ _remove_keys(cf, _CLOUDFRONT_REMOVE_KEYS)
64
+ for route in cf.get("routes", []):
65
+ _remove_keys(route, _ROUTE_REMOVE_KEYS)
66
+
67
+
68
+ def _clean_section(section: dict) -> None:
69
+ """awscontainer / cloudfront セクションをクリーンアップする"""
70
+ if "awscontainer" in section:
71
+ ac = section["awscontainer"]
72
+ _remove_keys(ac, _AWSCONTAINER_REMOVE_KEYS)
73
+ for key, value in _AWSCONTAINER_DUMMY_VALUES.items():
74
+ if key in ac:
75
+ ac[key] = value
76
+ if "django" in ac:
77
+ _remove_keys(ac["django"], _DJANGO_REMOVE_KEYS)
78
+ if "cloudfront" in section:
79
+ for cf in section["cloudfront"].values():
80
+ _clean_cloudfront(cf)
81
+
82
+
83
+ def _clean_data(data: dict) -> dict:
84
+ """pocket.toml のデータからランタイムに不要な設定を除外する"""
85
+ result = copy.deepcopy(data)
86
+ _remove_keys(result, _TOPLEVEL_REMOVE_KEYS)
87
+ _clean_section(result)
88
+ for stage in result.get("general", {}).get("stages", []):
89
+ if stage in result:
90
+ _clean_section(result[stage])
91
+ return result
92
+
93
+
94
+ def _to_toml(data: dict, prefix: str = "") -> str:
95
+ """dict を TOML 文字列に変換する(簡易実装)"""
96
+ lines: list[str] = []
97
+ # まずスカラー値とリストを出力
98
+ for key, value in data.items():
99
+ if isinstance(value, dict):
100
+ continue
101
+ lines.append(_format_value(key, value))
102
+
103
+ # dict 値をセクションとして出力
104
+ for key, value in data.items():
105
+ if not isinstance(value, dict):
106
+ continue
107
+ section = f"{prefix}{key}" if prefix else key
108
+ # dict の中身が全て dict なら、各サブキーをサブセクションに
109
+ if all(isinstance(v, dict) for v in value.values()) and value:
110
+ for sub_key, sub_value in value.items():
111
+ lines.append("")
112
+ lines.append(f"[{section}.{sub_key}]")
113
+ lines.append(_to_toml(sub_value, prefix=f"{section}.{sub_key}."))
114
+ continue
115
+ lines.append("")
116
+ lines.append(f"[{section}]")
117
+ lines.append(_to_toml(value, prefix=f"{section}."))
118
+
119
+ return "\n".join(lines)
120
+
121
+
122
+ def _format_value(key: str, value) -> str:
123
+ if isinstance(value, bool):
124
+ return f"{key} = {'true' if value else 'false'}"
125
+ if isinstance(value, int):
126
+ return f"{key} = {value}"
127
+ if isinstance(value, str):
128
+ return f'{key} = "{value}"'
129
+ if isinstance(value, list):
130
+ return f"{key} = {_format_list(value)}"
131
+ return f"{key} = {value!r}"
132
+
133
+
134
+ def _format_list(items: list) -> str:
135
+ if not items:
136
+ return "[]"
137
+ if all(isinstance(i, str) for i in items):
138
+ return "[%s]" % ", ".join(f'"{i}"' for i in items)
139
+ if all(isinstance(i, dict) for i in items):
140
+ parts = []
141
+ for item in items:
142
+ kvs = ", ".join(f"{k} = {_format_inline_value(v)}" for k, v in item.items())
143
+ parts.append("{ %s }" % kvs)
144
+ return "[\n %s,\n]" % ",\n ".join(parts)
145
+ return repr(items)
146
+
147
+
148
+ def _format_inline_value(value) -> str:
149
+ if isinstance(value, bool):
150
+ return "true" if value else "false"
151
+ if isinstance(value, int):
152
+ return str(value)
153
+ if isinstance(value, str):
154
+ return f'"{value}"'
155
+ return repr(value)
156
+
157
+
158
+ def generate_runtime_config(output_path: Path) -> None:
159
+ """pocket.runtime.toml を生成する(プログラムから呼び出し用)"""
160
+ toml_path = get_toml_path()
161
+ data = tomllib.loads(toml_path.read_text())
162
+ cleaned = _clean_data(data)
163
+ toml_str = _to_toml(cleaned).strip() + "\n"
164
+ output_path.write_text(toml_str)
165
+
166
+
167
+ @click.command("runtime-config")
168
+ @click.argument("output", default="-")
169
+ def runtime_config(output: str):
170
+ """Lambda ランタイム用の pocket.toml を生成する
171
+
172
+ ビルド時のみ必要な設定(dockerfile_path, managed_assets 等)を
173
+ 除外した pocket.toml を出力する。Dockerfile 内で使用する。
174
+
175
+ OUTPUT: 出力先ファイルパス(省略時は標準出力)
176
+ """
177
+ if output == "-":
178
+ toml_path = get_toml_path()
179
+ data = tomllib.loads(toml_path.read_text())
180
+ cleaned = _clean_data(data)
181
+ toml_str = _to_toml(cleaned).strip() + "\n"
182
+ click.echo(toml_str, nl=False)
183
+ else:
184
+ generate_runtime_config(Path(output))
185
+ click.echo("runtime-config を出力しました: %s" % output)