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.
- magic_pocket_cli-0.2.0.dist-info/METADATA +14 -0
- magic_pocket_cli-0.2.0.dist-info/RECORD +65 -0
- magic_pocket_cli-0.2.0.dist-info/WHEEL +4 -0
- magic_pocket_cli-0.2.0.dist-info/entry_points.txt +2 -0
- pocket_cli/__init__.py +0 -0
- pocket_cli/cli/__init__.py +0 -0
- pocket_cli/cli/aws_auth.py +48 -0
- pocket_cli/cli/awscontainer_cli.py +328 -0
- pocket_cli/cli/cloudfront_cli.py +116 -0
- pocket_cli/cli/cloudfront_keys_cli.py +68 -0
- pocket_cli/cli/cloudfront_waf_cli.py +68 -0
- pocket_cli/cli/deploy_cli.py +274 -0
- pocket_cli/cli/destroy_cli.py +358 -0
- pocket_cli/cli/dsql_cli.py +60 -0
- pocket_cli/cli/main_cli.py +91 -0
- pocket_cli/cli/migrate_cli.py +148 -0
- pocket_cli/cli/neon_cli.py +97 -0
- pocket_cli/cli/permissions_cli.py +46 -0
- pocket_cli/cli/rds_cli.py +63 -0
- pocket_cli/cli/runtime_config_cli.py +185 -0
- pocket_cli/cli/s3_cli.py +69 -0
- pocket_cli/cli/status_cli.py +56 -0
- pocket_cli/cli/tidb_cli.py +73 -0
- pocket_cli/cli/vpc_cli.py +92 -0
- pocket_cli/cli/waf_cli.py +182 -0
- pocket_cli/django_cli.py +412 -0
- pocket_cli/mediator.py +220 -0
- pocket_cli/resources/__init__.py +0 -0
- pocket_cli/resources/aws/__init__.py +0 -0
- pocket_cli/resources/aws/builders/__init__.py +57 -0
- pocket_cli/resources/aws/builders/codebuild.py +363 -0
- pocket_cli/resources/aws/builders/depot.py +84 -0
- pocket_cli/resources/aws/builders/docker.py +34 -0
- pocket_cli/resources/aws/builders/dockerignore.py +44 -0
- pocket_cli/resources/aws/cloudformation.py +790 -0
- pocket_cli/resources/aws/ecr.py +145 -0
- pocket_cli/resources/aws/efs.py +138 -0
- pocket_cli/resources/aws/lambdahandler.py +182 -0
- pocket_cli/resources/aws/s3_utils.py +58 -0
- pocket_cli/resources/aws/state.py +74 -0
- pocket_cli/resources/awscontainer.py +265 -0
- pocket_cli/resources/cloudfront.py +491 -0
- pocket_cli/resources/cloudfront_acm.py +55 -0
- pocket_cli/resources/cloudfront_keys.py +81 -0
- pocket_cli/resources/cloudfront_waf.py +67 -0
- pocket_cli/resources/dsql.py +142 -0
- pocket_cli/resources/neon.py +353 -0
- pocket_cli/resources/rds.py +680 -0
- pocket_cli/resources/s3.py +307 -0
- pocket_cli/resources/tidb.py +298 -0
- pocket_cli/resources/upstash.py +152 -0
- pocket_cli/resources/vpc.py +67 -0
- pocket_cli/templates/cloudformation/awscontainer.yaml +516 -0
- pocket_cli/templates/cloudformation/cf_function_api_host.js +5 -0
- pocket_cli/templates/cloudformation/cf_function_spa_auth.js +28 -0
- pocket_cli/templates/cloudformation/cf_function_spa_fallback.js +8 -0
- pocket_cli/templates/cloudformation/cloudfront.yaml +309 -0
- pocket_cli/templates/cloudformation/cloudfront_acm.yaml +43 -0
- pocket_cli/templates/cloudformation/cloudfront_keys.yaml +32 -0
- pocket_cli/templates/cloudformation/cloudfront_waf.yaml +97 -0
- pocket_cli/templates/cloudformation/vpc.yaml +213 -0
- pocket_cli/templates/init/django-dotenv.env +3 -0
- pocket_cli/templates/init/django-settings.py +140 -0
- pocket_cli/templates/init/pocket.Dockerfile +26 -0
- 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)
|