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,142 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import boto3
|
|
8
|
+
from botocore.exceptions import ClientError
|
|
9
|
+
|
|
10
|
+
from pocket.resources.base import ResourceStatus
|
|
11
|
+
from pocket.utils import echo
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from pocket.context import DsqlContext
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Dsql:
|
|
18
|
+
context: DsqlContext
|
|
19
|
+
|
|
20
|
+
def __init__(self, context: DsqlContext) -> None:
|
|
21
|
+
self.context = context
|
|
22
|
+
self._client = boto3.client("dsql", region_name=context.region)
|
|
23
|
+
|
|
24
|
+
@cached_property
|
|
25
|
+
def cluster(self) -> dict | None:
|
|
26
|
+
"""Name タグで DSQL クラスターを検索"""
|
|
27
|
+
paginator = self._client.get_paginator("list_clusters")
|
|
28
|
+
for page in paginator.paginate():
|
|
29
|
+
for cluster in page["clusters"]:
|
|
30
|
+
identifier = cluster["identifier"]
|
|
31
|
+
try:
|
|
32
|
+
detail = self._client.get_cluster(Identifier=identifier)
|
|
33
|
+
tags = self._client.list_tags_for_resource(
|
|
34
|
+
ResourceArn=detail["arn"]
|
|
35
|
+
)
|
|
36
|
+
if tags.get("tags", {}).get("Name") == self.context.tag_name:
|
|
37
|
+
return detail
|
|
38
|
+
except ClientError:
|
|
39
|
+
continue
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def identifier(self) -> str | None:
|
|
44
|
+
if self.cluster:
|
|
45
|
+
return self.cluster["identifier"]
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def endpoint(self) -> str | None:
|
|
50
|
+
if self.identifier:
|
|
51
|
+
return f"{self.identifier}.dsql.{self.context.region}.on.aws"
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def arn(self) -> str | None:
|
|
56
|
+
if self.cluster:
|
|
57
|
+
return self.cluster["arn"]
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def status(self) -> ResourceStatus:
|
|
62
|
+
if self.cluster is None:
|
|
63
|
+
return "NOEXIST"
|
|
64
|
+
cluster_status = self.cluster["status"]
|
|
65
|
+
if cluster_status in ("CREATING", "UPDATING", "DELETING"):
|
|
66
|
+
return "PROGRESS"
|
|
67
|
+
if cluster_status == "ACTIVE":
|
|
68
|
+
return "COMPLETED"
|
|
69
|
+
return "FAILED"
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def description(self):
|
|
73
|
+
return "Create Aurora DSQL cluster: %s" % self.context.tag_name
|
|
74
|
+
|
|
75
|
+
def state_info(self):
|
|
76
|
+
return {
|
|
77
|
+
"dsql": {
|
|
78
|
+
"tag_name": self.context.tag_name,
|
|
79
|
+
"identifier": self.identifier,
|
|
80
|
+
"endpoint": self.endpoint,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def deploy_init(self):
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
def create(self):
|
|
88
|
+
echo.log("Creating DSQL cluster: %s" % self.context.tag_name)
|
|
89
|
+
res = self._client.create_cluster(
|
|
90
|
+
deletionProtectionEnabled=self.context.deletion_protection,
|
|
91
|
+
tags={"Name": self.context.tag_name},
|
|
92
|
+
)
|
|
93
|
+
identifier = res["identifier"]
|
|
94
|
+
echo.log("Cluster ID: %s" % identifier)
|
|
95
|
+
echo.log("Waiting for DSQL cluster to become active...")
|
|
96
|
+
self._wait_active(identifier, timeout=600)
|
|
97
|
+
self.clear_cache()
|
|
98
|
+
echo.success("DSQL cluster is now active.")
|
|
99
|
+
echo.success("Endpoint: %s" % self.endpoint)
|
|
100
|
+
|
|
101
|
+
def delete(self):
|
|
102
|
+
if not self.identifier:
|
|
103
|
+
return
|
|
104
|
+
echo.log("Deleting DSQL cluster: %s" % self.identifier)
|
|
105
|
+
self._client.delete_cluster(Identifier=self.identifier)
|
|
106
|
+
echo.log("Waiting for DSQL cluster deletion...")
|
|
107
|
+
self._wait_deleted(self.identifier, timeout=600)
|
|
108
|
+
echo.success("DSQL cluster was deleted.")
|
|
109
|
+
|
|
110
|
+
def _wait_active(self, identifier: str, timeout: int = 600, interval: int = 5):
|
|
111
|
+
for i in range(timeout // interval):
|
|
112
|
+
try:
|
|
113
|
+
res = self._client.get_cluster(Identifier=identifier)
|
|
114
|
+
if res["status"] == "ACTIVE":
|
|
115
|
+
print("")
|
|
116
|
+
return
|
|
117
|
+
except ClientError:
|
|
118
|
+
pass
|
|
119
|
+
if i == 0:
|
|
120
|
+
print("Waiting for cluster to be active", end="", flush=True)
|
|
121
|
+
print(".", end="", flush=True)
|
|
122
|
+
time.sleep(interval)
|
|
123
|
+
raise TimeoutError("Cluster did not become active within %s seconds" % timeout)
|
|
124
|
+
|
|
125
|
+
def _wait_deleted(self, identifier: str, timeout: int = 600, interval: int = 5):
|
|
126
|
+
for i in range(timeout // interval):
|
|
127
|
+
try:
|
|
128
|
+
self._client.get_cluster(Identifier=identifier)
|
|
129
|
+
except ClientError as e:
|
|
130
|
+
if e.response["Error"]["Code"] == "ResourceNotFoundException":
|
|
131
|
+
print("")
|
|
132
|
+
return
|
|
133
|
+
raise
|
|
134
|
+
if i == 0:
|
|
135
|
+
print("Waiting for cluster deletion", end="", flush=True)
|
|
136
|
+
print(".", end="", flush=True)
|
|
137
|
+
time.sleep(interval)
|
|
138
|
+
raise TimeoutError("Cluster not deleted within %s seconds" % timeout)
|
|
139
|
+
|
|
140
|
+
def clear_cache(self):
|
|
141
|
+
if "cluster" in self.__dict__:
|
|
142
|
+
del self.__dict__["cluster"]
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
from functools import cached_property
|
|
9
|
+
from typing import TYPE_CHECKING, Literal
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
from pocket.resources.base import ResourceStatus
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from pocket.context import NeonContext
|
|
18
|
+
|
|
19
|
+
logging.basicConfig()
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
logger.setLevel(level=os.getenv("POCKET_LOGGER_LEVEL", "WARNING").upper())
|
|
22
|
+
|
|
23
|
+
ResourceType = Literal["branches", "databases", "endpoints", "roles"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NeonResourceIsNotReady(Exception):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NeonNotFound(Exception):
|
|
31
|
+
"""Neon API が 404 を返したことを示す例外。
|
|
32
|
+
|
|
33
|
+
`branch` / `role` などの個別取得で対象が存在しない場合にこの例外を投げる。
|
|
34
|
+
呼び出し側は存在しないことを明示的に判定できる。
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Project(BaseModel):
|
|
41
|
+
id: str
|
|
42
|
+
name: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Branch(BaseModel):
|
|
46
|
+
id: str
|
|
47
|
+
name: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Database(BaseModel):
|
|
51
|
+
name: str
|
|
52
|
+
owner_name: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Endpoint(BaseModel):
|
|
56
|
+
id: str
|
|
57
|
+
host: str
|
|
58
|
+
autoscaling_limit_min_cu: float
|
|
59
|
+
autoscaling_limit_max_cu: float
|
|
60
|
+
type: Literal["read_write", "read_only"]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Role(BaseModel):
|
|
64
|
+
name: str
|
|
65
|
+
password: str | None = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class NeonApi:
|
|
69
|
+
endpoint = "https://console.neon.tech/api/v2/"
|
|
70
|
+
|
|
71
|
+
def __init__(self, key) -> None:
|
|
72
|
+
self.key = key
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def header(self):
|
|
76
|
+
return {
|
|
77
|
+
"Accept": "application/json",
|
|
78
|
+
"Authorization": "Bearer %s" % self.key,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
def get(self, path):
|
|
82
|
+
logger.info("GET %s" % self.endpoint + path)
|
|
83
|
+
res = requests.get(self.endpoint + path, headers=self.header)
|
|
84
|
+
logger.debug(res.status_code)
|
|
85
|
+
logger.debug(json.dumps(res.json(), indent=2))
|
|
86
|
+
if 200 <= res.status_code < 300:
|
|
87
|
+
return res
|
|
88
|
+
if res.status_code == 404:
|
|
89
|
+
raise NeonNotFound("%s: %s" % (res.status_code, res.json()["message"]))
|
|
90
|
+
if res.status_code == 401:
|
|
91
|
+
print("Used API key: %s" % (self.key[:5] + "..." + self.key[-5:]))
|
|
92
|
+
print("API key length: %s" % len(self.key))
|
|
93
|
+
raise Exception("%s: %s" % (res.status_code, res.json()["message"]))
|
|
94
|
+
|
|
95
|
+
def post(self, path, data=None):
|
|
96
|
+
logger.warning("POST %s" % self.endpoint + path)
|
|
97
|
+
logger.debug(json.dumps(data, indent=2))
|
|
98
|
+
res = requests.post(self.endpoint + path, headers=self.header, json=data)
|
|
99
|
+
logger.debug(res.status_code)
|
|
100
|
+
logger.debug(json.dumps(res.json(), indent=2))
|
|
101
|
+
if 200 <= res.status_code < 300:
|
|
102
|
+
time.sleep(2)
|
|
103
|
+
return res
|
|
104
|
+
if res.status_code == 401:
|
|
105
|
+
print("Used API key: %s" % (self.key[:5] + "..." + self.key[-5:]))
|
|
106
|
+
print("API key length: %s" % len(self.key))
|
|
107
|
+
raise Exception("%s: %s" % (res.status_code, res.json()["message"]))
|
|
108
|
+
|
|
109
|
+
def delete(self, path, data=None):
|
|
110
|
+
logger.warning("DELETE %s" % self.endpoint + path)
|
|
111
|
+
logger.debug(json.dumps(data, indent=2))
|
|
112
|
+
res = requests.delete(self.endpoint + path, headers=self.header, json=data)
|
|
113
|
+
logger.debug(res.status_code)
|
|
114
|
+
logger.debug(json.dumps(res.json(), indent=2))
|
|
115
|
+
if 200 <= res.status_code < 300:
|
|
116
|
+
time.sleep(2)
|
|
117
|
+
return res
|
|
118
|
+
raise Exception("%s: %s" % (res.status_code, res.json()["message"]))
|
|
119
|
+
|
|
120
|
+
def projects_url(self):
|
|
121
|
+
return self.endpoint + "projects"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Neon:
|
|
125
|
+
context: NeonContext
|
|
126
|
+
|
|
127
|
+
def __init__(self, context: NeonContext) -> None:
|
|
128
|
+
self.context = context
|
|
129
|
+
|
|
130
|
+
def get_resource_path(self, resource_type: ResourceType) -> str:
|
|
131
|
+
requirements = {
|
|
132
|
+
"branches": ["project"],
|
|
133
|
+
"databases": ["project", "branch"],
|
|
134
|
+
"endpoints": ["project"],
|
|
135
|
+
"roles": ["project", "branch"],
|
|
136
|
+
}
|
|
137
|
+
path_templates = {
|
|
138
|
+
"branches": "projects/%(project_id)s/branches",
|
|
139
|
+
"databases": "projects/%(project_id)s/branches/%(branch_id)s/databases",
|
|
140
|
+
"endpoints": "projects/%(project_id)s/endpoints",
|
|
141
|
+
"roles": "projects/%(project_id)s/branches/%(branch_id)s/roles",
|
|
142
|
+
}
|
|
143
|
+
path_context = {}
|
|
144
|
+
for requirement in requirements[resource_type]:
|
|
145
|
+
if not getattr(self, requirement):
|
|
146
|
+
raise Exception("%s not found" % requirement)
|
|
147
|
+
path_context[requirement + "_id"] = getattr(self, requirement).id
|
|
148
|
+
return path_templates[resource_type] % path_context
|
|
149
|
+
|
|
150
|
+
def construct_path(
|
|
151
|
+
self, resource_type: ResourceType, resource_id: str | None = None
|
|
152
|
+
):
|
|
153
|
+
path = self.get_resource_path(resource_type)
|
|
154
|
+
if resource_id:
|
|
155
|
+
path += "/" + resource_id
|
|
156
|
+
return path
|
|
157
|
+
|
|
158
|
+
def get(self, resource_type: ResourceType, resource_id: str | None = None):
|
|
159
|
+
path = self.construct_path(resource_type, resource_id)
|
|
160
|
+
return self.api.get(path)
|
|
161
|
+
|
|
162
|
+
def post(
|
|
163
|
+
self, resource_type: ResourceType, resource_id: str | None = None, data=None
|
|
164
|
+
):
|
|
165
|
+
path = self.construct_path(resource_type, resource_id)
|
|
166
|
+
return self.api.post(path, data)
|
|
167
|
+
|
|
168
|
+
def delete(
|
|
169
|
+
self, resource_type: ResourceType, resource_id: str | None = None, data=None
|
|
170
|
+
):
|
|
171
|
+
path = self.construct_path(resource_type, resource_id)
|
|
172
|
+
return self.api.delete(path, data)
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def api(self):
|
|
176
|
+
return NeonApi(self.context.api_key)
|
|
177
|
+
|
|
178
|
+
@cached_property
|
|
179
|
+
def role(self) -> Role | None:
|
|
180
|
+
if not self.branch:
|
|
181
|
+
return None
|
|
182
|
+
try:
|
|
183
|
+
res = self.get("roles", self.context.role_name)
|
|
184
|
+
except NeonNotFound:
|
|
185
|
+
return None
|
|
186
|
+
return Role(**res.json()["role"])
|
|
187
|
+
|
|
188
|
+
@cached_property
|
|
189
|
+
def project(self) -> Project:
|
|
190
|
+
"""project_nameからNeonプロジェクトを解決する。
|
|
191
|
+
|
|
192
|
+
組織キー: GET /projects で一覧取得し name で検索。
|
|
193
|
+
プロジェクトスコープキー: エラーから project_id を取得し直接アクセス。
|
|
194
|
+
"""
|
|
195
|
+
res = requests.get(self.api.endpoint + "projects", headers=self.api.header)
|
|
196
|
+
if 200 <= res.status_code < 300:
|
|
197
|
+
for p in res.json().get("projects", []):
|
|
198
|
+
if p["name"] == self.context.project_name:
|
|
199
|
+
return Project(**p)
|
|
200
|
+
raise ValueError(f"Neon project '{self.context.project_name}' not found")
|
|
201
|
+
|
|
202
|
+
# プロジェクトスコープキー: エラーから project_id をパース
|
|
203
|
+
message = res.json().get("message", "")
|
|
204
|
+
match = re.search(r'subject_project_id:"([^"]+)"', message)
|
|
205
|
+
if not match:
|
|
206
|
+
raise ValueError(
|
|
207
|
+
f"Failed to resolve Neon project: {res.status_code}: {message}"
|
|
208
|
+
)
|
|
209
|
+
project_id = match.group(1)
|
|
210
|
+
project_res = self.api.get(f"projects/{project_id}")
|
|
211
|
+
project_data = project_res.json()["project"]
|
|
212
|
+
if project_data["name"] != self.context.project_name:
|
|
213
|
+
raise ValueError(
|
|
214
|
+
f"Neon project name mismatch: "
|
|
215
|
+
f"config='{self.context.project_name}', "
|
|
216
|
+
f"actual='{project_data['name']}'"
|
|
217
|
+
)
|
|
218
|
+
return Project(id=project_data["id"], name=project_data["name"])
|
|
219
|
+
|
|
220
|
+
@cached_property
|
|
221
|
+
def branch(self) -> Branch | None:
|
|
222
|
+
if self.project:
|
|
223
|
+
for branch in self.get("branches").json()["branches"]:
|
|
224
|
+
if branch["name"] == self.context.branch_name:
|
|
225
|
+
return Branch(**branch)
|
|
226
|
+
|
|
227
|
+
@cached_property
|
|
228
|
+
def database(self) -> Database | None:
|
|
229
|
+
if self.branch:
|
|
230
|
+
for database in self.get("databases").json()["databases"]:
|
|
231
|
+
if database["name"] == self.context.name:
|
|
232
|
+
return Database(**database)
|
|
233
|
+
|
|
234
|
+
@cached_property
|
|
235
|
+
def endpoint(self) -> Endpoint | None:
|
|
236
|
+
if self.branch:
|
|
237
|
+
for endpoint in self.get("endpoints").json()["endpoints"]:
|
|
238
|
+
if endpoint["branch_id"] == self.branch.id:
|
|
239
|
+
return Endpoint(**endpoint)
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def database_url(self):
|
|
243
|
+
if self.role is None or self.endpoint is None:
|
|
244
|
+
raise NeonResourceIsNotReady("Create role and endpoint first")
|
|
245
|
+
if self.role.password is None:
|
|
246
|
+
self.set_role_password()
|
|
247
|
+
return "postgres://%s:%s@%s:5432/%s?sslmode=require" % (
|
|
248
|
+
self.context.role_name,
|
|
249
|
+
self.role.password,
|
|
250
|
+
self.endpoint.host,
|
|
251
|
+
self.context.name,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def status(self) -> ResourceStatus:
|
|
256
|
+
if self.context.skip_check_existing:
|
|
257
|
+
# 存在確認の Neon API call を skip し COMPLETED 固定。deploy ロールに
|
|
258
|
+
# Neon credentials を渡さず deploy を完走させる用途 (settings 参照)。
|
|
259
|
+
return "COMPLETED"
|
|
260
|
+
if self.working:
|
|
261
|
+
return "COMPLETED"
|
|
262
|
+
return "NOEXIST"
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def working(self):
|
|
266
|
+
check = [self.branch, self.database, self.endpoint, self.role]
|
|
267
|
+
logger.info(str(check))
|
|
268
|
+
return all(check)
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def description(self):
|
|
272
|
+
return "Create Neon branch, database, role, and endpoint"
|
|
273
|
+
|
|
274
|
+
def create_new(self):
|
|
275
|
+
self.create()
|
|
276
|
+
self.reset_database()
|
|
277
|
+
|
|
278
|
+
def state_info(self):
|
|
279
|
+
return {
|
|
280
|
+
"neon": {
|
|
281
|
+
"project_name": self.context.project_name,
|
|
282
|
+
"branch_name": self.context.branch_name,
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
def deploy_init(self):
|
|
287
|
+
pass
|
|
288
|
+
|
|
289
|
+
def create(self):
|
|
290
|
+
self.create_branch()
|
|
291
|
+
self.ensure_role()
|
|
292
|
+
self.ensure_database()
|
|
293
|
+
|
|
294
|
+
def create_branch(self, base_branch: Branch | None = None):
|
|
295
|
+
if self.branch is None:
|
|
296
|
+
del self.branch
|
|
297
|
+
del self.endpoint
|
|
298
|
+
data = {
|
|
299
|
+
"branch": {
|
|
300
|
+
"name": self.context.branch_name,
|
|
301
|
+
},
|
|
302
|
+
"endpoints": [{"type": "read_write"}],
|
|
303
|
+
}
|
|
304
|
+
if base_branch:
|
|
305
|
+
data["branch"]["parent_id"] = base_branch.id
|
|
306
|
+
self.post("branches", data=data)
|
|
307
|
+
|
|
308
|
+
def delete_branch(self):
|
|
309
|
+
if not self.endpoint or not self.branch:
|
|
310
|
+
raise Exception("Branch or endpoint not found. Something is wrong.")
|
|
311
|
+
self.delete("endpoints", self.endpoint.id)
|
|
312
|
+
self.delete("branches", self.branch.id)
|
|
313
|
+
|
|
314
|
+
def ensure_database(self):
|
|
315
|
+
if self.database is None:
|
|
316
|
+
self.create_database()
|
|
317
|
+
|
|
318
|
+
def create_database(self):
|
|
319
|
+
if self.database is None:
|
|
320
|
+
del self.database
|
|
321
|
+
data = {
|
|
322
|
+
"database": {
|
|
323
|
+
"name": self.context.name,
|
|
324
|
+
"owner_name": self.context.role_name,
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
self.post("databases", data=data)
|
|
328
|
+
|
|
329
|
+
def reset_database(self):
|
|
330
|
+
if self.database:
|
|
331
|
+
self.delete("databases", self.context.name)
|
|
332
|
+
self.create_database()
|
|
333
|
+
|
|
334
|
+
def create_role(self):
|
|
335
|
+
if self.role is None:
|
|
336
|
+
del self.role
|
|
337
|
+
data = {"role": {"name": self.context.role_name}}
|
|
338
|
+
self.post("roles", data=data)
|
|
339
|
+
|
|
340
|
+
def set_role_password(self):
|
|
341
|
+
if self.role is None:
|
|
342
|
+
raise Exception("Create role first")
|
|
343
|
+
if self.role.password is None:
|
|
344
|
+
self.role.password = self.get(
|
|
345
|
+
"roles", self.role.name + "/reveal_password"
|
|
346
|
+
).json()["password"]
|
|
347
|
+
|
|
348
|
+
def ensure_role(self):
|
|
349
|
+
if self.role is None:
|
|
350
|
+
del self.role
|
|
351
|
+
data = {"role": {"name": self.context.role_name}}
|
|
352
|
+
self.post("roles", data=data)
|
|
353
|
+
self.set_role_password()
|