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,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import boto3
|
|
7
|
+
from pydantic import BaseModel, Field, computed_field
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from pocket_cli.resources.aws.builders import Builder
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RepositoryDetail(BaseModel):
|
|
14
|
+
uri: str | None = Field(alias="repositoryUri", default=None)
|
|
15
|
+
arn: str | None = Field(alias="repositoryArn", default=None)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ImageDetail(BaseModel):
|
|
19
|
+
image_digest: str | None = Field(alias="imageDigest", default=None)
|
|
20
|
+
|
|
21
|
+
@computed_field
|
|
22
|
+
def hash(self) -> str | None:
|
|
23
|
+
if self.image_digest:
|
|
24
|
+
alg, hash = self.image_digest.split(":")
|
|
25
|
+
if alg == "sha256":
|
|
26
|
+
return hash
|
|
27
|
+
raise ValueError("unsupported algorithm")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Ecr:
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
region_name: str,
|
|
34
|
+
name: str,
|
|
35
|
+
tag: str,
|
|
36
|
+
dockerfile_path: str,
|
|
37
|
+
platform: str,
|
|
38
|
+
builder: Builder | None = None,
|
|
39
|
+
):
|
|
40
|
+
self.client = boto3.client("ecr", region_name=region_name)
|
|
41
|
+
self.name = name
|
|
42
|
+
self.tag = tag
|
|
43
|
+
self.dockerfile_path = dockerfile_path
|
|
44
|
+
self.platform = platform
|
|
45
|
+
self._builder = builder
|
|
46
|
+
|
|
47
|
+
@cached_property
|
|
48
|
+
def info(self) -> RepositoryDetail:
|
|
49
|
+
for repository in self.client.describe_repositories()["repositories"]:
|
|
50
|
+
if repository["repositoryName"] == self.name:
|
|
51
|
+
return RepositoryDetail(**repository)
|
|
52
|
+
return RepositoryDetail()
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def uri(self):
|
|
56
|
+
return self.info.uri
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def arn(self):
|
|
60
|
+
return self.info.arn
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def target(self):
|
|
64
|
+
if self.uri:
|
|
65
|
+
return self.uri + ":" + self.tag
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def image_detail(self):
|
|
69
|
+
data = self.client.describe_images(repositoryName=self.name)
|
|
70
|
+
for detail in data["imageDetails"]:
|
|
71
|
+
if image_tags := detail.get("imageTags"):
|
|
72
|
+
if self.tag in image_tags:
|
|
73
|
+
return ImageDetail(**detail)
|
|
74
|
+
return ImageDetail()
|
|
75
|
+
|
|
76
|
+
def create(self):
|
|
77
|
+
print("Creating repository ...")
|
|
78
|
+
print(" %s" % self.name)
|
|
79
|
+
self.client.create_repository(repositoryName=self.name)
|
|
80
|
+
if hasattr(self, "info"):
|
|
81
|
+
del self.info
|
|
82
|
+
print(" %s" % self.uri)
|
|
83
|
+
|
|
84
|
+
def ensure_exists(self):
|
|
85
|
+
if not self.info.uri:
|
|
86
|
+
self.create()
|
|
87
|
+
|
|
88
|
+
def build_and_push(self, tag: str | None = None):
|
|
89
|
+
if self.uri is None:
|
|
90
|
+
raise ValueError("target is not defined")
|
|
91
|
+
# tag 省略時は self.tag (= stage) を使う。build once 用に commit hash 等を
|
|
92
|
+
# 焼きたい場合は tag を明示する。
|
|
93
|
+
target = self.uri + ":" + (tag or self.tag)
|
|
94
|
+
if self._builder is None:
|
|
95
|
+
raise ValueError("builder is not configured")
|
|
96
|
+
self._builder.build_and_push(
|
|
97
|
+
target=target,
|
|
98
|
+
dockerfile_path=self.dockerfile_path,
|
|
99
|
+
platform=self.platform,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def retag(self, source_tag: str, dest_tag: str):
|
|
103
|
+
"""source_tag の image に dest_tag を付与する (タグの付け替え。build しない)。
|
|
104
|
+
|
|
105
|
+
build once の昇格用: `:<commit hash>` の image へ `:<stage>` タグを移す。
|
|
106
|
+
source_tag の image が存在しない場合は ValueError。
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
images = self.client.batch_get_image(
|
|
110
|
+
repositoryName=self.name,
|
|
111
|
+
imageIds=[{"imageTag": source_tag}],
|
|
112
|
+
)["images"]
|
|
113
|
+
except self.client.exceptions.RepositoryNotFoundException:
|
|
114
|
+
raise ValueError(
|
|
115
|
+
"ECR repository '%s' が存在しません。"
|
|
116
|
+
"先に `pocket django build` を実行してください。" % self.name
|
|
117
|
+
)
|
|
118
|
+
if not images:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
"image :%s が ECR repository '%s' に存在しません。"
|
|
121
|
+
"先に `pocket django build` を実行してください。"
|
|
122
|
+
% (source_tag, self.name)
|
|
123
|
+
)
|
|
124
|
+
try:
|
|
125
|
+
self.client.put_image(
|
|
126
|
+
repositoryName=self.name,
|
|
127
|
+
imageManifest=images[0]["imageManifest"],
|
|
128
|
+
imageTag=dest_tag,
|
|
129
|
+
)
|
|
130
|
+
except self.client.exceptions.ImageAlreadyExistsException:
|
|
131
|
+
pass # dest_tag が既に同じ digest を指している (再昇格の冪等性)
|
|
132
|
+
|
|
133
|
+
def exists(self) -> bool:
|
|
134
|
+
return self.info.uri is not None
|
|
135
|
+
|
|
136
|
+
def delete(self):
|
|
137
|
+
if not self.exists():
|
|
138
|
+
return
|
|
139
|
+
self.client.delete_repository(repositoryName=self.name, force=True)
|
|
140
|
+
if hasattr(self, "info"):
|
|
141
|
+
del self.info
|
|
142
|
+
|
|
143
|
+
def sync(self):
|
|
144
|
+
self.ensure_exists()
|
|
145
|
+
self.build_and_push()
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import uuid
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import boto3
|
|
9
|
+
|
|
10
|
+
from pocket.resources.base import ResourceStatus
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from pocket.general_context import EfsContext
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Efs:
|
|
17
|
+
context: EfsContext
|
|
18
|
+
|
|
19
|
+
def __init__(self, context: EfsContext) -> None:
|
|
20
|
+
self.context = context
|
|
21
|
+
self.client = boto3.client("efs", region_name=context.region)
|
|
22
|
+
|
|
23
|
+
@cached_property
|
|
24
|
+
def description(self):
|
|
25
|
+
for fs in self._iter_all_efs():
|
|
26
|
+
for tag in fs["Tags"]:
|
|
27
|
+
if tag["Key"] == "Name" and tag["Value"] == self.context.name:
|
|
28
|
+
return fs
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
@cached_property
|
|
32
|
+
def lifecycle_policies(self):
|
|
33
|
+
res = self.client.describe_lifecycle_configuration(
|
|
34
|
+
FileSystemId=self.filesystem_id
|
|
35
|
+
)
|
|
36
|
+
return res["LifecyclePolicies"]
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def lifecycle_policies_should_be(self):
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
"TransitionToIA": "AFTER_30_DAYS",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"TransitionToPrimaryStorageClass": "AFTER_1_ACCESS",
|
|
46
|
+
},
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def filesystem_id(self):
|
|
51
|
+
if self.description:
|
|
52
|
+
return self.description["FileSystemId"]
|
|
53
|
+
|
|
54
|
+
def clear_status(self):
|
|
55
|
+
if hasattr(self, "description"):
|
|
56
|
+
del self.description
|
|
57
|
+
if hasattr(self, "lifecycle_policies"):
|
|
58
|
+
del self.lifecycle_policies
|
|
59
|
+
|
|
60
|
+
def wait_status(self, status: ResourceStatus, timeout=60):
|
|
61
|
+
max_iter = 100
|
|
62
|
+
interval = 3
|
|
63
|
+
if (timeout < 0) or ((max_iter * interval) < timeout):
|
|
64
|
+
raise Exception("timeout value is out of range")
|
|
65
|
+
for i in range(max_iter):
|
|
66
|
+
self.clear_status()
|
|
67
|
+
if self.status == status:
|
|
68
|
+
print("")
|
|
69
|
+
return
|
|
70
|
+
if i == 0:
|
|
71
|
+
print("Waiting for efs status to be %s" % status, end="", flush=True)
|
|
72
|
+
print(".", end="", flush=True)
|
|
73
|
+
time.sleep(interval)
|
|
74
|
+
|
|
75
|
+
def create(self):
|
|
76
|
+
res = self.client.create_file_system(
|
|
77
|
+
CreationToken=str(uuid.uuid4()),
|
|
78
|
+
Encrypted=True,
|
|
79
|
+
Backup=True,
|
|
80
|
+
Tags=[
|
|
81
|
+
{
|
|
82
|
+
"Key": "Name",
|
|
83
|
+
"Value": self.context.name,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
)
|
|
87
|
+
filesystem_id = res["FileSystemId"]
|
|
88
|
+
print(res)
|
|
89
|
+
print(filesystem_id)
|
|
90
|
+
self.wait_status("REQUIRE_UPDATE")
|
|
91
|
+
self.ensure_lifecycle_policies()
|
|
92
|
+
|
|
93
|
+
def update(self):
|
|
94
|
+
self.client.put_lifecycle_configuration(
|
|
95
|
+
FileSystemId=self.filesystem_id,
|
|
96
|
+
LifecyclePolicies=self.lifecycle_policies_should_be,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def ensure_lifecycle_policies(self):
|
|
100
|
+
if self.lifecycle_policies != self.lifecycle_policies_should_be:
|
|
101
|
+
self.update()
|
|
102
|
+
|
|
103
|
+
def delete(self):
|
|
104
|
+
self.client.delete_file_system(FileSystemId=self.filesystem_id)
|
|
105
|
+
|
|
106
|
+
def _iter_all_efs(self):
|
|
107
|
+
res = self.client.describe_file_systems()
|
|
108
|
+
if res.get("NextMarker"):
|
|
109
|
+
raise Exception("Your efs number is over 100. Please implement here.")
|
|
110
|
+
return res["FileSystems"]
|
|
111
|
+
|
|
112
|
+
def exists(self):
|
|
113
|
+
if self.description:
|
|
114
|
+
return True
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
def ensure_exists(self):
|
|
118
|
+
if self.exists():
|
|
119
|
+
return
|
|
120
|
+
self.create()
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def status(self) -> ResourceStatus:
|
|
124
|
+
if not self.exists():
|
|
125
|
+
return "NOEXIST"
|
|
126
|
+
assert self.description
|
|
127
|
+
if self.description["LifeCycleState"] != "available":
|
|
128
|
+
return "PROGRESS"
|
|
129
|
+
if self.lifecycle_policies != [
|
|
130
|
+
{
|
|
131
|
+
"TransitionToIA": "AFTER_30_DAYS",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"TransitionToPrimaryStorageClass": "AFTER_1_ACCESS",
|
|
135
|
+
},
|
|
136
|
+
]:
|
|
137
|
+
return "REQUIRE_UPDATE"
|
|
138
|
+
return "COMPLETED"
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import time
|
|
5
|
+
from email.utils import parsedate_to_datetime
|
|
6
|
+
from functools import cached_property
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import boto3
|
|
10
|
+
from botocore.exceptions import ClientError
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
from pocket.resources.base import ResourceStatus
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from pocket.context import LambdaHandlerContext
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Configuration(BaseModel):
|
|
20
|
+
hash: str | None = Field(alias="CodeSha256", default=None)
|
|
21
|
+
last_update_status: str | None = Field(alias="LastUpdateStatus", default=None)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LambdaHandler:
|
|
25
|
+
context: LambdaHandlerContext
|
|
26
|
+
|
|
27
|
+
def __init__(self, context: LambdaHandlerContext) -> None:
|
|
28
|
+
self.context = context
|
|
29
|
+
self.client = boto3.client("lambda", region_name=context.region)
|
|
30
|
+
self.logs_client = boto3.client("logs", region_name=self.context.region)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def name(self):
|
|
34
|
+
return self.context.function_name
|
|
35
|
+
|
|
36
|
+
@cached_property
|
|
37
|
+
def configuration(self):
|
|
38
|
+
try:
|
|
39
|
+
data = self.client.get_function(FunctionName=self.name)["Configuration"]
|
|
40
|
+
return Configuration(**data)
|
|
41
|
+
except ClientError:
|
|
42
|
+
return Configuration()
|
|
43
|
+
|
|
44
|
+
def refresh(self):
|
|
45
|
+
try:
|
|
46
|
+
del self.configuration
|
|
47
|
+
except AttributeError:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def status(self) -> ResourceStatus:
|
|
52
|
+
match self.configuration.last_update_status:
|
|
53
|
+
case None:
|
|
54
|
+
return "NOEXIST"
|
|
55
|
+
case "InProgress":
|
|
56
|
+
return "PROGRESS"
|
|
57
|
+
case "Failed":
|
|
58
|
+
return "FAILED"
|
|
59
|
+
case "Successful":
|
|
60
|
+
return "COMPLETED"
|
|
61
|
+
case _:
|
|
62
|
+
raise Exception("unexpected status")
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def queueurl(self) -> str | None:
|
|
66
|
+
if self.context.sqs:
|
|
67
|
+
res = boto3.client("sqs").get_queue_url(QueueName=self.context.sqs.name)
|
|
68
|
+
return res["QueueUrl"]
|
|
69
|
+
|
|
70
|
+
def update(self, image_uri, wait=False):
|
|
71
|
+
res = self.client.update_function_code(
|
|
72
|
+
FunctionName=self.name, ImageUri=image_uri
|
|
73
|
+
)
|
|
74
|
+
print(f"lambda function {self.name} was updated.")
|
|
75
|
+
show_keys = ["FunctionName", "Timeout", "MemorySize", "Version", "Environment"]
|
|
76
|
+
for key, value in res.items():
|
|
77
|
+
if key in show_keys:
|
|
78
|
+
print(f" - {key}: {value}")
|
|
79
|
+
if wait:
|
|
80
|
+
self.wait_update()
|
|
81
|
+
|
|
82
|
+
def wait_update(self, interval=3, limit=60):
|
|
83
|
+
print(f"waiting lambda fanction {self.name} update.", end="", flush=True)
|
|
84
|
+
for _i in range(limit // interval):
|
|
85
|
+
time.sleep(interval)
|
|
86
|
+
self.refresh()
|
|
87
|
+
if self.status == "PROGRESS":
|
|
88
|
+
print(".", end="", flush=True)
|
|
89
|
+
else:
|
|
90
|
+
print(f"\nlambda function {self.name} was updated.")
|
|
91
|
+
break
|
|
92
|
+
else:
|
|
93
|
+
raise Exception("Lambda couldn't stop updating. Please check.")
|
|
94
|
+
|
|
95
|
+
def invoke(self, payload: str):
|
|
96
|
+
return self.client.invoke(
|
|
97
|
+
FunctionName=self.name, InvocationType="Event", Payload=payload
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def show_logs(self, invoke_http_response, timeout_seconds=120):
|
|
101
|
+
res = invoke_http_response
|
|
102
|
+
request_id = res["ResponseMetadata"]["RequestId"]
|
|
103
|
+
created_at_rfc1123 = res["ResponseMetadata"]["HTTPHeaders"]["date"]
|
|
104
|
+
created_at = parsedate_to_datetime(created_at_rfc1123)
|
|
105
|
+
print("lambda request_id:", request_id)
|
|
106
|
+
print("lambda created_at:", created_at)
|
|
107
|
+
start_pattern = '"START RequestId: %s"' % request_id
|
|
108
|
+
report_prefix = "REPORT RequestId: %s" % request_id
|
|
109
|
+
events = self._find_events(start_pattern, created_at)
|
|
110
|
+
print("Log stream found: %s" % events[0]["logStreamName"])
|
|
111
|
+
printed = []
|
|
112
|
+
sleep_seconds = 5
|
|
113
|
+
for _i in range(timeout_seconds // sleep_seconds):
|
|
114
|
+
res = self.logs_client.filter_log_events(
|
|
115
|
+
logGroupName=self.context.log_group_name,
|
|
116
|
+
logStreamNames=[events[0]["logStreamName"]],
|
|
117
|
+
startTime=events[0]["timestamp"],
|
|
118
|
+
)
|
|
119
|
+
messages = [event["message"] for event in res["events"]]
|
|
120
|
+
if messages[: len(printed)] != printed:
|
|
121
|
+
raise Exception("log stream changed")
|
|
122
|
+
for message in messages[len(printed) :]:
|
|
123
|
+
print(message.strip())
|
|
124
|
+
printed.append(message)
|
|
125
|
+
time.sleep(0.05)
|
|
126
|
+
if message.startswith(report_prefix):
|
|
127
|
+
return
|
|
128
|
+
time.sleep(sleep_seconds)
|
|
129
|
+
print("Timeout %s seconds. Please check logs in cloudwatch." % timeout_seconds)
|
|
130
|
+
|
|
131
|
+
def _get_recent_log_stream_names(self, limit: int):
|
|
132
|
+
try:
|
|
133
|
+
res = self.logs_client.describe_log_streams(
|
|
134
|
+
logGroupName=self.context.log_group_name,
|
|
135
|
+
orderBy="LastEventTime",
|
|
136
|
+
descending=True,
|
|
137
|
+
limit=limit,
|
|
138
|
+
)
|
|
139
|
+
except self.logs_client.exceptions.ResourceNotFoundException:
|
|
140
|
+
return []
|
|
141
|
+
return [s["logStreamName"] for s in res["logStreams"]]
|
|
142
|
+
|
|
143
|
+
def _find_events(
|
|
144
|
+
self,
|
|
145
|
+
filter_pattern: str,
|
|
146
|
+
created_at: datetime.datetime,
|
|
147
|
+
log_stream_names: list[str] | None = None,
|
|
148
|
+
log_stream_limit: int = 3,
|
|
149
|
+
):
|
|
150
|
+
for i in range(20):
|
|
151
|
+
if i != 0:
|
|
152
|
+
msg = "Waiting for log stream." if i == 1 else "."
|
|
153
|
+
print(msg, end="", flush=True)
|
|
154
|
+
time.sleep(3)
|
|
155
|
+
target_log_stream_names = (
|
|
156
|
+
log_stream_names or self._get_recent_log_stream_names(log_stream_limit)
|
|
157
|
+
)
|
|
158
|
+
if not target_log_stream_names:
|
|
159
|
+
continue
|
|
160
|
+
kwargs = {
|
|
161
|
+
"logGroupName": self.context.log_group_name,
|
|
162
|
+
"logStreamNames": target_log_stream_names,
|
|
163
|
+
"filterPattern": filter_pattern,
|
|
164
|
+
"startTime": int(created_at.timestamp() * 1000),
|
|
165
|
+
}
|
|
166
|
+
events = []
|
|
167
|
+
for _j in range(10):
|
|
168
|
+
res = self.logs_client.filter_log_events(**kwargs)
|
|
169
|
+
if res["events"]:
|
|
170
|
+
events += res["events"]
|
|
171
|
+
if "nextToken" in res:
|
|
172
|
+
kwargs["nextToken"] = res["nextToken"]
|
|
173
|
+
else:
|
|
174
|
+
break
|
|
175
|
+
if events:
|
|
176
|
+
return events
|
|
177
|
+
print(
|
|
178
|
+
"Searched %s log stream below..." % len(target_log_stream_names) # pyright: ignore
|
|
179
|
+
)
|
|
180
|
+
for s in target_log_stream_names: # pyright: ignore
|
|
181
|
+
print(" - %s" % s)
|
|
182
|
+
raise Exception("log stream not found")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from botocore.exceptions import ClientError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def bucket_exists(client, bucket_name: str) -> bool:
|
|
7
|
+
"""バケットの存在確認。404以外のエラーはClientErrorとして再送出"""
|
|
8
|
+
try:
|
|
9
|
+
client.head_bucket(Bucket=bucket_name)
|
|
10
|
+
return True
|
|
11
|
+
except ClientError as e:
|
|
12
|
+
if e.response.get("Error", {}).get("Code") == "404":
|
|
13
|
+
return False
|
|
14
|
+
raise
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_bucket(client, bucket_name: str, region: str):
|
|
18
|
+
"""リージョンを考慮してバケットを作成"""
|
|
19
|
+
if region == "us-east-1":
|
|
20
|
+
client.create_bucket(Bucket=bucket_name)
|
|
21
|
+
else:
|
|
22
|
+
client.create_bucket(
|
|
23
|
+
Bucket=bucket_name,
|
|
24
|
+
CreateBucketConfiguration={"LocationConstraint": region},
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def empty_bucket(client, bucket_name: str):
|
|
29
|
+
"""バケット内の全オブジェクト(バージョン含む)を削除"""
|
|
30
|
+
# 通常オブジェクトの削除
|
|
31
|
+
paginator = client.get_paginator("list_objects_v2")
|
|
32
|
+
for page in paginator.paginate(Bucket=bucket_name):
|
|
33
|
+
objects = page.get("Contents", [])
|
|
34
|
+
if not objects:
|
|
35
|
+
continue
|
|
36
|
+
delete_keys = [{"Key": obj["Key"]} for obj in objects]
|
|
37
|
+
client.delete_objects(Bucket=bucket_name, Delete={"Objects": delete_keys})
|
|
38
|
+
|
|
39
|
+
# バージョニングが有効な場合のバージョン・DeleteMarker削除
|
|
40
|
+
paginator = client.get_paginator("list_object_versions")
|
|
41
|
+
for page in paginator.paginate(Bucket=bucket_name):
|
|
42
|
+
delete_keys = []
|
|
43
|
+
for version in page.get("Versions", []):
|
|
44
|
+
delete_keys.append(
|
|
45
|
+
{"Key": version["Key"], "VersionId": version["VersionId"]}
|
|
46
|
+
)
|
|
47
|
+
for marker in page.get("DeleteMarkers", []):
|
|
48
|
+
delete_keys.append({"Key": marker["Key"], "VersionId": marker["VersionId"]})
|
|
49
|
+
if delete_keys:
|
|
50
|
+
client.delete_objects(Bucket=bucket_name, Delete={"Objects": delete_keys})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def delete_bucket_with_contents(client, bucket_name: str):
|
|
54
|
+
"""バケットを中身ごと削除(存在しなければ no-op)"""
|
|
55
|
+
if not bucket_exists(client, bucket_name):
|
|
56
|
+
return
|
|
57
|
+
empty_bucket(client, bucket_name)
|
|
58
|
+
client.delete_bucket(Bucket=bucket_name)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import boto3
|
|
6
|
+
from botocore.exceptions import ClientError
|
|
7
|
+
|
|
8
|
+
from pocket.settings import _deep_merge
|
|
9
|
+
from pocket_cli.resources.aws.s3_utils import (
|
|
10
|
+
bucket_exists,
|
|
11
|
+
create_bucket,
|
|
12
|
+
delete_bucket_with_contents,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StateStore:
|
|
17
|
+
STATE_KEY = "resources.json"
|
|
18
|
+
|
|
19
|
+
def __init__(self, bucket_name: str, region: str):
|
|
20
|
+
self.bucket_name = bucket_name
|
|
21
|
+
self.region = region
|
|
22
|
+
self.client = boto3.client("s3", region_name=region)
|
|
23
|
+
self._state: dict | None = None
|
|
24
|
+
|
|
25
|
+
def ensure_bucket(self):
|
|
26
|
+
"""state バケットが存在しなければ作成(全公開ブロック)"""
|
|
27
|
+
if bucket_exists(self.client, self.bucket_name):
|
|
28
|
+
return
|
|
29
|
+
create_bucket(self.client, self.bucket_name, self.region)
|
|
30
|
+
self.client.put_public_access_block(
|
|
31
|
+
Bucket=self.bucket_name,
|
|
32
|
+
PublicAccessBlockConfiguration={
|
|
33
|
+
"BlockPublicAcls": True,
|
|
34
|
+
"IgnorePublicAcls": True,
|
|
35
|
+
"BlockPublicPolicy": True,
|
|
36
|
+
"RestrictPublicBuckets": True,
|
|
37
|
+
},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def load(self) -> dict:
|
|
41
|
+
"""S3から resources.json を読み込み。存在しなければ空stateを返す"""
|
|
42
|
+
if self._state is not None:
|
|
43
|
+
return self._state
|
|
44
|
+
try:
|
|
45
|
+
response = self.client.get_object(
|
|
46
|
+
Bucket=self.bucket_name, Key=self.STATE_KEY
|
|
47
|
+
)
|
|
48
|
+
self._state = json.loads(response["Body"].read().decode("utf-8"))
|
|
49
|
+
except ClientError as e:
|
|
50
|
+
if e.response.get("Error", {}).get("Code") == "NoSuchKey":
|
|
51
|
+
self._state = {"version": 1, "resources": {}}
|
|
52
|
+
else:
|
|
53
|
+
raise
|
|
54
|
+
return self._state # type: ignore[return-value]
|
|
55
|
+
|
|
56
|
+
def save(self):
|
|
57
|
+
"""現在のstateをS3に書き込み"""
|
|
58
|
+
state = self.load()
|
|
59
|
+
self.client.put_object(
|
|
60
|
+
Bucket=self.bucket_name,
|
|
61
|
+
Key=self.STATE_KEY,
|
|
62
|
+
Body=json.dumps(state, indent=2).encode("utf-8"),
|
|
63
|
+
ContentType="application/json",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def record(self, info: dict):
|
|
67
|
+
"""リソース情報を deep merge して保存"""
|
|
68
|
+
state = self.load()
|
|
69
|
+
_deep_merge(state["resources"], info)
|
|
70
|
+
self.save()
|
|
71
|
+
|
|
72
|
+
def delete_bucket(self):
|
|
73
|
+
"""ステートバケットを中身ごと削除"""
|
|
74
|
+
delete_bucket_with_contents(self.client, self.bucket_name)
|