magic-pocket-cli 0.5.0__tar.gz → 0.7.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 (66) hide show
  1. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/PKG-INFO +2 -2
  2. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/deploy_cli.py +7 -34
  3. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/main_cli.py +2 -0
  4. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/neon_cli.py +33 -0
  5. magic_pocket_cli-0.7.0/pocket_cli/cli/store_url_helper.py +75 -0
  6. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/tidb_cli.py +33 -0
  7. magic_pocket_cli-0.7.0/pocket_cli/cli/upstash_cli.py +39 -0
  8. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/django_cli.py +47 -25
  9. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/mediator.py +64 -0
  10. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/neon.py +2 -4
  11. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/tidb.py +2 -3
  12. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/upstash.py +2 -3
  13. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pyproject.toml +2 -2
  14. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/.gitignore +0 -0
  15. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/__init__.py +0 -0
  16. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/__init__.py +0 -0
  17. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/aws_auth.py +0 -0
  18. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/awscontainer_cli.py +0 -0
  19. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/cloudfront_cli.py +0 -0
  20. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/cloudfront_keys_cli.py +0 -0
  21. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/cloudfront_waf_cli.py +0 -0
  22. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/destroy_cli.py +0 -0
  23. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/dsql_cli.py +0 -0
  24. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/migrate_cli.py +0 -0
  25. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/permissions_cli.py +0 -0
  26. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/rds_cli.py +0 -0
  27. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/runtime_config_cli.py +0 -0
  28. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/s3_cli.py +0 -0
  29. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/status_cli.py +0 -0
  30. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/vpc_cli.py +0 -0
  31. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/waf_cli.py +0 -0
  32. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/__init__.py +0 -0
  33. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/__init__.py +0 -0
  34. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/builders/__init__.py +0 -0
  35. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/builders/codebuild.py +0 -0
  36. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/builders/depot.py +0 -0
  37. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/builders/docker.py +0 -0
  38. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/builders/dockerignore.py +0 -0
  39. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/cloudformation.py +0 -0
  40. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/ecr.py +0 -0
  41. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/efs.py +0 -0
  42. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/lambdahandler.py +0 -0
  43. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/s3_utils.py +0 -0
  44. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/state.py +0 -0
  45. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/awscontainer.py +0 -0
  46. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/cloudfront.py +0 -0
  47. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/cloudfront_acm.py +0 -0
  48. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/cloudfront_keys.py +0 -0
  49. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/cloudfront_waf.py +0 -0
  50. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/dsql.py +0 -0
  51. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/rds.py +0 -0
  52. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/s3.py +0 -0
  53. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/vpc.py +0 -0
  54. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/cloudformation/awscontainer.yaml +0 -0
  55. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/cloudformation/cf_function_api_host.js +0 -0
  56. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/cloudformation/cf_function_spa_auth.js +0 -0
  57. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/cloudformation/cf_function_spa_fallback.js +0 -0
  58. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/cloudformation/cloudfront.yaml +0 -0
  59. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/cloudformation/cloudfront_acm.yaml +0 -0
  60. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/cloudformation/cloudfront_keys.yaml +0 -0
  61. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/cloudformation/cloudfront_waf.yaml +0 -0
  62. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/cloudformation/vpc.yaml +0 -0
  63. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/init/django-dotenv.env +0 -0
  64. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/init/django-settings.py +0 -0
  65. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/init/pocket.Dockerfile +0 -0
  66. {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/init/pocket_simple.toml +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: magic-pocket-cli
3
- Version: 0.5.0
3
+ Version: 0.7.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.5.0
10
+ Requires-Dist: magic-pocket>=0.7.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
@@ -55,11 +55,13 @@ def get_resources(context: Context, *, state_bucket: str = ""):
55
55
  for _name, cf_ctx in context.cloudfront.items():
56
56
  if cf_ctx.waf is not None:
57
57
  resources.append(CloudFrontWaf(cf_ctx))
58
- if context.neon:
58
+ # provisioning="command" の DB は deploy が管理しない (credential 不要)。
59
+ # provisioning は `pocket <db> store-url` に一任し、deploy は stored-read のみ。
60
+ if context.neon and context.neon.provisioning != "command":
59
61
  resources.append(Neon(context.neon))
60
- if context.tidb:
62
+ if context.tidb and context.tidb.provisioning != "command":
61
63
  resources.append(TiDb(context.tidb))
62
- if context.upstash:
64
+ if context.upstash and context.upstash.provisioning != "command":
63
65
  resources.append(Upstash(context.upstash))
64
66
  if context.s3:
65
67
  resources.append(S3(context.s3, cloudfront_contexts=context.cloudfront))
@@ -150,19 +152,6 @@ def deploy_resources(context: Context, *, state_bucket: str = ""):
150
152
  hook()
151
153
 
152
154
 
153
- def apply_skip_check_existing(context: Context) -> None:
154
- """DB リソース (neon/tidb/upstash) の存在確認を一律 skip させる。
155
-
156
- `--skip-check-existing` 指定時に呼ぶ。pocket.toml を編集せず、その deploy
157
- 実行に限り外部 SaaS API への存在確認 call を回避する (deploy ロールに
158
- DB credentials を渡さず deploy を完走させる用途)。toml 側の
159
- `skip_check_existing` フラグと同義で、こちらは実行時上書き。
160
- """
161
- for db_ctx in (context.neon, context.tidb, context.upstash):
162
- if db_ctx is not None:
163
- db_ctx.skip_check_existing = True
164
-
165
-
166
155
  def build_image(context: Context, *, tag: str) -> str:
167
156
  """awscontainer image を指定 tag で build & push する (deploy はしない)。
168
157
 
@@ -205,19 +194,11 @@ def _deploy_pipeline(context: Context, *, openpath=None, skip_frontend=False):
205
194
  @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
206
195
  @click.option("--openpath")
207
196
  @click.option("--skip-frontend", is_flag=True, default=False)
208
- @click.option(
209
- "--skip-check-existing",
210
- is_flag=True,
211
- default=False,
212
- help="neon/tidb/upstash の存在確認 API を skip し COMPLETED 扱いで deploy",
213
- )
214
- def deploy(stage: str, openpath, skip_frontend, skip_check_existing):
197
+ def deploy(stage: str, openpath, skip_frontend):
215
198
  from pocket_cli.cli.aws_auth import check_aws_credentials
216
199
 
217
200
  check_aws_credentials()
218
201
  context = Context.from_toml(stage=stage)
219
- if skip_check_existing:
220
- apply_skip_check_existing(context)
221
202
  _deploy_pipeline(context, openpath=openpath, skip_frontend=skip_frontend)
222
203
 
223
204
 
@@ -226,13 +207,7 @@ def deploy(stage: str, openpath, skip_frontend, skip_check_existing):
226
207
  @click.option("--commit-hash", required=True, help="昇格する image の git commit hash")
227
208
  @click.option("--openpath")
228
209
  @click.option("--skip-frontend", is_flag=True, default=False)
229
- @click.option(
230
- "--skip-check-existing",
231
- is_flag=True,
232
- default=False,
233
- help="neon/tidb/upstash の存在確認 API を skip し COMPLETED 扱いで deploy",
234
- )
235
- def promote(stage: str, commit_hash, openpath, skip_frontend, skip_check_existing):
210
+ def promote(stage: str, commit_hash, openpath, skip_frontend):
236
211
  """build 済みの :<commit-hash> image へ stage を向けて deploy する (再ビルドなし)。
237
212
 
238
213
  `pocket django build` で push した image に :<stage> タグを移し、
@@ -244,8 +219,6 @@ def promote(stage: str, commit_hash, openpath, skip_frontend, skip_check_existin
244
219
  context = Context.from_toml(stage=stage)
245
220
  if context.awscontainer is None:
246
221
  raise click.ClickException("awscontainer がこの stage に設定されていません。")
247
- if skip_check_existing:
248
- apply_skip_check_existing(context)
249
222
  context.awscontainer.promote_commit_hash = commit_hash
250
223
  _deploy_pipeline(context, openpath=openpath, skip_frontend=skip_frontend)
251
224
 
@@ -18,6 +18,7 @@ from pocket_cli.cli import (
18
18
  s3_cli,
19
19
  status_cli,
20
20
  tidb_cli,
21
+ upstash_cli,
21
22
  vpc_cli,
22
23
  waf_cli,
23
24
  )
@@ -83,6 +84,7 @@ resource.add_command(vpc_cli.vpc)
83
84
  resource.add_command(awscontainer_cli.awscontainer)
84
85
  resource.add_command(neon_cli.neon)
85
86
  resource.add_command(tidb_cli.tidb)
87
+ resource.add_command(upstash_cli.upstash)
86
88
  resource.add_command(dsql_cli.dsql)
87
89
  resource.add_command(rds_cli.rds)
88
90
  resource.add_command(s3_cli.s3)
@@ -4,6 +4,7 @@ import click
4
4
 
5
5
  from pocket.context import Context
6
6
  from pocket.utils import echo
7
+ from pocket_cli.cli.store_url_helper import run_store_url
7
8
  from pocket_cli.resources.neon import Neon
8
9
 
9
10
 
@@ -67,6 +68,38 @@ def delete(stage):
67
68
  echo.success("Branch was deleted successfully.")
68
69
 
69
70
 
71
+ @neon.command()
72
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
73
+ @click.option(
74
+ "--key", default=None, help="保存先 user secret のキー (複数候補時に必須)"
75
+ )
76
+ @click.option("--force", is_flag=True, help="既存 secret があっても上書きする")
77
+ def store_url(stage, key, force):
78
+ """branch/role/db を ensure し DATABASE_URL を stored user secret に保存する。
79
+
80
+ provisioning="command" で deploy を Neon credential なしにするための provisioning
81
+ ステップ。Neon の URL は reveal_password 方式で冪等なので何度実行しても同じ値。
82
+ """
83
+
84
+ def ensure_and_compute_url(context):
85
+ neon = Neon(context.neon)
86
+ if not neon.branch:
87
+ neon.create_branch(neon.parent_branch)
88
+ neon.ensure_role()
89
+ neon.ensure_database()
90
+ # ensure 後の状態を確実に反映するため fresh instance で URL を算出する。
91
+ return Neon(context.neon).database_url
92
+
93
+ run_store_url(
94
+ stage=stage,
95
+ secret_type="neon_database_url", # noqa: S106 (secret type 名であって credential ではない)
96
+ db_label="Neon",
97
+ key=key,
98
+ force=force,
99
+ ensure_and_compute_url=ensure_and_compute_url,
100
+ )
101
+
102
+
70
103
  @neon.command()
71
104
  @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
72
105
  def status(stage):
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable
4
+
5
+ import click
6
+
7
+ from pocket.context import Context
8
+ from pocket.utils import echo
9
+ from pocket_cli.mediator import Mediator
10
+
11
+
12
+ def run_store_url(
13
+ *,
14
+ stage: str,
15
+ secret_type: str,
16
+ db_label: str,
17
+ key: str | None,
18
+ force: bool,
19
+ ensure_and_compute_url: Callable[[Context], str],
20
+ ) -> None:
21
+ """`pocket <db> store-url` の共通処理。
22
+
23
+ 対象 stored user secret を特定 → (既存かつ非 force なら no-op) → リソースを
24
+ ensure し URL を算出 → 正準名へ書き込む。`ensure_and_compute_url` は DB ごとの
25
+ ensure + URL 算出。
26
+ """
27
+ context = Context.from_toml(stage=stage)
28
+ sc = context.awscontainer.secrets if context.awscontainer else None
29
+ if sc is None:
30
+ raise click.ClickException(
31
+ "awscontainer.secrets が設定されていません。"
32
+ "[awscontainer.secrets.user] に DATABASE_URL を宣言してください。"
33
+ )
34
+
35
+ if key is not None:
36
+ spec = sc.user.get(key)
37
+ if spec is None:
38
+ raise click.ClickException("secrets.user に '%s' がありません。" % key)
39
+ if spec.type != secret_type:
40
+ raise click.ClickException(
41
+ "secrets.user '%s' は type=%s ではありません (type=%s)。"
42
+ % (key, secret_type, spec.type)
43
+ )
44
+ target_key = key
45
+ else:
46
+ candidates = [k for k, spec in sc.user.items() if spec.type == secret_type]
47
+ if not candidates:
48
+ raise click.ClickException(
49
+ "type=%s の stored user secret が宣言されていません。"
50
+ "[awscontainer.secrets.user] に "
51
+ '`DATABASE_URL = { type = "%s" }` を追加してください。'
52
+ % (secret_type, secret_type)
53
+ )
54
+ if len(candidates) > 1:
55
+ raise click.ClickException(
56
+ "type=%s の user secret が複数あります (%s)。--key で指定してください。"
57
+ % (secret_type, ", ".join(candidates))
58
+ )
59
+ target_key = candidates[0]
60
+
61
+ spec = sc.user[target_key]
62
+ mediator = Mediator(context)
63
+
64
+ if not force and mediator.stored_secret_exists(spec):
65
+ echo.warning(
66
+ "%s は既に存在します (%s)。rotate する場合は --force を付けてください。"
67
+ % (target_key, spec.name)
68
+ )
69
+ return
70
+
71
+ url = ensure_and_compute_url(context)
72
+ mediator.store_user_secret(spec, url)
73
+ echo.success(
74
+ "%s URL を %s (%s) に保存しました。" % (db_label, target_key, spec.name)
75
+ )
@@ -4,6 +4,7 @@ import click
4
4
 
5
5
  from pocket.context import Context
6
6
  from pocket.utils import echo
7
+ from pocket_cli.cli.store_url_helper import run_store_url
7
8
  from pocket_cli.resources.tidb import TiDb
8
9
 
9
10
 
@@ -51,6 +52,38 @@ def delete(stage):
51
52
  echo.success("Cluster was deleted successfully.")
52
53
 
53
54
 
55
+ @tidb.command()
56
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
57
+ @click.option(
58
+ "--key", default=None, help="保存先 user secret のキー (複数候補時に必須)"
59
+ )
60
+ @click.option("--force", is_flag=True, help="既存 secret があっても上書きする")
61
+ def store_url(stage, key, force):
62
+ """cluster/db を ensure し DATABASE_URL を stored user secret に保存する。
63
+
64
+ provisioning="command" で deploy を TiDB credential なしにするための provisioning
65
+ ステップ。注意: TiDB serverless は password reveal API が無いため、本コマンドは
66
+ 実行のたびに root password をローテーションする (既存 secret は --force が必要。
67
+ 実行後は consumer の redeploy が前提)。
68
+ """
69
+
70
+ def ensure_and_compute_url(context):
71
+ # TiDB は password reveal が無く ensure/url 算出で password を reset するため、
72
+ # 同一インスタンスを使い回して password を整合させる (fresh instance にしない)。
73
+ resource = TiDb(context.tidb)
74
+ resource.create()
75
+ return resource.database_url
76
+
77
+ run_store_url(
78
+ stage=stage,
79
+ secret_type="tidb_database_url", # noqa: S106 (secret type 名であって credential ではない)
80
+ db_label="TiDB",
81
+ key=key,
82
+ force=force,
83
+ ensure_and_compute_url=ensure_and_compute_url,
84
+ )
85
+
86
+
54
87
  @tidb.command()
55
88
  @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
56
89
  def status(stage):
@@ -0,0 +1,39 @@
1
+ import click
2
+
3
+ from pocket_cli.cli.store_url_helper import run_store_url
4
+ from pocket_cli.resources.upstash import Upstash
5
+
6
+
7
+ @click.group()
8
+ def upstash():
9
+ pass
10
+
11
+
12
+ @upstash.command()
13
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
14
+ @click.option(
15
+ "--key", default=None, help="保存先 user secret のキー (複数候補時に必須)"
16
+ )
17
+ @click.option("--force", is_flag=True, help="既存 secret があっても上書きする")
18
+ def store_url(stage, key, force):
19
+ """database を ensure し REDIS_URL を stored user secret に保存する。
20
+
21
+ provisioning="command" で deploy を Upstash credential なしにするための
22
+ provisioning ステップ。Upstash の URL は database の password を読み出すだけで
23
+ 冪等なので、何度実行しても同じ値になる。
24
+ """
25
+
26
+ def ensure_and_compute_url(context):
27
+ resource = Upstash(context.upstash)
28
+ resource.create()
29
+ # ensure 後の状態を確実に反映するため fresh instance で URL を算出する。
30
+ return Upstash(context.upstash).redis_url
31
+
32
+ run_store_url(
33
+ stage=stage,
34
+ secret_type="upstash_redis_url", # noqa: S106 (secret type 名であって credential ではない)
35
+ db_label="Upstash",
36
+ key=key,
37
+ force=force,
38
+ ensure_and_compute_url=ensure_and_compute_url,
39
+ )
@@ -82,13 +82,7 @@ def _update_dotenv(jinja2_env):
82
82
  @click.option(
83
83
  "--yes", "-y", is_flag=True, default=False, help="確認プロンプトをスキップ"
84
84
  )
85
- @click.option(
86
- "--skip-check-existing",
87
- is_flag=True,
88
- default=False,
89
- help="neon/tidb/upstash の存在確認 API を skip し COMPLETED 扱いで deploy",
90
- )
91
- def deploy(stage: str, openpath, yes, skip_check_existing):
85
+ def deploy(stage: str, openpath, yes):
92
86
  from pocket_cli.cli.deploy_cli import deploy as pocket_deploy
93
87
 
94
88
  # pocket deploy を実行(インフラ + SPA フロントエンド)
@@ -98,7 +92,6 @@ def deploy(stage: str, openpath, yes, skip_check_existing):
98
92
  stage=stage,
99
93
  openpath=None,
100
94
  skip_frontend=False,
101
- skip_check_existing=skip_check_existing,
102
95
  )
103
96
  _django_post_deploy(stage, yes=yes, openpath=openpath)
104
97
 
@@ -106,7 +99,12 @@ def deploy(stage: str, openpath, yes, skip_check_existing):
106
99
  def _django_post_deploy(stage: str, *, yes: bool, openpath):
107
100
  """deploy / promote 共通の Django 固有後処理 (collectstatic + migrate + URL)。"""
108
101
  context = Context.from_toml(stage=stage)
109
- if yes or click.confirm("deploystatic?", default=True):
102
+ if _staticfiles_publish_mode(context) == "command":
103
+ echo.info(
104
+ 'staticfiles is publish = "command": skipping deploystatic. '
105
+ "Publish with `pocket django deploystatic --stage %s`." % stage
106
+ )
107
+ elif yes or click.confirm("deploystatic?", default=True):
110
108
  collectstatic_locally(stage)
111
109
  upload_collected_staticfiles(stage)
112
110
  handler = _get_management_command_handler(context)
@@ -129,13 +127,7 @@ def _django_post_deploy(stage: str, *, yes: bool, openpath):
129
127
  @click.option(
130
128
  "--yes", "-y", is_flag=True, default=False, help="確認プロンプトをスキップ"
131
129
  )
132
- @click.option(
133
- "--skip-check-existing",
134
- is_flag=True,
135
- default=False,
136
- help="neon/tidb/upstash の存在確認 API を skip し COMPLETED 扱いで deploy",
137
- )
138
- def promote(stage: str, commit_hash, openpath, yes, skip_check_existing):
130
+ def promote(stage: str, commit_hash, openpath, yes):
139
131
  """build 済みの :<commit-hash> image へ stage を向けて deploy する (再ビルドなし)。
140
132
 
141
133
  `pocket django build` で push した image に :<stage> タグを移し、インフラ/Lambda
@@ -151,7 +143,6 @@ def promote(stage: str, commit_hash, openpath, yes, skip_check_existing):
151
143
  commit_hash=commit_hash,
152
144
  openpath=None,
153
145
  skip_frontend=False,
154
- skip_check_existing=skip_check_existing,
155
146
  )
156
147
  _django_post_deploy(stage, yes=yes, openpath=openpath)
157
148
 
@@ -195,6 +186,14 @@ def build(stage: str, allow_dirty: bool):
195
186
  echo.success("built and pushed: %s" % target)
196
187
 
197
188
 
189
+ def _staticfiles_publish_mode(context: Context) -> str:
190
+ if context.awscontainer and context.awscontainer.django:
191
+ storage = context.awscontainer.django.storages.get("staticfiles")
192
+ if storage:
193
+ return storage.publish
194
+ return "deploy"
195
+
196
+
198
197
  def _get_management_command_handler(context: Context, key: str | None = None):
199
198
  if not context.awscontainer:
200
199
  raise Exception("awscontainer is not configured for this stage")
@@ -271,7 +270,7 @@ def clear_staticfiles_override_env():
271
270
  os.environ.pop("POCKET_STATICFILES_LOCATION_OVERRIDE", None)
272
271
 
273
272
 
274
- def upload_collected_staticfiles(stage: str):
273
+ def upload_collected_staticfiles(stage: str, *, delete: bool = False):
275
274
  from pocket.django.utils import get_static_storage_s3_options
276
275
 
277
276
  s3_options = get_static_storage_s3_options(stage=stage)
@@ -281,9 +280,15 @@ def upload_collected_staticfiles(stage: str):
281
280
  echo.info("Bucket: %s" % s3_bucket_name)
282
281
  echo.info("Location: %s" % s3_location)
283
282
  echo.info("Uploading static files...")
283
+ cmd = "aws s3 sync %s s3://%s/%s/" % (
284
+ local_storage["OPTIONS"]["location"],
285
+ s3_bucket_name,
286
+ s3_location,
287
+ )
288
+ if delete:
289
+ cmd += " --delete"
284
290
  run( # noqa: S602 aws s3 sync を pocket.toml 設定値から構築 (信頼境界内)
285
- "aws s3 sync %s s3://%s/%s/ --delete"
286
- % (local_storage["OPTIONS"]["location"], s3_bucket_name, s3_location),
291
+ cmd,
287
292
  shell=True, # nosemgrep
288
293
  check=True,
289
294
  )
@@ -313,11 +318,14 @@ def _get_project_dir(stage: str) -> str | None:
313
318
  return None
314
319
 
315
320
 
316
- def collectstatic_locally(stage: str):
321
+ def collectstatic_locally(stage: str, *, link: bool = False):
317
322
  local_storage = get_deploystatic_local_storage(stage)
318
323
  echo.info("collectstatic to %s..." % local_storage["OPTIONS"]["location"])
319
324
  set_staticfiles_override_env(local_storage)
320
- cmd = _build_python_command(["manage.py", "collectstatic", "--noinput"])
325
+ args = ["manage.py", "collectstatic", "--noinput"]
326
+ if link:
327
+ args.append("--link")
328
+ cmd = _build_python_command(args)
321
329
  project_dir = _get_project_dir(stage)
322
330
  run(cmd, check=True, cwd=project_dir) # noqa: S603 shell=False + 制御された引数
323
331
  clear_staticfiles_override_env()
@@ -326,10 +334,24 @@ def collectstatic_locally(stage: str):
326
334
  @django.command()
327
335
  @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
328
336
  @click.option("--skip-collectstatic", is_flag=True, default=False)
329
- def deploystatic(stage: str, skip_collectstatic: bool):
337
+ @click.option(
338
+ "--delete",
339
+ is_flag=True,
340
+ default=False,
341
+ help="collectstatic 出力に無い S3 上のファイルを削除する (aws s3 sync --delete)。"
342
+ " 旧デプロイのアセットを参照中のリクエストや rollback を壊しうるため opt-in",
343
+ )
344
+ @click.option(
345
+ "--link",
346
+ is_flag=True,
347
+ default=False,
348
+ help="collectstatic に --link を渡す (大容量資産の複製コスト削減。"
349
+ " aws s3 sync は symlink を追うので upload 互換)",
350
+ )
351
+ def deploystatic(stage: str, skip_collectstatic: bool, delete: bool, link: bool):
330
352
  if not skip_collectstatic:
331
- collectstatic_locally(stage)
332
- upload_collected_staticfiles(stage)
353
+ collectstatic_locally(stage, link=link)
354
+ upload_collected_staticfiles(stage, delete=delete)
333
355
 
334
356
 
335
357
  @django.command(
@@ -41,6 +41,18 @@ class Mediator:
41
41
  generated: dict[str, str | dict[str, str]] = {}
42
42
  for key, managed_secret in sc.managed.items():
43
43
  if key not in sc.pocket_store.secrets:
44
+ if managed_secret.type in (
45
+ "neon_database_url",
46
+ "tidb_database_url",
47
+ "upstash_redis_url",
48
+ ):
49
+ echo.warning(
50
+ "computed DB/KVS URL (managed type=%s) は deprecated です。"
51
+ "deploy が管理 API を叩いて URL を算出する方式から、"
52
+ "[<db>] provisioning + [awscontainer.secrets.user] の type + "
53
+ "`pocket <db> store-url` (stored) への移行を推奨します。"
54
+ % managed_secret.type
55
+ )
44
56
  value = self._generate_secret(managed_secret)
45
57
  if value is None:
46
58
  msg = "Secret generation for %s is failed." % key
@@ -117,6 +129,58 @@ class Mediator:
117
129
  return False
118
130
  raise
119
131
 
132
+ def stored_secret_exists(self, spec) -> bool:
133
+ """stored mode user secret (spec.name) が store に存在するか。
134
+
135
+ store-url の冪等判定用。
136
+ """
137
+ if (
138
+ self.context.awscontainer is None
139
+ or self.context.awscontainer.secrets is None
140
+ or spec.name is None
141
+ ):
142
+ return False
143
+ sc = self.context.awscontainer.secrets
144
+ store = spec.store or sc.store
145
+ return self._stored_secret_exists(spec.name, store, sc.region)
146
+
147
+ def store_user_secret(self, spec, value: str) -> None:
148
+ """stored mode user secret の正準名 (spec.name) に単一値を書き込む。
149
+
150
+ `pocket <db> store-url` から使う。pocket_store (managed 集約) ではなく
151
+ user secret の導出名 ({pocket_key}-user/{KEY}) に直接 put する。
152
+ 読み側 _stored_secret_exists と対称。
153
+ """
154
+ import boto3
155
+ from botocore.exceptions import ClientError
156
+
157
+ if (
158
+ self.context.awscontainer is None
159
+ or self.context.awscontainer.secrets is None
160
+ ):
161
+ raise RuntimeError("awscontainer secrets is not configured")
162
+ sc = self.context.awscontainer.secrets
163
+ if spec.name is None:
164
+ raise RuntimeError("user secret name is not resolved")
165
+ store = spec.store or sc.store
166
+ if store == "ssm":
167
+ boto3.client("ssm", region_name=sc.region).put_parameter(
168
+ Name=spec.name, Value=value, Type="SecureString", Overwrite=True
169
+ )
170
+ return
171
+ client = boto3.client("secretsmanager", region_name=sc.region)
172
+ try:
173
+ client.create_secret(
174
+ Name=spec.name,
175
+ SecretString=value,
176
+ Tags=[{"Key": "Name", "Value": spec.name}],
177
+ )
178
+ except ClientError as e:
179
+ if e.response["Error"]["Code"] == "ResourceExistsException":
180
+ client.put_secret_value(SecretId=spec.name, SecretString=value)
181
+ else:
182
+ raise
183
+
120
184
  def _cleanup_orphaned_secrets(self):
121
185
  """SSM/SM にあるが managed 定義にないシークレットを削除する"""
122
186
  if self.context.awscontainer is None:
@@ -278,10 +278,8 @@ class Neon:
278
278
 
279
279
  @property
280
280
  def status(self) -> ResourceStatus:
281
- if self.context.skip_check_existing:
282
- # 存在確認の Neon API call skip し COMPLETED 固定。deploy ロールに
283
- # Neon credentials を渡さず deploy を完走させる用途 (settings 参照)。
284
- return "COMPLETED"
281
+ # provisioning="command" の Neon は get_resources で除外されるため、ここに
282
+ # 到達するのは deploy Neon を管理する provisioning="deploy" の場合のみ。
285
283
  if self.working:
286
284
  return "COMPLETED"
287
285
  return "NOEXIST"
@@ -160,9 +160,8 @@ class TiDb:
160
160
 
161
161
  @property
162
162
  def status(self) -> ResourceStatus:
163
- if self.context.skip_check_existing:
164
- # 存在確認の TiDB API call skip し COMPLETED 固定 (settings 参照)。
165
- return "COMPLETED"
163
+ # provisioning="command" の TiDB は get_resources で除外されるため、ここに
164
+ # 到達するのは deploy TiDB を管理する provisioning="deploy" の場合のみ。
166
165
  if not self.context.public_key or not self.context.private_key:
167
166
  return "NOEXIST"
168
167
  if self.cluster and self.cluster.status == "ACTIVE":
@@ -96,9 +96,8 @@ class Upstash:
96
96
 
97
97
  @property
98
98
  def status(self) -> ResourceStatus:
99
- if self.context.skip_check_existing:
100
- # 存在確認の Upstash API call skip し COMPLETED 固定 (settings 参照)。
101
- return "COMPLETED"
99
+ # provisioning="command" の Upstash は get_resources で除外されるため、ここに
100
+ # 到達するのは deploy Upstash を管理する provisioning="deploy" の場合のみ。
102
101
  if self.database and self.database.state == "active":
103
102
  return "COMPLETED"
104
103
  return "NOEXIST"
@@ -1,10 +1,10 @@
1
1
  [project]
2
2
  name = "magic-pocket-cli"
3
- version = "0.5.0"
3
+ version = "0.7.0"
4
4
  description = "CLI and deploy tools for magic-pocket."
5
5
  requires-python = ">=3.10"
6
6
  dependencies = [
7
- "magic-pocket>=0.5.0",
7
+ "magic-pocket>=0.7.0",
8
8
  "click>=8.1.7",
9
9
  "jinja2>=3.1.3",
10
10
  "python-on-whales>=0.68.0",