magic-pocket-cli 0.2.0__py3-none-any.whl

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 (65) hide show
  1. magic_pocket_cli-0.2.0.dist-info/METADATA +14 -0
  2. magic_pocket_cli-0.2.0.dist-info/RECORD +65 -0
  3. magic_pocket_cli-0.2.0.dist-info/WHEEL +4 -0
  4. magic_pocket_cli-0.2.0.dist-info/entry_points.txt +2 -0
  5. pocket_cli/__init__.py +0 -0
  6. pocket_cli/cli/__init__.py +0 -0
  7. pocket_cli/cli/aws_auth.py +48 -0
  8. pocket_cli/cli/awscontainer_cli.py +328 -0
  9. pocket_cli/cli/cloudfront_cli.py +116 -0
  10. pocket_cli/cli/cloudfront_keys_cli.py +68 -0
  11. pocket_cli/cli/cloudfront_waf_cli.py +68 -0
  12. pocket_cli/cli/deploy_cli.py +274 -0
  13. pocket_cli/cli/destroy_cli.py +358 -0
  14. pocket_cli/cli/dsql_cli.py +60 -0
  15. pocket_cli/cli/main_cli.py +91 -0
  16. pocket_cli/cli/migrate_cli.py +148 -0
  17. pocket_cli/cli/neon_cli.py +97 -0
  18. pocket_cli/cli/permissions_cli.py +46 -0
  19. pocket_cli/cli/rds_cli.py +63 -0
  20. pocket_cli/cli/runtime_config_cli.py +185 -0
  21. pocket_cli/cli/s3_cli.py +69 -0
  22. pocket_cli/cli/status_cli.py +56 -0
  23. pocket_cli/cli/tidb_cli.py +73 -0
  24. pocket_cli/cli/vpc_cli.py +92 -0
  25. pocket_cli/cli/waf_cli.py +182 -0
  26. pocket_cli/django_cli.py +412 -0
  27. pocket_cli/mediator.py +220 -0
  28. pocket_cli/resources/__init__.py +0 -0
  29. pocket_cli/resources/aws/__init__.py +0 -0
  30. pocket_cli/resources/aws/builders/__init__.py +57 -0
  31. pocket_cli/resources/aws/builders/codebuild.py +363 -0
  32. pocket_cli/resources/aws/builders/depot.py +84 -0
  33. pocket_cli/resources/aws/builders/docker.py +34 -0
  34. pocket_cli/resources/aws/builders/dockerignore.py +44 -0
  35. pocket_cli/resources/aws/cloudformation.py +790 -0
  36. pocket_cli/resources/aws/ecr.py +145 -0
  37. pocket_cli/resources/aws/efs.py +138 -0
  38. pocket_cli/resources/aws/lambdahandler.py +182 -0
  39. pocket_cli/resources/aws/s3_utils.py +58 -0
  40. pocket_cli/resources/aws/state.py +74 -0
  41. pocket_cli/resources/awscontainer.py +265 -0
  42. pocket_cli/resources/cloudfront.py +491 -0
  43. pocket_cli/resources/cloudfront_acm.py +55 -0
  44. pocket_cli/resources/cloudfront_keys.py +81 -0
  45. pocket_cli/resources/cloudfront_waf.py +67 -0
  46. pocket_cli/resources/dsql.py +142 -0
  47. pocket_cli/resources/neon.py +353 -0
  48. pocket_cli/resources/rds.py +680 -0
  49. pocket_cli/resources/s3.py +307 -0
  50. pocket_cli/resources/tidb.py +298 -0
  51. pocket_cli/resources/upstash.py +152 -0
  52. pocket_cli/resources/vpc.py +67 -0
  53. pocket_cli/templates/cloudformation/awscontainer.yaml +516 -0
  54. pocket_cli/templates/cloudformation/cf_function_api_host.js +5 -0
  55. pocket_cli/templates/cloudformation/cf_function_spa_auth.js +28 -0
  56. pocket_cli/templates/cloudformation/cf_function_spa_fallback.js +8 -0
  57. pocket_cli/templates/cloudformation/cloudfront.yaml +309 -0
  58. pocket_cli/templates/cloudformation/cloudfront_acm.yaml +43 -0
  59. pocket_cli/templates/cloudformation/cloudfront_keys.yaml +32 -0
  60. pocket_cli/templates/cloudformation/cloudfront_waf.yaml +97 -0
  61. pocket_cli/templates/cloudformation/vpc.yaml +213 -0
  62. pocket_cli/templates/init/django-dotenv.env +3 -0
  63. pocket_cli/templates/init/django-settings.py +140 -0
  64. pocket_cli/templates/init/pocket.Dockerfile +26 -0
  65. pocket_cli/templates/init/pocket_simple.toml +31 -0
