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.
- magic_pocket_cli-0.2.0.dist-info/METADATA +14 -0
- magic_pocket_cli-0.2.0.dist-info/RECORD +65 -0
- magic_pocket_cli-0.2.0.dist-info/WHEEL +4 -0
- magic_pocket_cli-0.2.0.dist-info/entry_points.txt +2 -0
- pocket_cli/__init__.py +0 -0
- pocket_cli/cli/__init__.py +0 -0
- pocket_cli/cli/aws_auth.py +48 -0
- pocket_cli/cli/awscontainer_cli.py +328 -0
- pocket_cli/cli/cloudfront_cli.py +116 -0
- pocket_cli/cli/cloudfront_keys_cli.py +68 -0
- pocket_cli/cli/cloudfront_waf_cli.py +68 -0
- pocket_cli/cli/deploy_cli.py +274 -0
- pocket_cli/cli/destroy_cli.py +358 -0
- pocket_cli/cli/dsql_cli.py +60 -0
- pocket_cli/cli/main_cli.py +91 -0
- pocket_cli/cli/migrate_cli.py +148 -0
- pocket_cli/cli/neon_cli.py +97 -0
- pocket_cli/cli/permissions_cli.py +46 -0
- pocket_cli/cli/rds_cli.py +63 -0
- pocket_cli/cli/runtime_config_cli.py +185 -0
- pocket_cli/cli/s3_cli.py +69 -0
- pocket_cli/cli/status_cli.py +56 -0
- pocket_cli/cli/tidb_cli.py +73 -0
- pocket_cli/cli/vpc_cli.py +92 -0
- pocket_cli/cli/waf_cli.py +182 -0
- pocket_cli/django_cli.py +412 -0
- pocket_cli/mediator.py +220 -0
- pocket_cli/resources/__init__.py +0 -0
- pocket_cli/resources/aws/__init__.py +0 -0
- pocket_cli/resources/aws/builders/__init__.py +57 -0
- pocket_cli/resources/aws/builders/codebuild.py +363 -0
- pocket_cli/resources/aws/builders/depot.py +84 -0
- pocket_cli/resources/aws/builders/docker.py +34 -0
- pocket_cli/resources/aws/builders/dockerignore.py +44 -0
- pocket_cli/resources/aws/cloudformation.py +790 -0
- pocket_cli/resources/aws/ecr.py +145 -0
- pocket_cli/resources/aws/efs.py +138 -0
- pocket_cli/resources/aws/lambdahandler.py +182 -0
- pocket_cli/resources/aws/s3_utils.py +58 -0
- pocket_cli/resources/aws/state.py +74 -0
- pocket_cli/resources/awscontainer.py +265 -0
- pocket_cli/resources/cloudfront.py +491 -0
- pocket_cli/resources/cloudfront_acm.py +55 -0
- pocket_cli/resources/cloudfront_keys.py +81 -0
- pocket_cli/resources/cloudfront_waf.py +67 -0
- pocket_cli/resources/dsql.py +142 -0
- pocket_cli/resources/neon.py +353 -0
- pocket_cli/resources/rds.py +680 -0
- pocket_cli/resources/s3.py +307 -0
- pocket_cli/resources/tidb.py +298 -0
- pocket_cli/resources/upstash.py +152 -0
- pocket_cli/resources/vpc.py +67 -0
- pocket_cli/templates/cloudformation/awscontainer.yaml +516 -0
- pocket_cli/templates/cloudformation/cf_function_api_host.js +5 -0
- pocket_cli/templates/cloudformation/cf_function_spa_auth.js +28 -0
- pocket_cli/templates/cloudformation/cf_function_spa_fallback.js +8 -0
- pocket_cli/templates/cloudformation/cloudfront.yaml +309 -0
- pocket_cli/templates/cloudformation/cloudfront_acm.yaml +43 -0
- pocket_cli/templates/cloudformation/cloudfront_keys.yaml +32 -0
- pocket_cli/templates/cloudformation/cloudfront_waf.yaml +97 -0
- pocket_cli/templates/cloudformation/vpc.yaml +213 -0
- pocket_cli/templates/init/django-dotenv.env +3 -0
- pocket_cli/templates/init/django-settings.py +140 -0
- pocket_cli/templates/init/pocket.Dockerfile +26 -0
- 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)
|