magic-pocket-cli 0.17.0__tar.gz → 0.18.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. magic_pocket_cli-0.18.0/.gitignore +42 -0
  2. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/PKG-INFO +2 -2
  3. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/awscontainer_cli.py +36 -23
  4. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/cloudfront_cli.py +14 -6
  5. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/cloudfront_keys_cli.py +15 -6
  6. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/cloudfront_waf_cli.py +14 -6
  7. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/deploy_cli.py +40 -27
  8. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/destroy_cli.py +93 -46
  9. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/dsql_cli.py +4 -3
  10. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/migrate_cli.py +70 -11
  11. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/neon_cli.py +16 -6
  12. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/rds_cli.py +2 -3
  13. magic_pocket_cli-0.18.0/pocket_cli/cli/resource_helper.py +25 -0
  14. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/runtime_config_cli.py +35 -20
  15. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/s3_cli.py +4 -4
  16. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/store_url_helper.py +2 -4
  17. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/tidb_cli.py +30 -6
  18. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/upstash_cli.py +4 -0
  19. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/url_helper.py +16 -13
  20. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/django_cli.py +32 -37
  21. magic_pocket_cli-0.18.0/pocket_cli/mediator.py +235 -0
  22. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/aws/builders/codebuild.py +10 -26
  23. magic_pocket_cli-0.18.0/pocket_cli/resources/aws/builders/dockerignore.py +81 -0
  24. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/aws/cloudformation.py +66 -91
  25. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/aws/ecr.py +21 -8
  26. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/aws/efs.py +23 -30
  27. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/aws/lambdahandler.py +60 -14
  28. magic_pocket_cli-0.18.0/pocket_cli/resources/aws/poll.py +35 -0
  29. magic_pocket_cli-0.18.0/pocket_cli/resources/aws/stack_backed.py +62 -0
  30. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/aws/state.py +17 -0
  31. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/cloudfront.py +11 -8
  32. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/cloudfront_acm.py +6 -20
  33. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/cloudfront_keys.py +22 -19
  34. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/cloudfront_waf.py +4 -21
  35. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/dsql.py +25 -19
  36. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/rds.py +76 -88
  37. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/tidb.py +6 -2
  38. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/templates/cloudformation/awscontainer.yaml +21 -0
  39. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pyproject.toml +2 -2
  40. magic_pocket_cli-0.17.0/.gitignore +0 -31
  41. magic_pocket_cli-0.17.0/pocket_cli/mediator.py +0 -463
  42. magic_pocket_cli-0.17.0/pocket_cli/resources/aws/builders/dockerignore.py +0 -44
  43. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/__init__.py +0 -0
  44. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/__init__.py +0 -0
  45. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/aws_auth.py +0 -0
  46. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/main_cli.py +0 -0
  47. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/permissions_cli.py +0 -0
  48. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/status_cli.py +0 -0
  49. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/vpc_cli.py +0 -0
  50. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/cli/waf_cli.py +0 -0
  51. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/__init__.py +0 -0
  52. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/aws/__init__.py +0 -0
  53. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/aws/builders/__init__.py +0 -0
  54. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/aws/builders/depot.py +0 -0
  55. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/aws/builders/docker.py +0 -0
  56. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/aws/s3_utils.py +0 -0
  57. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/awscontainer.py +0 -0
  58. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/neon.py +0 -0
  59. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/s3.py +0 -0
  60. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/upstash.py +0 -0
  61. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/resources/vpc.py +0 -0
  62. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/templates/cloudformation/cf_function_api_host.js +0 -0
  63. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/templates/cloudformation/cf_function_host_redirect_prelude.js +0 -0
  64. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/templates/cloudformation/cf_function_spa_auth.js +0 -0
  65. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/templates/cloudformation/cf_function_spa_fallback.js +0 -0
  66. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/templates/cloudformation/cloudfront.yaml +0 -0
  67. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/templates/cloudformation/cloudfront_acm.yaml +0 -0
  68. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/templates/cloudformation/cloudfront_keys.yaml +0 -0
  69. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/templates/cloudformation/cloudfront_waf.yaml +0 -0
  70. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/templates/cloudformation/vpc.yaml +0 -0
  71. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/templates/init/django-dotenv.env +0 -0
  72. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/templates/init/django-settings.py +0 -0
  73. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/templates/init/pocket.Dockerfile +0 -0
  74. {magic_pocket_cli-0.17.0 → magic_pocket_cli-0.18.0}/pocket_cli/templates/init/pocket_simple.toml +0 -0
