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,69 @@
1
+ from pprint import pprint
2
+
3
+ import click
4
+
5
+ from pocket.context import Context
6
+ from pocket.utils import echo
7
+ from pocket_cli.resources.s3 import S3
8
+
9
+
10
+ @click.group()
11
+ def s3():
12
+ pass
13
+
14
+
15
+ def get_s3_resource(stage):
16
+ context = Context.from_toml(stage=stage)
17
+ if not context.s3:
18
+ echo.danger("s3 is not configured for this stage")
19
+ raise Exception("s3 is not configured for this stage")
20
+ return S3(context=context.s3)
21
+
22
+
23
+ @s3.command()
24
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
25
+ def context(stage):
26
+ storage = get_s3_resource(stage)
27
+ pprint(storage.context.model_dump())
28
+
29
+
30
+ @s3.command()
31
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
32
+ def create(stage):
33
+ """S3 バケットを作成、または既存バケットの設定 (PAB / CORS / versioning /
34
+ lifecycle) を冪等に reconcile する。"""
35
+ storage = get_s3_resource(stage)
36
+ already_exists = storage.exists()
37
+ storage.ensure_exists()
38
+ if already_exists:
39
+ echo.success(
40
+ "Reconciled: bucket %s (PAB / CORS / versioning / lifecycle)"
41
+ % storage.context.bucket_name
42
+ )
43
+ else:
44
+ echo.success("Created: bucket %s" % storage.context.bucket_name)
45
+
46
+
47
+ @s3.command()
48
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
49
+ def destroy(stage):
50
+ storage = get_s3_resource(stage)
51
+ if not storage.exists():
52
+ echo.warning("No S3 bucket found.")
53
+ return
54
+ echo.danger("S3バケットの全データが失われます。")
55
+ click.confirm(
56
+ "バケット '%s' を削除しますか?" % storage.context.bucket_name, abort=True
57
+ )
58
+ storage.delete()
59
+ echo.success("S3 bucket was deleted.")
60
+
61
+
62
+ @s3.command()
63
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
64
+ def status(stage):
65
+ storage = get_s3_resource(stage)
66
+ if storage.exists():
67
+ echo.success("Storage found")
68
+ else:
69
+ echo.warning("Storage not found")
@@ -0,0 +1,56 @@
1
+ import click
2
+
3
+ from pocket.context import Context
4
+ from pocket.resources.aws.secretsmanager import PocketSecretIsNotReady
5
+ from pocket.utils import echo
6
+ from pocket_cli.cli.deploy_cli import get_resources
7
+ from pocket_cli.resources.awscontainer import AwsContainer
8
+
9
+
10
+ def show_status_message(resource):
11
+ target_name = resource.__class__.__name__
12
+ message = f"{target_name} status: {resource.status}"
13
+ echo_fn = {
14
+ "NOEXIST": echo.info,
15
+ "REQUIRE_UPDATE": echo.warning,
16
+ "PROGRESS": echo.warning,
17
+ "COMPLETED": echo.success,
18
+ "FAILED": echo.danger,
19
+ }[resource.status]
20
+ echo_fn(message)
21
+
22
+
23
+ def show_info_message(resource):
24
+ if hasattr(resource, "description"):
25
+ echo.info(resource.description)
26
+ if isinstance(resource, AwsContainer) and resource.context.secrets:
27
+ if resource.context.secrets.managed:
28
+ try:
29
+ _ = resource.context.secrets.pocket_store.arn
30
+ except PocketSecretIsNotReady:
31
+ echo.warning(
32
+ "Because pocket managed secrets is not ready yet, "
33
+ "the context is not ready to use."
34
+ )
35
+ echo.warning("Just use it for reference.")
36
+ echo.info("You can create pocket managed secrets by running the below.")
37
+ echo.info("pocket resource awscontainer secrets create-pocket-managed")
38
+
39
+ try:
40
+ _ = resource.context.secrets.allowed_sm_resources
41
+ except PocketSecretIsNotReady:
42
+ echo.warning("Please create pocket secrets first.")
43
+ return
44
+ print(resource.context.model_dump_json(indent=2))
45
+
46
+
47
+ @click.command()
48
+ @click.option("--show-info", is_flag=True, default=False)
49
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
50
+ def status(stage, show_info):
51
+ context = Context.from_toml(stage=stage)
52
+ resources = get_resources(context)
53
+ for resource in resources:
54
+ show_status_message(resource)
55
+ if show_info:
56
+ show_info_message(resource)
@@ -0,0 +1,73 @@
1
+ from pprint import pprint
2
+
3
+ import click
4
+
5
+ from pocket.context import Context
6
+ from pocket.utils import echo
7
+ from pocket_cli.resources.tidb import TiDb
8
+
9
+
10
+ @click.group()
11
+ def tidb():
12
+ pass
13
+
14
+
15
+ def get_tidb_resource(stage):
16
+ context = Context.from_toml(stage=stage)
17
+ if not context.tidb:
18
+ echo.danger("tidb is not configured for this stage")
19
+ raise ValueError("tidb is not configured for this stage")
20
+ return TiDb(context=context.tidb)
21
+
22
+
23
+ @tidb.command()
24
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
25
+ def context(stage):
26
+ resource = get_tidb_resource(stage)
27
+ pprint(resource.context.model_dump())
28
+
29
+
30
+ @tidb.command()
31
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
32
+ def create(stage):
33
+ resource = get_tidb_resource(stage)
34
+ resource.create()
35
+ echo.success("TiDB cluster and database created")
36
+
37
+
38
+ @tidb.command()
39
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
40
+ def reset_database(stage):
41
+ resource = get_tidb_resource(stage)
42
+ resource.reset_database()
43
+ echo.success("Reset database")
44
+
45
+
46
+ @tidb.command()
47
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
48
+ def delete(stage):
49
+ resource = get_tidb_resource(stage)
50
+ resource.delete_cluster()
51
+ echo.success("Cluster was deleted successfully.")
52
+
53
+
54
+ @tidb.command()
55
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
56
+ def status(stage):
57
+ resource = get_tidb_resource(stage)
58
+ if resource.project:
59
+ echo.success("Project found")
60
+ else:
61
+ echo.warning("Project not found")
62
+ return
63
+ if resource.cluster:
64
+ echo.success(
65
+ "Cluster found: %s (%s)" % (resource.cluster.name, resource.cluster.status)
66
+ )
67
+ else:
68
+ echo.warning("Cluster not found")
69
+ return
70
+ if resource.cluster.status == "ACTIVE":
71
+ echo.success("Database url: %s" % resource.database_url)
72
+ else:
73
+ echo.warning("Cluster status: %s" % resource.cluster.status)
@@ -0,0 +1,92 @@
1
+ import click
2
+
3
+ from pocket.general_context import VpcContext
4
+ from pocket.utils import echo
5
+ from pocket_cli.resources.vpc import Vpc
6
+
7
+
8
+ @click.group()
9
+ def vpc():
10
+ pass
11
+
12
+
13
+ def get_vpc_resource():
14
+ vpc_context = VpcContext.from_toml()
15
+ return Vpc(vpc_context)
16
+
17
+
18
+ @vpc.command()
19
+ def yaml():
20
+ vpc = get_vpc_resource()
21
+ print(vpc.stack.yaml)
22
+
23
+
24
+ @vpc.command()
25
+ def yaml_diff():
26
+ vpc = get_vpc_resource()
27
+ print(vpc.stack.yaml_diff.to_json(indent=2))
28
+
29
+
30
+ @vpc.command()
31
+ def create():
32
+ vpc = get_vpc_resource()
33
+ if not vpc.context.manage:
34
+ echo.danger("外部 VPC は他で管理されています。")
35
+ return
36
+ if not vpc.status == "NOEXIST":
37
+ echo.warning("AWS vpc is already created.")
38
+ else:
39
+ vpc.create()
40
+ echo.success("Created: vpc")
41
+
42
+
43
+ @vpc.command()
44
+ def update():
45
+ vpc = get_vpc_resource()
46
+ if not vpc.context.manage:
47
+ echo.danger("外部 VPC は他で管理されています。")
48
+ return
49
+ if vpc.status == "NOEXIST":
50
+ echo.warning("vpc has not created yet.")
51
+ return
52
+ if vpc.status == "FAILED":
53
+ echo.danger("vpc has failed. Please check console.")
54
+ return
55
+ if vpc.status == "PROGRESS":
56
+ echo.warning("vpc is updating. Please wait.")
57
+ return
58
+ vpc.update()
59
+
60
+
61
+ @vpc.command()
62
+ def destroy():
63
+ vpc = get_vpc_resource()
64
+ if not vpc.context.manage:
65
+ echo.danger("外部 VPC は他で管理されています。")
66
+ return
67
+ if vpc.stack.consumers:
68
+ echo.danger("VPC に consumer がいるため削除できません:")
69
+ for c in vpc.stack.consumers:
70
+ echo.info(" - %s" % c)
71
+ return
72
+ has_stack = vpc.stack.status != "NOEXIST"
73
+ has_efs = vpc.efs and vpc.efs.exists()
74
+ if not has_stack and not has_efs:
75
+ echo.warning("No VPC resources found.")
76
+ return
77
+ click.confirm("VPC を削除しますか?", abort=True)
78
+ vpc.delete()
79
+ echo.success("VPC was destroyed.")
80
+
81
+
82
+ @vpc.command()
83
+ def status():
84
+ vpc = get_vpc_resource()
85
+ if vpc.status == "COMPLETED":
86
+ echo.success("Vpc has been created.")
87
+ elif vpc.status == "NOEXIST":
88
+ echo.warning("Vpc has not created yet.")
89
+ elif vpc.status == "FAILED":
90
+ echo.danger("Vpc has failed. Please check console.")
91
+ else:
92
+ echo.warning("Vpc stack status: %s" % vpc.stack.status)
@@ -0,0 +1,182 @@
1
+ """`pocket waf ip ...` — IPSet を CFN を介さず直接更新する CLI。
2
+
3
+ WebACL / IPSet 自体は CFn (CloudFrontWafStack) が us-east-1 に作成し、
4
+ Addresses は CFn template 上は常に空。実際の CIDR 一覧はこの CLI が
5
+ `update_ip_set` boto3 で side-channel 更新する (1 件追加で CFn を回したくない
6
+ ため)。CFn 視点では Addresses は常に drift 状態だが、これは仕様。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import urllib.request
12
+
13
+ import boto3
14
+ import click
15
+
16
+ from pocket.context import CloudFrontContext, Context
17
+ from pocket.utils import echo
18
+ from pocket_cli.resources.aws.cloudformation import CloudFrontWafStack
19
+
20
+
21
+ @click.group()
22
+ def waf():
23
+ pass
24
+
25
+
26
+ @waf.group()
27
+ def ip():
28
+ pass
29
+
30
+
31
+ def _get_cf_context(stage: str, name: str) -> CloudFrontContext:
32
+ context = Context.from_toml(stage=stage)
33
+ if not context.cloudfront:
34
+ raise click.ClickException("cloudfront is not configured for this stage")
35
+ if name not in context.cloudfront:
36
+ raise click.ClickException("cloudfront '%s' is not configured" % name)
37
+ cf_ctx = context.cloudfront[name]
38
+ if cf_ctx.waf is None:
39
+ raise click.ClickException(
40
+ "cloudfront '%s' has no [cloudfront.%s.waf] block" % (name, name)
41
+ )
42
+ if not cf_ctx.waf.enable_ip_set:
43
+ raise click.ClickException(
44
+ "cloudfront '%s' is configured with [cloudfront.%s.waf]"
45
+ " enable_ip_set = false, so there is no IPSet to operate on."
46
+ " Set `enable_ip_set = true` (default) to use `pocket waf ip ...`."
47
+ % (name, name)
48
+ )
49
+ return cf_ctx
50
+
51
+
52
+ def _get_ip_set_meta(cf_ctx: CloudFrontContext) -> tuple[str, str]:
53
+ """CFn stack output から IPSet の (Name, Id) を取得する。"""
54
+ stack = CloudFrontWafStack(cf_ctx)
55
+ output = stack.output
56
+ if not output:
57
+ raise click.ClickException(
58
+ "WAF stack '%s' が見つかりません。先に `pocket deploy` を実行してください。"
59
+ % stack.name
60
+ )
61
+ name = output.get("IPSetName")
62
+ set_id = output.get("IPSetId")
63
+ if not name or not set_id:
64
+ raise click.ClickException(
65
+ "WAF stack output に IPSetName / IPSetId がありません: %s" % stack.name
66
+ )
67
+ return name, set_id
68
+
69
+
70
+ def _wafv2_client():
71
+ # Scope=CLOUDFRONT は us-east-1 でしか操作できない
72
+ return boto3.client("wafv2", region_name="us-east-1")
73
+
74
+
75
+ def _fetch_ip_set(client, set_name: str, set_id: str) -> tuple[list[str], str]:
76
+ res = client.get_ip_set(Name=set_name, Scope="CLOUDFRONT", Id=set_id)
77
+ return list(res["IPSet"]["Addresses"]), res["LockToken"]
78
+
79
+
80
+ def _write_ip_set(client, set_name: str, set_id: str, addresses: list[str], lock: str):
81
+ client.update_ip_set(
82
+ Name=set_name,
83
+ Scope="CLOUDFRONT",
84
+ Id=set_id,
85
+ Addresses=addresses,
86
+ LockToken=lock,
87
+ )
88
+
89
+
90
+ def _detect_self_ipv4() -> str:
91
+ """1 次: AWS checkip、fallback: ipify。/32 を付けた CIDR を返す。"""
92
+ for url in ("https://checkip.amazonaws.com", "https://api.ipify.org"):
93
+ try:
94
+ with urllib.request.urlopen(url, timeout=5) as r:
95
+ ip = r.read().decode("utf-8").strip()
96
+ if ip:
97
+ return f"{ip}/32"
98
+ except Exception as e: # noqa: BLE001
99
+ echo.warning("IP 検出失敗 (%s): %s" % (url, e))
100
+ raise click.ClickException("自分の Global IP を取得できませんでした")
101
+
102
+
103
+ @ip.command("list")
104
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
105
+ @click.option("--name", required=True, help="CloudFront name (pocket.toml の key)")
106
+ def ip_list(stage, name):
107
+ """IPSet 内 CIDR 一覧を表示する。"""
108
+ cf_ctx = _get_cf_context(stage, name)
109
+ set_name, set_id = _get_ip_set_meta(cf_ctx)
110
+ client = _wafv2_client()
111
+ addresses, _ = _fetch_ip_set(client, set_name, set_id)
112
+ if not addresses:
113
+ echo.warning(
114
+ "IPSet は空です (deny-all 状態)。"
115
+ "`pocket waf ip add self --name %s --stage %s` で自分の IP を"
116
+ "追加してください。" % (name, stage)
117
+ )
118
+ return
119
+ for cidr in addresses:
120
+ click.echo(cidr)
121
+
122
+
123
+ @ip.command("add")
124
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
125
+ @click.option("--name", required=True, help="CloudFront name (pocket.toml の key)")
126
+ @click.argument("cidr")
127
+ def ip_add(stage, name, cidr):
128
+ """任意 CIDR を IPSet に追加。`self` で自分の IP を追加 (重複は dedup)。"""
129
+ cf_ctx = _get_cf_context(stage, name)
130
+ set_name, set_id = _get_ip_set_meta(cf_ctx)
131
+ client = _wafv2_client()
132
+ if cidr == "self":
133
+ cidr = _detect_self_ipv4()
134
+ echo.info("検出した自分の IP: %s" % cidr)
135
+ addresses, lock = _fetch_ip_set(client, set_name, set_id)
136
+ if cidr in addresses:
137
+ echo.info("%s は既に登録済みです。" % cidr)
138
+ return
139
+ new_addresses = addresses + [cidr]
140
+ _write_ip_set(client, set_name, set_id, new_addresses, lock)
141
+ echo.success("追加しました: %s (合計 %d entries)" % (cidr, len(new_addresses)))
142
+
143
+
144
+ @ip.command("remove")
145
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
146
+ @click.option("--name", required=True, help="CloudFront name (pocket.toml の key)")
147
+ @click.argument("cidr")
148
+ def ip_remove(stage, name, cidr):
149
+ """指定 CIDR を IPSet から削除。"""
150
+ cf_ctx = _get_cf_context(stage, name)
151
+ set_name, set_id = _get_ip_set_meta(cf_ctx)
152
+ client = _wafv2_client()
153
+ addresses, lock = _fetch_ip_set(client, set_name, set_id)
154
+ if cidr not in addresses:
155
+ echo.warning("%s は IPSet に存在しません。" % cidr)
156
+ return
157
+ new_addresses = [a for a in addresses if a != cidr]
158
+ _write_ip_set(client, set_name, set_id, new_addresses, lock)
159
+ echo.success("削除しました: %s (残り %d entries)" % (cidr, len(new_addresses)))
160
+
161
+
162
+ @ip.command("clear")
163
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
164
+ @click.option("--name", required=True, help="CloudFront name (pocket.toml の key)")
165
+ @click.option("--yes", is_flag=True, default=False, help="確認プロンプトをスキップ")
166
+ def ip_clear(stage, name, yes):
167
+ """IPSet を空にする (= deny-all 状態に戻す)。確認プロンプトあり。"""
168
+ cf_ctx = _get_cf_context(stage, name)
169
+ set_name, set_id = _get_ip_set_meta(cf_ctx)
170
+ client = _wafv2_client()
171
+ addresses, lock = _fetch_ip_set(client, set_name, set_id)
172
+ if not addresses:
173
+ echo.info("IPSet は既に空です。")
174
+ return
175
+ if not yes:
176
+ click.confirm(
177
+ "%d 件の CIDR を全削除して deny-all 状態にします。よろしいですか?"
178
+ % len(addresses),
179
+ abort=True,
180
+ )
181
+ _write_ip_set(client, set_name, set_id, [], lock)
182
+ echo.success("IPSet を全削除しました (deny-all 状態)。")