magic-pocket-cli 0.11.0__tar.gz → 0.13.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.11.0 → magic_pocket_cli-0.13.0}/PKG-INFO +2 -2
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/deploy_cli.py +6 -1
- magic_pocket_cli-0.13.0/pocket_cli/cli/migrate_cli.py +310 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/url_helper.py +10 -5
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/mediator.py +137 -39
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/aws/cloudformation.py +70 -14
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/cloudfront.py +18 -63
- magic_pocket_cli-0.13.0/pocket_cli/templates/cloudformation/cf_function_host_redirect_prelude.js +13 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/templates/cloudformation/cloudfront.yaml +45 -24
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/templates/cloudformation/cloudfront_acm.yaml +13 -19
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pyproject.toml +2 -2
- magic_pocket_cli-0.11.0/pocket_cli/cli/migrate_cli.py +0 -148
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/.gitignore +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/__init__.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/__init__.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/aws_auth.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/awscontainer_cli.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/cloudfront_cli.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/cloudfront_keys_cli.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/cloudfront_waf_cli.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/destroy_cli.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/dsql_cli.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/main_cli.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/neon_cli.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/permissions_cli.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/rds_cli.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/runtime_config_cli.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/s3_cli.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/status_cli.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/store_url_helper.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/tidb_cli.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/upstash_cli.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/vpc_cli.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/cli/waf_cli.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/django_cli.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/__init__.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/aws/__init__.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/aws/builders/__init__.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/aws/builders/codebuild.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/aws/builders/depot.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/aws/builders/docker.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/aws/builders/dockerignore.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/aws/ecr.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/aws/efs.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/aws/lambdahandler.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/aws/s3_utils.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/aws/state.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/awscontainer.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/cloudfront_acm.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/cloudfront_keys.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/cloudfront_waf.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/dsql.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/neon.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/rds.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/s3.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/tidb.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/upstash.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/resources/vpc.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/templates/cloudformation/awscontainer.yaml +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/templates/cloudformation/cf_function_api_host.js +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/templates/cloudformation/cf_function_spa_auth.js +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/templates/cloudformation/cf_function_spa_fallback.js +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/templates/cloudformation/cloudfront_keys.yaml +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/templates/cloudformation/cloudfront_waf.yaml +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/templates/cloudformation/vpc.yaml +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/templates/init/django-dotenv.env +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/templates/init/django-settings.py +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.0}/pocket_cli/templates/init/pocket.Dockerfile +0 -0
- {magic_pocket_cli-0.11.0 → magic_pocket_cli-0.13.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.13.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.13.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
|
|
@@ -3,7 +3,7 @@ import webbrowser
|
|
|
3
3
|
|
|
4
4
|
import click
|
|
5
5
|
|
|
6
|
-
from pocket.context import Context
|
|
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
9
|
from pocket_cli.resources.aws.state import StateStore
|
|
@@ -173,6 +173,11 @@ def _deploy_pipeline(context: Context, *, openpath=None, skip_frontend=False):
|
|
|
173
173
|
promote 時は context.awscontainer.promote_commit_hash が設定済みで、
|
|
174
174
|
deploy_init 内の image build が retag に置き換わる以外は deploy と同一。
|
|
175
175
|
"""
|
|
176
|
+
# DEPLOY_HASH の解決結果を deploy 時に 1 回可視化する (env 伝播漏れで
|
|
177
|
+
# 黙って git short hash に落ちる footgun の早期発見用)。
|
|
178
|
+
deploy_hash_message = deploy_hash_report(context)
|
|
179
|
+
if deploy_hash_message:
|
|
180
|
+
echo.info(deploy_hash_message)
|
|
176
181
|
# CodeBuildがソースアップロードにstate bucketを必要とするため、先に作成
|
|
177
182
|
state_store = _create_state_store(context)
|
|
178
183
|
state_store.ensure_bucket()
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""pocket migrate: バージョン間のデータ/スタック移行をまとめて実行する。
|
|
2
|
+
|
|
3
|
+
サブコマンド:
|
|
4
|
+
|
|
5
|
+
- ``template-hash``: 旧バージョンでデプロイされたスタックに pocket:template_hash
|
|
6
|
+
タグを付与し、yaml_synced の判定をハッシュベースに移行する。
|
|
7
|
+
- ``secret-paths``: stored user secret を旧キー基準パス ({pocket_key}-user/{key})
|
|
8
|
+
から新 type 基準パス ({pocket_key}-user/{type}) へ移設する (0.11→0.12)。
|
|
9
|
+
|
|
10
|
+
サブコマンド無指定 (``pocket migrate``) なら全 migration を冪等に順次実行する。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
import yaml
|
|
19
|
+
|
|
20
|
+
from pocket import __version__
|
|
21
|
+
from pocket.context import Context
|
|
22
|
+
from pocket.utils import echo
|
|
23
|
+
from pocket_cli.cli.deploy_cli import get_resources
|
|
24
|
+
|
|
25
|
+
# --- template-hash migration -------------------------------------------------
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _get_existing_stacks(context: Context) -> list:
|
|
29
|
+
"""デプロイ済みの全スタックを収集する"""
|
|
30
|
+
stacks = []
|
|
31
|
+
for resource in get_resources(context):
|
|
32
|
+
if hasattr(resource, "stack") and resource.stack.exists:
|
|
33
|
+
stacks.append(resource.stack)
|
|
34
|
+
return stacks
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _compute_uploaded_hash(stack) -> str | None:
|
|
38
|
+
"""uploaded template からハッシュを計算する"""
|
|
39
|
+
uploaded = stack.uploaded_template
|
|
40
|
+
if uploaded is None:
|
|
41
|
+
return None
|
|
42
|
+
if isinstance(uploaded, str):
|
|
43
|
+
return hashlib.sha256(uploaded.encode()).hexdigest()[:16]
|
|
44
|
+
return hashlib.sha256(
|
|
45
|
+
yaml.dump(dict(uploaded), sort_keys=True).encode()
|
|
46
|
+
).hexdigest()[:16]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _backfill_tags(stacks: list) -> None:
|
|
50
|
+
"""タグがないスタックに uploaded template のハッシュでタグを付与する"""
|
|
51
|
+
for stack in stacks:
|
|
52
|
+
if stack._deployed_template_hash is not None:
|
|
53
|
+
continue
|
|
54
|
+
h = _compute_uploaded_hash(stack)
|
|
55
|
+
if h is None:
|
|
56
|
+
continue
|
|
57
|
+
echo.log("タグ付与 (uploaded hash): %s (%s)" % (stack.name, h))
|
|
58
|
+
stack.client.update_stack(
|
|
59
|
+
StackName=stack.name,
|
|
60
|
+
UsePreviousTemplate=True,
|
|
61
|
+
Capabilities=stack.capabilities,
|
|
62
|
+
Tags=stack.stack_tags + [{"Key": "pocket:template_hash", "Value": h}],
|
|
63
|
+
)
|
|
64
|
+
stack.wait_status("COMPLETED", timeout=300, interval=5)
|
|
65
|
+
stack.clear_status()
|
|
66
|
+
echo.success("完了: %s" % stack.name)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _find_stacks_needing_update(stacks: list) -> list:
|
|
70
|
+
"""ローカルテンプレートとハッシュが異なるスタックを返す"""
|
|
71
|
+
targets = []
|
|
72
|
+
for stack in stacks:
|
|
73
|
+
if stack._deployed_template_hash == stack._template_hash:
|
|
74
|
+
echo.info("%s: タグは最新です" % stack.name)
|
|
75
|
+
continue
|
|
76
|
+
targets.append(stack)
|
|
77
|
+
return targets
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _apply_local_tags(targets: list) -> None:
|
|
81
|
+
"""ローカルテンプレートのハッシュでタグを更新する"""
|
|
82
|
+
for stack in targets:
|
|
83
|
+
echo.log("タグ更新 (local hash): %s" % stack.name)
|
|
84
|
+
stack.client.update_stack(
|
|
85
|
+
StackName=stack.name,
|
|
86
|
+
UsePreviousTemplate=True,
|
|
87
|
+
Capabilities=stack.capabilities,
|
|
88
|
+
Tags=stack._build_tags(),
|
|
89
|
+
)
|
|
90
|
+
stack.wait_status("COMPLETED", timeout=300, interval=5)
|
|
91
|
+
echo.success("完了: %s" % stack.name)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _check_real_diffs(stacks: list) -> list[str]:
|
|
95
|
+
"""非 ASCII 文字化け以外のテンプレート差分があるスタックを返す"""
|
|
96
|
+
needs_deploy = []
|
|
97
|
+
for stack in stacks:
|
|
98
|
+
diff = stack.yaml_diff
|
|
99
|
+
if diff == {}:
|
|
100
|
+
continue
|
|
101
|
+
if set(diff.keys()) == {"values_changed"}:
|
|
102
|
+
all_garbled = all(
|
|
103
|
+
isinstance(c.get("old_value"), str) and "??" in c["old_value"]
|
|
104
|
+
for c in diff["values_changed"].values()
|
|
105
|
+
)
|
|
106
|
+
if all_garbled:
|
|
107
|
+
continue
|
|
108
|
+
needs_deploy.append(stack.name)
|
|
109
|
+
return needs_deploy
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _run_template_hash(stage: str, *, yes: bool) -> None:
|
|
113
|
+
"""スタックのテンプレートハッシュタグを一括付与する (冪等)。
|
|
114
|
+
|
|
115
|
+
未 deploy のテンプレ差分がある場合は SystemExit(1) で中断する
|
|
116
|
+
(前提条件ガード: 先に deploy が必要)。
|
|
117
|
+
"""
|
|
118
|
+
context = Context.from_toml(stage=stage)
|
|
119
|
+
stacks = _get_existing_stacks(context)
|
|
120
|
+
|
|
121
|
+
if not stacks:
|
|
122
|
+
echo.warning("template-hash: 対象のスタックがありません。")
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
# 1. タグがないスタックに uploaded template のハッシュで仮タグを付与
|
|
126
|
+
_backfill_tags(stacks)
|
|
127
|
+
|
|
128
|
+
# 2. ローカルとの差分チェック(非 ASCII 文字化け以外の差分があれば中断)
|
|
129
|
+
needs_deploy = _check_real_diffs(stacks)
|
|
130
|
+
if needs_deploy:
|
|
131
|
+
echo.danger("以下のスタックにテンプレートの差分があります:")
|
|
132
|
+
for name in needs_deploy:
|
|
133
|
+
echo.info(" - %s" % name)
|
|
134
|
+
echo.danger(
|
|
135
|
+
"先に pocket deploy --stage=%s で最新バージョンを"
|
|
136
|
+
"デプロイしてから再実行してください。" % stage
|
|
137
|
+
)
|
|
138
|
+
raise SystemExit(1)
|
|
139
|
+
|
|
140
|
+
# 3. ローカルテンプレートのハッシュでタグを更新
|
|
141
|
+
targets = _find_stacks_needing_update(stacks)
|
|
142
|
+
if not targets:
|
|
143
|
+
echo.success("template-hash: 全スタックのタグが最新です。")
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
echo.info("以下のスタックのタグをローカルハッシュに更新します:")
|
|
147
|
+
for stack in targets:
|
|
148
|
+
echo.info(" - %s" % stack.name)
|
|
149
|
+
if not yes:
|
|
150
|
+
click.confirm("実行しますか?", abort=True)
|
|
151
|
+
|
|
152
|
+
_apply_local_tags(targets)
|
|
153
|
+
echo.success("template-hash: 全スタックの移行が完了しました。")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# --- secret-paths migration (0.11 -> 0.12) -----------------------------------
|
|
157
|
+
|
|
158
|
+
_SECRET_PATH_LABELS = {
|
|
159
|
+
"would-migrate": "移設予定 (copy→旧削除)",
|
|
160
|
+
"would-clean": "旧パス削除予定 (移行済)",
|
|
161
|
+
"migrated": "移設完了",
|
|
162
|
+
"cleaned": "旧パス削除 (移行済)",
|
|
163
|
+
"already": "移行不要",
|
|
164
|
+
"missing": "未 provision (store-url で作成してください)",
|
|
165
|
+
"skip-name": "name モード (対象外)",
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _report_secret_path(info: dict) -> None:
|
|
170
|
+
status = info["status"]
|
|
171
|
+
label = _SECRET_PATH_LABELS.get(status, status)
|
|
172
|
+
if status in ("migrated", "cleaned"):
|
|
173
|
+
echo.success(
|
|
174
|
+
"%s: %s (%s → %s)" % (info["key"], label, info["old"], info["new"])
|
|
175
|
+
)
|
|
176
|
+
elif status in ("would-migrate", "would-clean"):
|
|
177
|
+
echo.info("%s: %s (%s → %s)" % (info["key"], label, info["old"], info["new"]))
|
|
178
|
+
elif status == "missing":
|
|
179
|
+
echo.warning("%s: %s (%s)" % (info["key"], label, info.get("new")))
|
|
180
|
+
else:
|
|
181
|
+
echo.info("%s: %s" % (info["key"], label))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _warn_runtime_bump_required(stage: str) -> None:
|
|
185
|
+
"""secret-paths 移設で旧パスが削除されることの事前警告。
|
|
186
|
+
|
|
187
|
+
移設は copy→旧削除を一括で行うため、この stage に deploy 済みの Lambda runtime
|
|
188
|
+
(magic-pocket[django]) が古いままだと、旧パス (削除済) を参照して DATABASE_URL を
|
|
189
|
+
解決できず cold start の INIT で落ちる。移設後に runtime を CLI と同版へ bump して
|
|
190
|
+
再デプロイするまで stage が黙って壊れる footgun を、移設前に明示する
|
|
191
|
+
(template-hash が「先に deploy」で中断するのと同じ思想の事前ガード)。
|
|
192
|
+
"""
|
|
193
|
+
echo.warning(
|
|
194
|
+
"⚠ 移設すると旧パスは削除されます (copy→旧削除は一括)。この stage に "
|
|
195
|
+
"deploy 済みの Lambda runtime (magic-pocket[django]) が古いままだと、"
|
|
196
|
+
"旧パスを参照して DATABASE_URL を解決できず cold start の INIT で落ちます。"
|
|
197
|
+
)
|
|
198
|
+
echo.warning(
|
|
199
|
+
" 移設後は速やかに runtime を CLI と同版へ上げて再デプロイしてください:\n"
|
|
200
|
+
" uv add 'magic-pocket[django]>=%s'\n"
|
|
201
|
+
" pocket deploy --stage=%s (Django なら pocket django deploy --stage=%s)"
|
|
202
|
+
% (__version__, stage, stage)
|
|
203
|
+
)
|
|
204
|
+
echo.warning(
|
|
205
|
+
" ※ copy と旧削除が一括のため完全な無停止移行にはなりません。warm instance は "
|
|
206
|
+
"recycle まで生存するので、移設直後に再デプロイすれば影響を最小化できます。"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _run_secret_paths(stage: str, *, yes: bool, dry_run: bool) -> None:
|
|
211
|
+
"""stored user secret を旧キー基準→新 type 基準パスへ移設する (冪等)。"""
|
|
212
|
+
from pocket_cli.mediator import Mediator
|
|
213
|
+
|
|
214
|
+
context = Context.from_toml(stage=stage)
|
|
215
|
+
sc = context.awscontainer.secrets if context.awscontainer else None
|
|
216
|
+
type_specs = (
|
|
217
|
+
[(k, s) for k, s in sc.user.items() if s.type is not None] if sc else []
|
|
218
|
+
)
|
|
219
|
+
if not type_specs:
|
|
220
|
+
echo.info("secret-paths: type-mode の user secret がありません。")
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
mediator = Mediator(context)
|
|
224
|
+
|
|
225
|
+
# まず dry-run で計画を出す (AWS への書込みなし)
|
|
226
|
+
plans = [
|
|
227
|
+
mediator.migrate_user_secret_path(k, s, dry_run=True) for k, s in type_specs
|
|
228
|
+
]
|
|
229
|
+
for info in plans:
|
|
230
|
+
_report_secret_path(info)
|
|
231
|
+
to_act = [p for p in plans if p["status"] in ("would-migrate", "would-clean")]
|
|
232
|
+
if not to_act:
|
|
233
|
+
echo.success(
|
|
234
|
+
"secret-paths: 移設対象はありません (全て移行済み or 未 provision)。"
|
|
235
|
+
)
|
|
236
|
+
return
|
|
237
|
+
_warn_runtime_bump_required(stage)
|
|
238
|
+
if dry_run:
|
|
239
|
+
echo.info("secret-paths: --dry-run のため実行はしません。")
|
|
240
|
+
return
|
|
241
|
+
if not yes:
|
|
242
|
+
click.confirm("上記の移設を実行しますか?", abort=True)
|
|
243
|
+
|
|
244
|
+
for key, spec in type_specs:
|
|
245
|
+
info = mediator.migrate_user_secret_path(key, spec, dry_run=False)
|
|
246
|
+
_report_secret_path(info)
|
|
247
|
+
echo.success("secret-paths: 移設が完了しました。")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# --- CLI 配線 -----------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@click.group(invoke_without_command=True)
|
|
254
|
+
@click.option("--stage", envvar="POCKET_DEPLOY_STAGE", default=None)
|
|
255
|
+
@click.option(
|
|
256
|
+
"--yes", "-y", is_flag=True, default=False, help="確認プロンプトをスキップ"
|
|
257
|
+
)
|
|
258
|
+
@click.pass_context
|
|
259
|
+
def migrate(ctx: click.Context, stage: str | None, yes: bool):
|
|
260
|
+
"""バージョン間の移行をまとめて実行する。
|
|
261
|
+
|
|
262
|
+
サブコマンド無指定なら全 migration (secret-paths → template-hash) を
|
|
263
|
+
冪等に順次実行する。
|
|
264
|
+
"""
|
|
265
|
+
if ctx.invoked_subcommand is not None:
|
|
266
|
+
return
|
|
267
|
+
# bare 実行: 全 migration を冪等実行
|
|
268
|
+
from pocket_cli.cli.aws_auth import check_aws_credentials
|
|
269
|
+
|
|
270
|
+
check_aws_credentials()
|
|
271
|
+
resolved_stage: str = stage or click.prompt("Stage")
|
|
272
|
+
# secret-paths を先に (高速・ガードで中断しない)、template-hash を後に。
|
|
273
|
+
_run_secret_paths(resolved_stage, yes=yes, dry_run=False)
|
|
274
|
+
try:
|
|
275
|
+
_run_template_hash(resolved_stage, yes=yes)
|
|
276
|
+
except SystemExit:
|
|
277
|
+
echo.warning(
|
|
278
|
+
"template-hash はテンプレ差分のため中断しました "
|
|
279
|
+
"(secret-paths は完了済み)。pocket deploy --stage=%s の後に "
|
|
280
|
+
"pocket migrate を再実行してください。" % resolved_stage
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@migrate.command(name="template-hash")
|
|
285
|
+
@click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
|
|
286
|
+
@click.option(
|
|
287
|
+
"--yes", "-y", is_flag=True, default=False, help="確認プロンプトをスキップ"
|
|
288
|
+
)
|
|
289
|
+
def template_hash(stage: str, yes: bool):
|
|
290
|
+
"""スタックのテンプレートハッシュタグを一括付与する。"""
|
|
291
|
+
from pocket_cli.cli.aws_auth import check_aws_credentials
|
|
292
|
+
|
|
293
|
+
check_aws_credentials()
|
|
294
|
+
_run_template_hash(stage, yes=yes)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@migrate.command(name="secret-paths")
|
|
298
|
+
@click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
|
|
299
|
+
@click.option(
|
|
300
|
+
"--yes", "-y", is_flag=True, default=False, help="確認プロンプトをスキップ"
|
|
301
|
+
)
|
|
302
|
+
@click.option(
|
|
303
|
+
"--dry-run", is_flag=True, default=False, help="移設内容を表示のみ (書込みなし)"
|
|
304
|
+
)
|
|
305
|
+
def secret_paths(stage: str, yes: bool, dry_run: bool):
|
|
306
|
+
"""stored user secret を旧キー基準→新 type 基準パスへ移設する (0.11→0.12)。"""
|
|
307
|
+
from pocket_cli.cli.aws_auth import check_aws_credentials
|
|
308
|
+
|
|
309
|
+
check_aws_credentials()
|
|
310
|
+
_run_secret_paths(stage, yes=yes, dry_run=dry_run)
|
|
@@ -63,19 +63,24 @@ def run_get_url(
|
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
def _read_stored_url(context: Context, secret_type: str) -> str | None:
|
|
66
|
-
"""``type=<secret_type>`` の stored
|
|
66
|
+
"""``type=<secret_type>`` の stored URL を読む。未 provision なら None。
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
consumer の user secret 宣言が在れば ``spec.store`` override を尊重して読む。
|
|
69
|
+
宣言が無くても (dual-declaration で DATABASE_URL が別 backend を指す等)、
|
|
70
|
+
type 基準の正準パスを直接構築して読む — つまり「その backend の stored URL」を
|
|
71
|
+
consumer 宣言の有無と無関係に引ける。両方無ければ None (→ live fallback)。
|
|
69
72
|
"""
|
|
70
73
|
sc = context.awscontainer.secrets if context.awscontainer else None
|
|
71
74
|
if sc is None:
|
|
72
75
|
return None
|
|
76
|
+
mediator = Mediator(context)
|
|
73
77
|
specs = [spec for spec in sc.user.values() if spec.type == secret_type]
|
|
74
|
-
if not specs:
|
|
75
|
-
return None
|
|
76
78
|
if len(specs) > 1:
|
|
79
|
+
# 通常は settings の check_user_type_unique で弾かれる (防御的)。
|
|
77
80
|
raise click.ClickException(
|
|
78
81
|
"type=%s の stored user secret が複数宣言されているため曖昧です。"
|
|
79
82
|
% secret_type
|
|
80
83
|
)
|
|
81
|
-
|
|
84
|
+
if specs:
|
|
85
|
+
return mediator.read_user_secret(specs[0])
|
|
86
|
+
return mediator.read_stored_url_by_type(secret_type)
|
|
@@ -144,52 +144,92 @@ class Mediator:
|
|
|
144
144
|
store = spec.store or sc.store
|
|
145
145
|
return self._stored_secret_exists(spec.name, store, sc.region)
|
|
146
146
|
|
|
147
|
-
def
|
|
148
|
-
"""
|
|
147
|
+
def _require_secrets(self):
|
|
148
|
+
"""awscontainer.secrets (SecretsContext) を返す。未設定なら raise。"""
|
|
149
|
+
ac = self.context.awscontainer
|
|
150
|
+
if ac is None or ac.secrets is None:
|
|
151
|
+
raise RuntimeError("awscontainer secrets is not configured")
|
|
152
|
+
return ac.secrets
|
|
149
153
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
読み側 _stored_secret_exists と対称。
|
|
153
|
-
"""
|
|
154
|
+
def _put_stored_value(self, name: str, store: str, value: str, region: str) -> None:
|
|
155
|
+
"""正準名 (name) に単一値を書き込む (ssm=SecureString / sm=create|put)。"""
|
|
154
156
|
import boto3
|
|
155
157
|
from botocore.exceptions import ClientError
|
|
156
158
|
|
|
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
159
|
if store == "ssm":
|
|
167
|
-
boto3.client("ssm", region_name=
|
|
168
|
-
Name=
|
|
160
|
+
boto3.client("ssm", region_name=region).put_parameter(
|
|
161
|
+
Name=name, Value=value, Type="SecureString", Overwrite=True
|
|
169
162
|
)
|
|
170
163
|
return
|
|
171
|
-
client = boto3.client("secretsmanager", region_name=
|
|
164
|
+
client = boto3.client("secretsmanager", region_name=region)
|
|
172
165
|
try:
|
|
173
166
|
client.create_secret(
|
|
174
|
-
Name=
|
|
167
|
+
Name=name,
|
|
175
168
|
SecretString=value,
|
|
176
|
-
Tags=[{"Key": "Name", "Value":
|
|
169
|
+
Tags=[{"Key": "Name", "Value": name}],
|
|
177
170
|
)
|
|
178
171
|
except ClientError as e:
|
|
179
172
|
if e.response["Error"]["Code"] == "ResourceExistsException":
|
|
180
|
-
client.put_secret_value(SecretId=
|
|
173
|
+
client.put_secret_value(SecretId=name, SecretString=value)
|
|
181
174
|
else:
|
|
182
175
|
raise
|
|
183
176
|
|
|
177
|
+
def _read_stored_value(self, name: str, store: str, region: str) -> str | None:
|
|
178
|
+
"""正準名 (name) の値を読む。未 provision (NotFound) なら None。"""
|
|
179
|
+
import boto3
|
|
180
|
+
from botocore.exceptions import ClientError
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
if store == "ssm":
|
|
184
|
+
res = boto3.client("ssm", region_name=region).get_parameter(
|
|
185
|
+
Name=name, WithDecryption=True
|
|
186
|
+
)
|
|
187
|
+
return res["Parameter"]["Value"]
|
|
188
|
+
res = boto3.client("secretsmanager", region_name=region).get_secret_value(
|
|
189
|
+
SecretId=name
|
|
190
|
+
)
|
|
191
|
+
return res["SecretString"]
|
|
192
|
+
except ClientError as e:
|
|
193
|
+
code = e.response.get("Error", {}).get("Code", "")
|
|
194
|
+
if code in ("ParameterNotFound", "ResourceNotFoundException"):
|
|
195
|
+
return None
|
|
196
|
+
raise
|
|
197
|
+
|
|
198
|
+
def _delete_stored_value(self, name: str, store: str, region: str) -> None:
|
|
199
|
+
"""正準名 (name) を削除する (migrate の旧パス cleanup 用)。"""
|
|
200
|
+
import boto3
|
|
201
|
+
|
|
202
|
+
if store == "ssm":
|
|
203
|
+
boto3.client("ssm", region_name=region).delete_parameter(Name=name)
|
|
204
|
+
else:
|
|
205
|
+
boto3.client("secretsmanager", region_name=region).delete_secret(
|
|
206
|
+
SecretId=name
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def store_user_secret(self, spec, value: str) -> None:
|
|
210
|
+
"""stored mode user secret の正準名 (spec.name) に単一値を書き込む。
|
|
211
|
+
|
|
212
|
+
`pocket <db> store-url` から使う。pocket_store (managed 集約) ではなく
|
|
213
|
+
user secret の type 基準導出名 ({pocket_key}-user/{type}) に直接 put する。
|
|
214
|
+
読み側 _stored_secret_exists と対称。
|
|
215
|
+
"""
|
|
216
|
+
if (
|
|
217
|
+
self.context.awscontainer is None
|
|
218
|
+
or self.context.awscontainer.secrets is None
|
|
219
|
+
):
|
|
220
|
+
raise RuntimeError("awscontainer secrets is not configured")
|
|
221
|
+
sc = self.context.awscontainer.secrets
|
|
222
|
+
if spec.name is None:
|
|
223
|
+
raise RuntimeError("user secret name is not resolved")
|
|
224
|
+
store = spec.store or sc.store
|
|
225
|
+
self._put_stored_value(spec.name, store, value, sc.region)
|
|
226
|
+
|
|
184
227
|
def read_user_secret(self, spec) -> str | None:
|
|
185
228
|
"""stored mode user secret (spec.name) の値を読む。未 provision なら None。
|
|
186
229
|
|
|
187
230
|
store_user_secret / _stored_secret_exists と対称の読み取り。
|
|
188
231
|
`pocket <db> url` の stored-first 解決に使う。管理 API は叩かない。
|
|
189
232
|
"""
|
|
190
|
-
import boto3
|
|
191
|
-
from botocore.exceptions import ClientError
|
|
192
|
-
|
|
193
233
|
if (
|
|
194
234
|
self.context.awscontainer is None
|
|
195
235
|
or self.context.awscontainer.secrets is None
|
|
@@ -198,21 +238,79 @@ class Mediator:
|
|
|
198
238
|
return None
|
|
199
239
|
sc = self.context.awscontainer.secrets
|
|
200
240
|
store = spec.store or sc.store
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
241
|
+
return self._read_stored_value(spec.name, store, sc.region)
|
|
242
|
+
|
|
243
|
+
def read_stored_url_by_type(self, secret_type: str) -> str | None:
|
|
244
|
+
"""type 基準の正準パスを (consumer 宣言なしで) 構築して stored URL を読む。
|
|
245
|
+
|
|
246
|
+
dual-declaration で consumer (DATABASE_URL) が別 backend を指していても、
|
|
247
|
+
「その backend の stored URL」を type から直接引ける。store は secrets.store
|
|
248
|
+
既定 (per-type override は宣言でしか表現できないため)。
|
|
249
|
+
"""
|
|
250
|
+
if (
|
|
251
|
+
self.context.awscontainer is None
|
|
252
|
+
or self.context.awscontainer.secrets is None
|
|
253
|
+
):
|
|
254
|
+
return None
|
|
255
|
+
sc = self.context.awscontainer.secrets
|
|
256
|
+
name = sc.stored_url_name(secret_type)
|
|
257
|
+
return self._read_stored_value(name, sc.store, sc.region)
|
|
258
|
+
|
|
259
|
+
def migrate_user_secret_path(
|
|
260
|
+
self, key: str, spec, *, dry_run: bool = False
|
|
261
|
+
) -> dict:
|
|
262
|
+
"""0.11→0.12: user secret を旧キー基準パス→新 type 基準パスへ移設する。
|
|
263
|
+
|
|
264
|
+
旧 /{pocket_key}-user/{key} の値を新 /{pocket_key}-user/{type} へ copy し、
|
|
265
|
+
verify 後に旧を delete する。冪等 (新在+旧在なら旧のみ delete して cleanup を
|
|
266
|
+
完了)。戻り値の status: ``migrated`` / ``cleaned`` / ``already`` / ``missing``
|
|
267
|
+
/ ``skip-name`` (dry_run では ``would-migrate`` / ``would-clean``)。
|
|
268
|
+
"""
|
|
269
|
+
from pocket.context import user_secret_path
|
|
270
|
+
|
|
271
|
+
sc = self._require_secrets()
|
|
272
|
+
if spec.type is None: # name モードは移設対象外
|
|
273
|
+
return {"status": "skip-name", "key": key}
|
|
274
|
+
store = spec.store or sc.store
|
|
275
|
+
old_name = user_secret_path(sc.pocket_key, key, store)
|
|
276
|
+
new_name = sc.stored_url_name(spec.type, store)
|
|
277
|
+
info = {
|
|
278
|
+
"status": None,
|
|
279
|
+
"key": key,
|
|
280
|
+
"type": spec.type,
|
|
281
|
+
"old": old_name,
|
|
282
|
+
"new": new_name,
|
|
283
|
+
}
|
|
284
|
+
if old_name == new_name: # 既に type == key (レア) なら移設不要
|
|
285
|
+
info["status"] = "already"
|
|
286
|
+
return info
|
|
287
|
+
region = sc.region
|
|
288
|
+
new_exists = self._stored_secret_exists(new_name, store, region)
|
|
289
|
+
old_exists = self._stored_secret_exists(old_name, store, region)
|
|
290
|
+
if new_exists:
|
|
291
|
+
if old_exists:
|
|
292
|
+
if not dry_run:
|
|
293
|
+
self._delete_stored_value(old_name, store, region)
|
|
294
|
+
info["status"] = "would-clean" if dry_run else "cleaned"
|
|
295
|
+
else:
|
|
296
|
+
info["status"] = "already"
|
|
297
|
+
return info
|
|
298
|
+
if not old_exists:
|
|
299
|
+
info["status"] = "missing"
|
|
300
|
+
return info
|
|
301
|
+
if dry_run:
|
|
302
|
+
info["status"] = "would-migrate"
|
|
303
|
+
return info
|
|
304
|
+
value = self._read_stored_value(old_name, store, region)
|
|
305
|
+
if value is None:
|
|
306
|
+
info["status"] = "missing"
|
|
307
|
+
return info
|
|
308
|
+
self._put_stored_value(new_name, store, value, region)
|
|
309
|
+
if self._read_stored_value(new_name, store, region) != value:
|
|
310
|
+
raise RuntimeError("copy verify failed: %s" % new_name)
|
|
311
|
+
self._delete_stored_value(old_name, store, region)
|
|
312
|
+
info["status"] = "migrated"
|
|
313
|
+
return info
|
|
216
314
|
|
|
217
315
|
def _cleanup_orphaned_secrets(self):
|
|
218
316
|
"""SSM/SM にあるが managed 定義にないシークレットを削除する"""
|