@@ -0,0 +1,42 @@
1
+ # python generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # venv
10
+ .venv
11
+
12
+ # editors
13
+ .vscode/
14
+
15
+ # local env
16
+ .env
17
+ .envrc.local
18
+
19
+ # Rust
20
+ /target/
21
+
22
+ # Zensical build output
23
+ site/
24
+
25
+ # Vendor wheels (built locally for examples)
26
+ vendor/
27
+
28
+ # Forge
29
+ forge.local.toml
30
+
31
+ /CLAUDE.md
32
+
33
+ # Activity Docs (作業ローカルの working doc。公開 repo には出さない)
34
+ /docs/activity/
35
+
36
+ # example deploy 生成物 (pocket が生成: runtime config / collectstatic 出力 / cache)
37
+ /example-neon/pocket.runtime.toml
38
+ /example-neon/pocket_cache/
39
+ /example-neon/staticfiles/
40
+ /example-tidb/pocket.runtime.toml
41
+ /example-tidb/pocket_cache/
42
+ /example-tidb/staticfiles/
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: magic-pocket-cli
3
- Version: 0.17.0
3
+ Version: 0.18.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.17.0
10
+ Requires-Dist: magic-pocket>=0.18.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
@@ -9,6 +9,11 @@ from botocore.exceptions import ClientError
9
9
  from pocket.context import Context
10
10
  from pocket.runtime import get_secrets
11
11
  from pocket.utils import echo
12
+ from pocket_cli.cli.destroy_cli import (
13
+ _collect_awscontainer_targets,
14
+ _destroy_awscontainer,
15
+ )
16
+ from pocket_cli.cli.resource_helper import require_configured
12
17
  from pocket_cli.mediator import Mediator
13
18
  from pocket_cli.resources.awscontainer import AwsContainer
14
19
 
@@ -20,10 +25,11 @@ def awscontainer():
20
25
 
21
26
  def get_awscontainer_resource(stage):
22
27
  context = Context.from_toml(stage=stage)
23
- if not context.awscontainer:
24
- echo.danger("awscontainer is not configured for this stage")
25
- raise Exception("awscontainer is not configured for this stage")
26
- return AwsContainer(context=context.awscontainer)
28
+ return AwsContainer(
29
+ context=require_configured(
30
+ context.awscontainer, "awscontainer is not configured for this stage"
31
+ )
32
+ )
27
33
 
28
34
 
29
35
  @awscontainer.command()
@@ -135,25 +141,32 @@ def create(stage):
135
141
  @awscontainer.command()
136
142
  @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
137
143
  @click.option("--with-secrets", is_flag=True, default=False)