@@ -0,0 +1,363 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import time
6
+ import zipfile
7
+ from pathlib import Path
8
+
9
+ import boto3
10
+ from botocore.exceptions import ClientError
11
+
12
+ from pocket_cli.resources.aws.builders.dockerignore import (
13
+ load_dockerignore,
14
+ should_include,
15
+ )
16
+
17
+ # CodeBuildイメージ
18
+ IMAGE_AMD64 = "aws/codebuild/amazonlinux-x86_64-standard:5.0"
19
+ IMAGE_ARM64 = "aws/codebuild/amazonlinux-aarch64-standard:3.0"
20
+
21
+ BUILDSPEC = (
22
+ "version: 0.2\n"
23
+ "phases:\n"
24
+ " pre_build:\n"
25
+ " commands:\n"
26
+ " - >-\n"
27
+ " aws ecr get-login-password --region $AWS_DEFAULT_REGION\n"
28
+ " | docker login --username AWS\n"
29
+ " --password-stdin $ECR_HOST\n"
30
+ " build:\n"
31
+ " commands:\n"
32
+ " - docker build -t $IMAGE_TAG -f $DOCKERFILE .\n"
33
+ " post_build:\n"
34
+ " commands:\n"
35
+ " - docker push $IMAGE_TAG\n"
36
+ )
37
+
38
+ POLL_INTERVAL = 10
39
+
40
+
41
+ class CodeBuildBuilder:
42
+ def __init__(
43
+ self,
44
+ *,
45
+ region: str,
46
+ resource_prefix: str,
47
+ state_bucket: str,
48
+ compute_type: str = "BUILD_GENERAL1_MEDIUM",
49
+ permissions_boundary: str | None = None,
50
+ ) -> None:
51
+ self.region = region
52
+ self.resource_prefix = resource_prefix
53
+ self.state_bucket = state_bucket
54
+ self.compute_type = compute_type
55
+ self.permissions_boundary = (
56
+ os.environ.get("CODEBUILD_PERMISSIONS_BOUNDARY") or permissions_boundary
57
+ )
58
+
59
+ self.codebuild = boto3.client("codebuild", region_name=region)
60
+ self.iam = boto3.client("iam", region_name=region)
61
+ self.s3 = boto3.client("s3", region_name=region)
62
+ self.sts = boto3.client("sts", region_name=region)
63
+
64
+ self._project_name = f"{resource_prefix}codebuild"
65
+ self._role_name = f"forge-{resource_prefix}codebuild-role"
66
+ self._source_key = f"codebuild/{self._project_name}/source.zip"
67
+
68
+ def build_and_push(
69
+ self,
70
+ *,
71
+ target: str,
72
+ dockerfile_path: str,
73
+ platform: str,
74
+ ) -> None:
75
+ print("CodeBuild でイメージをビルドします...")
76
+ print(" target: %s" % target)
77
+ print(" dockerfile: %s" % dockerfile_path)
78
+ print(" platform: %s" % platform)
79
+
80
+ account_id = self.sts.get_caller_identity()["Account"]
81
+ ecr_host = f"{account_id}.dkr.ecr.{self.region}.amazonaws.com"
82
+
83
+ role_arn = self._ensure_role(account_id)
84
+ self._ensure_project(platform, role_arn)
85
+ self._upload_source(dockerfile_path)
86
+
87
+ build_id = self._start_build(
88
+ target=target,
89
+ dockerfile_path=dockerfile_path,
90
+ ecr_host=ecr_host,
91
+ )
92
+ self._wait_build(build_id)
93
+
94
+ # ソースzip削除
95
+ self.s3.delete_object(Bucket=self.state_bucket, Key=self._source_key)
96
+ print("CodeBuild ビルド完了")
97
+
98
+ def delete(self) -> None:
99
+ """CodeBuildプロジェクトとIAMロールを削除"""
100
+ self._delete_project()
101
+ self._delete_role()
102
+
103
+ # --- IAM ロール ---
104
+
105
+ def _ensure_role(self, account_id: str) -> str:
106
+ try:
107
+ resp = self.iam.get_role(RoleName=self._role_name)
108
+ return resp["Role"]["Arn"] # type: ignore[return-value]
109
+ except ClientError as e:
110
+ if e.response["Error"]["Code"] != "NoSuchEntity":
111
+ raise
112
+
113
+ print(" CodeBuild用IAMロールを作成: %s" % self._role_name)
114
+ assume_role_policy = json.dumps(
115
+ {
116
+ "Version": "2012-10-17",
117
+ "Statement": [
118
+ {
119
+ "Effect": "Allow",
120
+ "Principal": {"Service": "codebuild.amazonaws.com"},
121
+ "Action": "sts:AssumeRole",
122
+ }
123
+ ],
124
+ }
125
+ )
126
+
127
+ create_kwargs: dict = {
128
+ "RoleName": self._role_name,
129
+ "AssumeRolePolicyDocument": assume_role_policy,
130
+ }
131
+ if self.permissions_boundary:
132
+ create_kwargs["PermissionsBoundary"] = self.permissions_boundary
133
+
134
+ resp = self.iam.create_role(**create_kwargs)
135
+ role_arn: str = resp["Role"]["Arn"]
136
+
137
+ policy = json.dumps(
138
+ {
139
+ "Version": "2012-10-17",
140
+ "Statement": [
141
+ {
142
+ "Effect": "Allow",
143
+ "Action": [
144
+ "ecr:GetAuthorizationToken",
145
+ ],
146
+ "Resource": "*",
147
+ },
148
+ {
149
+ "Effect": "Allow",
150
+ "Action": [
151
+ "ecr:BatchCheckLayerAvailability",
152
+ "ecr:GetDownloadUrlForLayer",
153
+ "ecr:BatchGetImage",
154
+ "ecr:PutImage",
155
+ "ecr:InitiateLayerUpload",
156
+ "ecr:UploadLayerPart",
157
+ "ecr:CompleteLayerUpload",
158
+ ],
159
+ "Resource": (
160
+ f"arn:aws:ecr:{self.region}:{account_id}:repository/*"
161
+ ),
162
+ },
163
+ {
164
+ "Effect": "Allow",
165
+ "Action": [
166
+ "s3:GetObject",
167
+ "s3:GetObjectVersion",
168
+ ],
169
+ "Resource": (f"arn:aws:s3:::{self.state_bucket}/codebuild/*"),
170
+ },
171
+ {
172
+ "Effect": "Allow",
173
+ "Action": [
174
+ "logs:CreateLogGroup",
175
+ "logs:CreateLogStream",
176
+ "logs:PutLogEvents",
177
+ ],
178
+ "Resource": (
179
+ f"arn:aws:logs:{self.region}"
180
+ f":{account_id}:log-group:"
181
+ f"/aws/codebuild/"
182
+ f"{self._project_name}*"
183
+ ),
184
+ },
185
+ ],
186
+ }
187
+ )
188
+ self.iam.put_role_policy(
189
+ RoleName=self._role_name,
190
+ PolicyName="codebuild-policy",
191
+ PolicyDocument=policy,
192
+ )
193
+
194
+ # IAMロールの伝播待ち
195
+ print(" IAMロール伝播を待機中...")
196
+ time.sleep(10)
197
+ return role_arn
198
+
199
+ def _delete_role(self) -> None:
200
+ try:
201
+ # インラインポリシー削除
202
+ policies = self.iam.list_role_policies(RoleName=self._role_name)
203
+ for policy_name in policies["PolicyNames"]:
204
+ self.iam.delete_role_policy(
205
+ RoleName=self._role_name, PolicyName=policy_name
206
+ )
207
+ self.iam.delete_role(RoleName=self._role_name)
208
+ print(" IAMロール削除: %s" % self._role_name)
209
+ except ClientError as e:
210
+ if e.response["Error"]["Code"] == "NoSuchEntity":
211
+ return
212
+ raise
213
+
214
+ # --- CodeBuild プロジェクト ---
215
+
216
+ def _ensure_project(self, platform: str, role_arn: str) -> None:
217
+ env_type, image = self._env_for_platform(platform)
218
+
219
+ project_config = {
220
+ "name": self._project_name,
221
+ "source": {
222
+ "type": "S3",
223
+ "location": f"{self.state_bucket}/{self._source_key}",
224
+ },
225
+ "artifacts": {"type": "NO_ARTIFACTS"},
226
+ "environment": {
227
+ "type": env_type,
228
+ "image": image,
229
+ "computeType": self.compute_type,
230
+ "privilegedMode": True,
231
+ },
232
+ "serviceRole": role_arn,
233
+ }
234
+
235
+ try:
236
+ self.codebuild.batch_get_projects(names=[self._project_name])
237
+ existing = self.codebuild.batch_get_projects(names=[self._project_name])
238
+ if existing["projects"]:
239
+ self.codebuild.update_project(**project_config)
240
+ print(" CodeBuildプロジェクト更新: %s" % self._project_name)
241
+ return
242
+ except ClientError:
243
+ pass
244
+
245
+ self.codebuild.create_project(**project_config)
246
+ print(" CodeBuildプロジェクト作成: %s" % self._project_name)
247
+
248
+ def _delete_project(self) -> None:
249
+ try:
250
+ self.codebuild.delete_project(name=self._project_name)
251
+ print(" CodeBuildプロジェクト削除: %s" % self._project_name)
252
+ except ClientError as e:
253
+ if e.response["Error"]["Code"] == "ResourceNotFoundException":
254
+ return
255
+ raise
256
+
257
+ @staticmethod
258
+ def _env_for_platform(platform: str) -> tuple[str, str]:
259
+ if "arm64" in platform or "aarch64" in platform:
260
+ return "ARM_CONTAINER", IMAGE_ARM64
261
+ return "LINUX_CONTAINER", IMAGE_AMD64
262
+
263
+ # --- ソースアップロード ---
264
+
265
+ def _upload_source(self, dockerfile_path: str) -> None:
266
+ import tempfile
267
+
268
+ print(" ソースをS3にアップロード中...")
269
+ context_dir = Path(".").resolve()
270
+ spec = load_dockerignore(context_dir)
271
+
272
+ # SpooledTemporaryFile: 50MB までメモリ、超えたらディスクへ自動退避
273
+ buf = tempfile.SpooledTemporaryFile(max_size=50 * 1024 * 1024)
274
+ with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
275
+ # os.walk でディレクトリ単位の枝刈り (rglob より高速)
276
+ for dirpath, dirnames, filenames in os.walk(context_dir):
277
+ rel_dir = os.path.relpath(dirpath, context_dir)
278
+ if rel_dir == ".":
279
+ rel_dir = ""
280
+ # 除外ディレクトリを in-place で枝刈り
281
+ dirnames[:] = sorted(
282
+ d
283
+ for d in dirnames
284
+ if should_include(os.path.join(rel_dir, d) if rel_dir else d, spec)
285
+ )
286
+ for filename in sorted(filenames):
287
+ rel = os.path.join(rel_dir, filename) if rel_dir else filename
288
+ if not should_include(rel, spec):
289
+ continue
290
+ zf.write(os.path.join(dirpath, filename), rel)
291
+
292
+ zip_size_mb = buf.tell() / (1024 * 1024)
293
+ buf.seek(0)
294
+ # upload_fileobj でストリーミングアップロード
295
+ self.s3.upload_fileobj(buf, self.state_bucket, self._source_key)
296
+ buf.close()
297
+ print(" アップロード完了 (%.1f MB)" % zip_size_mb)
298
+
299
+ # --- ビルド実行 ---
300
+
301
+ def _start_build(
302
+ self,
303
+ *,
304
+ target: str,
305
+ dockerfile_path: str,
306
+ ecr_host: str,
307
+ ) -> str:
308
+ resp = self.codebuild.start_build(
309
+ projectName=self._project_name,
310
+ buildspecOverride=BUILDSPEC,
311
+ environmentVariablesOverride=[
312
+ {"name": "IMAGE_TAG", "value": target, "type": "PLAINTEXT"},
313
+ {"name": "DOCKERFILE", "value": dockerfile_path, "type": "PLAINTEXT"},
314
+ {"name": "ECR_HOST", "value": ecr_host, "type": "PLAINTEXT"},
315
+ ],
316
+ )
317
+ build_id: str = resp["build"]["id"]
318
+ print(" ビルド開始: %s" % build_id)
319
+ return build_id
320
+
321
+ def _wait_build(self, build_id: str) -> None:
322
+ print(" ビルド完了を待機中...")
323
+ while True:
324
+ resp = self.codebuild.batch_get_builds(ids=[build_id])
325
+ build = resp["builds"][0]
326
+ status = build["buildStatus"]
327
+
328
+ if status == "SUCCEEDED":
329
+ return
330
+ if status in ("FAILED", "FAULT", "STOPPED", "TIMED_OUT"):
331
+ phases = build.get("phases", [])
332
+ _print_failed_phases(phases)
333
+ raise RuntimeError(
334
+ "CodeBuild ビルド失敗: %s (status=%s)" % (build_id, status)
335
+ )
336
+
337
+ time.sleep(POLL_INTERVAL)
338
+
339
+ # --- リソース存在チェック ---
340
+
341
+ def project_exists(self) -> bool:
342
+ resp = self.codebuild.batch_get_projects(names=[self._project_name])
343
+ return len(resp.get("projects", [])) > 0
344
+
345
+ def role_exists(self) -> bool:
346
+ try:
347
+ self.iam.get_role(RoleName=self._role_name)
348
+ return True
349
+ except ClientError as e:
350
+ if e.response["Error"]["Code"] == "NoSuchEntity":
351
+ return False
352
+ raise
353
+
354
+
355
+ def _print_failed_phases(phases: list[dict]) -> None:
356
+ print(" --- CodeBuild フェーズ情報 ---")
357
+ for phase in phases:
358
+ phase_type = phase.get("phaseType", "?")
359
+ phase_status = phase.get("phaseStatus", "?")
360
+ if phase_status not in ("SUCCEEDED", "?"):
361
+ contexts = phase.get("contexts", [])
362
+ msg = contexts[0].get("message", "") if contexts else ""
363
+ print(" %s: %s %s" % (phase_type, phase_status, msg))
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import subprocess
5
+
6
+ from python_on_whales import docker
7
+
8
+
9
+ class DepotBuilder:
10
+ def __init__(
11
+ self,
12
+ *,
13
+ region: str,
14
+ project_id: str | None = None,
15
+ ) -> None:
16
+ self.region = region
17
+ self.project_id = project_id or os.environ.get("DEPOT_PROJECT_ID")
18
+
19
+ def _get_token(self) -> str:
20
+ token = os.environ.get("DEPOT_TOKEN")
21
+ if not token:
22
+ raise RuntimeError("DEPOT_TOKEN 環境変数が設定されていません")
23
+ return token
24
+
25
+ def build_and_push(
26
+ self,
27
+ *,
28
+ target: str,
29
+ dockerfile_path: str,
30
+ platform: str,
31
+ ) -> None:
32
+ token = self._get_token()
33
+
34
+ print("ECR にログインします...")
35
+ docker.login_ecr(region_name=self.region)
36
+
37
+ print("Depot でイメージをビルドします...")
38
+ print(" target: %s" % target)
39
+ print(" dockerfile: %s" % dockerfile_path)
40
+ print(" platform: %s" % platform)
41
+
42
+ cmd = [
43
+ "depot",
44
+ "build",
45
+ ".",
46
+ "--file",
47
+ dockerfile_path,
48
+ "--tag",
49
+ target,
50
+ "--platform",
51
+ platform,
52
+ "--push",
53
+ "--provenance=false",
54
+ ]
55
+
56
+ if self.project_id:
57
+ cmd.extend(["--project", self.project_id])
58
+ print(" project: %s" % self.project_id)
59
+
60
+ env: dict[str, str] = {
61
+ "PATH": os.environ.get("PATH", ""),
62
+ "HOME": os.environ.get("HOME", ""),
63
+ "DEPOT_TOKEN": token,
64
+ "AWS_DEFAULT_REGION": self.region,
65
+ "AWS_REGION": self.region,
66
+ }
67
+ for key in (
68
+ "AWS_ACCESS_KEY_ID",
69
+ "AWS_SECRET_ACCESS_KEY",
70
+ "AWS_SESSION_TOKEN",
71
+ ):
72
+ if key in os.environ:
73
+ env[key] = os.environ[key]
74
+ # DEPOT_ prefix の環境変数を一括 passthrough
75
+ for key, value in os.environ.items():
76
+ if key.startswith("DEPOT_") and key not in env:
77
+ env[key] = value
78
+ if self.project_id:
79
+ env["DEPOT_PROJECT_ID"] = self.project_id
80
+ subprocess.run(cmd, check=True, env=env)
81
+ print("Depot ビルド完了")
82
+
83
+ def delete(self) -> None:
84
+ pass
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from python_on_whales import docker
4
+
5
+
6
+ class DockerBuilder:
7
+ def __init__(self, *, region: str) -> None:
8
+ self.region = region
9
+
10
+ def build_and_push(
11
+ self,
12
+ *,
13
+ target: str,
14
+ dockerfile_path: str,
15
+ platform: str,
16
+ ) -> None:
17
+ print("Building docker image...")
18
+ print(" dockerpath: %s" % dockerfile_path)
19
+ print(" tags: %s" % target)
20
+ print(" platforms: %s" % platform)
21
+ print("Logging in to ecr...")
22
+ docker.login_ecr(region_name=self.region)
23
+ print("Pushing docker image...")
24
+ docker.build(
25
+ ".",
26
+ file=str(dockerfile_path),
27
+ tags=target,
28
+ platforms=[platform],
29
+ provenance=False,
30
+ push=True,
31
+ )
32
+
33
+ def delete(self) -> None:
34
+ pass
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import pathspec
6
+
7
+ DEFAULT_EXCLUDES = [
8
+ ".git",
9
+ ".venv",
10
+ "__pycache__",
11
+ "node_modules",
12
+ ".mypy_cache",
13
+ ".pytest_cache",
14
+ ".ruff_cache",
15
+ "*.pyc",
16
+ ".env",
17
+ ".forge",
18
+ ]
19
+
20
+
21
+ def load_dockerignore(dockerfile_dir: Path) -> pathspec.PathSpec:
22
+ """
23
+ .dockerignore を読み込み、PathSpec を返す。
24
+ ファイルがなければデフォルトの除外パターンを使う。
25
+
26
+ pathspec の gitignore を使用。dockerignore は gitignore とほぼ同一の
27
+ パターン仕様(`**`、末尾 `/`、否定 `!`、文字クラス等)を持つため、
28
+ pathspec の gitignore で実用上問題なくカバーできる。
29
+ """
30
+ dockerignore_path = dockerfile_dir / ".dockerignore"
31
+ if not dockerignore_path.exists():
32
+ lines = list(DEFAULT_EXCLUDES)
33
+ else:
34
+ lines = [
35
+ stripped
36
+ for line in dockerignore_path.read_text().splitlines()
37
+ if (stripped := line.strip()) and not stripped.startswith("#")
38
+ ]
39
+ return pathspec.PathSpec.from_lines("gitignore", lines)
40
+
41
+
42
+ def should_include(path: str, spec: pathspec.PathSpec) -> bool:
43
+ """パスがパターンにマッチしないか(= 含めるべきか)を判定する。"""
44
+ return not spec.match_file(path)