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,265 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import boto3
|
|
7
|
+
|
|
8
|
+
from pocket import context
|
|
9
|
+
from pocket.resources.base import ResourceStatus
|
|
10
|
+
from pocket.utils import echo
|
|
11
|
+
from pocket_cli.cli.runtime_config_cli import generate_runtime_config
|
|
12
|
+
from pocket_cli.mediator import Mediator
|
|
13
|
+
from pocket_cli.resources.aws.builders import Builder, create_builder
|
|
14
|
+
from pocket_cli.resources.aws.cloudformation import ContainerStack
|
|
15
|
+
from pocket_cli.resources.aws.ecr import Ecr
|
|
16
|
+
from pocket_cli.resources.aws.lambdahandler import LambdaHandler
|
|
17
|
+
from pocket_cli.resources.vpc import Vpc
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from pocket.context import (
|
|
21
|
+
AwsContainerContext,
|
|
22
|
+
DsqlContext,
|
|
23
|
+
RdsContext,
|
|
24
|
+
SchedulerContext,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NotCreatedYetError(Exception):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class NoApiEndpointError(Exception):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AwsContainer:
|
|
37
|
+
"""This is abstructed resource to run container in aws.
|
|
38
|
+
This class depends on aws resources.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
context: AwsContainerContext
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
context: context.AwsContainerContext,
|
|
46
|
+
*,
|
|
47
|
+
state_bucket: str = "",
|
|
48
|
+
rds_context: RdsContext | None = None,
|
|
49
|
+
dsql_context: DsqlContext | None = None,
|
|
50
|
+
scheduler_context: SchedulerContext | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
self.context = context
|
|
53
|
+
self.client = boto3.client("lambda", region_name=context.region)
|
|
54
|
+
self._state_bucket = state_bucket
|
|
55
|
+
self._rds_context = rds_context
|
|
56
|
+
self._dsql_context = dsql_context
|
|
57
|
+
self._scheduler_context = scheduler_context
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def builder(self) -> Builder:
|
|
61
|
+
return create_builder(
|
|
62
|
+
self.context.build,
|
|
63
|
+
region=self.context.region,
|
|
64
|
+
resource_prefix=self.context.resource_prefix,
|
|
65
|
+
state_bucket=self._state_bucket,
|
|
66
|
+
permissions_boundary=self.context.permissions_boundary,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def image_uri(self):
|
|
71
|
+
if self.ecr.uri:
|
|
72
|
+
return self.ecr.uri + ":" + self.context.stage
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def ecr(self):
|
|
76
|
+
return Ecr(
|
|
77
|
+
self.context.region,
|
|
78
|
+
self.context.ecr_name,
|
|
79
|
+
self.context.stage,
|
|
80
|
+
self.context.dockerfile_path,
|
|
81
|
+
self.context.platform,
|
|
82
|
+
builder=self.builder,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def build(self, tag: str):
|
|
86
|
+
"""指定 tag (commit hash 等) で image を build & push する (deploy はしない)。
|
|
87
|
+
|
|
88
|
+
build once 用。`:stage` タグは付けない。昇格は deploy 側で行う。
|
|
89
|
+
pocket.runtime.toml は Dockerfile の COPY で image に焼き込まれるため、
|
|
90
|
+
deploy_init と同様に build 前へ生成する。
|
|
91
|
+
"""
|
|
92
|
+
generate_runtime_config(self._runtime_toml_path())
|
|
93
|
+
ecr = self.ecr
|
|
94
|
+
ecr.ensure_exists()
|
|
95
|
+
ecr.build_and_push(tag=tag)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def stack(self):
|
|
99
|
+
return ContainerStack(
|
|
100
|
+
self.context,
|
|
101
|
+
rds_context=self._rds_context,
|
|
102
|
+
dsql_context=self._dsql_context,
|
|
103
|
+
scheduler_context=self._scheduler_context,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def handlers(self):
|
|
108
|
+
handlers: dict[str, LambdaHandler] = {}
|
|
109
|
+
for key, handler in self.context.handlers.items():
|
|
110
|
+
handlers[key] = LambdaHandler(handler)
|
|
111
|
+
return handlers
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def handlers_updating(self):
|
|
115
|
+
return any(handler.status == "PROGRESS" for handler in self.handlers.values())
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def status(self) -> ResourceStatus:
|
|
119
|
+
handler_status_list: list[ResourceStatus] = [
|
|
120
|
+
handler.status for handler in self.handlers.values()
|
|
121
|
+
]
|
|
122
|
+
if ("FAILED" in handler_status_list) or (self.stack.status == "FAILED"):
|
|
123
|
+
return "FAILED"
|
|
124
|
+
if ("PROGRESS" in handler_status_list) or (self.stack.status == "PROGRESS"):
|
|
125
|
+
return "PROGRESS"
|
|
126
|
+
if self.stack.status in ["NOEXIST", "REQUIRE_UPDATE"]:
|
|
127
|
+
return self.stack.status
|
|
128
|
+
for handler in self.handlers.values():
|
|
129
|
+
if handler.configuration.hash != self.ecr.image_detail.hash:
|
|
130
|
+
return "REQUIRE_UPDATE"
|
|
131
|
+
return "COMPLETED"
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def description(self):
|
|
135
|
+
msg = "Create aws cloudformation stack: %s\nCreate ecr repository: %s" % (
|
|
136
|
+
self.stack.name,
|
|
137
|
+
self.ecr.name,
|
|
138
|
+
)
|
|
139
|
+
if self.context.secrets and self.context.secrets.managed:
|
|
140
|
+
msg += "\nCreate pocket managed secrets (%s): %s" % (
|
|
141
|
+
self.context.secrets.store,
|
|
142
|
+
self.context.secrets.pocket_key,
|
|
143
|
+
)
|
|
144
|
+
return msg
|
|
145
|
+
|
|
146
|
+
def _require_acm_manual_upadte(self):
|
|
147
|
+
manual_cert_ref_names = []
|
|
148
|
+
for handler in self.handlers.values():
|
|
149
|
+
apig_c = handler.context.apigateway
|
|
150
|
+
if apig_c and apig_c.domain and not apig_c.create_records:
|
|
151
|
+
manual_cert_ref_names.append(
|
|
152
|
+
handler.context.cloudformation_cert_ref_name
|
|
153
|
+
)
|
|
154
|
+
yaml_diff = self.stack.yaml_diff
|
|
155
|
+
for ref_name in manual_cert_ref_names:
|
|
156
|
+
if t := yaml_diff.get("type_changes", {}).get("root", {}).get("old_type"):
|
|
157
|
+
if t == "NoneType":
|
|
158
|
+
return True
|
|
159
|
+
resource = "root['Resources']['%s']" % ref_name
|
|
160
|
+
if resource in yaml_diff.get("dictionary_item_added", []):
|
|
161
|
+
return True
|
|
162
|
+
for changed_resource in yaml_diff.get("values_changed", {}).keys():
|
|
163
|
+
if changed_resource.startswith(resource):
|
|
164
|
+
return True
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
def show_acm_manual_request(self):
|
|
168
|
+
if self._require_acm_manual_upadte():
|
|
169
|
+
w = echo.warning
|
|
170
|
+
w("You need to request ACM manually to complete stack events.")
|
|
171
|
+
w("See CloudFormation stack log.")
|
|
172
|
+
w("Probably, you need to request dns A record to WsgiRegionalDomainName")
|
|
173
|
+
|
|
174
|
+
def state_info(self):
|
|
175
|
+
return {"ecr": {"repository_name": self.context.ecr_name}}
|
|
176
|
+
|
|
177
|
+
def _runtime_toml_path(self) -> Path:
|
|
178
|
+
"""pocket.runtime.toml の出力先を返す。
|
|
179
|
+
|
|
180
|
+
django.project_dir が設定されていればその中に、
|
|
181
|
+
なければ CWD に配置する。
|
|
182
|
+
"""
|
|
183
|
+
if self.context.django and self.context.django.project_dir:
|
|
184
|
+
return Path(self.context.django.project_dir) / "pocket.runtime.toml"
|
|
185
|
+
return Path("pocket.runtime.toml")
|
|
186
|
+
|
|
187
|
+
def deploy_init(self):
|
|
188
|
+
if self.context.promote_commit_hash:
|
|
189
|
+
# 昇格 (promote): 既存 :<hash> image へ :<stage> タグを移す。build しない。
|
|
190
|
+
# runtime config は build 時に image へ焼き込み済みのため生成もしない。
|
|
191
|
+
self.ecr.retag(self.context.promote_commit_hash, self.context.stage)
|
|
192
|
+
else:
|
|
193
|
+
generate_runtime_config(self._runtime_toml_path())
|
|
194
|
+
self.ecr.sync()
|
|
195
|
+
if self.context.vpc and not self.context.vpc.manage:
|
|
196
|
+
vpc_stack = Vpc(self.context.vpc).stack
|
|
197
|
+
if vpc_stack.status == "NOEXIST":
|
|
198
|
+
raise ValueError(
|
|
199
|
+
f"外部 VPC スタック '{vpc_stack.name}' が見つかりません。"
|
|
200
|
+
)
|
|
201
|
+
if not vpc_stack.is_sharable:
|
|
202
|
+
raise ValueError(
|
|
203
|
+
f"VPC '{vpc_stack.name}' は共有が許可されていません "
|
|
204
|
+
"(sharable = true が必要です)"
|
|
205
|
+
)
|
|
206
|
+
vpc_stack.add_consumer_tag(self.context.slug)
|
|
207
|
+
|
|
208
|
+
def _wait_vpc_if_needed(self):
|
|
209
|
+
"""managed VPC の場合、スタック完了を待つ"""
|
|
210
|
+
if self.context.vpc and self.context.vpc.manage:
|
|
211
|
+
Vpc(self.context.vpc).stack.wait_status("COMPLETED")
|
|
212
|
+
|
|
213
|
+
def create(self, mediator: Mediator):
|
|
214
|
+
self._wait_vpc_if_needed()
|
|
215
|
+
print("Creating secrets ...")
|
|
216
|
+
mediator.ensure_pocket_managed_secrets()
|
|
217
|
+
print("Creating cloudformation stack for awscontainer ...")
|
|
218
|
+
self.show_acm_manual_request()
|
|
219
|
+
self.stack.create()
|
|
220
|
+
self.stack.wait_status("COMPLETED", timeout=600, interval=10)
|
|
221
|
+
|
|
222
|
+
def update(self, mediator: Mediator):
|
|
223
|
+
mediator.ensure_pocket_managed_secrets()
|
|
224
|
+
self.show_acm_manual_request()
|
|
225
|
+
for key, handler in self.handlers.items():
|
|
226
|
+
if handler.status == "NOEXIST":
|
|
227
|
+
print(f"function {key} was not found and skipped.")
|
|
228
|
+
else:
|
|
229
|
+
handler.update(image_uri=self.image_uri)
|
|
230
|
+
for handler in self.handlers.values():
|
|
231
|
+
handler.wait_update()
|
|
232
|
+
if not self.stack.yaml_synced:
|
|
233
|
+
self.stack.update()
|
|
234
|
+
self.stack.wait_status("COMPLETED", timeout=600, interval=10)
|
|
235
|
+
|
|
236
|
+
def get_host(self, key: str):
|
|
237
|
+
handler = self.handlers[key]
|
|
238
|
+
if handler.context.apigateway is None:
|
|
239
|
+
raise NoApiEndpointError(f"ApiGateway is not defined in {key}")
|
|
240
|
+
if handler.context.apigateway.domain:
|
|
241
|
+
return handler.context.apigateway.domain
|
|
242
|
+
apiendpoint_key = key.capitalize() + "ApiEndpoint"
|
|
243
|
+
if self.stack.output and apiendpoint_key in self.stack.output:
|
|
244
|
+
return self.stack.output[apiendpoint_key][len("https://") :]
|
|
245
|
+
raise NotCreatedYetError(f"ApiGateway endpoint for {key} is not created yet.")
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def hosts(self) -> dict[str, str | None]:
|
|
249
|
+
data = {}
|
|
250
|
+
for key in self.handlers:
|
|
251
|
+
try:
|
|
252
|
+
data[key] = self.get_host(key)
|
|
253
|
+
except NotCreatedYetError:
|
|
254
|
+
data[key] = None
|
|
255
|
+
except NoApiEndpointError:
|
|
256
|
+
pass
|
|
257
|
+
return data
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def endpoints(self):
|
|
261
|
+
return {key: f"https://{host}" for key, host in self.hosts.items() if host}
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def queueurls(self) -> dict[str, str | None]:
|
|
265
|
+
return {key: handler.queueurl for key, handler in self.handlers.items()}
|