138
- def destroy(stage, with_secrets):
139
- ac = get_awscontainer_resource(stage)
140
- if ac.stack.status == "NOEXIST":
141
- echo.warning("No AWS lambda container found.")
142
- else:
143
- ac.stack.delete()
144
- echo.success("Aws lambda container was destroyed.")
145
- if ac.ecr.exists():
146
- ac.ecr.delete()
147
- echo.success("ECR repository was deleted.")
148
- else:
149
- echo.warning("No ECR repository found.")
150
- if with_secrets:
151
- _confirm_delete_pocket_managed_secrets(ac)
152
- if ac.context.secrets:
153
- ac.context.secrets.pocket_store.delete_secrets()
154
- echo.success("Pocket managed secrets were deleted.")
155
- else:
156
- echo.warning("Pocket managed secrets still exists.")
144
+ @click.option(
145
+ "--yes", "-y", is_flag=True, default=False, help="確認プロンプトをスキップ"
146
+ )
147
+ def destroy(stage, with_secrets, yes):
148
+ """AwsContainer 関連リソースを削除する。
149
+
150
+ トップレベル `pocket destroy` と同じ実装を使う (共有 ECR の削除ガード /
151
+ stack 削除完了待ち / CodeBuild / log group 掃除を含む)
152
+ """
153
+ context = Context.from_toml(stage=stage)
154
+ if not context.awscontainer:
155
+ echo.warning("awscontainer is not configured for this stage")
156
+ return
157
+ targets = _collect_awscontainer_targets(context, with_secrets)
158
+ if not targets:
159
+ echo.warning("削除対象のリソースが見つかりません。")
160
+ return
161
+ echo.danger("以下のリソースを削除します:")
162
+ for target in targets:
163
+ echo.info(" - %s" % target)
164
+ echo.danger("この操作は取り消せません!")
165
+ if not yes:
166
+ click.confirm(
167
+ "stage '%s' の AwsContainer リソースを削除しますか?" % stage, abort=True
168
+ )
169
+ _destroy_awscontainer(context, with_secrets)
157
170
 
158
171
 
159
172
  @awscontainer.command()
@@ -2,6 +2,7 @@ import click
2
2
 
3
3
  from pocket.context import Context
4
4
  from pocket.utils import echo
5
+ from pocket_cli.cli.resource_helper import require_configured
5
6
  from pocket_cli.resources.cloudfront import CloudFront
6
7
 
7
8
 
@@ -12,13 +13,12 @@ def cloudfront():
12
13
 
13
14
  def get_cloudfront_resources(stage, name=None):
14
15
  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")
16
+ require_configured(
17
+ context.cloudfront, "cloudfront is not configured for this stage"
18
+ )
18
19
  if name:
19
20
  if name not in context.cloudfront:
20
- echo.danger("cloudfront '%s' is not configured" % name)
21
- raise Exception("cloudfront '%s' is not configured" % name)
21
+ raise click.ClickException("cloudfront '%s' is not configured" % name)
22
22
  return [CloudFront(context.cloudfront[name])]
23
23
  return [CloudFront(cf_ctx) for cf_ctx in context.cloudfront.values()]
24
24
 
@@ -63,7 +63,15 @@ def create(stage, name):
63
63
  @cloudfront.command()
64
64
  @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
65
65
  @click.option("--name", default=None)
66
- def destroy(stage, name):
66
+ @click.option(
67
+ "--yes", "-y", is_flag=True, default=False, help="確認プロンプトをスキップ"
68
+ )
69
+ def destroy(stage, name, yes):
70
+ if not yes:
71
+ click.confirm(
72
+ "stage '%s' の CloudFront distribution を削除しますか?" % stage,
73
+ abort=True,
74
+ )
67
75
  for cf in get_cloudfront_resources(stage, name):
68
76
  echo.info("[%s]" % cf.context.name)
69
77
  cf.delete()
@@ -2,6 +2,7 @@ import click
2
2
 
3
3
  from pocket.context import Context
4
4
  from pocket.utils import echo
5
+ from pocket_cli.cli.resource_helper import require_configured
5
6
  from pocket_cli.resources.cloudfront_keys import CloudFrontKeys
6
7
 
7
8
 
@@ -12,9 +13,9 @@ def cloudfront_keys():
12
13
 
13
14
  def get_cloudfront_keys_resources(stage, name=None):
14
15
  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")
16
+ require_configured(
17
+ context.cloudfront, "cloudfront is not configured for this stage"
18
+ )
18
19
  results = []
19
20
  for cf_name, cf_ctx in context.cloudfront.items():
20
21
  if not cf_ctx.signing_key:
@@ -23,8 +24,7 @@ def get_cloudfront_keys_resources(stage, name=None):
23
24
  continue
