magic-pocket-cli 0.35.0__tar.gz → 0.37.0__tar.gz

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 (87) hide show
  1. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/PKG-INFO +2 -2
  2. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/backup_cli.py +1 -1
  3. magic_pocket_cli-0.37.0/pocket_cli/cli/cleanup_deprecated_cli.py +140 -0
  4. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/dsql_cli.py +2 -3
  5. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/inbound_cli.py +53 -36
  6. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/main_cli.py +2 -0
  7. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/aws/backup_common.py +37 -8
  8. magic_pocket_cli-0.37.0/pocket_cli/resources/aws/backup_legacy.py +124 -0
  9. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/aws/builders/codebuild.py +23 -10
  10. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/backup.py +45 -11
  11. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/dsql.py +6 -3
  12. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/inbound.py +19 -4
  13. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pyproject.toml +2 -2
  14. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/.gitignore +0 -0
  15. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/__init__.py +0 -0
  16. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/__init__.py +0 -0
  17. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/aws_auth.py +0 -0
  18. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/build_cli.py +0 -0
  19. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/cloudfront_cli.py +0 -0
  20. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/cloudfront_keys_cli.py +0 -0
  21. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/cloudfront_waf_cli.py +0 -0
  22. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/container_cli.py +0 -0
  23. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/deploy_cli.py +0 -0
  24. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/destroy_cli.py +0 -0
  25. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/image_cli.py +0 -0
  26. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/interaction.py +0 -0
  27. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/migrate_cli.py +0 -0
  28. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/neon_cli.py +0 -0
  29. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/permissions_cli.py +0 -0
  30. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/rds_cli.py +0 -0
  31. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/removed_flags.py +0 -0
  32. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/resource_helper.py +0 -0
  33. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/runtime_config_cli.py +0 -0
  34. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/s3_cli.py +0 -0
  35. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/status_cli.py +0 -0
  36. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/store_url_helper.py +0 -0
  37. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/tidb_cli.py +0 -0
  38. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/upstash_cli.py +0 -0
  39. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/url_helper.py +0 -0
  40. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/vpc_cli.py +0 -0
  41. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/cli/waf_cli.py +0 -0
  42. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/django_cli.py +0 -0
  43. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/mediator.py +0 -0
  44. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/migrations.py +0 -0
  45. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/__init__.py +0 -0
  46. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/aws/__init__.py +0 -0
  47. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/aws/builders/__init__.py +0 -0
  48. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/aws/builders/context_check.py +0 -0
  49. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/aws/builders/depot.py +0 -0
  50. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/aws/builders/docker.py +0 -0
  51. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/aws/builders/dockerignore.py +0 -0
  52. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/aws/cloudformation.py +0 -0
  53. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/aws/ecr.py +0 -0
  54. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/aws/efs.py +0 -0
  55. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/aws/lambdahandler.py +0 -0
  56. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/aws/poll.py +0 -0
  57. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/aws/s3_utils.py +0 -0
  58. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/aws/stack_backed.py +0 -0
  59. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/aws/state.py +0 -0
  60. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/cloudfront.py +0 -0
  61. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/cloudfront_acm.py +0 -0
  62. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/cloudfront_keys.py +0 -0
  63. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/cloudfront_waf.py +0 -0
  64. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/container.py +0 -0
  65. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/inbound_template.py +0 -0
  66. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/neon.py +0 -0
  67. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/rds.py +0 -0
  68. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/s3.py +0 -0
  69. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/sqs_alert.py +0 -0
  70. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/tidb.py +0 -0
  71. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/upstash.py +0 -0
  72. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/resources/vpc.py +0 -0
  73. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/templates/cloudformation/cf_function_api_host.js +0 -0
  74. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/templates/cloudformation/cf_function_basic_auth_prelude.js +0 -0
  75. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/templates/cloudformation/cf_function_host_redirect_prelude.js +0 -0
  76. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/templates/cloudformation/cf_function_spa_auth.js +0 -0
  77. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/templates/cloudformation/cf_function_spa_fallback.js +0 -0
  78. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/templates/cloudformation/cloudfront.yaml +0 -0
  79. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/templates/cloudformation/cloudfront_acm.yaml +0 -0
  80. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/templates/cloudformation/cloudfront_keys.yaml +0 -0
  81. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/templates/cloudformation/cloudfront_waf.yaml +0 -0
  82. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/templates/cloudformation/container.yaml +0 -0
  83. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/templates/cloudformation/vpc.yaml +0 -0
  84. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/templates/init/django-dotenv.env +0 -0
  85. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/templates/init/django-settings.py +0 -0
  86. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/templates/init/pocket.Dockerfile +0 -0
  87. {magic_pocket_cli-0.35.0 → magic_pocket_cli-0.37.0}/pocket_cli/templates/init/pocket_simple.toml +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: magic-pocket-cli
3
- Version: 0.35.0
3
+ Version: 0.37.0
4
4
  Summary: CLI and deploy tools for magic-pocket.
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: awscrt>=0.19.0
7
7
  Requires-Dist: click>=8.1.7
8
8
  Requires-Dist: deepdiff>=6.7.1
9
9
  Requires-Dist: jinja2>=3.1.3
10
- Requires-Dist: magic-pocket<0.36,>=0.35.0
10
+ Requires-Dist: magic-pocket<0.38,>=0.37.0
11
11
  Requires-Dist: pathspec>=1.0.4
12
12
  Requires-Dist: python-on-whales>=0.68.0
13
13
  Requires-Dist: pyyaml>=6.0.1
@@ -38,7 +38,7 @@ def backup():
38
38
  def cleanup(stage: str, yes: bool):
