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,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()}