24
25
  results.append(CloudFrontKeys(cf_ctx))
25
26
  if name and not results:
26
- echo.danger("cloudfront_keys '%s' is not configured" % name)
27
- raise Exception("cloudfront_keys '%s' is not configured" % name)
27
+ raise click.ClickException("cloudfront_keys '%s' is not configured" % name)
28
28
  return results
29
29
 
30
30
 
@@ -61,7 +61,16 @@ def status(stage, name):
61
61
  @cloudfront_keys.command()
62
62
  @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
63
63
  @click.option("--name", default=None)
64
- def destroy(stage, name):
64
+ @click.option(
65
+ "--yes", "-y", is_flag=True, default=False, help="確認プロンプトをスキップ"
66
+ )
67
+ def destroy(stage, name, yes):
68
+ if not yes:
69
+ click.confirm(
70
+ "stage '%s' の CloudFront signing key リソースを削除しますか?"
71
+ "(署名済み URL が無効になります)" % stage,
72
+ abort=True,
73
+ )
65
74
  for cfk in get_cloudfront_keys_resources(stage, name):
66
75
  echo.info("[%s]" % cfk.context.name)
67
76
  cfk.delete()
@@ -2,6 +2,7 @@ import click
2
2
 
3
3
  from pocket.context import Context
4
4
  from pocket.utils import echo
5
+ from pocket_cli.cli.resource_helper import require_configured
5
6
  from pocket_cli.resources.cloudfront_waf import CloudFrontWaf
6
7
 
7
8
 
@@ -12,9 +13,9 @@ def cloudfront_waf():
12
13
 
13
14
  def get_cloudfront_waf_resources(stage, name=None):
14
15
  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")
16
+ require_configured(
17
+ context.cloudfront, "cloudfront is not configured for this stage"
18
+ )
18
19
  results = []
19
20
  for cf_name, cf_ctx in context.cloudfront.items():
20
21
  if cf_ctx.waf is None:
@@ -23,8 +24,7 @@ def get_cloudfront_waf_resources(stage, name=None):
23
24
  continue
24
25
  results.append(CloudFrontWaf(cf_ctx))
25
26
  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)
27
+ raise click.ClickException("cloudfront_waf '%s' is not configured" % name)
28
28
  return results
29
29
 
30
30
 
@@ -61,7 +61,15 @@ def status(stage, name):
61
61
  @cloudfront_waf.command()
62
62
  @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
63
63
  @click.option("--name", default=None)
64
- def destroy(stage, name):
64
+ @click.option(
65
+ "--yes", "-y", is_flag=True, default=False, help="確認プロンプトをスキップ"
66
+ )
67
+ def destroy(stage, name, yes):
68
+ if not yes:
69
+ click.confirm(
70
+ "stage '%s' の WAF WebACL を削除しますか?(IP 制限が解除されます)" % stage,
71
+ abort=True,
72
+ )
65
73
  for waf in get_cloudfront_waf_resources(stage, name):
66
74
  echo.info("[%s]" % waf.context.name)
67
75
  waf.delete()
@@ -6,7 +6,7 @@ import click
6
6
  from pocket.context import Context, deploy_hash_report
7
7
  from pocket.utils import echo
8
8
  from pocket_cli.mediator import Mediator
9
- from pocket_cli.resources.aws.state import StateStore
9
+ from pocket_cli.resources.aws.state import StateStore, create_state_store
10
10
  from pocket_cli.resources.awscontainer import AwsContainer
11
11
  from pocket_cli.resources.cloudfront import CloudFront
12
12
  from pocket_cli.resources.cloudfront_acm import CloudFrontAcm
@@ -72,15 +72,7 @@ def get_resources(context: Context, *, state_bucket: str = ""):
72
72
 
73
73
 
74
74
  def _create_state_store(context: Context) -> StateStore:
75
- if not context.general:
76
- raise RuntimeError("general context is not configured")
77
- resource_prefix = context.general.prefix_template.format(
78
- stage=context.stage,
79
- project=context.project_name,
80
- namespace=context.general.namespace,
81
- )
82
- bucket_name = f"{resource_prefix}state"
83
- return StateStore(bucket_name, context.general.region)
75
+ return create_state_store(context)
84
76
 
85
77
 
86
78
  def deploy_init_resources(context: Context, *, state_bucket: str = ""):
@@ -114,6 +106,43 @@ def upload_managed_assets(context: Context):
114
106
  cf.upload_managed_assets()
115
107
 
116
108
 
109
+ def _deploy_resource(resource, mediator: Mediator, state_store: StateStore):
110
+ target_name = resource.__class__.__name__
111
+ # template hash に影響する入力 (secret 値等) を status 判定前に読み込む。
112
+ # 空のまま hash を計算すると deploy 済み hash と一致せず、secret 焼き込み
113
+ # 構成 (enable_origin_verify / signing_key) で毎回 REQUIRE_UPDATE になる
114
+ prepare = getattr(resource, "prepare_deploy", None)
115
+ if prepare is not None:
116
+ prepare(mediator)
117
+ if resource.status == "NOEXIST":
118
+ echo.log("Creating %s..." % target_name)
119
+ if "mediator" in inspect.signature(resource.create).parameters:
120
+ resource.create(mediator)
121
+ else:
122
+ resource.create()
123
+ state_store.record(resource.state_info())
124
+ elif resource.status == "REQUIRE_UPDATE":
125
+ echo.log("Updating %s..." % target_name)
126
+ if "mediator" in inspect.signature(resource.update).parameters:
127
+ resource.update(mediator)
128
+ else:
129
+ resource.update()
130
+ state_store.record(resource.state_info())
131
+ elif resource.status == "FAILED":
132
+ # 黙ってスキップすると deploy が exit 0 で成功表示になる
133
+ raise RuntimeError(
134
+ "%s が FAILED 状態です。AWS コンソール等で状態を確認して"
135
+ "解消してから再実行してください。" % target_name
136
+ )
137
+ elif resource.status == "PROGRESS":
138
+ raise RuntimeError(
139
+ "%s の前回操作が進行中です。完了を待ってから再実行してください。"
140
+ % target_name
141
+ )
142
+ else:
143
+ echo.log("%s is already the latest version." % target_name)
144
+
145
+
117
146
  def deploy_resources(context: Context, *, state_bucket: str = ""):
118
147
  state_store = _create_state_store(context)
119
148
  # state bucket は deploy_init_resources の前に作成済み
@@ -123,23 +152,7 @@ def deploy_resources(context: Context, *, state_bucket: str = ""):
123
152
  mediator = Mediator(context)
124
153
  resources = get_resources(context, state_bucket=state_bucket)
125
154
  for resource in resources:
126
- target_name = resource.__class__.__name__
127
- if resource.status == "NOEXIST":
128
- echo.log("Creating %s..." % target_name)
129
- if "mediator" in inspect.signature(resource.create).parameters:
130
- resource.create(mediator)
131
- else:
132
- resource.create()
133
- state_store.record(resource.state_info())
134
- elif resource.status == "REQUIRE_UPDATE":
135
- echo.log("Updating %s..." % target_name)
136
- if "mediator" in inspect.signature(resource.update).parameters:
137
- resource.update(mediator)
138
- else:
139
- resource.update()
140
- state_store.record(resource.state_info())
141
- else:
142
- echo.log("%s is already the latest version." % target_name)
155
+ _deploy_resource(resource, mediator, state_store)
143
156
  # stack 作成/更新が終わった後の後付け状態 (bucket policy / KVS など) を
144
157
  # 冪等に確保する。wait_status が timeout した次の deploy でも復旧できる。
145
158
  for resource in resources:
@@ -4,7 +4,10 @@ import click
4
4
  from pocket.context import Context
5
5
  from pocket.utils import echo
6
6
  from pocket_cli.resources.aws.builders.codebuild import CodeBuildBuilder