39
39
  """この stage のバックアップデータ (recovery point) を削除する。
40
40
 
41
- [backup] deletable = true の宣言が必要。pocket 管理 vault (pocket-backup)
41
+ [backup] deletable = true の宣言が必要。この stage の pocket 管理 vault
42
42
  にある、現存する対象 DB (dsql / managed rds) の recovery point だけを消す。
43
43
  削除済み cluster の分は ARN で引けないため対象外 (AWS Backup コンソールから
44
44
  削除する)。plan (スケジュール) には触らない。
@@ -0,0 +1,140 @@
1
+ """pocket cleanup-deprecated: 旧バージョンが残した account 単位リソースの掃除。
2
+
3
+ stage 単位の移行 (`pocket migrate` / deploy の migrate フェーズ) と違い、account
4
+ 内の全 stage・全 project が共有していたリソースを対象にする。どの stage からも
5
+ 参照されなくなったことを検査した上で、利用者の明示操作でだけ削除する
6
+ (deploy から自動では呼ばない)。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import time
12
+
13
+ import click
14
+
15
+ from pocket.settings import Settings
16
+ from pocket.utils import echo
17
+ from pocket_cli.cli import interaction
18
+ from pocket_cli.resources.aws.backup_common import (
19
+ LEGACY_BACKUP_ROLE_NAME,
20
+ LEGACY_BACKUP_VAULT_NAME,
21
+ )
22
+ from pocket_cli.resources.aws.backup_legacy import LegacyBackupCleanup
23
+
24
+ _VAULT_DELETE_ATTEMPTS = 12
25
+ _VAULT_DELETE_INTERVAL = 10
26
+
27
+
28
+ @click.command(name="cleanup-deprecated")
29
+ @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
30
+ @click.option(
31
+ "--delete-recovery-points",
32
+ is_flag=True,
33
+ default=False,
34
+ help="旧 vault に残るバックアップデータ (recovery point) も削除する"
35
+ " (account 内の他 project の分も含む)",
36
+ )
37
+ @click.option("--dry-run", is_flag=True, default=False, help="対象の表示だけ行う")
38
+ @click.option(
39
+ "--yes", "-y", is_flag=True, default=False, help="確認プロンプトをスキップ"
40
+ )
41
+ def cleanup_deprecated(
42
+ stage: str, delete_recovery_points: bool, dry_run: bool, yes: bool
43
+ ):
44
+ """旧バージョンが残した account 共有リソースを削除する。
45
+
46
+ 対象は 0.36 以前の backup vault (pocket-backup) とサービスロール
47
+ (forge-pocket-backup-role)。AWS account を共有している全 stage・全 project を
48
+ 0.37.0 以上で deploy し終えてから、account/region ごとに 1 回実行する
49
+ (--stage は region の解決に使う)。旧リソースをまだ参照している plan /
50
+ selection があれば中止する。IAM ロールは全 region 共通なので、複数 region で
51
+ pocket を使っている場合は全 region の更新後に実行すること。
52
+ """
53
+ interaction.set_assume_yes(yes)
54
+ region = Settings.from_toml(stage=stage).region
55
+ cleanup = LegacyBackupCleanup(region)
56
+ _abort_if_referenced(cleanup, region)
57
+
58
+ vault_exists = cleanup.vault_exists()
59
+ role_exists = cleanup.role_exists()
60
+ if not vault_exists and not role_exists:
61
+ echo.success("削除対象の旧リソースはありません (region: %s)。" % region)
62
+ return
63
+
64
+ points = cleanup.recovery_points() if vault_exists else []
65
+ delete_vault = vault_exists and (not points or delete_recovery_points)
66
+ if vault_exists:
67
+ echo.info(
68
+ "backup vault: %s (recovery point %d 件)"
69
+ % (LEGACY_BACKUP_VAULT_NAME, len(points))
70
+ )
71
+ if role_exists:
72
+ echo.info("IAM role: %s" % LEGACY_BACKUP_ROLE_NAME)
73
+ if vault_exists and not delete_vault:
74
+ _explain_kept_vault(cleanup, points)
75
+ if dry_run or not (delete_vault or role_exists):
76
+ return
77
+
78
+ _confirm(points if delete_vault else [])
79
+ if role_exists and cleanup.delete_role():
80
+ echo.log("Deleted IAM role: %s" % LEGACY_BACKUP_ROLE_NAME)
81
+ if delete_vault and not _delete_vault(cleanup, points):
82
+ raise click.ClickException(
83
+ "recovery point の削除がまだ反映されておらず、vault を削除できません"
84
+ "でした。少し待って再実行してください。"
85
+ )
86
+ echo.success("旧リソースの掃除が完了しました。")
87
+
88
+
89
+ def _abort_if_referenced(cleanup: LegacyBackupCleanup, region: str) -> None:
90
+ references = cleanup.references()
91
+ if not references:
92
+ return
93
+ for ref in references:
94
+ echo.warning(" - %s" % ref)
95
+ raise click.ClickException(
96
+ "旧リソースをまだ参照している設定があります (region: %s)。"
97
+ "該当する stage を 0.37.0 以上の pocket で deploy してから"
98
+ "再実行してください。" % region
99
+ )
100
+
101
+
102
+ def _confirm(points_to_delete: list[dict]) -> None:
103
+ if points_to_delete:
104
+ text = (
105
+ "旧 vault の recovery point %d 件を削除します。account 内の他 project の"
106
+ "バックアップデータも含まれ、復元できなくなります。続行しますか?"
107
+ % len(points_to_delete)
108
+ )
109
+ else:
110
+ text = "上記の旧リソースを削除しますか?"
111
+ interaction.confirm(text, default=False, abort=True)
112
+
113
+
114
+ def _delete_vault(cleanup: LegacyBackupCleanup, points: list[dict]) -> bool:
115
+ if points:
116
+ cleanup.delete_recovery_points(points)
117
+ echo.log("Deleted %d recovery points" % len(points))
118
+ # recovery point の削除は非同期で、直後の DeleteBackupVault は「空でない」と
119
+ # 拒否される (実測)。反映を少し待つ
120
+ for attempt in range(_VAULT_DELETE_ATTEMPTS):
121
+ if cleanup.delete_vault():
122
+ echo.log("Deleted backup vault: %s" % LEGACY_BACKUP_VAULT_NAME)
123
+ return True
124
+ if attempt + 1 < _VAULT_DELETE_ATTEMPTS:
125
+ time.sleep(_VAULT_DELETE_INTERVAL)
126
+ return False
127
+
128
+
129
+ def _explain_kept_vault(cleanup: LegacyBackupCleanup, points: list[dict]) -> None:
130
+ expiry = cleanup.last_expiry(points)
131
+ when = (
132
+ "%s 以降" % expiry.strftime("%Y-%m-%d")
133
+ if expiry
134
+ else "無期限保持のものがあるため自動では空になりません"
135
+ )
136
+ echo.warning(
137
+ "recovery point が残っているため vault は削除しません。保持期限で失効して"
138
+ "空になった後 (%s) に再実行するか、--delete-recovery-points で"
139
+ "データごと削除してください。" % when
140
+ )
@@ -6,7 +6,6 @@ from botocore.exceptions import ClientError
6
6
  from pocket.context import Context
