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,68 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from pocket.context import Context
|
|
4
|
+
from pocket.utils import echo
|
|
5
|
+
from pocket_cli.resources.cloudfront_waf import CloudFrontWaf
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.group()
|
|
9
|
+
def cloudfront_waf():
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_cloudfront_waf_resources(stage, name=None):
|
|
14
|
+
context = Context.from_toml(stage=stage)
|
|
15
|
+
if not context.cloudfront:
|
|
16
|
+
echo.danger("cloudfront is not configured for this stage")
|
|
17
|
+
raise Exception("cloudfront is not configured for this stage")
|
|
18
|
+
results = []
|
|
19
|
+
for cf_name, cf_ctx in context.cloudfront.items():
|
|
20
|
+
if cf_ctx.waf is None:
|
|
21
|
+
continue
|
|
22
|
+
if name and cf_name != name:
|
|
23
|
+
continue
|
|
24
|
+
results.append(CloudFrontWaf(cf_ctx))
|
|
25
|
+
if name and not results:
|
|
26
|
+
echo.danger("cloudfront_waf '%s' is not configured" % name)
|
|
27
|
+
raise Exception("cloudfront_waf '%s' is not configured" % name)
|
|
28
|
+
return results
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@cloudfront_waf.command()
|
|
32
|
+
@click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
|
|
33
|
+
@click.option("--name", default=None)
|
|
34
|
+
def yaml(stage, name):
|
|
35
|
+
for waf in get_cloudfront_waf_resources(stage, name):
|
|
36
|
+
echo.info("[%s]" % waf.context.name)
|
|
37
|
+
print(waf.stack.yaml)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@cloudfront_waf.command()
|
|
41
|
+
@click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
|
|
42
|
+
@click.option("--name", default=None)
|
|
43
|
+
def yaml_diff(stage, name):
|
|
44
|
+
for waf in get_cloudfront_waf_resources(stage, name):
|
|
45
|
+
echo.info("[%s]" % waf.context.name)
|
|
46
|
+
print(waf.stack.yaml_diff.to_json(indent=2))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@cloudfront_waf.command()
|
|
50
|
+
@click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
|
|
51
|
+
@click.option("--name", default=None)
|
|
52
|
+
def status(stage, name):
|
|
53
|
+
for waf in get_cloudfront_waf_resources(stage, name):
|
|
54
|
+
echo.info("[%s]" % waf.context.name)
|
|
55
|
+
if waf.status == "COMPLETED":
|
|
56
|
+
echo.success("COMPLETED")
|
|
57
|
+
else:
|
|
58
|
+
print(waf.status)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@cloudfront_waf.command()
|
|
62
|
+
@click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
|
|
63
|
+
@click.option("--name", default=None)
|
|
64
|
+
def destroy(stage, name):
|
|
65
|
+
for waf in get_cloudfront_waf_resources(stage, name):
|
|
66
|
+
echo.info("[%s]" % waf.context.name)
|
|
67
|
+
waf.delete()
|
|
68
|
+
echo.success("cloudfront_waf was deleted successfully.")
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import webbrowser
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from pocket.context import Context
|
|
7
|
+
from pocket.utils import echo
|
|
8
|
+
from pocket_cli.mediator import Mediator
|
|
9
|
+
from pocket_cli.resources.aws.state import StateStore
|
|
10
|
+
from pocket_cli.resources.awscontainer import AwsContainer
|
|
11
|
+
from pocket_cli.resources.cloudfront import CloudFront
|
|
12
|
+
from pocket_cli.resources.cloudfront_acm import CloudFrontAcm
|
|
13
|
+
from pocket_cli.resources.cloudfront_keys import CloudFrontKeys
|
|
14
|
+
from pocket_cli.resources.cloudfront_waf import CloudFrontWaf
|
|
15
|
+
from pocket_cli.resources.dsql import Dsql
|
|
16
|
+
from pocket_cli.resources.neon import Neon
|
|
17
|
+
from pocket_cli.resources.rds import Rds
|
|
18
|
+
from pocket_cli.resources.s3 import S3
|
|
19
|
+
from pocket_cli.resources.tidb import TiDb
|
|
20
|
+
from pocket_cli.resources.upstash import Upstash
|
|
21
|
+
from pocket_cli.resources.vpc import Vpc
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _append_infra_resources(resources, context: Context, state_bucket: str):
|
|
25
|
+
"""VPC / RDS / CloudFrontKeys / AwsContainer をまとめて追加"""
|
|
26
|
+
if context.awscontainer and context.awscontainer.vpc:
|
|
27
|
+
if context.awscontainer.vpc.manage:
|
|
28
|
+
resources.append(Vpc(context.awscontainer.vpc))
|
|
29
|
+
if context.dsql:
|
|
30
|
+
resources.append(Dsql(context.dsql))
|
|
31
|
+
if context.rds:
|
|
32
|
+
resources.append(Rds(context.rds))
|
|
33
|
+
for _name, cf_ctx in context.cloudfront.items():
|
|
34
|
+
if cf_ctx.signing_key:
|
|
35
|
+
resources.append(CloudFrontKeys(cf_ctx))
|
|
36
|
+
if context.awscontainer:
|
|
37
|
+
resources.append(
|
|
38
|
+
AwsContainer(
|
|
39
|
+
context.awscontainer,
|
|
40
|
+
state_bucket=state_bucket,
|
|
41
|
+
rds_context=context.rds,
|
|
42
|
+
dsql_context=context.dsql,
|
|
43
|
+
scheduler_context=context.scheduler,
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_resources(context: Context, *, state_bucket: str = ""):
|
|
49
|
+
resources = []
|
|
50
|
+
# ACM 証明書を最初にデプロイ(us-east-1、DNS 検証に時間がかかる)
|
|
51
|
+
for _name, cf_ctx in context.cloudfront.items():
|
|
52
|
+
if cf_ctx.domain:
|
|
53
|
+
resources.append(CloudFrontAcm(cf_ctx))
|
|
54
|
+
# WAF (IPSet + WebACL) も us-east-1 必須、CloudFront stack より前に作成
|
|
55
|
+
for _name, cf_ctx in context.cloudfront.items():
|
|
56
|
+
if cf_ctx.waf is not None:
|
|
57
|
+
resources.append(CloudFrontWaf(cf_ctx))
|
|
58
|
+
if context.neon:
|
|
59
|
+
resources.append(Neon(context.neon))
|
|
60
|
+
if context.tidb:
|
|
61
|
+
resources.append(TiDb(context.tidb))
|
|
62
|
+
if context.upstash:
|
|
63
|
+
resources.append(Upstash(context.upstash))
|
|
64
|
+
if context.s3:
|
|
65
|
+
resources.append(S3(context.s3, cloudfront_contexts=context.cloudfront))
|
|
66
|
+
_append_infra_resources(resources, context, state_bucket)
|
|
67
|
+
for _name, cf_ctx in context.cloudfront.items():
|
|
68
|
+
resources.append(CloudFront(cf_ctx))
|
|
69
|
+
return resources
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _create_state_store(context: Context) -> StateStore:
|
|
73
|
+
assert context.general
|
|
74
|
+
resource_prefix = context.general.prefix_template.format(
|
|
75
|
+
stage=context.stage,
|
|
76
|
+
project=context.project_name,
|
|
77
|
+
namespace=context.general.namespace,
|
|
78
|
+
)
|
|
79
|
+
bucket_name = f"{resource_prefix}state"
|
|
80
|
+
return StateStore(bucket_name, context.general.region)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def deploy_init_resources(context: Context, *, state_bucket: str = ""):
|
|
84
|
+
for resource in get_resources(context, state_bucket=state_bucket):
|
|
85
|
+
target_name = resource.__class__.__name__
|
|
86
|
+
echo.log("Deploy init %s..." % target_name)
|
|
87
|
+
resource.deploy_init()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def deploy_frontend(context: Context, *, skip_build: bool = False):
|
|
91
|
+
for _name, cf_ctx in context.cloudfront.items():
|
|
92
|
+
cf = CloudFront(cf_ctx)
|
|
93
|
+
if not cf_ctx.uploadable_routes:
|
|
94
|
+
continue
|
|
95
|
+
if cf.status == "NOEXIST":
|
|
96
|
+
echo.warning("CloudFront '%s' が未作成です。スキップします。" % cf_ctx.name)
|
|
97
|
+
continue
|
|
98
|
+
cf.upload(skip_build=skip_build)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def upload_managed_assets(context: Context):
|
|
102
|
+
"""CloudFront resource ごとに managed_assets を S3 に同期する。
|
|
103
|
+
|
|
104
|
+
deploy_resources の後で呼ぶことで、CFn stack の有無に関わらず毎回実行される。
|
|
105
|
+
差分検知 (ローカル MD5 vs S3 ETag) により変更ファイルのみ PutObject される。
|
|
106
|
+
"""
|
|
107
|
+
for _name, cf_ctx in context.cloudfront.items():
|
|
108
|
+
if not cf_ctx.managed_assets:
|
|
109
|
+
continue
|
|
110
|
+
cf = CloudFront(cf_ctx)
|
|
111
|
+
cf.upload_managed_assets()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def deploy_resources(context: Context, *, state_bucket: str = ""):
|
|
115
|
+
state_store = _create_state_store(context)
|
|
116
|
+
# state bucket は deploy_init_resources の前に作成済み
|
|
117
|
+
# ここでは念のため再確認
|
|
118
|
+
state_store.ensure_bucket()
|
|
119
|
+
|
|
120
|
+
mediator = Mediator(context)
|
|
121
|
+
resources = get_resources(context, state_bucket=state_bucket)
|
|
122
|
+
for resource in resources:
|
|
123
|
+
target_name = resource.__class__.__name__
|
|
124
|
+
if resource.status == "NOEXIST":
|
|
125
|
+
echo.log("Creating %s..." % target_name)
|
|
126
|
+
if "mediator" in inspect.signature(resource.create).parameters:
|
|
127
|
+
resource.create(mediator)
|
|
128
|
+
else:
|
|
129
|
+
resource.create()
|
|
130
|
+
state_store.record(resource.state_info())
|
|
131
|
+
elif resource.status == "REQUIRE_UPDATE":
|
|
132
|
+
echo.log("Updating %s..." % target_name)
|
|
133
|
+
if "mediator" in inspect.signature(resource.update).parameters:
|
|
134
|
+
resource.update(mediator)
|
|
135
|
+
else:
|
|
136
|
+
resource.update()
|
|
137
|
+
state_store.record(resource.state_info())
|
|
138
|
+
else:
|
|
139
|
+
echo.log("%s is already the latest version." % target_name)
|
|
140
|
+
# stack 作成/更新が終わった後の後付け状態 (bucket policy / KVS など) を
|
|
141
|
+
# 冪等に確保する。wait_status が timeout した次の deploy でも復旧できる。
|
|
142
|
+
for resource in resources:
|
|
143
|
+
hook = getattr(resource, "ensure_post_deploy_state", None)
|
|
144
|
+
if hook is None:
|
|
145
|
+
continue
|
|
146
|
+
if "mediator" in inspect.signature(hook).parameters:
|
|
147
|
+
hook(mediator)
|
|
148
|
+
else:
|
|
149
|
+
hook()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def apply_skip_check_existing(context: Context) -> None:
|
|
153
|
+
"""DB リソース (neon/tidb/upstash) の存在確認を一律 skip させる。
|
|
154
|
+
|
|
155
|
+
`--skip-check-existing` 指定時に呼ぶ。pocket.toml を編集せず、その deploy
|
|
156
|
+
実行に限り外部 SaaS API への存在確認 call を回避する (deploy ロールに
|
|
157
|
+
DB credentials を渡さず deploy を完走させる用途)。toml 側の
|
|
158
|
+
`skip_check_existing` フラグと同義で、こちらは実行時上書き。
|
|
159
|
+
"""
|
|
160
|
+
for db_ctx in (context.neon, context.tidb, context.upstash):
|
|
161
|
+
if db_ctx is not None:
|
|
162
|
+
db_ctx.skip_check_existing = True
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def build_image(context: Context, *, tag: str) -> str:
|
|
166
|
+
"""awscontainer image を指定 tag で build & push する (deploy はしない)。
|
|
167
|
+
|
|
168
|
+
build once 用。codebuild backend は source upload に state bucket を要するため、
|
|
169
|
+
deploy と同様に先に state bucket を確保してから build する。戻り値は ecr_name:tag。
|
|
170
|
+
"""
|
|
171
|
+
if context.awscontainer is None:
|
|
172
|
+
raise click.ClickException("awscontainer がこの stage に設定されていません。")
|
|
173
|
+
state_store = _create_state_store(context)
|
|
174
|
+
state_store.ensure_bucket()
|
|
175
|
+
ac = AwsContainer(context.awscontainer, state_bucket=state_store.bucket_name)
|
|
176
|
+
ac.build(tag)
|
|
177
|
+
return f"{context.awscontainer.ecr_name}:{tag}"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _deploy_pipeline(context: Context, *, openpath=None, skip_frontend=False):
|
|
181
|
+
"""deploy / promote 共通のパイプライン本体。
|
|
182
|
+
|
|
183
|
+
promote 時は context.awscontainer.promote_commit_hash が設定済みで、
|
|
184
|
+
deploy_init 内の image build が retag に置き換わる以外は deploy と同一。
|
|
185
|
+
"""
|
|
186
|
+
# CodeBuildがソースアップロードにstate bucketを必要とするため、先に作成
|
|
187
|
+
state_store = _create_state_store(context)
|
|
188
|
+
state_store.ensure_bucket()
|
|
189
|
+
state_bucket = state_store.bucket_name
|
|
190
|
+
deploy_init_resources(context, state_bucket=state_bucket)
|
|
191
|
+
deploy_resources(context, state_bucket=state_bucket)
|
|
192
|
+
upload_managed_assets(context)
|
|
193
|
+
if not skip_frontend:
|
|
194
|
+
deploy_frontend(context)
|
|
195
|
+
# デプロイ完了後の URL 表示
|
|
196
|
+
url = _get_deploy_url(context)
|
|
197
|
+
if url:
|
|
198
|
+
echo.success(f"url: {url}")
|
|
199
|
+
if openpath:
|
|
200
|
+
webbrowser.open(url + "/" + openpath)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@click.command()
|
|
204
|
+
@click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
|
|
205
|
+
@click.option("--openpath")
|
|
206
|
+
@click.option("--skip-frontend", is_flag=True, default=False)
|
|
207
|
+
@click.option(
|
|
208
|
+
"--skip-check-existing",
|
|
209
|
+
is_flag=True,
|
|
210
|
+
default=False,
|
|
211
|
+
help="neon/tidb/upstash の存在確認 API を skip し COMPLETED 扱いで deploy",
|
|
212
|
+
)
|
|
213
|
+
def deploy(stage: str, openpath, skip_frontend, skip_check_existing):
|
|
214
|
+
from pocket_cli.cli.aws_auth import check_aws_credentials
|
|
215
|
+
|
|
216
|
+
check_aws_credentials()
|
|
217
|
+
context = Context.from_toml(stage=stage)
|
|
218
|
+
if skip_check_existing:
|
|
219
|
+
apply_skip_check_existing(context)
|
|
220
|
+
_deploy_pipeline(context, openpath=openpath, skip_frontend=skip_frontend)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@click.command()
|
|
224
|
+
@click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
|
|
225
|
+
@click.option("--commit-hash", required=True, help="昇格する image の git commit hash")
|
|
226
|
+
@click.option("--openpath")
|
|
227
|
+
@click.option("--skip-frontend", is_flag=True, default=False)
|
|
228
|
+
@click.option(
|
|
229
|
+
"--skip-check-existing",
|
|
230
|
+
is_flag=True,
|
|
231
|
+
default=False,
|
|
232
|
+
help="neon/tidb/upstash の存在確認 API を skip し COMPLETED 扱いで deploy",
|
|
233
|
+
)
|
|
234
|
+
def promote(stage: str, commit_hash, openpath, skip_frontend, skip_check_existing):
|
|
235
|
+
"""build 済みの :<commit-hash> image へ stage を向けて deploy する (再ビルドなし)。
|
|
236
|
+
|
|
237
|
+
`pocket django build` で push した image に :<stage> タグを移し、
|
|
238
|
+
インフラ/Lambda を更新する。image build は行わない (build once の昇格)。
|
|
239
|
+
"""
|
|
240
|
+
from pocket_cli.cli.aws_auth import check_aws_credentials
|
|
241
|
+
|
|
242
|
+
check_aws_credentials()
|
|
243
|
+
context = Context.from_toml(stage=stage)
|
|
244
|
+
if context.awscontainer is None:
|
|
245
|
+
raise click.ClickException("awscontainer がこの stage に設定されていません。")
|
|
246
|
+
if skip_check_existing:
|
|
247
|
+
apply_skip_check_existing(context)
|
|
248
|
+
context.awscontainer.promote_commit_hash = commit_hash
|
|
249
|
+
_deploy_pipeline(context, openpath=openpath, skip_frontend=skip_frontend)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _get_deploy_url(context: Context) -> str | None:
|
|
253
|
+
"""デプロイ後に表示する URL を決定する。
|
|
254
|
+
|
|
255
|
+
CloudFront がある場合はそのドメイン(カスタム or 自動生成)を優先し、
|
|
256
|
+
なければ API Gateway の wsgi エンドポイントを返す。
|
|
257
|
+
"""
|
|
258
|
+
# CloudFront ドメインを優先
|
|
259
|
+
for _name, cf_ctx in context.cloudfront.items():
|
|
260
|
+
if cf_ctx.domain:
|
|
261
|
+
return f"https://{cf_ctx.domain}"
|
|
262
|
+
cf = CloudFront(cf_ctx)
|
|
263
|
+
if cf.stack.output:
|
|
264
|
+
domain = cf.stack.output.get("DistributionDomainName")
|
|
265
|
+
if domain:
|
|
266
|
+
return f"https://{domain}"
|
|
267
|
+
|
|
268
|
+
# フォールバック: API Gateway
|
|
269
|
+
if context.awscontainer:
|
|
270
|
+
ac = AwsContainer(context.awscontainer)
|
|
271
|
+
endpoint = ac.endpoints.get("wsgi")
|
|
272
|
+
if endpoint:
|
|
273
|
+
return endpoint
|
|
274
|
+
return None
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import boto3
|
|
2
|
+
import click
|
|
3
|
+
|
|
4
|
+
from pocket.context import Context
|
|
5
|
+
from pocket.utils import echo
|
|
6
|
+
from pocket_cli.resources.aws.builders.codebuild import CodeBuildBuilder
|
|
7
|
+
from pocket_cli.resources.aws.state import StateStore
|
|
8
|
+
from pocket_cli.resources.awscontainer import AwsContainer
|
|
9
|
+
from pocket_cli.resources.cloudfront import CloudFront
|
|
10
|
+
from pocket_cli.resources.cloudfront_acm import CloudFrontAcm
|
|
11
|
+
from pocket_cli.resources.cloudfront_keys import CloudFrontKeys
|
|
12
|
+
from pocket_cli.resources.dsql import Dsql
|
|
13
|
+
from pocket_cli.resources.neon import Neon
|
|
14
|
+
from pocket_cli.resources.rds import Rds
|
|
15
|
+
from pocket_cli.resources.s3 import S3
|
|
16
|
+
from pocket_cli.resources.tidb import TiDb
|
|
17
|
+
from pocket_cli.resources.upstash import Upstash
|
|
18
|
+
from pocket_cli.resources.vpc import Vpc
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _create_state_store(context: Context) -> StateStore:
|
|
22
|
+
assert context.general
|
|
23
|
+
resource_prefix = context.general.prefix_template.format(
|
|
24
|
+
stage=context.stage,
|
|
25
|
+
project=context.project_name,
|
|
26
|
+
namespace=context.general.namespace,
|
|
27
|
+
)
|
|
28
|
+
bucket_name = f"{resource_prefix}state"
|
|
29
|
+
return StateStore(bucket_name, context.general.region)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _create_codebuild_builder(context: Context) -> CodeBuildBuilder | None:
|
|
33
|
+
"""CodeBuildBuilder インスタンスを作成(リソース確認用)"""
|
|
34
|
+
if not context.awscontainer or not context.general:
|
|
35
|
+
return None
|
|
36
|
+
resource_prefix = context.general.prefix_template.format(
|
|
37
|
+
stage=context.stage,
|
|
38
|
+
project=context.project_name,
|
|
39
|
+
namespace=context.general.namespace,
|
|
40
|
+
)
|
|
41
|
+
state_bucket = f"{resource_prefix}state"
|
|
42
|
+
return CodeBuildBuilder(
|
|
43
|
+
region=context.general.region,
|
|
44
|
+
resource_prefix=resource_prefix,
|
|
45
|
+
state_bucket=state_bucket,
|
|
46
|
+
permissions_boundary=context.awscontainer.permissions_boundary,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _collect_vpc_targets(context: Context) -> list[str]:
|
|
51
|
+
"""VPC 関連の削除対象を収集"""
|
|
52
|
+
if not context.awscontainer or not context.awscontainer.vpc:
|
|
53
|
+
return []
|
|
54
|
+
if not context.awscontainer.vpc.manage:
|
|
55
|
+
return ["VPC (外部 VPC consumer タグ削除)"]
|
|
56
|
+
vpc = Vpc(context.awscontainer.vpc)
|
|
57
|
+
vpc_parts = []
|
|
58
|
+
if vpc.stack.status != "NOEXIST":
|
|
59
|
+
vpc_parts.append("CFNスタック")
|
|
60
|
+
if vpc.stack.consumers:
|
|
61
|
+
vpc_parts.append("consumers: %s" % ", ".join(vpc.stack.consumers))
|
|
62
|
+
if vpc.efs and vpc.efs.exists():
|
|
63
|
+
vpc_parts.append("EFS")
|
|
64
|
+
if vpc_parts:
|
|
65
|
+
return ["VPC (%s)" % " + ".join(vpc_parts)]
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _collect_awscontainer_targets(context: Context, with_secrets: bool):
|
|
70
|
+
"""AwsContainer 関連の削除対象を収集"""
|
|
71
|
+
targets: list[str] = []
|
|
72
|
+
if not context.awscontainer:
|
|
73
|
+
return targets
|
|
74
|
+
|
|
75
|
+
ac = AwsContainer(context.awscontainer)
|
|
76
|
+
parts = ["CFNスタック"]
|
|
77
|
+
if ac.ecr.exists():
|
|
78
|
+
if context.awscontainer.ecr_name_overridden:
|
|
79
|
+
parts.append("ECR は ecr_name 明示指定のため削除対象外")
|
|
80
|
+
else:
|
|
81
|
+
parts.append("ECR")
|
|
82
|
+
if with_secrets and context.awscontainer.secrets:
|
|
83
|
+
parts.append("secrets")
|
|
84
|
+
|
|
85
|
+
# CodeBuildリソースの存在チェック(設定に関わらず)
|
|
86
|
+
cb = _create_codebuild_builder(context)
|
|
87
|
+
if cb and (cb.project_exists() or cb.role_exists()):
|
|
88
|
+
parts.append("CodeBuild")
|
|
89
|
+
|
|
90
|
+
targets.append("AwsContainer (%s)" % " + ".join(parts))
|
|
91
|
+
targets.extend(_collect_vpc_targets(context))
|
|
92
|
+
|
|
93
|
+
return targets
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _collect_database_targets(context: Context) -> list[str]:
|
|
97
|
+
"""データベース関連の削除対象を収集"""
|
|
98
|
+
targets: list[str] = []
|
|
99
|
+
if context.dsql:
|
|
100
|
+
dsql = Dsql(context.dsql)
|
|
101
|
+
if dsql.status != "NOEXIST":
|
|
102
|
+
targets.append("DSQL クラスター: %s" % context.dsql.tag_name)
|
|
103
|
+
if context.rds:
|
|
104
|
+
rds = Rds(context.rds)
|
|
105
|
+
if rds.status != "NOEXIST":
|
|
106
|
+
targets.append("RDS Aurora クラスター: %s" % context.rds.cluster_identifier)
|
|
107
|
+
if context.tidb:
|
|
108
|
+
targets.append("TiDB クラスタ")
|
|
109
|
+
if context.upstash:
|
|
110
|
+
targets.append("Upstash Redis: %s" % context.upstash.database_name)
|
|
111
|
+
if context.neon:
|
|
112
|
+
targets.append("Neon ブランチ")
|
|
113
|
+
return targets
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _collect_targets(context: Context, with_secrets: bool, with_state_bucket: bool):
|
|
117
|
+
"""削除対象のリソース一覧を収集"""
|
|
118
|
+
targets: list[str] = []
|
|
119
|
+
|
|
120
|
+
for name, cf_ctx in context.cloudfront.items():
|
|
121
|
+
targets.append("CloudFront '%s' (CFNスタック + バケットポリシー)" % name)
|
|
122
|
+
if cf_ctx.domain:
|
|
123
|
+
targets.append("CloudFront ACM '%s' (us-east-1 証明書)" % name)
|
|
124
|
+
|
|
125
|
+
targets.extend(_collect_awscontainer_targets(context, with_secrets))
|
|
126
|
+
targets.extend(_collect_database_targets(context))
|
|
127
|
+
|
|
128
|
+
for name, cf_ctx in context.cloudfront.items():
|
|
129
|
+
if cf_ctx.signing_key:
|
|
130
|
+
targets.append("CloudFrontKeys '%s' (CFNスタック)" % name)
|
|
131
|
+
|
|
132
|
+
if context.s3 and S3(context.s3).exists():
|
|
133
|
+
targets.append("S3 バケット: %s" % context.s3.bucket_name)
|
|
134
|
+
|
|
135
|
+
if with_state_bucket:
|
|
136
|
+
targets.append("ステートバケット")
|
|
137
|
+
|
|
138
|
+
return targets
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _destroy_codebuild(context: Context) -> None:
|
|
142
|
+
"""CodeBuildプロジェクト + IAMロールを削除(設定に関わらず存在すれば削除)"""
|
|
143
|
+
cb = _create_codebuild_builder(context)
|
|
144
|
+
if cb is None:
|
|
145
|
+
return
|
|
146
|
+
if cb.project_exists() or cb.role_exists():
|
|
147
|
+
echo.log("Destroying CodeBuild resources...")
|
|
148
|
+
cb.delete()
|
|
149
|
+
echo.success("CodeBuild resources were deleted.")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _destroy_log_groups(context: Context):
|
|
153
|
+
"""Lambda が自動作成したロググループを削除"""
|
|
154
|
+
if not context.awscontainer:
|
|
155
|
+
return
|
|
156
|
+
logs_client = boto3.client("logs", region_name=context.awscontainer.region)
|
|
157
|
+
for handler_ctx in context.awscontainer.handlers.values():
|
|
158
|
+
log_group_name = handler_ctx.log_group_name
|
|
159
|
+
try:
|
|
160
|
+
logs_client.delete_log_group(logGroupName=log_group_name)
|
|
161
|
+
echo.log("Deleted log group: %s" % log_group_name)
|
|
162
|
+
except logs_client.exceptions.ResourceNotFoundException:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _destroy_awscontainer(context: Context, with_secrets: bool):
|
|
167
|
+
"""AwsContainer 関連リソースを削除"""
|
|
168
|
+
if not context.awscontainer:
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
ac = AwsContainer(context.awscontainer)
|
|
172
|
+
if ac.stack.status != "NOEXIST":
|
|
173
|
+
echo.log("Destroying AwsContainer stack...")
|
|
174
|
+
ac.stack.delete()
|
|
175
|
+
echo.success("AwsContainer stack was destroyed.")
|
|
176
|
+
|
|
177
|
+
if ac.ecr.exists():
|
|
178
|
+
if context.awscontainer.ecr_name_overridden:
|
|
179
|
+
echo.warning(
|
|
180
|
+
"ECR repository '%s' は ecr_name で明示指定されているため削除"
|
|
181
|
+
"しません (他 stage と共有の可能性があります)。"
|
|
182
|
+
"不要な場合は手動で削除してください。" % context.awscontainer.ecr_name
|
|
183
|
+
)
|
|
184
|
+
else:
|
|
185
|
+
echo.log("Destroying ECR repository...")
|
|
186
|
+
ac.ecr.delete()
|
|
187
|
+
echo.success("ECR repository was deleted.")
|
|
188
|
+
|
|
189
|
+
_destroy_codebuild(context)
|
|
190
|
+
|
|
191
|
+
_destroy_log_groups(context)
|
|
192
|
+
|
|
193
|
+
if with_secrets and context.awscontainer.secrets:
|
|
194
|
+
echo.log("Destroying pocket managed secrets...")
|
|
195
|
+
context.awscontainer.secrets.pocket_store.delete_secrets()
|
|
196
|
+
echo.success("Pocket managed secrets were deleted.")
|
|
197
|
+
|
|
198
|
+
_destroy_vpc(context)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _destroy_dsql(context: Context):
|
|
202
|
+
"""DSQL クラスターを削除"""
|
|
203
|
+
if not context.dsql:
|
|
204
|
+
return
|
|
205
|
+
dsql = Dsql(context.dsql)
|
|
206
|
+
if dsql.status == "NOEXIST":
|
|
207
|
+
return
|
|
208
|
+
echo.log("Destroying DSQL cluster...")
|
|
209
|
+
dsql.delete()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _destroy_rds(context: Context):
|
|
213
|
+
"""RDS Aurora クラスターを削除"""
|
|
214
|
+
if not context.rds:
|
|
215
|
+
return
|
|
216
|
+
rds = Rds(context.rds)
|
|
217
|
+
if rds.status == "NOEXIST":
|
|
218
|
+
return
|
|
219
|
+
echo.log("Destroying RDS Aurora cluster...")
|
|
220
|
+
rds.delete()
|
|
221
|
+
echo.success("RDS Aurora cluster was destroyed. Final snapshot was created.")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _destroy_vpc(context: Context):
|
|
225
|
+
"""VPC 関連リソースを削除"""
|
|
226
|
+
if not context.awscontainer or not context.awscontainer.vpc:
|
|
227
|
+
return
|
|
228
|
+
vpc_ctx = context.awscontainer.vpc
|
|
229
|
+
if not vpc_ctx.manage:
|
|
230
|
+
# 外部 VPC: consumer タグのみ削除
|
|
231
|
+
from pocket_cli.resources.aws.cloudformation import VpcStack
|
|
232
|
+
|
|
233
|
+
vpc_stack = VpcStack(vpc_ctx)
|
|
234
|
+
if vpc_stack.status != "NOEXIST":
|
|
235
|
+
slug = context.stage + "-" + context.project_name
|
|
236
|
+
vpc_stack.remove_consumer_tag(slug)
|
|
237
|
+
echo.log("外部 VPC の consumer タグを削除しました。")
|
|
238
|
+
return
|
|
239
|
+
# managed VPC: consumer チェック後に削除
|
|
240
|
+
vpc = Vpc(vpc_ctx)
|
|
241
|
+
if vpc.stack.consumers:
|
|
242
|
+
echo.danger("VPC に consumer がいるため削除できません:")
|
|
243
|
+
for c in vpc.stack.consumers:
|
|
244
|
+
echo.info(" - %s" % c)
|
|
245
|
+
return
|
|
246
|
+
has_stack = vpc.stack.status != "NOEXIST"
|
|
247
|
+
has_efs = vpc.efs and vpc.efs.exists()
|
|
248
|
+
if has_stack or has_efs:
|
|
249
|
+
echo.log("Destroying VPC...")
|
|
250
|
+
vpc.delete()
|
|
251
|
+
echo.success("VPC was destroyed.")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _destroy_cloudfront_and_acm(context: Context):
|
|
255
|
+
"""CloudFront ディストリビューションと ACM 証明書を削除"""
|
|
256
|
+
for name, cf_ctx in context.cloudfront.items():
|
|
257
|
+
cf = CloudFront(cf_ctx)
|
|
258
|
+
if cf.stack.status != "NOEXIST":
|
|
259
|
+
echo.log("Destroying CloudFront '%s'..." % name)
|
|
260
|
+
cf.delete()
|
|
261
|
+
echo.success("CloudFront '%s' was destroyed." % name)
|
|
262
|
+
if cf_ctx.domain:
|
|
263
|
+
acm = CloudFrontAcm(cf_ctx)
|
|
264
|
+
if acm.stack.status != "NOEXIST":
|
|
265
|
+
echo.log("Destroying CloudFront ACM '%s'..." % name)
|
|
266
|
+
acm.delete()
|
|
267
|
+
echo.success("CloudFront ACM '%s' was destroyed." % name)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _destroy_resources(context: Context, with_secrets: bool, with_state_bucket: bool):
|
|
271
|
+
"""リソースをデプロイの逆順で削除"""
|
|
272
|
+
# 1. CloudFront + ACM
|
|
273
|
+
_destroy_cloudfront_and_acm(context)
|
|
274
|
+
|
|
275
|
+
# 2. AwsContainer (CFNスタック + ECR + secrets) + 3. VPC
|
|
276
|
+
_destroy_awscontainer(context, with_secrets)
|
|
277
|
+
|
|
278
|
+
# 2.5. DSQL
|
|
279
|
+
_destroy_dsql(context)
|
|
280
|
+
|
|
281
|
+
# 2.6. RDS(AwsContainer の後、VPC の前)
|
|
282
|
+
_destroy_rds(context)
|
|
283
|
+
|
|
284
|
+
# 3.5. CloudFrontKeys(AwsContainer の後、S3 の前)
|
|
285
|
+
for name, cf_ctx in context.cloudfront.items():
|
|
286
|
+
if cf_ctx.signing_key:
|
|
287
|
+
cfk = CloudFrontKeys(cf_ctx)
|
|
288
|
+
if cfk.stack.status != "NOEXIST":
|
|
289
|
+
echo.log("Destroying CloudFrontKeys '%s'..." % name)
|
|
290
|
+
cfk.delete()
|
|
291
|
+
echo.success("CloudFrontKeys '%s' was destroyed." % name)
|
|
292
|
+
|
|
293
|
+
# 4. S3 バケット
|
|
294
|
+
if context.s3 and S3(context.s3).exists():
|
|
295
|
+
echo.log("Destroying S3 bucket...")
|
|
296
|
+
S3(context.s3).delete()
|
|
297
|
+
echo.success("S3 bucket was deleted.")
|
|
298
|
+
|
|
299
|
+
# 5. TiDB クラスタ
|
|
300
|
+
if context.tidb and TiDb(context.tidb).cluster:
|
|
301
|
+
echo.log("Destroying TiDB cluster...")
|
|
302
|
+
TiDb(context.tidb).delete_cluster()
|
|
303
|
+
echo.success("TiDB cluster was deleted.")
|
|
304
|
+
|
|
305
|
+
# 5.5. Upstash Redis
|
|
306
|
+
if context.upstash:
|
|
307
|
+
upstash = Upstash(context.upstash)
|
|
308
|
+
if upstash.database:
|
|
309
|
+
upstash.delete_database()
|
|
310
|
+
|
|
311
|
+
# 6. Neon ブランチ
|
|
312
|
+
if context.neon and Neon(context.neon).branch:
|
|
313
|
+
echo.log("Destroying Neon branch...")
|
|
314
|
+
Neon(context.neon).delete_branch()
|
|
315
|
+
echo.success("Neon branch was deleted.")
|
|
316
|
+
|
|
317
|
+
# 7. ステートバケット
|
|
318
|
+
if with_state_bucket:
|
|
319
|
+
state_store = _create_state_store(context)
|
|
320
|
+
echo.log("Destroying state bucket...")
|
|
321
|
+
state_store.delete_bucket()
|
|
322
|
+
echo.success("State bucket was deleted.")
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@click.command()
|
|
326
|
+
@click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
|
|
327
|
+
@click.option(
|
|
328
|
+
"--without-secrets",
|
|
329
|
+
is_flag=True,
|
|
330
|
+
default=False,
|
|
331
|
+
help="シークレットを削除せずに残す",
|
|
332
|
+
)
|
|
333
|
+
@click.option("--with-state-bucket", is_flag=True, default=False)
|
|
334
|
+
@click.option(
|
|
335
|
+
"--yes", "-y", is_flag=True, default=False, help="確認プロンプトをスキップ"
|
|
336
|
+
)
|
|
337
|
+
def destroy(stage: str, without_secrets: bool, with_state_bucket: bool, yes: bool):
|
|
338
|
+
"""ステージの全リソースを一括削除"""
|
|
339
|
+
from pocket_cli.cli.aws_auth import check_aws_credentials
|
|
340
|
+
|
|
341
|
+
check_aws_credentials()
|
|
342
|
+
context = Context.from_toml(stage=stage)
|
|
343
|
+
with_secrets = not without_secrets
|
|
344
|
+
targets = _collect_targets(context, with_secrets, with_state_bucket)
|
|
345
|
+
|
|
346
|
+
if not targets:
|
|
347
|
+
echo.warning("削除対象のリソースが見つかりません。")
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
echo.danger("以下のリソースを削除します:")
|
|
351
|
+
for target in targets:
|
|
352
|
+
echo.info(" - %s" % target)
|
|
353
|
+
echo.danger("この操作は取り消せません!")
|
|
354
|
+
if not yes:
|
|
355
|
+
click.confirm("stage '%s' の全リソースを削除しますか?" % stage, abort=True)
|
|
356
|
+
|
|
357
|
+
_destroy_resources(context, with_secrets, with_state_bucket)
|
|
358
|
+
echo.success("stage '%s' の全リソースを削除しました。" % stage)
|