7
- from pocket_cli.resources.aws.state import StateStore
7
+ from pocket_cli.resources.aws.state import (
8
+ context_resource_prefix,
9
+ create_state_store,
10
+ )
8
11
  from pocket_cli.resources.awscontainer import AwsContainer
9
12
  from pocket_cli.resources.cloudfront import CloudFront
10
13
  from pocket_cli.resources.cloudfront_acm import CloudFrontAcm
@@ -18,32 +21,15 @@ from pocket_cli.resources.upstash import Upstash
18
21
  from pocket_cli.resources.vpc import Vpc
19
22
 
20
23
 
21
- def _create_state_store(context: Context) -> StateStore:
22
- if not context.general:
23
- raise RuntimeError("general context is not configured")
24
- resource_prefix = context.general.prefix_template.format(
25
- stage=context.stage,
26
- project=context.project_name,
27
- namespace=context.general.namespace,
28
- )
29
- bucket_name = f"{resource_prefix}state"
30
- return StateStore(bucket_name, context.general.region)
31
-
32
-
33
24
  def _create_codebuild_builder(context: Context) -> CodeBuildBuilder | None:
34
25
  """CodeBuildBuilder インスタンスを作成(リソース確認用)"""
35
26
  if not context.awscontainer or not context.general:
36
27
  return None
37
- resource_prefix = context.general.prefix_template.format(
38
- stage=context.stage,
39
- project=context.project_name,
40
- namespace=context.general.namespace,
41
- )
42
- state_bucket = f"{resource_prefix}state"
28
+ resource_prefix = context_resource_prefix(context)
43
29
  return CodeBuildBuilder(
44
30
  region=context.general.region,
45
31
  resource_prefix=resource_prefix,
46
- state_bucket=state_bucket,
32
+ state_bucket=f"{resource_prefix}state",
47
33
  permissions_boundary=context.awscontainer.permissions_boundary,
48
34
  )
49
35
 
@@ -89,13 +75,18 @@ def _collect_awscontainer_targets(context: Context, with_secrets: bool):
89
75
  parts.append("CodeBuild")
90
76
 
91
77
  targets.append("AwsContainer (%s)" % " + ".join(parts))
92
- targets.extend(_collect_vpc_targets(context))
93
78
 
94
79
  return targets
95
80
 
96
81
 
97
- def _collect_database_targets(context: Context) -> list[str]:
98
- """データベース関連の削除対象を収集"""
82
+ def _warn_command_provisioned(label: str, hint: str):
83
+ echo.warning(
84
+ '%s は provisioning="command" のため destroy では削除しません。'
85
+ "%s で削除してください。" % (label, hint)
86
+ )
87
+
88
+
89
+ def _collect_aws_database_targets(context: Context) -> list[str]:
99
90
  targets: list[str] = []
100
91
  if context.dsql:
101
92
  dsql = Dsql(context.dsql)
@@ -105,15 +96,36 @@ def _collect_database_targets(context: Context) -> list[str]:
105
96
  rds = Rds(context.rds)
106
97
  if rds.status != "NOEXIST":
107
98
  targets.append("RDS Aurora クラスター: %s" % context.rds.cluster_identifier)
108
- if context.tidb:
109
- targets.append("TiDB クラスタ")
110
- if context.upstash:
111
- targets.append("Upstash Redis: %s" % context.upstash.database_name)
112
- if context.neon:
113
- targets.append("Neon ブランチ")
114
99
  return targets
115
100
 
116
101
 
