magic-pocket-cli 0.34.0__tar.gz → 0.35.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.
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/PKG-INFO +2 -2
- magic_pocket_cli-0.35.0/pocket_cli/cli/build_cli.py +44 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/deploy_cli.py +15 -1
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/destroy_cli.py +93 -1
- magic_pocket_cli-0.35.0/pocket_cli/cli/inbound_cli.py +288 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/main_cli.py +4 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/status_cli.py +21 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/vpc_cli.py +54 -14
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/django_cli.py +3 -38
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/migrations.py +53 -20
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/aws/cloudformation.py +46 -8
- magic_pocket_cli-0.35.0/pocket_cli/resources/aws/s3_utils.py +105 -0
- magic_pocket_cli-0.35.0/pocket_cli/resources/inbound.py +276 -0
- magic_pocket_cli-0.35.0/pocket_cli/resources/inbound_template.py +309 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/rds.py +25 -14
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/sqs_alert.py +30 -7
- magic_pocket_cli-0.35.0/pocket_cli/resources/vpc.py +148 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/templates/cloudformation/cf_function_spa_auth.js +6 -1
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/templates/cloudformation/container.yaml +48 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pyproject.toml +2 -2
- magic_pocket_cli-0.34.0/pocket_cli/resources/aws/s3_utils.py +0 -58
- magic_pocket_cli-0.34.0/pocket_cli/resources/vpc.py +0 -67
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/.gitignore +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/__init__.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/__init__.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/aws_auth.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/backup_cli.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/cloudfront_cli.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/cloudfront_keys_cli.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/cloudfront_waf_cli.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/container_cli.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/dsql_cli.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/image_cli.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/interaction.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/migrate_cli.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/neon_cli.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/permissions_cli.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/rds_cli.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/removed_flags.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/resource_helper.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/runtime_config_cli.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/s3_cli.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/store_url_helper.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/tidb_cli.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/upstash_cli.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/url_helper.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/cli/waf_cli.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/mediator.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/__init__.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/aws/__init__.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/aws/backup_common.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/aws/builders/__init__.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/aws/builders/codebuild.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/aws/builders/context_check.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/aws/builders/depot.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/aws/builders/docker.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/aws/builders/dockerignore.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/aws/ecr.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/aws/efs.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/aws/lambdahandler.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/aws/poll.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/aws/stack_backed.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/aws/state.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/backup.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/cloudfront.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/cloudfront_acm.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/cloudfront_keys.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/cloudfront_waf.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/container.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/dsql.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/neon.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/s3.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/tidb.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/resources/upstash.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/templates/cloudformation/cf_function_api_host.js +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/templates/cloudformation/cf_function_basic_auth_prelude.js +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/templates/cloudformation/cf_function_host_redirect_prelude.js +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/templates/cloudformation/cf_function_spa_fallback.js +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/templates/cloudformation/cloudfront.yaml +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/templates/cloudformation/cloudfront_acm.yaml +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/templates/cloudformation/cloudfront_keys.yaml +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/templates/cloudformation/cloudfront_waf.yaml +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/templates/cloudformation/vpc.yaml +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/templates/init/django-dotenv.env +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/templates/init/django-settings.py +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/templates/init/pocket.Dockerfile +0 -0
- {magic_pocket_cli-0.34.0 → magic_pocket_cli-0.35.0}/pocket_cli/templates/init/pocket_simple.toml +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: magic-pocket-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.35.0
|
|
4
4
|
Summary: CLI and deploy tools for magic-pocket.
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: awscrt>=0.19.0
|
|
7
7
|
Requires-Dist: click>=8.1.7
|
|
8
8
|
Requires-Dist: deepdiff>=6.7.1
|
|
9
9
|
Requires-Dist: jinja2>=3.1.3
|
|
10
|
-
Requires-Dist: magic-pocket<0.
|
|
10
|
+
Requires-Dist: magic-pocket<0.36,>=0.35.0
|
|
11
11
|
Requires-Dist: pathspec>=1.0.4
|
|
12
12
|
Requires-Dist: python-on-whales>=0.68.0
|
|
13
13
|
Requires-Dist: pyyaml>=6.0.1
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""フレームワーク共通の build once コマンド。"""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from pocket.context import Context, get_commit_hash, is_working_tree_dirty
|
|
6
|
+
from pocket.utils import echo
|
|
7
|
+
from pocket_cli.cli.aws_auth import check_aws_credentials
|
|
8
|
+
from pocket_cli.cli.deploy_cli import build_image
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command()
|
|
12
|
+
@click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
|
|
13
|
+
@click.option(
|
|
14
|
+
"--allow-dirty",
|
|
15
|
+
is_flag=True,
|
|
16
|
+
default=False,
|
|
17
|
+
help="working tree が dirty でも build する (ローカル検証用)",
|
|
18
|
+
)
|
|
19
|
+
def build(stage: str, allow_dirty: bool):
|
|
20
|
+
"""現在の作業ツリーを build し、git commit hash をタグにして ECR へ push する。
|
|
21
|
+
|
|
22
|
+
deploy はしない (build once)。`pocket promote --commit-hash <sha>` で
|
|
23
|
+
このイメージへ昇格する。タグは COMMIT_HASH 環境変数があればそれを、なければ
|
|
24
|
+
`git rev-parse HEAD` を使う。
|
|
25
|
+
|
|
26
|
+
commit hash = image 内容 の同一性が前提のため、working tree が dirty の場合は
|
|
27
|
+
エラーになる (--allow-dirty で回避可能)。
|
|
28
|
+
"""
|
|
29
|
+
check_aws_credentials()
|
|
30
|
+
if not allow_dirty and is_working_tree_dirty():
|
|
31
|
+
raise click.ClickException(
|
|
32
|
+
"working tree に未コミットの変更があります。build once では"
|
|
33
|
+
" commit hash と image 内容の一致が前提のため、commit してから"
|
|
34
|
+
" build してください (--allow-dirty で回避できますが、その image の"
|
|
35
|
+
" 昇格は推奨しません)。"
|
|
36
|
+
)
|
|
37
|
+
try:
|
|
38
|
+
tag = get_commit_hash()
|
|
39
|
+
except RuntimeError as e:
|
|
40
|
+
raise click.ClickException(str(e)) from e
|
|
41
|
+
context = Context.from_toml(stage=stage)
|
|
42
|
+
targets = build_image(context, tag=tag)
|
|
43
|
+
for target in targets:
|
|
44
|
+
echo.success("built and pushed: %s" % target)
|
|
@@ -4,6 +4,7 @@ import webbrowser
|
|
|
4
4
|
import click
|
|
5
5
|
|
|
6
6
|
from pocket.context import Context, deploy_hash_report
|
|
7
|
+
from pocket.settings import parse_handler_ref
|
|
7
8
|
from pocket.utils import echo
|
|
8
9
|
from pocket_cli import migrations
|
|
9
10
|
from pocket_cli.cli import interaction
|
|
@@ -20,6 +21,11 @@ from pocket_cli.resources.cloudfront_keys import CloudFrontKeys
|
|
|
20
21
|
from pocket_cli.resources.cloudfront_waf import CloudFrontWaf
|
|
21
22
|
from pocket_cli.resources.container import Container
|
|
22
23
|
from pocket_cli.resources.dsql import Dsql
|
|
24
|
+
from pocket_cli.resources.inbound import (
|
|
25
|
+
Inbound,
|
|
26
|
+
check_removed_inbound,
|
|
27
|
+
echo_inbound_details,
|
|
28
|
+
)
|
|
23
29
|
from pocket_cli.resources.neon import Neon
|
|
24
30
|
from pocket_cli.resources.rds import Rds
|
|
25
31
|
from pocket_cli.resources.s3 import S3
|
|
@@ -82,6 +88,7 @@ def get_resources(context: Context, *, state_bucket: str = ""):
|
|
|
82
88
|
if context.s3:
|
|
83
89
|
resources.append(S3(context.s3, cloudfront_contexts=context.cloudfront))
|
|
84
90
|
_append_infra_resources(resources, context, state_bucket)
|
|
91
|
+
resources.extend(Inbound(ctx) for ctx in context.inbound.values())
|
|
85
92
|
for _name, cf_ctx in context.cloudfront.items():
|
|
86
93
|
resources.append(CloudFront(cf_ctx))
|
|
87
94
|
return resources
|
|
@@ -209,6 +216,11 @@ def _deploy_pipeline(context: Context, *, openpath=None, skip_frontend=False):
|
|
|
209
216
|
"""
|
|
210
217
|
# DEPLOY_HASH の解決結果を deploy 時に 1 回可視化する (env 伝播漏れで
|
|
211
218
|
# 黙って git short hash に落ちる footgun の早期発見用)。
|
|
219
|
+
for inlet in context.inbound.values():
|
|
220
|
+
container_name, _ = parse_handler_ref(inlet.config.handler)
|
|
221
|
+
if not context.container[container_name].permissions_boundary:
|
|
222
|
+
raise ValueError("inbound workerにpermissions_boundaryが必要です")
|
|
223
|
+
Inbound(inlet).prepare_deploy()
|
|
212
224
|
deploy_hash_message = deploy_hash_report(context)
|
|
213
225
|
if deploy_hash_message:
|
|
214
226
|
echo.info(deploy_hash_message)
|
|
@@ -216,6 +228,7 @@ def _deploy_pipeline(context: Context, *, openpath=None, skip_frontend=False):
|
|
|
216
228
|
state_store = _create_state_store(context)
|
|
217
229
|
state_store.ensure_bucket()
|
|
218
230
|
state_bucket = state_store.bucket_name
|
|
231
|
+
check_removed_inbound(context, state_store)
|
|
219
232
|
deploy_init_resources(context, state_bucket=state_bucket)
|
|
220
233
|
deploy_resources(context, state_bucket=state_bucket)
|
|
221
234
|
# リリース跨ぎ移行の掃除フェーズ (旧配置の削除は deploy 成功後にしか
|
|
@@ -229,6 +242,7 @@ def _deploy_pipeline(context: Context, *, openpath=None, skip_frontend=False):
|
|
|
229
242
|
# DLQ アラートの email 購読が未確認のままだと通知が届かないため、
|
|
230
243
|
# deploy の最後に購読状態を確認して警告する
|
|
231
244
|
echo_dead_letter_alert_warnings(context)
|
|
245
|
+
echo_inbound_details(context)
|
|
232
246
|
# デプロイ完了後の URL 表示
|
|
233
247
|
url = _get_deploy_url(context)
|
|
234
248
|
if url:
|
|
@@ -266,7 +280,7 @@ def deploy(stage: str, openpath, skip_frontend, yes):
|
|
|
266
280
|
def promote(stage: str, commit_hash, openpath, skip_frontend, yes):
|
|
267
281
|
"""build 済みの :<commit-hash> image へ stage を向けて deploy する (再ビルドなし)。
|
|
268
282
|
|
|
269
|
-
`pocket
|
|
283
|
+
`pocket build` で push した image に :<stage> タグを移し、
|
|
270
284
|
インフラ/Lambda を更新する。image build は行わない (build once の昇格)。
|
|
271
285
|
"""
|
|
272
286
|
from pocket_cli.cli.aws_auth import check_aws_credentials
|
|
@@ -3,6 +3,12 @@ import click
|
|
|
3
3
|
|
|
4
4
|
from pocket.context import Context
|
|
5
5
|
from pocket.utils import echo
|
|
6
|
+
from pocket_cli.migrations import (
|
|
7
|
+
legacy_container_stack_exists,
|
|
8
|
+
legacy_container_stack_name,
|
|
9
|
+
legacy_ecr_repo_exists,
|
|
10
|
+
legacy_ecr_repo_name,
|
|
11
|
+
)
|
|
6
12
|
from pocket_cli.resources.aws.builders.codebuild import CodeBuildBuilder
|
|
7
13
|
from pocket_cli.resources.aws.state import (
|
|
8
14
|
context_resource_prefix,
|
|
@@ -14,6 +20,7 @@ from pocket_cli.resources.cloudfront_acm import CloudFrontAcm
|
|
|
14
20
|
from pocket_cli.resources.cloudfront_keys import CloudFrontKeys
|
|
15
21
|
from pocket_cli.resources.container import Container
|
|
16
22
|
from pocket_cli.resources.dsql import Dsql
|
|
23
|
+
from pocket_cli.resources.inbound import Inbound
|
|
17
24
|
from pocket_cli.resources.neon import Neon
|
|
18
25
|
from pocket_cli.resources.rds import Rds
|
|
19
26
|
from pocket_cli.resources.s3 import S3
|
|
@@ -89,6 +96,8 @@ def _collect_container_targets(context: Context, with_secrets: bool):
|
|
|
89
96
|
parts.append("ECR")
|
|
90
97
|
targets.append("Container '%s' (%s)" % (c_name, " + ".join(parts)))
|
|
91
98
|
|
|
99
|
+
targets.extend(_collect_legacy_container_targets(context))
|
|
100
|
+
|
|
92
101
|
parts = []
|
|
93
102
|
has_secrets = context.secrets or any(c.secrets for c in context.container.values())
|
|
94
103
|
if with_secrets and has_secrets:
|
|
@@ -105,6 +114,38 @@ def _collect_container_targets(context: Context, with_secrets: bool):
|
|
|
105
114
|
return targets
|
|
106
115
|
|
|
107
116
|
|
|
117
|
+
def _collect_legacy_container_targets(context: Context) -> list[str]:
|
|
118
|
+
"""0.29.0 以前の単数 [awscontainer] 命名 ({slug}-container) の残骸を削除対象にする。
|
|
119
|
+
|
|
120
|
+
移行 deploy を経ずに旧 stack だけ消したいケース (region 移設・廃止) では
|
|
121
|
+
現行命名の stack が NOEXIST で destroy の対象から漏れるため (KN1456)。
|
|
122
|
+
"""
|
|
123
|
+
targets: list[str] = []
|
|
124
|
+
if legacy_container_stack_exists(context):
|
|
125
|
+
targets.append(
|
|
126
|
+
"旧形式の container stack '%s' (0.29.0 以前の [awscontainer] 命名。"
|
|
127
|
+
"CFNスタック)" % legacy_container_stack_name(context)
|
|
128
|
+
)
|
|
129
|
+
if legacy_ecr_repo_exists(context):
|
|
130
|
+
targets.append(
|
|
131
|
+
"旧形式の ECR repository '%s' (0.29.0 以前の命名)"
|
|
132
|
+
% legacy_ecr_repo_name(context)
|
|
133
|
+
)
|
|
134
|
+
return targets
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _neon_credential_missing(context: Context) -> bool:
|
|
138
|
+
"""[neon] 宣言があるのに NEON_API_KEY が無ければ warning を出して True。"""
|
|
139
|
+
if not context.neon or context.neon.api_key:
|
|
140
|
+
return False
|
|
141
|
+
echo.warning(
|
|
142
|
+
"Neon: NEON_API_KEY が未設定のため Neon の確認・削除をスキップします "
|
|
143
|
+
"(stack の削除に Neon の資格情報は不要です。Neon branch は "
|
|
144
|
+
"pocket resource neon delete か Neon コンソールで削除してください)。"
|
|
145
|
+
)
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
|
|
108
149
|
def _warn_command_provisioned(label: str, hint: str):
|
|
109
150
|
echo.warning(
|
|
110
151
|
'%s は provisioning="command" のため destroy では削除しません。'
|
|
@@ -144,7 +185,11 @@ def _collect_external_database_targets(context: Context) -> list[str]:
|
|
|
144
185
|
if context.upstash and context.upstash.provisioning != "command":
|
|
145
186
|
if Upstash(context.upstash).database:
|
|
146
187
|
targets.append("Upstash Redis: %s" % context.upstash.database_name)
|
|
147
|
-
if
|
|
188
|
+
if (
|
|
189
|
+
context.neon
|
|
190
|
+
and context.neon.provisioning != "command"
|
|
191
|
+
and not _neon_credential_missing(context)
|
|
192
|
+
):
|
|
148
193
|
neon = Neon(context.neon)
|
|
149
194
|
if neon.branch:
|
|
150
195
|
plan = neon.destroy_plan()
|
|
@@ -176,6 +221,9 @@ def _collect_targets(context: Context, with_secrets: bool, with_state_bucket: bo
|
|
|
176
221
|
if cf_ctx.domain:
|
|
177
222
|
targets.append("CloudFront ACM '%s' (us-east-1 証明書)" % name)
|
|
178
223
|
|
|
224
|
+
targets.extend(
|
|
225
|
+
f"Inbound {name} (原本バケットと配送DLQは保持)" for name in context.inbound
|
|
226
|
+
)
|
|
179
227
|
targets.extend(_collect_container_targets(context, with_secrets))
|
|
180
228
|
targets.extend(_collect_database_targets(context))
|
|
181
229
|
targets.extend(_collect_vpc_targets(context))
|
|
@@ -247,6 +295,8 @@ def _destroy_containers(context: Context, with_secrets: bool):
|
|
|
247
295
|
c.ecr.delete()
|
|
248
296
|
echo.success("ECR repository was deleted.")
|
|
249
297
|
|
|
298
|
+
_destroy_legacy_containers(context)
|
|
299
|
+
|
|
250
300
|
_destroy_codebuild(context)
|
|
251
301
|
|
|
252
302
|
_destroy_log_groups(context)
|
|
@@ -263,6 +313,43 @@ def _destroy_containers(context: Context, with_secrets: bool):
|
|
|
263
313
|
echo.success("Pocket managed secrets were deleted.")
|
|
264
314
|
|
|
265
315
|
|
|
316
|
+
def _destroy_legacy_containers(context: Context):
|
|
317
|
+
"""0.29.0 以前の命名の container stack / ECR repo / log group を削除する。"""
|
|
318
|
+
if not context.general:
|
|
319
|
+
return
|
|
320
|
+
region = context.general.region
|
|
321
|
+
if legacy_container_stack_exists(context):
|
|
322
|
+
stack_name = legacy_container_stack_name(context)
|
|
323
|
+
cfn = boto3.client("cloudformation", region_name=region)
|
|
324
|
+
echo.log("Destroying legacy container stack '%s'..." % stack_name)
|
|
325
|
+
cfn.delete_stack(StackName=stack_name)
|
|
326
|
+
# 現行 stack と同様、ENI 解放を含む削除完了を待たないと後続の VPC 削除が
|
|
327
|
+
# subnet 使用中で DELETE_FAILED になる
|
|
328
|
+
cfn.get_waiter("stack_delete_complete").wait(
|
|
329
|
+
StackName=stack_name, WaiterConfig={"Delay": 10, "MaxAttempts": 180}
|
|
330
|
+
)
|
|
331
|
+
echo.success("Legacy container stack '%s' was destroyed." % stack_name)
|
|
332
|
+
if legacy_ecr_repo_exists(context):
|
|
333
|
+
repo = legacy_ecr_repo_name(context)
|
|
334
|
+
ecr = boto3.client("ecr", region_name=region)
|
|
335
|
+
echo.log("Destroying legacy ECR repository '%s'..." % repo)
|
|
336
|
+
ecr.delete_repository(repositoryName=repo, force=True)
|
|
337
|
+
echo.success("Legacy ECR repository was deleted.")
|
|
338
|
+
# 旧命名の Lambda ({prefix}{handler}) が自動作成した log group
|
|
339
|
+
prefix = context_resource_prefix(context)
|
|
340
|
+
logs_client = boto3.client("logs", region_name=region)
|
|
341
|
+
handler_keys = sorted(
|
|
342
|
+
{key for c in context.container.values() for key in c.handlers}
|
|
343
|
+
)
|
|
344
|
+
for key in handler_keys:
|
|
345
|
+
log_group_name = f"/aws/lambda/{prefix}{key}"
|
|
346
|
+
try:
|
|
347
|
+
logs_client.delete_log_group(logGroupName=log_group_name)
|
|
348
|
+
echo.log("Deleted legacy log group: %s" % log_group_name)
|
|
349
|
+
except logs_client.exceptions.ResourceNotFoundException:
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
|
|
266
353
|
def _destroy_backup(context: Context, yes: bool):
|
|
267
354
|
"""backup plan / selection を削除し、recovery point の扱いを案内する。
|
|
268
355
|
|
|
@@ -386,6 +473,8 @@ def _destroy_neon(context: Context):
|
|
|
386
473
|
if context.neon.provisioning == "command":
|
|
387
474
|
_warn_command_provisioned("Neon", "pocket resource neon delete")
|
|
388
475
|
return
|
|
476
|
+
if _neon_credential_missing(context):
|
|
477
|
+
return
|
|
389
478
|
neon = Neon(context.neon)
|
|
390
479
|
if not neon.branch:
|
|
391
480
|
return
|
|
@@ -431,6 +520,9 @@ def _destroy_resources(
|
|
|
431
520
|
yes: bool = False,
|
|
432
521
|
):
|
|
433
522
|
"""リソースをデプロイの逆順で削除"""
|
|
523
|
+
for inlet in context.inbound.values():
|
|
524
|
+
Inbound(inlet).delete()
|
|
525
|
+
|
|
434
526
|
# 1. CloudFront + ACM
|
|
435
527
|
_destroy_cloudfront_and_acm(context)
|
|
436
528
|
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""受信口の初期設定と原本・受信情報の運用コマンド。"""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
import boto3
|
|
7
|
+
import click
|
|
8
|
+
from botocore.config import Config
|
|
9
|
+
|
|
10
|
+
from pocket.inbound import SETUP_NOTIFICATION, receipt_id, validate_receipt_id
|
|
11
|
+
from pocket.inbound_context import InboundContext
|
|
12
|
+
from pocket.settings import Settings, parse_handler_ref
|
|
13
|
+
from pocket_cli.resources.inbound import Inbound
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
@click.option("--stage", envvar="POCKET_DEPLOY_STAGE", required=True)
|
|
18
|
+
@click.option("--name", required=True, help="inboundの設定名")
|
|
19
|
+
@click.pass_context
|
|
20
|
+
def inbound(ctx, stage, name):
|
|
21
|
+
"""SES受信口を管理する。"""
|
|
22
|
+
settings = Settings.from_toml(stage=stage)
|
|
23
|
+
if name not in settings.inbound:
|
|
24
|
+
raise click.ClickException(f"inbound.{name} がありません")
|
|
25
|
+
ctx.obj = Inbound(InboundContext.from_settings(name, settings))
|
|
26
|
+
ctx.meta["settings"] = settings
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@inbound.command()
|
|
30
|
+
@click.pass_obj
|
|
31
|
+
def init(resource):
|
|
32
|
+
"""共有rule setの初期化とドメイン検証の申請(初回のみ)。"""
|
|
33
|
+
ses = boto3.client("ses", region_name=resource.context.region)
|
|
34
|
+
active = ses.describe_active_receipt_rule_set().get("Metadata", {}).get("Name")
|
|
35
|
+
if not active:
|
|
36
|
+
name = "pocket-inbound"
|
|
37
|
+
names = [
|
|
38
|
+
item["Name"]
|
|
39
|
+
for page in ses.get_paginator("list_receipt_rule_sets").paginate()
|
|
40
|
+
for item in page["RuleSets"]
|
|
41
|
+
]
|
|
42
|
+
if name in names:
|
|
43
|
+
raise click.ClickException(
|
|
44
|
+
"pocket-inboundが既にあります。内容を確認し管理者が有効化してください"
|
|
45
|
+
)
|
|
46
|
+
click.confirm(
|
|
47
|
+
"このaccount/regionで初期化を同時実行していないことを確認してください。"
|
|
48
|
+
"共有rule set pocket-inboundを作成・有効化しますか?",
|
|
49
|
+
abort=True,
|
|
50
|
+
)
|
|
51
|
+
if ses.describe_active_receipt_rule_set().get("Metadata"):
|
|
52
|
+
raise click.ClickException("active setが変わりました。再実行してください")
|
|
53
|
+
ses.create_receipt_rule_set(RuleSetName=name)
|
|
54
|
+
if ses.describe_active_receipt_rule_set().get("Metadata"):
|
|
55
|
+
raise click.ClickException(
|
|
56
|
+
"初期化中にactive setが変わりました。有効化を中止します"
|
|
57
|
+
)
|
|
58
|
+
ses.set_active_receipt_rule_set(RuleSetName=name)
|
|
59
|
+
active = ses.describe_active_receipt_rule_set().get("Metadata", {}).get("Name")
|
|
60
|
+
if active != name:
|
|
61
|
+
raise click.ClickException("有効化後のactive setが一致しません")
|
|
62
|
+
domain = resource.context.config.domain
|
|
63
|
+
result = ses.get_identity_verification_attributes(Identities=[domain])
|
|
64
|
+
identity = result.get("VerificationAttributes", {}).get(domain, {})
|
|
65
|
+
token = identity.get("VerificationToken")
|
|
66
|
+
if not token and identity.get("VerificationStatus") != "Success":
|
|
67
|
+
token = ses.verify_domain_identity(Domain=domain)["VerificationToken"]
|
|
68
|
+
click.echo(f"active rule set: {active}")
|
|
69
|
+
if token:
|
|
70
|
+
click.echo(f"TXT _amazonses.{domain} {token}")
|
|
71
|
+
click.echo(f"MX {domain} 10 inbound-smtp.{resource.context.region}.amazonaws.com")
|
|
72
|
+
click.echo("SES受信対応リージョンとMXの既存配送先を確認してDNSを登録してください。")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@inbound.command()
|
|
76
|
+
@click.pass_obj
|
|
77
|
+
def status(resource):
|
|
78
|
+
"""スタック・active set・検証状態・監視先を表示する。"""
|
|
79
|
+
resource.prepare_deploy()
|
|
80
|
+
click.echo(f"stack: {resource.stack.name} / {resource.status}")
|
|
81
|
+
click.echo(json.dumps(resource.stack.output or {}, ensure_ascii=False, indent=2))
|
|
82
|
+
if resource.context.config.delivery_alert.eventbridge:
|
|
83
|
+
click.echo(
|
|
84
|
+
"EventBridge: source=aws.cloudwatch, "
|
|
85
|
+
"detail-type=CloudWatch Alarm State Change"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@inbound.command(name="yaml")
|
|
90
|
+
@click.pass_obj
|
|
91
|
+
def show_yaml(resource):
|
|
92
|
+
"""active setと既存bindingを照会しテンプレートを表示する。"""
|
|
93
|
+
click.echo(resource.stack.yaml)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@inbound.command()
|
|
97
|
+
@click.pass_obj
|
|
98
|
+
def destroy(resource):
|
|
99
|
+
"""無効化・配送猶予・滞留解消後に自分のルールとスタックを削除する。"""
|
|
100
|
+
click.confirm(
|
|
101
|
+
"受信停止後36時間以上経過し、原本の照合・必要な退避を終えましたか?", abort=True
|
|
102
|
+
)
|
|
103
|
+
resource.delete()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@inbound.command()
|
|
107
|
+
@click.pass_obj
|
|
108
|
+
def reconcile(resource):
|
|
109
|
+
"""原本に対応する受信情報の欠落を検出する(復元・推測はしない)。"""
|
|
110
|
+
output = resource.stack.output
|
|
111
|
+
if not output:
|
|
112
|
+
raise click.ClickException("受信スタックがありません")
|
|
113
|
+
bucket = output["Bucket"]
|
|
114
|
+
s3 = boto3.client("s3", region_name=resource.context.region)
|
|
115
|
+
config = resource.context.config
|
|
116
|
+
metadata = {
|
|
117
|
+
item["Key"]
|
|
118
|
+
for page in s3.get_paginator("list_objects_v2").paginate(
|
|
119
|
+
Bucket=bucket, Prefix=config.metadata_prefix
|
|
120
|
+
)
|
|
121
|
+
for item in page.get("Contents", [])
|
|
122
|
+
}
|
|
123
|
+
missing = 0
|
|
124
|
+
for page in s3.get_paginator("list_objects_v2").paginate(
|
|
125
|
+
Bucket=bucket, Prefix=config.raw_prefix
|
|
126
|
+
):
|
|
127
|
+
for item in page.get("Contents", []):
|
|
128
|
+
if item["Key"] == config.raw_prefix + SETUP_NOTIFICATION:
|
|
129
|
+
continue
|
|
130
|
+
identifier = receipt_id(bucket, item["Key"])
|
|
131
|
+
if config.metadata_prefix + identifier + ".json" not in metadata:
|
|
132
|
+
click.echo(f"受信情報なし: {item['Key']} / {identifier}")
|
|
133
|
+
missing += 1
|
|
134
|
+
if missing:
|
|
135
|
+
raise click.ClickException(
|
|
136
|
+
f"{missing}件の受信情報がありません。通知の遅延・DLQを確認してください"
|
|
137
|
+
)
|
|
138
|
+
click.echo("原本に対応する受信情報を確認しました。")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@inbound.command(name="copy")
|
|
142
|
+
@click.option("--receipt-id", "identifier", required=True)
|
|
143
|
+
@click.option("--to-stage", required=True)
|
|
144
|
+
@click.option("--to-name", required=True)
|
|
145
|
+
@click.pass_obj
|
|
146
|
+
def copy_receipt(resource, identifier, to_stage, to_name):
|
|
147
|
+
"""指定した1件の原本と受信情報を別stageへコピーしmanifestを最後に保存する。"""
|
|
148
|
+
validate_receipt_id(identifier)
|
|
149
|
+
settings = Settings.from_toml(stage=to_stage)
|
|
150
|
+
if to_name not in settings.inbound:
|
|
151
|
+
raise click.ClickException(f"コピー先のinbound.{to_name}がありません")
|
|
152
|
+
target = Inbound(InboundContext.from_settings(to_name, settings))
|
|
153
|
+
source_output, target_output = resource.stack.output, target.stack.output
|
|
154
|
+
if not source_output or not target_output:
|
|
155
|
+
raise click.ClickException("コピー元と先の受信スタックが必要です")
|
|
156
|
+
source = boto3.client("s3", region_name=resource.context.region)
|
|
157
|
+
destination = boto3.client("s3", region_name=target.context.region)
|
|
158
|
+
key = resource.context.config.metadata_prefix + identifier + ".json"
|
|
159
|
+
metadata_bytes = source.get_object(Bucket=source_output["Bucket"], Key=key)[
|
|
160
|
+
"Body"
|
|
161
|
+
].read()
|
|
162
|
+
metadata = json.loads(metadata_bytes)
|
|
163
|
+
reference = metadata["source"]
|
|
164
|
+
if reference["bucket"] != source_output["Bucket"] or not reference[
|
|
165
|
+
"key"
|
|
166
|
+
].startswith(resource.context.config.raw_prefix):
|
|
167
|
+
raise click.ClickException("原本の参照先が設定と一致しません")
|
|
168
|
+
raw = source.get_object(
|
|
169
|
+
Bucket=reference["bucket"],
|
|
170
|
+
Key=reference["key"],
|
|
171
|
+
VersionId=reference["version_id"],
|
|
172
|
+
)["Body"].read()
|
|
173
|
+
if (
|
|
174
|
+
metadata["receipt_id"] != identifier
|
|
175
|
+
or hashlib.sha256(raw).hexdigest() != reference["sha256"]
|
|
176
|
+
):
|
|
177
|
+
raise click.ClickException("原本のhashまたは受信IDが一致しません")
|
|
178
|
+
# 同じ受信IDのcopyは同じ内容になる。manifestが揃うまで解析ジョブを投入しない。
|
|
179
|
+
prefix = target.context.config.import_prefix + identifier + "/"
|
|
180
|
+
for suffix, body, content_type in (
|
|
181
|
+
("raw.eml", raw, "message/rfc822"),
|
|
182
|
+
("metadata.json", metadata_bytes, "application/json"),
|
|
183
|
+
(
|
|
184
|
+
"manifest.json",
|
|
185
|
+
json.dumps({"schema_version": 1, "receipt_id": identifier}).encode(),
|
|
186
|
+
"application/json",
|
|
187
|
+
),
|
|
188
|
+
):
|
|
189
|
+
destination.put_object(
|
|
190
|
+
Bucket=target_output["Bucket"],
|
|
191
|
+
Key=prefix + suffix,
|
|
192
|
+
Body=body,
|
|
193
|
+
ContentType=content_type,
|
|
194
|
+
)
|
|
195
|
+
click.echo(prefix + "manifest.json")
|
|
196
|
+
click.echo(
|
|
197
|
+
"コピー完了。取り込み用handlerからReceiver.load_importで明示的に解析してください。"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@inbound.command(name="import")
|
|
202
|
+
@click.option("--manifest-key", required=True)
|
|
203
|
+
@click.option("--handler", "handler_ref", required=True, help="取り込み用handlerの参照")
|
|
204
|
+
@click.pass_context
|
|
205
|
+
def import_receipt(ctx, manifest_key, handler_ref):
|
|
206
|
+
"""コピー済みmanifestを指定し、利用側の取り込みhandlerを同期実行する。"""
|
|
207
|
+
resource = ctx.obj
|
|
208
|
+
settings = ctx.meta["settings"]
|
|
209
|
+
container, key, _ = settings.resolve_handler(handler_ref)
|
|
210
|
+
receiving_container, _ = parse_handler_ref(resource.context.config.handler)
|
|
211
|
+
if (
|
|
212
|
+
container != receiving_container
|
|
213
|
+
or handler_ref == resource.context.config.handler
|
|
214
|
+
):
|
|
215
|
+
raise click.ClickException(
|
|
216
|
+
"受信workerと同じcontainerの別handlerを指定してください"
|
|
217
|
+
)
|
|
218
|
+
if not manifest_key.startswith(
|
|
219
|
+
resource.context.config.import_prefix
|
|
220
|
+
) or not manifest_key.endswith("/manifest.json"):
|
|
221
|
+
raise click.ClickException("imports内のmanifest.jsonを指定してください")
|
|
222
|
+
prefix = settings.prefix_template.format(**settings.format_vars)
|
|
223
|
+
client = boto3.client(
|
|
224
|
+
"lambda",
|
|
225
|
+
region_name=resource.context.region,
|
|
226
|
+
config=Config(read_timeout=900, retries={"total_max_attempts": 1}),
|
|
227
|
+
)
|
|
228
|
+
response = client.invoke(
|
|
229
|
+
FunctionName=f"{prefix}{container}-{key}",
|
|
230
|
+
InvocationType="RequestResponse",
|
|
231
|
+
Payload=json.dumps(
|
|
232
|
+
{"inbound": resource.context.name, "manifest_key": manifest_key}
|
|
233
|
+
).encode(),
|
|
234
|
+
)
|
|
235
|
+
response["Payload"].read()
|
|
236
|
+
if response.get("FunctionError"):
|
|
237
|
+
raise click.ClickException(
|
|
238
|
+
"取り込みhandlerが失敗しました。Lambdaログを確認してください"
|
|
239
|
+
)
|
|
240
|
+
click.echo("取り込みhandlerの実行が完了しました。")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@inbound.command()
|
|
244
|
+
@click.option("--queue", type=click.Choice(["delivery", "worker"]), required=True)
|
|
245
|
+
@click.option("--limit", type=click.IntRange(1, 100), default=10, show_default=True)
|
|
246
|
+
@click.pass_obj
|
|
247
|
+
def redrive(resource, queue, limit):
|
|
248
|
+
"""DLQの保存形式を検証し、受信queueへ戻す。成功した送信だけ削除する。"""
|
|
249
|
+
output = resource.stack.output
|
|
250
|
+
if not output:
|
|
251
|
+
raise click.ClickException("受信スタックがありません")
|
|
252
|
+
sqs = boto3.client("sqs", region_name=resource.context.region)
|
|
253
|
+
source = output["DeliveryDLQUrl" if queue == "delivery" else "WorkerDLQUrl"]
|
|
254
|
+
count = 0
|
|
255
|
+
for _ in range(limit):
|
|
256
|
+
messages = sqs.receive_message(
|
|
257
|
+
QueueUrl=source, MaxNumberOfMessages=1, VisibilityTimeout=60
|
|
258
|
+
).get("Messages", [])
|
|
259
|
+
if not messages:
|
|
260
|
+
break
|
|
261
|
+
message = messages[0]
|
|
262
|
+
envelope = json.loads(message["Body"])
|
|
263
|
+
if (
|
|
264
|
+
envelope.get("Type") != "Notification"
|
|
265
|
+
or envelope.get("TopicArn") != output["TopicArn"]
|
|
266
|
+
):
|
|
267
|
+
raise click.ClickException(
|
|
268
|
+
"想定外の通知形式です。DLQから削除せず中止します"
|
|
269
|
+
)
|
|
270
|
+
notification = json.loads(envelope["Message"])
|
|
271
|
+
action = notification.get("receipt", {}).get("action", {})
|
|
272
|
+
if (
|
|
273
|
+
notification.get("notificationType") != "Received"
|
|
274
|
+
or action.get("type") != "S3"
|
|
275
|
+
or action.get("bucketName") != output["Bucket"]
|
|
276
|
+
or not action.get("objectKey", "").startswith(
|
|
277
|
+
resource.context.config.raw_prefix
|
|
278
|
+
)
|
|
279
|
+
):
|
|
280
|
+
raise click.ClickException(
|
|
281
|
+
"想定外のSES保存先です。DLQから削除せず中止します"
|
|
282
|
+
)
|
|
283
|
+
sqs.send_message(QueueUrl=output["QueueUrl"], MessageBody=message["Body"])
|
|
284
|
+
sqs.delete_message(QueueUrl=source, ReceiptHandle=message["ReceiptHandle"])
|
|
285
|
+
count += 1
|
|
286
|
+
click.echo(
|
|
287
|
+
f"{count}件を受信queueへ再投入しました。重複実行に備え業務側で冪等化してください。"
|
|
288
|
+
)
|
|
@@ -16,6 +16,7 @@ except ModuleNotFoundError as e:
|
|
|
16
16
|
|
|
17
17
|
from pocket_cli.cli import (
|
|
18
18
|
backup_cli,
|
|
19
|
+
build_cli,
|
|
19
20
|
cloudfront_cli,
|
|
20
21
|
cloudfront_keys_cli,
|
|
21
22
|
cloudfront_waf_cli,
|
|
@@ -24,6 +25,7 @@ from pocket_cli.cli import (
|
|
|
24
25
|
destroy_cli,
|
|
25
26
|
dsql_cli,
|
|
26
27
|
image_cli,
|
|
28
|
+
inbound_cli,
|
|
27
29
|
migrate_cli,
|
|
28
30
|
neon_cli,
|
|
29
31
|
permissions_cli,
|
|
@@ -121,6 +123,7 @@ def settings(stage):
|
|
|
121
123
|
print(json.dumps(mask_secret_values(s.model_dump(mode="json")), indent=2))
|
|
122
124
|
|
|
123
125
|
|
|
126
|
+
main.add_command(build_cli.build)
|
|
124
127
|
main.add_command(deploy_cli.deploy)
|
|
125
128
|
main.add_command(deploy_cli.promote)
|
|
126
129
|
main.add_command(destroy_cli.destroy)
|
|
@@ -138,6 +141,7 @@ def resource():
|
|
|
138
141
|
pass
|
|
139
142
|
|
|
140
143
|
|
|
144
|
+
resource.add_command(inbound_cli.inbound)
|
|
141
145
|
resource.add_command(vpc_cli.vpc)
|
|
142
146
|
resource.add_command(container_cli.container)
|
|
143
147
|
resource.add_command(neon_cli.neon)
|
|
@@ -5,9 +5,25 @@ from pocket.resources.aws.secretsmanager import PocketSecretIsNotReady
|
|
|
5
5
|
from pocket.utils import echo
|
|
6
6
|
from pocket_cli.cli.deploy_cli import get_resources
|
|
7
7
|
from pocket_cli.resources.container import Container
|
|
8
|
+
from pocket_cli.resources.inbound import echo_inbound_details
|
|
9
|
+
from pocket_cli.resources.neon import Neon
|
|
8
10
|
from pocket_cli.resources.sqs_alert import echo_dead_letter_alert_statuses
|
|
9
11
|
|
|
10
12
|
|
|
13
|
+
def skip_reason(resource) -> str | None:
|
|
14
|
+
"""status を取らずに skip する理由 (無ければ None)。
|
|
15
|
+
|
|
16
|
+
外部 provider の資格情報が無い環境 (例: NEON_API_KEY を持たない VM) でも、
|
|
17
|
+
stack の状態確認に provider は無関係なので止めずに続行する (KN1456)。
|
|
18
|
+
"""
|
|
19
|
+
if isinstance(resource, Neon) and not resource.has_credential:
|
|
20
|
+
return (
|
|
21
|
+
"Neon status: NEON_API_KEY が未設定のため状態確認をスキップします "
|
|
22
|
+
"(stack の状態確認・削除に Neon の資格情報は不要です)"
|
|
23
|
+
)
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
11
27
|
def show_status_message(resource):
|
|
12
28
|
target_name = resource.__class__.__name__
|
|
13
29
|
message = f"{target_name} status: {resource.status}"
|
|
@@ -54,8 +70,13 @@ def status(stage, show_info):
|
|
|
54
70
|
context = Context.from_toml(stage=stage)
|
|
55
71
|
resources = get_resources(context)
|
|
56
72
|
for resource in resources:
|
|
73
|
+
reason = skip_reason(resource)
|
|
74
|
+
if reason:
|
|
75
|
+
echo.warning(reason)
|
|
76
|
+
continue
|
|
57
77
|
show_status_message(resource)
|
|
58
78
|
if show_info:
|
|
59
79
|
show_info_message(resource)
|
|
60
80
|
# DLQ アラートの email 購読状態 (PendingConfirmation のままだと通知が届かない)
|
|
61
81
|
echo_dead_letter_alert_statuses(context)
|
|
82
|
+
echo_inbound_details(context)
|