7
7
  from pocket.utils import echo
8
8
  from pocket_cli.cli.resource_helper import require_configured
9
- from pocket_cli.resources.aws.backup_common import BACKUP_ROLE_NAME, BACKUP_VAULT_NAME
10
9
  from pocket_cli.resources.dsql import (
11
10
  BACKUP_TERMINAL_STATES,
12
11
  DEFAULT_ON_DEMAND_RETENTION_DAYS,
@@ -77,13 +76,13 @@ def endpoint(stage, format_):
77
76
  "--vault",
78
77
  default=None,
79
78
  help="バックアップ先の AWS Backup vault 名"
80
- " (省略時: pocket 管理の '%s' を自動作成して使用)" % BACKUP_VAULT_NAME,
79
+ " (省略時: pocket 管理の '{stage}-{project}-...-backup' を自動作成して使用)",
81
80
  )
82
81
  @click.option(
83
82
  "--iam-role-arn",
84
83
  default=None,
85
84
  help="AWS Backup が assume するロール ARN"
86
- " (省略時: pocket 管理の '%s' を自動作成して使用)" % BACKUP_ROLE_NAME,
85
+ " (省略時: pocket 管理の '{stage}-{project}-...-backup-role' を自動作成して使用)",
87
86
  )
88
87
  @click.option(
89
88
  "--retention-days",
@@ -10,7 +10,8 @@ from botocore.config import Config
10
10
  from pocket.inbound import SETUP_NOTIFICATION, receipt_id, validate_receipt_id
11
11
  from pocket.inbound_context import InboundContext
12
12
  from pocket.settings import Settings, parse_handler_ref
13
- from pocket_cli.resources.inbound import Inbound
13
+ from pocket_cli.cli import interaction
14
+ from pocket_cli.resources.inbound import Inbound, no_active_rule_set_guide
14
15
 
15
16
 
16
17
  @click.group()
@@ -27,48 +28,58 @@ def inbound(ctx, stage, name):
27
28
 
28
29
 
29
30
  @inbound.command()
31
+ @click.option(
32
+ "--format",
33
+ "output_format",
34
+ type=click.Choice(["text", "json"]),
35
+ default="text",
36
+ help="DNSレコードの出力形式",
37
+ )
30
38
  @click.pass_obj
31
- def init(resource):
32
- """共有rule setの初期化とドメイン検証の申請(初回のみ)。"""
33
- ses = boto3.client("ses", region_name=resource.context.region)
39
+ def init(resource, output_format):
40
+ """ドメイン検証を申請し、登録すべきDNSレコードを出力する(初回のみ)。
41
+
42
+ receipt rule setはaccount/regionで1つしかactiveにできない共有物のため、
43
+ project単位の道具であるpocketは作成も有効化もしない。
44
+ """
45
+ region = resource.context.region
46
+ ses = boto3.client("ses", region_name=region)
34
47
  active = ses.describe_active_receipt_rule_set().get("Metadata", {}).get("Name")
35
48
  if not active:
36
- name = "pocket-inbound"
37
- names = [
38
- item["Name"]
39
- for page in ses.get_paginator("list_receipt_rule_sets").paginate()
40
- for item in page["RuleSets"]
41
- ]
42
- if name in names:
43
- raise click.ClickException(
44
- "pocket-inboundが既にあります。内容を確認し管理者が有効化してください"
45
- )
46
- click.confirm(
47
- "このaccount/regionで初期化を同時実行していないことを確認してください。"
48
- "共有rule set pocket-inboundを作成・有効化しますか?",
49
- abort=True,
50
- )
51
- if ses.describe_active_receipt_rule_set().get("Metadata"):
52
- raise click.ClickException("active setが変わりました。再実行してください")
53
- ses.create_receipt_rule_set(RuleSetName=name)
54
- if ses.describe_active_receipt_rule_set().get("Metadata"):
55
- raise click.ClickException(
56
- "初期化中にactive setが変わりました。有効化を中止します"
57
- )
58
- ses.set_active_receipt_rule_set(RuleSetName=name)
59
- active = ses.describe_active_receipt_rule_set().get("Metadata", {}).get("Name")
60
- if active != name:
61
- raise click.ClickException("有効化後のactive setが一致しません")
49
+ raise click.ClickException(no_active_rule_set_guide(region))
62
50
  domain = resource.context.config.domain
63
51
  result = ses.get_identity_verification_attributes(Identities=[domain])
64
52
  identity = result.get("VerificationAttributes", {}).get(domain, {})
65
53
  token = identity.get("VerificationToken")
66
54
  if not token and identity.get("VerificationStatus") != "Success":
67
55
  token = ses.verify_domain_identity(Domain=domain)["VerificationToken"]
68
- click.echo(f"active rule set: {active}")
56
+ records = []
69
57
  if token:
70
- click.echo(f"TXT _amazonses.{domain} {token}")
71
- click.echo(f"MX {domain} 10 inbound-smtp.{resource.context.region}.amazonaws.com")
58
+ records.append({"type": "TXT", "name": f"_amazonses.{domain}", "value": token})
59
+ records.append(
60
+ {
61
+ "type": "MX",
62
+ "name": domain,
63
+ "priority": 10,
64
+ "value": f"inbound-smtp.{region}.amazonaws.com",
65
+ }
66
+ )
67
+ if output_format == "json":
68
+ payload = {
69
+ "active_rule_set": active,
70
+ "domain": domain,
71
+ "region": region,
72
+ "verification_status": identity.get("VerificationStatus") or "Pending",
73
+ "records": records,
74
+ }
75
+ click.echo(json.dumps(payload, ensure_ascii=False, indent=2))
76
+ return
77
+ click.echo(f"active rule set: {active}")
78
+ for record in records:
79
+ if record["type"] == "TXT":
80
+ click.echo(f"TXT {record['name']} {record['value']}")
81
+ else:
82
+ click.echo(f"MX {record['name']} {record['priority']} {record['value']}")
72
83
  click.echo("SES受信対応リージョンとMXの既存配送先を確認してDNSを登録してください。")
73
84
 
74
85
 
@@ -94,11 +105,17 @@ def show_yaml(resource):
94
105
 
95
106
 
96
107
  @inbound.command()
108
+ @click.option(
109
+ "--yes", "-y", is_flag=True, default=False, help="確認プロンプトをスキップ"
110
+ )
97
111
  @click.pass_obj
98
- def destroy(resource):
112
+ def destroy(resource, yes):
99
113
  """無効化・配送猶予・滞留解消後に自分のルールとスタックを削除する。"""
100
- click.confirm(
101
- "受信停止後36時間以上経過し、原本の照合・必要な退避を終えましたか?", abort=True
114
+ interaction.set_assume_yes(yes)
115
+ interaction.confirm(
116
+ "受信停止後36時間以上経過し、原本の照合・必要な退避を終えましたか?",
117
+ default=False,
118
+ abort=True,
102
119
  )
103
120
  resource.delete()
104
121
 
@@ -17,6 +17,7 @@ except ModuleNotFoundError as e:
17
17
  from pocket_cli.cli import (
18
18
  backup_cli,
19
19
  build_cli,
20
+ cleanup_deprecated_cli,
20
21
  cloudfront_cli,
21
22
  cloudfront_keys_cli,
22
23
  cloudfront_waf_cli,
@@ -134,6 +135,7 @@ main.add_command(migrate_cli.migrate)
134
135
  main.add_command(permissions_cli.permissions)
135
136
  main.add_command(waf_cli.waf)
136
137
  main.add_command(backup_cli.backup)
138
+ main.add_command(cleanup_deprecated_cli.cleanup_deprecated)
137
139
 
138
140
 
139
141
  @main.group()
@@ -18,9 +18,15 @@ from pocket.utils import echo
18
18
  # pocket 管理のバックアップ前提リソース。AWS Backup の Default vault /
19
19
  # AWSBackupDefaultServiceRole は console 初回操作で作られるもので、API しか
20
20
  # 使わないアカウントには存在しないため、pocket 側で冪等に ensure する。
21
- # ロール名の forge- prefix は codebuild ロールの命名に合わせている
22
- BACKUP_VAULT_NAME = "pocket-backup"
23
- BACKUP_ROLE_NAME = "forge-pocket-backup-role"
21
+ # 名前は stage 単位 ({resource_prefix}backup / {resource_prefix}backup-role) で、
22
+ # context が導出する。
23
+ #
24
+ # 0.36 以前は account 共有の固定名だった。vault は改名も recovery point の移動も
25
+ # できず、account 内の他 stage / project と共有なので pocket からは消せない。
26
+ # 旧 vault の recovery point は取得時の lifecycle で失効するまで残り、pocket の
27
+ # list / cleanup の対象外になる (docs の移行手順を参照)
28
+ LEGACY_BACKUP_VAULT_NAME = "pocket-backup"
29
+ LEGACY_BACKUP_ROLE_NAME = "forge-pocket-backup-role"
24
30
  _BACKUP_ROLE_POLICIES = (
25
31
  "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup",
26
32
  "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForRestores",
@@ -45,18 +51,20 @@ def ensure_backup_vault(backup_client, name: str) -> None:
45
51
  echo.log("Created backup vault: %s" % name)
46
52
 
47
53
 
48
- def ensure_backup_role(iam_client, permissions_boundary: str | None) -> str:
54
+ def ensure_backup_role(
55
+ iam_client, role_name: str, permissions_boundary: str | None
56
+ ) -> str:
49
57
  """AWS Backup サービスロールを冪等に ensure して ARN を返す。
50
58
 
51
59
  codebuild の _ensure_role と同型。boundary 必須のアカウントでも作成
52
60
  できるよう permissions_boundary を付与する。
53
61
  """
54
62
  try:
55
- return iam_client.get_role(RoleName=BACKUP_ROLE_NAME)["Role"]["Arn"]
63
+ return iam_client.get_role(RoleName=role_name)["Role"]["Arn"]
56
64
  except ClientError as e:
57
65
  if e.response["Error"]["Code"] != "NoSuchEntity":
58
66
  raise
59
- echo.log("Creating AWS Backup service role: %s" % BACKUP_ROLE_NAME)
67
+ echo.log("Creating AWS Backup service role: %s" % role_name)
60
68
  assume_role_policy = json.dumps(
61
69
  {
62
70
  "Version": "2012-10-17",
@@ -70,15 +78,36 @@ def ensure_backup_role(iam_client, permissions_boundary: str | None) -> str:
70
78
  }
71
79
  )
72
80
  create_kwargs: dict = {
73
- "RoleName": BACKUP_ROLE_NAME,
81
+ "RoleName": role_name,
74
82
  "AssumeRolePolicyDocument": assume_role_policy,
75
83
  }
76
84
  if permissions_boundary:
77
85
  create_kwargs["PermissionsBoundary"] = permissions_boundary
78
86
  role_arn: str = iam_client.create_role(**create_kwargs)["Role"]["Arn"]
79
87
  for policy_arn in _BACKUP_ROLE_POLICIES:
80
- iam_client.attach_role_policy(RoleName=BACKUP_ROLE_NAME, PolicyArn=policy_arn)
88
+ iam_client.attach_role_policy(RoleName=role_name, PolicyArn=policy_arn)
81
89
  # IAM ロールの伝播待ち (作成直後の StartBackupJob 失敗を避ける)
82
90
  echo.log("Waiting for IAM role propagation...")
83
91
  time.sleep(10)
84
92
  return role_arn
93
+
94
+
95
+ def delete_backup_role(iam_client, role_name: str) -> bool:
96
+ """stage 所有の AWS Backup サービスロールを削除する (無ければ False)。
97
+
98
+ ロールは「設定」側の付属物なので destroy で消す (vault と recovery point は
99
+ データなので残す)。復元等で再び必要になれば ensure_backup_role が作り直す。
100
+ """
101
+ try:
102
+ for policy_arn in _BACKUP_ROLE_POLICIES:
103
+ try:
104
+ iam_client.detach_role_policy(RoleName=role_name, PolicyArn=policy_arn)
105
+ except ClientError as e:
106
+ if e.response["Error"]["Code"] != "NoSuchEntity":
107
+ raise
108
+ iam_client.delete_role(RoleName=role_name)
109
+ except ClientError as e:
110
+ if e.response["Error"]["Code"] == "NoSuchEntity":
111
+ return False
112
+ raise
113
+ return True
@@ -0,0 +1,124 @@
1
+ """0.36 以前の account 共有 backup リソース (vault / サービスロール) の掃除。
2
+
3
+ 0.37.0 で vault / ロールを stage 単位の名前に改めたため、旧名のリソースは
4
+ どのコードからも参照されなくなる。ただし account 内の全 stage・全 project が
5
+ 共有していたので、deploy や destroy (= stage 単位の操作) からは消せない。
6
+ 「全 stage を更新し終えた」ことを利用者が確認した上で明示的に呼ぶ
7
+ `pocket cleanup-deprecated` がここを使う。
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from datetime import datetime
13
+
14
+ import boto3
15
+ from botocore.exceptions import ClientError
16
+
17
+ from pocket_cli.resources.aws.backup_common import (
18
+ LEGACY_BACKUP_ROLE_NAME,
19
+ LEGACY_BACKUP_VAULT_NAME,
20
+ delete_backup_role,
21
+ )
22
+
23
+
24
+ class LegacyBackupCleanup:
25
+ def __init__(self, region: str) -> None:
26
+ self.region = region
27
+ self._backup = boto3.client("backup", region_name=region)
28
+ self._iam = boto3.client("iam", region_name=region)
29
+
30
+ def references(self) -> list[str]:
31
+ """旧 vault / 旧ロールをまだ参照している plan・selection の説明一覧。
32
+
33
+ 1 件でもあれば、その stage は 0.36 以前のまま (未 deploy) なので、
34
+ 消すと定期バックアップが失敗し始める。掃除の前提条件として検査する。
35
+ """
36
+ found: list[str] = []
37
+ paginator = self._backup.get_paginator("list_backup_plans")
38
+ for page in paginator.paginate():
39
+ for item in page["BackupPlansList"]:
40
+ plan_id = item["BackupPlanId"]
41
+ plan_name = item["BackupPlanName"]
42
+ plan = self._backup.get_backup_plan(BackupPlanId=plan_id)["BackupPlan"]
43
+ if any(
44
+ rule.get("TargetBackupVaultName") == LEGACY_BACKUP_VAULT_NAME
45
+ for rule in plan.get("Rules", [])
46
+ ):
47
+ found.append(
48
+ "backup plan %s (保存先が %s)"
49
+ % (plan_name, LEGACY_BACKUP_VAULT_NAME)
50
+ )
51
+ found.extend(self._selection_references(plan_id, plan_name))
52
+ return found
53
+
54
+ def _selection_references(self, plan_id: str, plan_name: str) -> list[str]:
55
+ found: list[str] = []
56
+ suffix = ":role/%s" % LEGACY_BACKUP_ROLE_NAME
57
+ selections = self._backup.list_backup_selections(BackupPlanId=plan_id)
58
+ for item in selections["BackupSelectionsList"]:
59
+ detail = self._backup.get_backup_selection(
60
+ BackupPlanId=plan_id, SelectionId=item["SelectionId"]
61
+ )
62
+ if detail["BackupSelection"].get("IamRoleArn", "").endswith(suffix):
63
+ found.append(
64
+ "backup selection %s / %s (ロールが %s)"
65
+ % (plan_name, item["SelectionName"], LEGACY_BACKUP_ROLE_NAME)
66
+ )
67
+ return found
68
+
69
+ def vault_exists(self) -> bool:
70
+ """旧 vault があるか。
71
+
72
+ DescribeBackupVault は、vault が 1 つも無い account では存在しない vault に
73
+ AccessDeniedException を返し「未作成」を判定できないため、一覧で調べる。
74
+ """
75
+ paginator = self._backup.get_paginator("list_backup_vaults")
76
+ for page in paginator.paginate():
77
+ for vault in page["BackupVaultList"]:
78
+ if vault["BackupVaultName"] == LEGACY_BACKUP_VAULT_NAME:
79
+ return True
80
+ return False
81
+
82
+ def recovery_points(self) -> list[dict]:
83
+ points: list[dict] = []
84
+ paginator = self._backup.get_paginator("list_recovery_points_by_backup_vault")
85
+ for page in paginator.paginate(BackupVaultName=LEGACY_BACKUP_VAULT_NAME):
86
+ points.extend(page["RecoveryPoints"])
87
+ return points
88
+
89
+ @staticmethod
90
+ def last_expiry(points: list[dict]) -> datetime | None:
91
+ """全 recovery point が失効し終える日時 (無期限のものがあれば None)。"""
92
+ expiries = [p.get("CalculatedLifecycle", {}).get("DeleteAt") for p in points]
93
+ if not expiries or any(e is None for e in expiries):
94
+ return None
95
+ return max(expiries)
96
+
97
+ def delete_recovery_points(self, points: list[dict]) -> None:
98
+ for point in points:
99
+ self._backup.delete_recovery_point(
100
+ BackupVaultName=LEGACY_BACKUP_VAULT_NAME,
101
+ RecoveryPointArn=point["RecoveryPointArn"],
102
+ )
103
+
104
+ def delete_vault(self) -> bool:
105
+ """旧 vault を削除する。recovery point の削除が未反映で空でなければ False。"""
106
+ try:
107
+ self._backup.delete_backup_vault(BackupVaultName=LEGACY_BACKUP_VAULT_NAME)
108
+ except ClientError as e:
109
+ if e.response["Error"]["Code"] == "InvalidRequestException":
110
+ return False
111
+ raise
112
+ return True
113
+
114
+ def role_exists(self) -> bool:
115
+ try:
116
+ self._iam.get_role(RoleName=LEGACY_BACKUP_ROLE_NAME)
117
+ except ClientError as e:
118
+ if e.response["Error"]["Code"] == "NoSuchEntity":
119
+ return False
120
+ raise
121
+ return True
122
+
123
+ def delete_role(self) -> bool:
124
+ return delete_backup_role(self._iam, LEGACY_BACKUP_ROLE_NAME)
@@ -65,7 +65,12 @@ class CodeBuildBuilder:
65
65
  self.sts = boto3.client("sts", region_name=region)
66
66
 
67
67
  self._project_name = f"{resource_prefix}codebuild"
68
- self._role_name = f"forge-{resource_prefix}codebuild-role"
68
+ self._role_name = f"{resource_prefix}codebuild-role"
69
+ # 0.36 以前のロール名。forge- は開発環境由来の prefix で pocket の命名規約
70
+ # ({stage}-{project}-...) と無関係だったため改名した。ロールは stage の
71
+ # 所有物なので、新ロールへ付け替えた後に pocket が削除する (改名互換。
72
+ # いずれ削除予定)
73
+ self._legacy_role_name = f"forge-{resource_prefix}codebuild-role"
69
74
  self._source_key = f"codebuild/{self._project_name}/source.zip"
70
75
 
71
76
  def build_and_push(
@@ -85,6 +90,8 @@ class CodeBuildBuilder:
85
90
 
86
91
  role_arn = self._ensure_role(account_id)
87
92
  self._ensure_project(platform, role_arn)
93
+ # project の serviceRole を新ロールへ付け替えた後なので、旧名ロールは孤児
94
+ self._delete_role(self._legacy_role_name)
88
95
  self._upload_source(dockerfile_path)
89
96
 
90
97
  build_id = self._start_build(
@@ -101,7 +108,8 @@ class CodeBuildBuilder:
101
108
  def delete(self) -> None:
102
109
  """CodeBuildプロジェクトとIAMロールを削除"""
103
110
  self._delete_project()
104
- self._delete_role()
111
+ self._delete_role(self._role_name)
112
+ self._delete_role(self._legacy_role_name)
105
113
 
106
114
  # --- IAM ロール ---
107
115
 
@@ -199,16 +207,14 @@ class CodeBuildBuilder:
199
207
  time.sleep(10)
200
208
  return role_arn
201
209
 
202
- def _delete_role(self) -> None:
210
+ def _delete_role(self, role_name: str) -> None:
203
211
  try:
204
212
  # インラインポリシー削除
205
- policies = self.iam.list_role_policies(RoleName=self._role_name)
213
+ policies = self.iam.list_role_policies(RoleName=role_name)
206
214
  for policy_name in policies["PolicyNames"]:
207
- self.iam.delete_role_policy(
208
- RoleName=self._role_name, PolicyName=policy_name
209
- )
210
- self.iam.delete_role(RoleName=self._role_name)
211
- print(" IAMロール削除: %s" % self._role_name)
215
+ self.iam.delete_role_policy(RoleName=role_name, PolicyName=policy_name)
216
+ self.iam.delete_role(RoleName=role_name)
217
+ print(" IAMロール削除: %s" % role_name)
212
218
  except ClientError as e:
213
219
  if e.response["Error"]["Code"] == "NoSuchEntity":
214
220
  return
@@ -355,8 +361,15 @@ class CodeBuildBuilder:
355
361
  return len(resp.get("projects", [])) > 0
356
362
 
357
363
  def role_exists(self) -> bool:
364
+ """現行名・旧名いずれかのロールがあるか (destroy の対象判定用)。"""
365
+ return any(
366
+ self._role_exists(name)
367
+ for name in (self._role_name, self._legacy_role_name)
368
+ )
369
+
370
+ def _role_exists(self, role_name: str) -> bool:
358
371
  try:
359
- self.iam.get_role(RoleName=self._role_name)
372
+ self.iam.get_role(RoleName=role_name)
360
373
  return True
361
374
  except ClientError as e:
362
375
  if e.response["Error"]["Code"] == "NoSuchEntity":
@@ -8,7 +8,7 @@ from botocore.exceptions import ClientError
8
8
  from pocket.resources.base import ResourceStatus
9
9
  from pocket.utils import echo
10
10
  from pocket_cli.resources.aws.backup_common import (
11
- BACKUP_VAULT_NAME,
11
+ delete_backup_role,
12
12
  ensure_backup_role,
13
13
  ensure_backup_vault,
14
14
  )
@@ -151,8 +151,10 @@ class Backup:
151
151
  ]
152
152
  if not targets:
153
153
  return
154
- ensure_backup_vault(self._backup, BACKUP_VAULT_NAME)
155
- role_arn = ensure_backup_role(self._iam, self.context.permissions_boundary)
154
+ ensure_backup_vault(self._backup, self.context.vault_name)
155
+ role_arn = ensure_backup_role(
156
+ self._iam, self.context.role_name, self.context.permissions_boundary
157
+ )
156
158
  for plan, arn in targets:
157
159
  plan_id = self._ensure_plan(plan)
158
160
  self._ensure_selection(plan, plan_id, role_arn, [arn])
@@ -200,7 +202,7 @@ class Backup:
200
202
  rules.append(
201
203
  {
202
204
  "RuleName": rule.name,
203
- "TargetBackupVaultName": BACKUP_VAULT_NAME,
205
+ "TargetBackupVaultName": self.context.vault_name,
204
206
  "ScheduleExpression": rule.schedule_expression,
205
207
  "ScheduleExpressionTimezone": self.context.timezone,
206
208
  "Lifecycle": lifecycle,
@@ -251,8 +253,9 @@ class Backup:
251
253
  ) -> None:
252
254
  """対象 DB の ARN を指した selection を確保する。
253
255
 
254
- selection には更新 API が無いため、対象 ARN が変わっていたら
255
- (cluster 再作成など) 作り直す。
256
+ selection には更新 API が無いため、対象 ARN (cluster 再作成など) か
257
+ サービスロール (0.36 以前の account 共有ロールからの移行) が変わって
258
+ いたら作り直す。
256
259
  """
257
260
  selections = self._backup.list_backup_selections(BackupPlanId=plan_id)
258
261
  for item in selections["BackupSelectionsList"]:
@@ -261,7 +264,11 @@ class Backup:
261
264
  detail = self._backup.get_backup_selection(
262
265
  BackupPlanId=plan_id, SelectionId=item["SelectionId"]
263
266
  )
264
- if sorted(detail["BackupSelection"].get("Resources", [])) == arns:
267
+ current = detail["BackupSelection"]
268
+ if (
269
+ sorted(current.get("Resources", [])) == arns
270
+ and current.get("IamRoleArn") == role_arn
271
+ ):
265
272
  return
266
273
  self._backup.delete_backup_selection(
267
274
  BackupPlanId=plan_id, SelectionId=item["SelectionId"]
@@ -277,7 +284,7 @@ class Backup:
277
284
  )
278
285
 
279
286
  def delete(self) -> None:
280
- """backup plan / selection を削除する。
287
+ """backup plan / selection とサービスロールを削除する。
281
288
 
282
289
  recovery point と vault は消さない。バックアップ「設定」は stack の
283
290
  付属物だが、バックアップ「データ」は stack より長生きさせるべきで、
@@ -288,6 +295,10 @@ class Backup:
288
295
  持たない deploy role でも destroy 自体は完遂させたいので、権限エラーは
289
296
  警告に留める。
290
297
  """
298
+ self._delete_plans()
299
+ self._delete_role()
300
+
301
+ def _delete_plans(self) -> None:
291
302
  for plan_name in self.context.cleanup_plan_names:
292
303
  try:
293
304
  plan_id = self._find_plan_id(plan_name)
@@ -305,6 +316,26 @@ class Backup:
305
316
  self._delete_plan(plan_id)
306
317
  echo.log("Deleted backup plan: %s" % plan_name)
307
318
 
319
+ def _delete_role(self) -> None:
320
+ """stage 所有のサービスロールを削除する (selection を消した後に呼ぶ)。
321
+
322
+ 0.36 以前の account 共有ロール (forge-pocket-backup-role) は他 stage /
323
+ project が使っている可能性があるため触らない。
324
+ """
325
+ try:
326
+ if delete_backup_role(self._iam, self.context.role_name):
327
+ echo.log("Deleted backup service role: %s" % self.context.role_name)
328
+ except ClientError as e:
329
+ if e.response["Error"]["Code"] not in (
330
+ "AccessDenied",
331
+ "AccessDeniedException",
332
+ ):
333
+ raise
334
+ echo.warning(
335
+ "backup サービスロール (%s) を削除する権限がないため、スキップ"
336
+ "しました。不要なら手動で削除してください。" % self.context.role_name
337
+ )
338
+
308
339
  def _delete_plan(self, plan_id: str) -> None:
309
340
  """selection → plan の順に削除する (selection が残ると plan を消せない)。"""
310
341
  selections = self._backup.list_backup_selections(BackupPlanId=plan_id)
@@ -315,7 +346,7 @@ class Backup:
315
346
  self._backup.delete_backup_plan(BackupPlanId=plan_id)
316
347
 
317
348
  def list_recovery_points(self) -> list[dict] | None:
318
- """対象 DB の recovery point 一覧 (pocket-backup vault 内のもの)。
349
+ """対象 DB の recovery point 一覧 (pocket 管理 vault 内のもの)。
319
350
 
320
351
  現存する cluster の ARN でしか引けない (削除済み cluster の分は対象外 =
321
352
  console でしか消せない)。権限が無ければ None を返す (呼び出し側が案内を
@@ -334,8 +365,11 @@ class Backup:
334
365
  raise
335
366
  return None
336
367
  # pocket 管理の vault 分だけを扱う (--vault で利用者の vault に取った
337
- # オンデマンド分は利用者の管理物とみなし、pocket からは消さない)
338
- return [p for p in points if p.get("BackupVaultName") == BACKUP_VAULT_NAME]
368
+ # オンデマンド分は利用者の管理物とみなし、pocket からは消さない)。0.36 以前の
369
+ # account 共有 vault (pocket-backup) に残る分も同じ理由で対象外
370
+ return [
371
+ p for p in points if p.get("BackupVaultName") == self.context.vault_name
372
+ ]
339
373
 
340
374
  def delete_recovery_points(self, points: list[dict]) -> int:
341
375
  """recovery point を削除して件数を返す。
@@ -15,7 +15,6 @@ from pocket.secret_store import (
15
15
  )
16
16
  from pocket.utils import echo
17
17
  from pocket_cli.resources.aws.backup_common import (
18
- BACKUP_VAULT_NAME,
19
18
  ensure_backup_role,
20
19
  ensure_backup_vault,
21
20
  )
@@ -216,7 +215,7 @@ class Dsql:
216
215
  if not self.arn:
217
216
  raise ValueError("Cluster not found")
218
217
  if vault_name is None:
219
- vault_name = BACKUP_VAULT_NAME
218
+ vault_name = self.context.backup_vault_name
220
219
  self._ensure_backup_vault(vault_name)
221
220
  if iam_role_arn is None:
222
221
  iam_role_arn = self._ensure_backup_role()
@@ -261,7 +260,11 @@ class Dsql:
261
260
  ensure_backup_vault(self._backup, name)
262
261
 
263
262
  def _ensure_backup_role(self) -> str:
264
- return ensure_backup_role(self._iam, self.context.permissions_boundary)
263
+ return ensure_backup_role(
264
+ self._iam,
265
+ self.context.backup_role_name,
266
+ self.context.permissions_boundary,
267
+ )
265
268
 
266
269
  def latest_recovery_point(self) -> dict | None:
267
270
  """現用クラスターの最新 recovery point (完了済み) を返す。"""
@@ -14,16 +14,31 @@ from pocket_cli.resources.aws.stack_backed import StackBackedResource
14
14
  from pocket_cli.resources.inbound_template import build_template
15
15
 
16
16
 
17
+ def no_active_rule_set_guide(region: str) -> str:
18
+ """active setが無いaccount/region向けの案内。
19
+
20
+ receipt rule setはaccount/regionで1つしかactiveにできない共有物なので、
21
+ pocketは作成・有効化しない。名前はSESコンソールが自動作成する
22
+ default-rule-setに揃えて案内する(既にactiveなsetがあれば名前は問わない)。
23
+ """
24
+ return (
25
+ "active な receipt rule set がありません。"
26
+ "account/regionで1回だけ次を実行してください"
27
+ "(account単位の土台なので、pocketは作成・有効化しません):\n"
28
+ " aws ses create-receipt-rule-set"
29
+ f" --rule-set-name default-rule-set --region {region}\n"
30
+ " aws ses set-active-receipt-rule-set"
31
+ f" --rule-set-name default-rule-set --region {region}"
32
+ )
33
+
34
+
17
35
  def resolve_rules(
18
36
  active: dict, ctx: InboundContext, deployed: dict | None
19
37
  ) -> tuple[str, str | None]:
20
38
  """明示指定なしならactive setへ末尾追加。既存bindingの漂流は拒否する。"""
21
39
  name = active.get("Metadata", {}).get("Name")
22
40
  if not name:
23
- raise ValueError(
24
- "active receipt rule set がありません。"
25
- "pocket resource inbound init を実行してください"
26
- )
41
+ raise ValueError(no_active_rule_set_guide(ctx.region))
27
42
  if ctx.config.rule_set and ctx.config.rule_set != name:
28
43
  raise ValueError("明示したrule_setがactive setと一致しません")
29
44
  expected = (deployed or {}).get("RuleSet") or ctx.config.rule_set
@@ -1,12 +1,12 @@
1
1
  [project]
2
2
  name = "magic-pocket-cli"
3
- version = "0.35.0"
3
+ version = "0.37.0"
4
4
  description = "CLI and deploy tools for magic-pocket."
5
5
  requires-python = ">=3.10"
6
6
  dependencies = [
7
7
  # lib は同 minor に束縛する。0.x では minor が breaking の単位のため、
8
8
  # 上限なしだと cli を pin した消費者にも lib の破壊的変更が届いてしまう
9
- "magic-pocket>=0.35.0,<0.36",
9
+ "magic-pocket>=0.37.0,<0.38",
10
10
  "click>=8.1.7",
11
11
  "jinja2>=3.1.3",
12
12
  "python-on-whales>=0.68.0",