102
+ def _collect_external_database_targets(context: Context) -> list[str]:
103
+ """外部 DB (TiDB / Upstash / Neon) の削除対象を収集
104
+
105
+ provisioning="command" の DB は deploy 同様 destroy も管理しない
106
+ (credential レス運用のため provider API を叩かない)。
107
+ dsql/rds と同様に存在確認し、確認プロンプトの一覧を実態に合わせる。
108
+ """
109
+ targets: list[str] = []
110
+ if context.tidb and context.tidb.provisioning != "command":
111
+ if TiDb(context.tidb).cluster:
112
+ targets.append("TiDB クラスタ")
113
+ if context.upstash and context.upstash.provisioning != "command":
114
+ if Upstash(context.upstash).database:
115
+ targets.append("Upstash Redis: %s" % context.upstash.database_name)
116
+ if context.neon and context.neon.provisioning != "command":
117
+ if Neon(context.neon).branch:
118
+ targets.append("Neon ブランチ")
119
+ return targets
120
+
121
+
122
+ def _collect_database_targets(context: Context) -> list[str]:
123
+ """データベース関連の削除対象を収集"""
124
+ return _collect_aws_database_targets(context) + _collect_external_database_targets(
125
+ context
126
+ )
127
+
128
+
117
129
  def _collect_targets(context: Context, with_secrets: bool, with_state_bucket: bool):
118
130
  """削除対象のリソース一覧を収集"""
119
131
  targets: list[str] = []
