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.
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/PKG-INFO +2 -2
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/deploy_cli.py +7 -34
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/main_cli.py +2 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/neon_cli.py +33 -0
- magic_pocket_cli-0.7.0/pocket_cli/cli/store_url_helper.py +75 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/tidb_cli.py +33 -0
- magic_pocket_cli-0.7.0/pocket_cli/cli/upstash_cli.py +39 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/django_cli.py +47 -25
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/mediator.py +64 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/neon.py +2 -4
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/tidb.py +2 -3
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/upstash.py +2 -3
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pyproject.toml +2 -2
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/.gitignore +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/__init__.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/__init__.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/aws_auth.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/awscontainer_cli.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/cloudfront_cli.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/cloudfront_keys_cli.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/cloudfront_waf_cli.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/destroy_cli.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/dsql_cli.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/migrate_cli.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/permissions_cli.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/rds_cli.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/runtime_config_cli.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/s3_cli.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/status_cli.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/vpc_cli.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/cli/waf_cli.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/__init__.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/__init__.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/builders/__init__.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/builders/codebuild.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/builders/depot.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/builders/docker.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/builders/dockerignore.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/cloudformation.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/ecr.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/efs.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/lambdahandler.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/s3_utils.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/state.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/awscontainer.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/cloudfront.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/cloudfront_acm.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/cloudfront_keys.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/cloudfront_waf.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/dsql.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/rds.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/s3.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/vpc.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/cloudformation/awscontainer.yaml +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/cloudformation/cf_function_api_host.js +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/cloudformation/cf_function_spa_auth.js +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/cloudformation/cf_function_spa_fallback.js +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/cloudformation/cloudfront.yaml +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/cloudformation/cloudfront_acm.yaml +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/cloudformation/cloudfront_keys.yaml +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/cloudformation/cloudfront_waf.yaml +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/cloudformation/vpc.yaml +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/init/django-dotenv.env +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/init/django-settings.py +0 -0
- {magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/init/pocket.Dockerfile +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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.
|
|
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.
|
|
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",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/builders/__init__.py
RENAMED
|
File without changes
|
{magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/builders/codebuild.py
RENAMED
|
File without changes
|
{magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/builders/depot.py
RENAMED
|
File without changes
|
{magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/builders/docker.py
RENAMED
|
File without changes
|
{magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/builders/dockerignore.py
RENAMED
|
File without changes
|
{magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/resources/aws/cloudformation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/cloudformation/vpc.yaml
RENAMED
|
File without changes
|
{magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/init/django-dotenv.env
RENAMED
|
File without changes
|
{magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/init/django-settings.py
RENAMED
|
File without changes
|
{magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/init/pocket.Dockerfile
RENAMED
|
File without changes
|
{magic_pocket_cli-0.5.0 → magic_pocket_cli-0.7.0}/pocket_cli/templates/init/pocket_simple.toml
RENAMED
|
File without changes
|