@@ -125,6 +137,7 @@ def _collect_targets(context: Context, with_secrets: bool, with_state_bucket: bo
125
137
 
126
138
  targets.extend(_collect_awscontainer_targets(context, with_secrets))
127
139
  targets.extend(_collect_database_targets(context))
140
+ targets.extend(_collect_vpc_targets(context))
128
141
 
129
142
  for name, cf_ctx in context.cloudfront.items():
130
143
  if cf_ctx.signing_key:
@@ -173,6 +186,9 @@ def _destroy_awscontainer(context: Context, with_secrets: bool):
173
186
  if ac.stack.status != "NOEXIST":
174
187
  echo.log("Destroying AwsContainer stack...")
175
188
  ac.stack.delete()
189
+ # Lambda が VPC 内にある場合、ENI 解放を含む削除完了を待たないと
190
+ # 後続の VPC 削除が subnet 使用中で DELETE_FAILED になる
191
+ ac.stack.wait_status("NOEXIST", timeout=1800, interval=10)
176
192
  echo.success("AwsContainer stack was destroyed.")
177
193
 
178
194
  if ac.ecr.exists():
@@ -196,8 +212,6 @@ def _destroy_awscontainer(context: Context, with_secrets: bool):
196
212
  context.awscontainer.secrets.pocket_store.delete_secrets()
197
213
  echo.success("Pocket managed secrets were deleted.")
198
214
 
199
- _destroy_vpc(context)
200
-
201
215
 
202
216
  def _destroy_dsql(context: Context):
203
217
  """DSQL クラスターを削除"""
@@ -252,6 +266,44 @@ def _destroy_vpc(context: Context):
252
266
  echo.success("VPC was destroyed.")
253
267
 
254
268
 
269
+ def _destroy_tidb(context: Context):
270
+ """TiDB クラスタを削除 (provisioning="command" は管理外)"""
271
+ if not context.tidb:
272
+ return
273
+ if context.tidb.provisioning == "command":
274
+ _warn_command_provisioned("TiDB", "pocket resource tidb delete")
275
+ return
276
+ if TiDb(context.tidb).cluster:
277
+ echo.log("Destroying TiDB cluster...")
278
+ TiDb(context.tidb).delete_cluster()
279
+ echo.success("TiDB cluster was deleted.")
280
+
281
+
282
+ def _destroy_upstash(context: Context):
283
+ """Upstash Redis を削除 (provisioning="command" は管理外)"""
284
+ if not context.upstash:
285
+ return
286
+ if context.upstash.provisioning == "command":
287
+ _warn_command_provisioned("Upstash", "Upstash コンソール等")
288
+ return
289
+ upstash = Upstash(context.upstash)
290
+ if upstash.database:
291
+ upstash.delete_database()
292
+
293
+
294
+ def _destroy_neon(context: Context):
295
+ """Neon ブランチを削除 (provisioning="command" は管理外)"""
296
+ if not context.neon:
297
+ return
298
+ if context.neon.provisioning == "command":
299
+ _warn_command_provisioned("Neon", "pocket resource neon delete")
300
+ return
301
+ if Neon(context.neon).branch:
302
+ echo.log("Destroying Neon branch...")
303
+ Neon(context.neon).delete_branch()
304
+ echo.success("Neon branch was deleted.")
305
+
306
+
255
307
  def _destroy_cloudfront_and_acm(context: Context):
256
308
  """CloudFront ディストリビューションと ACM 証明書を削除"""
257
309
  for name, cf_ctx in context.cloudfront.items():
@@ -273,7 +325,7 @@ def _destroy_resources(context: Context, with_secrets: bool, with_state_bucket:
273
325
  # 1. CloudFront + ACM
274
326
  _destroy_cloudfront_and_acm(context)
275
327
 
276
- # 2. AwsContainer (CFNスタック + ECR + secrets) + 3. VPC
328
+ # 2. AwsContainer (CFNスタック + ECR + secrets)
277
329
  _destroy_awscontainer(context, with_secrets)
278
330
 
279
331
  # 2.5. DSQL
@@ -282,6 +334,10 @@ def _destroy_resources(context: Context, with_secrets: bool, with_state_bucket:
282
334
  # 2.6. RDS(AwsContainer の後、VPC の前)
283
335
  _destroy_rds(context)
284
336
 
337
+ # 3. VPC(RDS の後。RDS が VPC の subnet / SG を使用しているため、
338
+ # 先に VPC を消すと DELETE_FAILED になる)
339
+ _destroy_vpc(context)
340
+
285
341
  # 3.5. CloudFrontKeys(AwsContainer の後、S3 の前)
286
342
  for name, cf_ctx in context.cloudfront.items():
287
343
  if cf_ctx.signing_key:
@@ -297,27 +353,18 @@ def _destroy_resources(context: Context, with_secrets: bool, with_state_bucket:
297
353
  S3(context.s3).delete()
298
354
  echo.success("S3 bucket was deleted.")
299
355
 
300
- # 5. TiDB クラスタ
301
- if context.tidb and TiDb(context.tidb).cluster:
302
- echo.log("Destroying TiDB cluster...")
303
- TiDb(context.tidb).delete_cluster()
304
- echo.success("TiDB cluster was deleted.")
356
+ # 5. TiDB クラスタ (provisioning="command" は deploy 同様 destroy も管理しない)
357
+ _destroy_tidb(context)
305
358
 
306
359
  # 5.5. Upstash Redis
307
- if context.upstash:
308
- upstash = Upstash(context.upstash)
309
- if upstash.database:
310
- upstash.delete_database()
360
+ _destroy_upstash(context)
311
361
 
312
362
  # 6. Neon ブランチ
313
- if context.neon and Neon(context.neon).branch:
314
- echo.log("Destroying Neon branch...")
315
- Neon(context.neon).delete_branch()
316
- echo.success("Neon branch was deleted.")
363
+ _destroy_neon(context)
317
364
 
318
365
  # 7. ステートバケット
319
366
  if with_state_bucket:
320
- state_store = _create_state_store(context)
367
+ state_store = create_state_store(context)
321
368
  echo.log("Destroying state bucket...")
322
369
  state_store.delete_bucket()
323
370
  echo.success("State bucket was deleted.")
@@ -4,14 +4,15 @@ import click
4
4
 
5
5
  from pocket.context import Context
6
6
  from pocket.utils import echo
7
+ from pocket_cli.cli.resource_helper import require_configured
7
8
  from pocket_cli.resources.dsql import Dsql
8
9
 
9
10
 
10
11
  def _get_dsql_resource(stage: str) -> Dsql:
11
12
  context = Context.from_toml(stage=stage)
12
- if not context.dsql:
13
- raise click.ClickException("dsql is not configured for this stage")
14
- return Dsql(context.dsql)
13
+ return Dsql(
14
+ require_configured(context.dsql, "dsql is not configured for this stage")
15
+ )
15
16
 
16
17
 
17
18
  @click.group()