magic-pocket-cli 0.26.0__tar.gz → 0.28.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.
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/PKG-INFO +2 -2
- magic_pocket_cli-0.28.0/pocket_cli/cli/backup_cli.py +68 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/deploy_cli.py +5 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/destroy_cli.py +50 -2
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/dsql_cli.py +7 -4
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/main_cli.py +2 -0
- magic_pocket_cli-0.28.0/pocket_cli/resources/aws/backup_common.py +84 -0
- magic_pocket_cli-0.28.0/pocket_cli/resources/backup.py +320 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/dsql.py +48 -214
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/rds.py +13 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pyproject.toml +2 -2
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/.gitignore +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/__init__.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/__init__.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/aws_auth.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/awscontainer_cli.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/cloudfront_cli.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/cloudfront_keys_cli.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/cloudfront_waf_cli.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/image_cli.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/interaction.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/migrate_cli.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/neon_cli.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/permissions_cli.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/rds_cli.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/removed_flags.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/resource_helper.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/runtime_config_cli.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/s3_cli.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/status_cli.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/store_url_helper.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/tidb_cli.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/upstash_cli.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/url_helper.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/vpc_cli.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/cli/waf_cli.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/django_cli.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/mediator.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/__init__.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/__init__.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/builders/__init__.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/builders/codebuild.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/builders/context_check.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/builders/depot.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/builders/docker.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/builders/dockerignore.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/cloudformation.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/ecr.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/efs.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/lambdahandler.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/poll.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/s3_utils.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/stack_backed.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/state.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/awscontainer.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/cloudfront.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/cloudfront_acm.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/cloudfront_keys.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/cloudfront_waf.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/neon.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/s3.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/tidb.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/upstash.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/vpc.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/templates/cloudformation/awscontainer.yaml +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/templates/cloudformation/cf_function_api_host.js +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/templates/cloudformation/cf_function_basic_auth_prelude.js +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/templates/cloudformation/cf_function_host_redirect_prelude.js +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/templates/cloudformation/cf_function_spa_auth.js +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/templates/cloudformation/cf_function_spa_fallback.js +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/templates/cloudformation/cloudfront.yaml +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/templates/cloudformation/cloudfront_acm.yaml +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/templates/cloudformation/cloudfront_keys.yaml +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/templates/cloudformation/cloudfront_waf.yaml +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/templates/cloudformation/vpc.yaml +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/templates/init/django-dotenv.env +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/templates/init/django-settings.py +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/templates/init/pocket.Dockerfile +0 -0
- {magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.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.
|
|
3
|
+
Version: 0.28.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.
|
|
10
|
+
Requires-Dist: magic-pocket<0.29,>=0.28.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
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from pocket.context import Context
|
|
4
|
+
from pocket.utils import echo
|
|
5
|
+
from pocket_cli.resources.backup import Backup
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _get_backup_resource(stage: str) -> Backup:
|
|
9
|
+
context = Context.from_toml(stage=stage)
|
|
10
|
+
if context.backup is None:
|
|
11
|
+
raise click.ClickException(
|
|
12
|
+
"この stage には AWS Backup の対象 DB (dsql / managed rds) がありません。"
|
|
13
|
+
)
|
|
14
|
+
if not context.backup.declared:
|
|
15
|
+
raise click.ClickException(
|
|
16
|
+
"[backup] が宣言されていません。バックアップデータの削除には"
|
|
17
|
+
" [backup] の deletable = true 宣言が必要です。"
|
|
18
|
+
)
|
|
19
|
+
if not context.backup.deletable:
|
|
20
|
+
raise click.ClickException(
|
|
21
|
+
"バックアップデータ (recovery point) の削除には [backup] に"
|
|
22
|
+
" deletable = true の宣言が必要です。誤操作でデータを失わないための"
|
|
23
|
+
" ガードなので、削除する時だけ宣言することを推奨します。"
|
|
24
|
+
)
|
|
25
|
+
return Backup(context.backup, dsql_context=context.dsql, rds_context=context.rds)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@click.group()
|
|
29
|
+
def backup():
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@backup.command()
|
|
34
|
+
@click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
|
|
35
|
+
@click.option(
|
|
36
|
+
"--yes", "-y", is_flag=True, default=False, help="確認プロンプトをスキップ"
|
|
37
|
+
)
|
|
38
|
+
def cleanup(stage: str, yes: bool):
|
|
39
|
+
"""この stage のバックアップデータ (recovery point) を削除する。
|
|
40
|
+
|
|
41
|
+
[backup] deletable = true の宣言が必要。pocket 管理 vault (pocket-backup)
|
|
42
|
+
にある、現存する対象 DB (dsql / managed rds) の recovery point だけを消す。
|
|
43
|
+
削除済み cluster の分は ARN で引けないため対象外 (AWS Backup コンソールから
|
|
44
|
+
削除する)。plan (スケジュール) には触らない。
|
|
45
|
+
"""
|
|
46
|
+
r = _get_backup_resource(stage)
|
|
47
|
+
points = r.list_recovery_points()
|
|
48
|
+
if points is None:
|
|
49
|
+
raise click.ClickException(
|
|
50
|
+
"recovery point を列挙する権限がありません (backup:*)。"
|
|
51
|
+
)
|
|
52
|
+
if not points:
|
|
53
|
+
echo.info("削除対象の recovery point はありません。")
|
|
54
|
+
return
|
|
55
|
+
echo.warning("以下の recovery point を削除します:")
|
|
56
|
+
for point in points:
|
|
57
|
+
created = point.get("CreationDate")
|
|
58
|
+
echo.info(
|
|
59
|
+
" - %s (%s)"
|
|
60
|
+
% (point["RecoveryPointArn"], created.isoformat() if created else "-")
|
|
61
|
+
)
|
|
62
|
+
echo.danger("この操作は取り消せません!")
|
|
63
|
+
if not yes:
|
|
64
|
+
click.confirm(
|
|
65
|
+
"%d 件の recovery point を削除しますか?" % len(points), abort=True
|
|
66
|
+
)
|
|
67
|
+
deleted = r.delete_recovery_points(points)
|
|
68
|
+
echo.success("%d 件の recovery point を削除しました。" % deleted)
|
|
@@ -10,6 +10,7 @@ from pocket_cli.cli.removed_flags import removed_skip_check_existing
|
|
|
10
10
|
from pocket_cli.mediator import Mediator
|
|
11
11
|
from pocket_cli.resources.aws.state import StateStore, create_state_store
|
|
12
12
|
from pocket_cli.resources.awscontainer import AwsContainer
|
|
13
|
+
from pocket_cli.resources.backup import Backup
|
|
13
14
|
from pocket_cli.resources.cloudfront import CloudFront
|
|
14
15
|
from pocket_cli.resources.cloudfront_acm import CloudFrontAcm
|
|
15
16
|
from pocket_cli.resources.cloudfront_keys import CloudFrontKeys
|
|
@@ -32,6 +33,10 @@ def _append_infra_resources(resources, context: Context, state_bucket: str):
|
|
|
32
33
|
resources.append(Dsql(context.dsql))
|
|
33
34
|
if context.rds:
|
|
34
35
|
resources.append(Rds(context.rds))
|
|
36
|
+
if context.backup and context.backup.declared:
|
|
37
|
+
resources.append(
|
|
38
|
+
Backup(context.backup, dsql_context=context.dsql, rds_context=context.rds)
|
|
39
|
+
)
|
|
35
40
|
for _name, cf_ctx in context.cloudfront.items():
|
|
36
41
|
if cf_ctx.signing_key:
|
|
37
42
|
resources.append(CloudFrontKeys(cf_ctx))
|
|
@@ -9,6 +9,7 @@ from pocket_cli.resources.aws.state import (
|
|
|
9
9
|
create_state_store,
|
|
10
10
|
)
|
|
11
11
|
from pocket_cli.resources.awscontainer import AwsContainer
|
|
12
|
+
from pocket_cli.resources.backup import Backup
|
|
12
13
|
from pocket_cli.resources.cloudfront import CloudFront
|
|
13
14
|
from pocket_cli.resources.cloudfront_acm import CloudFrontAcm
|
|
14
15
|
from pocket_cli.resources.cloudfront_keys import CloudFrontKeys
|
|
@@ -88,6 +89,11 @@ def _warn_command_provisioned(label: str, hint: str):
|
|
|
88
89
|
|
|
89
90
|
def _collect_aws_database_targets(context: Context) -> list[str]:
|
|
90
91
|
targets: list[str] = []
|
|
92
|
+
if context.backup:
|
|
93
|
+
for plan_name in Backup(context.backup).existing_plan_names():
|
|
94
|
+
targets.append(
|
|
95
|
+
"AWS Backup plan: %s (recovery point と vault は残ります)" % plan_name
|
|
96
|
+
)
|
|
91
97
|
if context.dsql:
|
|
92
98
|
dsql = Dsql(context.dsql)
|
|
93
99
|
if dsql.status != "NOEXIST":
|
|
@@ -223,6 +229,40 @@ def _destroy_awscontainer(context: Context, with_secrets: bool):
|
|
|
223
229
|
echo.success("Pocket managed secrets were deleted.")
|
|
224
230
|
|
|
225
231
|
|
|
232
|
+
def _destroy_backup(context: Context, yes: bool):
|
|
233
|
+
"""backup plan / selection を削除し、recovery point の扱いを案内する。
|
|
234
|
+
|
|
235
|
+
plan は [backup] 宣言の有無に関わらず削除する (宣言を外した後の destroy でも
|
|
236
|
+
残さない)。selection が削除済み cluster の ARN を指したまま残ると以後の
|
|
237
|
+
backup job が失敗し続けるため、cluster 削除より先に消す。
|
|
238
|
+
|
|
239
|
+
recovery point (バックアップデータ) は既定では消さない。deletable = true の
|
|
240
|
+
宣言下で、対話の [y/N] に明示的に yes と答えた場合のみ削除する (--yes での
|
|
241
|
+
一括承認では削除しない。データ削除だけは暗黙に通さない)。cluster 削除後は
|
|
242
|
+
ARN で引けなくなるため、この確認も cluster 削除より先に行う。
|
|
243
|
+
"""
|
|
244
|
+
if not context.backup:
|
|
245
|
+
return
|
|
246
|
+
backup = Backup(context.backup, dsql_context=context.dsql, rds_context=context.rds)
|
|
247
|
+
backup.delete()
|
|
248
|
+
points = backup.list_recovery_points()
|
|
249
|
+
if not points: # None (権限なし) or 空
|
|
250
|
+
return
|
|
251
|
+
if context.backup.declared and context.backup.deletable and not yes:
|
|
252
|
+
echo.warning(
|
|
253
|
+
"バックアップデータ (recovery point) が %d 件あります。" % len(points)
|
|
254
|
+
)
|
|
255
|
+
if click.confirm("recovery point も削除しますか?", default=False):
|
|
256
|
+
backup.delete_recovery_points(points)
|
|
257
|
+
return
|
|
258
|
+
echo.warning(
|
|
259
|
+
"バックアップデータ (recovery point %d 件) は削除されず残ります"
|
|
260
|
+
" (保持期限まで課金対象)。削除するには [backup] deletable = true を"
|
|
261
|
+
" 宣言して `pocket backup cleanup` を実行するか、AWS Backup の"
|
|
262
|
+
" コンソールから削除してください。" % len(points)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
226
266
|
def _destroy_dsql(context: Context):
|
|
227
267
|
"""DSQL クラスターを削除"""
|
|
228
268
|
if not context.dsql:
|
|
@@ -352,7 +392,12 @@ def _destroy_cloudfront_and_acm(context: Context):
|
|
|
352
392
|
echo.success("CloudFront ACM '%s' was destroyed." % name)
|
|
353
393
|
|
|
354
394
|
|
|
355
|
-
def _destroy_resources(
|
|
395
|
+
def _destroy_resources(
|
|
396
|
+
context: Context,
|
|
397
|
+
with_secrets: bool,
|
|
398
|
+
with_state_bucket: bool,
|
|
399
|
+
yes: bool = False,
|
|
400
|
+
):
|
|
356
401
|
"""リソースをデプロイの逆順で削除"""
|
|
357
402
|
# 1. CloudFront + ACM
|
|
358
403
|
_destroy_cloudfront_and_acm(context)
|
|
@@ -360,6 +405,9 @@ def _destroy_resources(context: Context, with_secrets: bool, with_state_bucket:
|
|
|
360
405
|
# 2. AwsContainer (CFNスタック + ECR + secrets)
|
|
361
406
|
_destroy_awscontainer(context, with_secrets)
|
|
362
407
|
|
|
408
|
+
# 2.4. backup plan(DSQL / RDS の cluster 削除より先)
|
|
409
|
+
_destroy_backup(context, yes)
|
|
410
|
+
|
|
363
411
|
# 2.5. DSQL
|
|
364
412
|
_destroy_dsql(context)
|
|
365
413
|
|
|
@@ -434,5 +482,5 @@ def destroy(stage: str, without_secrets: bool, with_state_bucket: bool, yes: boo
|
|
|
434
482
|
if not yes:
|
|
435
483
|
click.confirm("stage '%s' の全リソースを削除しますか?" % stage, abort=True)
|
|
436
484
|
|
|
437
|
-
_destroy_resources(context, with_secrets, with_state_bucket)
|
|
485
|
+
_destroy_resources(context, with_secrets, with_state_bucket, yes=yes)
|
|
438
486
|
echo.success("stage '%s' の全リソースを削除しました。" % stage)
|
|
@@ -6,10 +6,10 @@ 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
|
|
9
10
|
from pocket_cli.resources.dsql import (
|
|
10
|
-
BACKUP_ROLE_NAME,
|
|
11
11
|
BACKUP_TERMINAL_STATES,
|
|
12
|
-
|
|
12
|
+
DEFAULT_ON_DEMAND_RETENTION_DAYS,
|
|
13
13
|
Dsql,
|
|
14
14
|
)
|
|
15
15
|
|
|
@@ -87,9 +87,12 @@ def endpoint(stage, format_):
|
|
|
87
87
|
)
|
|
88
88
|
@click.option(
|
|
89
89
|
"--retention-days",
|
|
90
|
-
type=
|
|
90
|
+
type=click.IntRange(min=0),
|
|
91
91
|
default=None,
|
|
92
|
-
help="recovery point の保持日数
|
|
92
|
+
help="recovery point の保持日数"
|
|
93
|
+
" (省略時: [backup] の delete_after_days、宣言が無ければ %d 日。"
|
|
94
|
+
"0 を指定すると無期限保持だが pocket からは削除できなくなる)"
|
|
95
|
+
% DEFAULT_ON_DEMAND_RETENTION_DAYS,
|
|
93
96
|
)
|
|
94
97
|
@click.option("--watch", is_flag=True, help="バックアップ完了まで待機する")
|
|
95
98
|
def backup(stage, vault, iam_role_arn, retention_days, watch):
|
|
@@ -4,6 +4,7 @@ from pocket import __version__
|
|
|
4
4
|
from pocket_cli import django_cli
|
|
5
5
|
from pocket_cli.cli import (
|
|
6
6
|
awscontainer_cli,
|
|
7
|
+
backup_cli,
|
|
7
8
|
cloudfront_cli,
|
|
8
9
|
cloudfront_keys_cli,
|
|
9
10
|
cloudfront_waf_cli,
|
|
@@ -80,6 +81,7 @@ main.add_command(runtime_config_cli.runtime_config)
|
|
|
80
81
|
main.add_command(migrate_cli.migrate)
|
|
81
82
|
main.add_command(permissions_cli.permissions)
|
|
82
83
|
main.add_command(waf_cli.waf)
|
|
84
|
+
main.add_command(backup_cli.backup)
|
|
83
85
|
|
|
84
86
|
|
|
85
87
|
@main.group()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""AWS Backup の前提リソース (vault / サービスロール) の共有 ensure。
|
|
2
|
+
|
|
3
|
+
定期バックアップ (resources/backup.py の Backup) と dsql のオンデマンド
|
|
4
|
+
バックアップ (resources/dsql.py) の両方が使うため、どちらからも import できる
|
|
5
|
+
場所に置く (backup.py は Dsql / Rds を import するので、dsql.py が backup.py を
|
|
6
|
+
import すると循環になる)。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
from botocore.exceptions import ClientError
|
|
15
|
+
|
|
16
|
+
from pocket.utils import echo
|
|
17
|
+
|
|
18
|
+
# pocket 管理のバックアップ前提リソース。AWS Backup の Default vault /
|
|
19
|
+
# AWSBackupDefaultServiceRole は console 初回操作で作られるもので、API しか
|
|
20
|
+
# 使わないアカウントには存在しないため、pocket 側で冪等に ensure する。
|
|
21
|
+
# ロール名の forge- prefix は codebuild ロールの命名に合わせている
|
|
22
|
+
BACKUP_VAULT_NAME = "pocket-backup"
|
|
23
|
+
BACKUP_ROLE_NAME = "forge-pocket-backup-role"
|
|
24
|
+
_BACKUP_ROLE_POLICIES = (
|
|
25
|
+
"arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup",
|
|
26
|
+
"arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForRestores",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def ensure_backup_vault(backup_client, name: str) -> None:
|
|
31
|
+
"""pocket 管理の vault を冪等に確保する。
|
|
32
|
+
|
|
33
|
+
describe → 無ければ create の順は不可。vault が 1 つも無いアカウントでは
|
|
34
|
+
AWS Backup が存在しない vault への Describe に ResourceNotFoundException
|
|
35
|
+
ではなく AccessDeniedException を返すため「未作成」を判定できない。
|
|
36
|
+
CreateBackupVault は同名 vault があると AlreadyExistsException を返すので
|
|
37
|
+
create を先に撃って握る (既存なら noop = 冪等)。
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
backup_client.create_backup_vault(BackupVaultName=name)
|
|
41
|
+
except ClientError as e:
|
|
42
|
+
if e.response["Error"]["Code"] == "AlreadyExistsException":
|
|
43
|
+
return
|
|
44
|
+
raise
|
|
45
|
+
echo.log("Created backup vault: %s" % name)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def ensure_backup_role(iam_client, permissions_boundary: str | None) -> str:
|
|
49
|
+
"""AWS Backup サービスロールを冪等に ensure して ARN を返す。
|
|
50
|
+
|
|
51
|
+
codebuild の _ensure_role と同型。boundary 必須のアカウントでも作成
|
|
52
|
+
できるよう permissions_boundary を付与する。
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
return iam_client.get_role(RoleName=BACKUP_ROLE_NAME)["Role"]["Arn"]
|
|
56
|
+
except ClientError as e:
|
|
57
|
+
if e.response["Error"]["Code"] != "NoSuchEntity":
|
|
58
|
+
raise
|
|
59
|
+
echo.log("Creating AWS Backup service role: %s" % BACKUP_ROLE_NAME)
|
|
60
|
+
assume_role_policy = json.dumps(
|
|
61
|
+
{
|
|
62
|
+
"Version": "2012-10-17",
|
|
63
|
+
"Statement": [
|
|
64
|
+
{
|
|
65
|
+
"Effect": "Allow",
|
|
66
|
+
"Principal": {"Service": "backup.amazonaws.com"},
|
|
67
|
+
"Action": "sts:AssumeRole",
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
create_kwargs: dict = {
|
|
73
|
+
"RoleName": BACKUP_ROLE_NAME,
|
|
74
|
+
"AssumeRolePolicyDocument": assume_role_policy,
|
|
75
|
+
}
|
|
76
|
+
if permissions_boundary:
|
|
77
|
+
create_kwargs["PermissionsBoundary"] = permissions_boundary
|
|
78
|
+
role_arn: str = iam_client.create_role(**create_kwargs)["Role"]["Arn"]
|
|
79
|
+
for policy_arn in _BACKUP_ROLE_POLICIES:
|
|
80
|
+
iam_client.attach_role_policy(RoleName=BACKUP_ROLE_NAME, PolicyArn=policy_arn)
|
|
81
|
+
# IAM ロールの伝播待ち (作成直後の StartBackupJob 失敗を避ける)
|
|
82
|
+
echo.log("Waiting for IAM role propagation...")
|
|
83
|
+
time.sleep(10)
|
|
84
|
+
return role_arn
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import boto3
|
|
6
|
+
from botocore.exceptions import ClientError
|
|
7
|
+
|
|
8
|
+
from pocket.resources.base import ResourceStatus
|
|
9
|
+
from pocket.utils import echo
|
|
10
|
+
from pocket_cli.resources.aws.backup_common import (
|
|
11
|
+
BACKUP_VAULT_NAME,
|
|
12
|
+
ensure_backup_role,
|
|
13
|
+
ensure_backup_vault,
|
|
14
|
+
)
|
|
15
|
+
from pocket_cli.resources.dsql import Dsql
|
|
16
|
+
from pocket_cli.resources.rds import Rds
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from pocket.context import (
|
|
20
|
+
BackupContext,
|
|
21
|
+
BackupPlanContext,
|
|
22
|
+
DsqlContext,
|
|
23
|
+
RdsContext,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# backup plan の drift 判定で比較する項目。AWS が既定値で補完する項目
|
|
27
|
+
# (StartWindowMinutes / RuleId 等) を含めると毎 deploy で update が走るため、
|
|
28
|
+
# pocket が宣言した項目だけを見る
|
|
29
|
+
_PLAN_RULE_KEYS = (
|
|
30
|
+
"TargetBackupVaultName",
|
|
31
|
+
"ScheduleExpression",
|
|
32
|
+
"ScheduleExpressionTimezone",
|
|
33
|
+
)
|
|
34
|
+
_PLAN_LIFECYCLE_KEYS = ("MoveToColdStorageAfterDays", "DeleteAfterDays")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _rules_by_name(rules: list[dict]) -> dict[str, dict]:
|
|
38
|
+
return {rule.get("RuleName", ""): rule for rule in rules}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _plan_matches(current: dict, desired: dict) -> bool:
|
|
42
|
+
"""rule 名で突き合わせて宣言項目を比較する (GetBackupPlan の順序に依存しない)。"""
|
|
43
|
+
current_rules = _rules_by_name(current.get("Rules", []))
|
|
44
|
+
desired_rules = _rules_by_name(desired["Rules"])
|
|
45
|
+
if set(current_rules) != set(desired_rules):
|
|
46
|
+
return False
|
|
47
|
+
for name, want in desired_rules.items():
|
|
48
|
+
cur = current_rules[name]
|
|
49
|
+
if any(cur.get(key) != want.get(key) for key in _PLAN_RULE_KEYS):
|
|
50
|
+
return False
|
|
51
|
+
cur_lifecycle = cur.get("Lifecycle", {})
|
|
52
|
+
want_lifecycle = want["Lifecycle"]
|
|
53
|
+
if any(
|
|
54
|
+
cur_lifecycle.get(key) != want_lifecycle.get(key)
|
|
55
|
+
for key in _PLAN_LIFECYCLE_KEYS
|
|
56
|
+
):
|
|
57
|
+
return False
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Backup:
|
|
62
|
+
"""[backup.dsql] / [backup.rds] 宣言に沿った AWS Backup plan の管理。
|
|
63
|
+
|
|
64
|
+
エンジンごとに rule 構成が違う (rds は daily を PITR が担う / cold storage
|
|
65
|
+
不可) ため、plan はエンジン別に provision する。plan 管理は cluster の
|
|
66
|
+
作成後に走る必要があるため、実作業は ensure_post_deploy_state で行う
|
|
67
|
+
(status は常に COMPLETED)。
|
|
68
|
+
|
|
69
|
+
バックアップ「データ」(recovery point) は原則触らない。削除できるのは
|
|
70
|
+
[backup] deletable = true の宣言下で、利用者が明示的に確認した場合のみ
|
|
71
|
+
(`pocket backup cleanup` / destroy の確認プロンプト)。
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
context: BackupContext
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
context: BackupContext,
|
|
79
|
+
dsql_context: DsqlContext | None = None,
|
|
80
|
+
rds_context: RdsContext | None = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
self.context = context
|
|
83
|
+
self._dsql_context = dsql_context
|
|
84
|
+
self._rds_context = rds_context
|
|
85
|
+
self._backup = boto3.client("backup", region_name=context.region)
|
|
86
|
+
self._iam = boto3.client("iam", region_name=context.region)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def status(self) -> ResourceStatus:
|
|
90
|
+
# plan は cluster 作成後でないと selection を張れないため、作成・更新は
|
|
91
|
+
# ensure_post_deploy_state で冪等に行う
|
|
92
|
+
return "COMPLETED"
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def description(self):
|
|
96
|
+
return "Configure AWS Backup plans: %s" % ", ".join(
|
|
97
|
+
plan.plan_name for plan in self.context.plans
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def deploy_init(self):
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
def create(self):
|
|
104
|
+
self.ensure_post_deploy_state()
|
|
105
|
+
|
|
106
|
+
def update(self):
|
|
107
|
+
self.ensure_post_deploy_state()
|
|
108
|
+
|
|
109
|
+
def state_info(self):
|
|
110
|
+
return {
|
|
111
|
+
"backup": {
|
|
112
|
+
"plans": [plan.plan_name for plan in self.context.plans],
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
def _service_target_arn(self, service: str) -> str | None:
|
|
117
|
+
if service == "dsql" and self._dsql_context:
|
|
118
|
+
return Dsql(self._dsql_context).backup_target_arn
|
|
119
|
+
if service == "rds" and self._rds_context:
|
|
120
|
+
return Rds(self._rds_context).backup_target_arn
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
def target_arns(self) -> list[str]:
|
|
124
|
+
"""recovery point 操作の対象 ARN 一覧 (未作成 cluster は除く)。
|
|
125
|
+
|
|
126
|
+
[backup] の宣言に依らず、stage に存在する対象 DB 全部を返す
|
|
127
|
+
(destroy 時のデータ残存案内は宣言を外した後も出すため)。
|
|
128
|
+
"""
|
|
129
|
+
arns = [self._service_target_arn(s) for s in ("dsql", "rds")]
|
|
130
|
+
return sorted(arn for arn in arns if arn is not None)
|
|
131
|
+
|
|
132
|
+
def ensure_post_deploy_state(self):
|
|
133
|
+
"""宣言されたエンジンごとに plan / selection を冪等に確保する。
|
|
134
|
+
|
|
135
|
+
未宣言なら何もしない (opt-in。宣言を外した後の既存 plan にも触らない。
|
|
136
|
+
plan の掃除は destroy の責務)。
|
|
137
|
+
"""
|
|
138
|
+
if not self.context.plans:
|
|
139
|
+
return
|
|
140
|
+
try:
|
|
141
|
+
targets = [
|
|
142
|
+
(plan, arn)
|
|
143
|
+
for plan in self.context.plans
|
|
144
|
+
if (arn := self._service_target_arn(plan.service)) is not None
|
|
145
|
+
]
|
|
146
|
+
if not targets:
|
|
147
|
+
return
|
|
148
|
+
ensure_backup_vault(self._backup, BACKUP_VAULT_NAME)
|
|
149
|
+
role_arn = ensure_backup_role(self._iam, self.context.permissions_boundary)
|
|
150
|
+
for plan, arn in targets:
|
|
151
|
+
plan_id = self._ensure_plan(plan)
|
|
152
|
+
self._ensure_selection(plan, plan_id, role_arn, [arn])
|
|
153
|
+
except ClientError as e:
|
|
154
|
+
if e.response["Error"]["Code"] != "AccessDeniedException":
|
|
155
|
+
raise
|
|
156
|
+
echo.warning(
|
|
157
|
+
"AWS Backup の設定に必要な権限が無いため、定期バックアップの"
|
|
158
|
+
" provisioning をスキップしました: %s" % e
|
|
159
|
+
)
|
|
160
|
+
echo.warning(
|
|
161
|
+
" deploy role に backup:* を付与してください"
|
|
162
|
+
" (`pocket permissions list` 参照)。"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def _plan_document(self, plan: BackupPlanContext) -> dict:
|
|
166
|
+
rules = []
|
|
167
|
+
for rule in plan.rules:
|
|
168
|
+
lifecycle: dict = {"DeleteAfterDays": rule.delete_after_days}
|
|
169
|
+
if rule.cold_storage_after_days:
|
|
170
|
+
lifecycle["MoveToColdStorageAfterDays"] = rule.cold_storage_after_days
|
|
171
|
+
rules.append(
|
|
172
|
+
{
|
|
173
|
+
"RuleName": rule.name,
|
|
174
|
+
"TargetBackupVaultName": BACKUP_VAULT_NAME,
|
|
175
|
+
"ScheduleExpression": rule.schedule_expression,
|
|
176
|
+
"ScheduleExpressionTimezone": self.context.timezone,
|
|
177
|
+
"Lifecycle": lifecycle,
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
return {"BackupPlanName": plan.plan_name, "Rules": rules}
|
|
181
|
+
|
|
182
|
+
def _find_plan_id(self, plan_name: str) -> str | None:
|
|
183
|
+
paginator = self._backup.get_paginator("list_backup_plans")
|
|
184
|
+
for page in paginator.paginate():
|
|
185
|
+
for plan in page["BackupPlansList"]:
|
|
186
|
+
if plan["BackupPlanName"] == plan_name:
|
|
187
|
+
return plan["BackupPlanId"]
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
def existing_plan_names(self) -> list[str]:
|
|
191
|
+
"""destroy の対象一覧表示用 (権限が無ければ「無し」扱い)。"""
|
|
192
|
+
try:
|
|
193
|
+
return [
|
|
194
|
+
name
|
|
195
|
+
for name in self.context.cleanup_plan_names
|
|
196
|
+
if self._find_plan_id(name) is not None
|
|
197
|
+
]
|
|
198
|
+
except ClientError as e:
|
|
199
|
+
if e.response["Error"]["Code"] != "AccessDeniedException":
|
|
200
|
+
raise
|
|
201
|
+
return []
|
|
202
|
+
|
|
203
|
+
def _ensure_plan(self, plan: BackupPlanContext) -> str:
|
|
204
|
+
document = self._plan_document(plan)
|
|
205
|
+
plan_id = self._find_plan_id(plan.plan_name)
|
|
206
|
+
if plan_id is None:
|
|
207
|
+
echo.log("Creating backup plan: %s" % plan.plan_name)
|
|
208
|
+
return self._backup.create_backup_plan(BackupPlan=document)["BackupPlanId"]
|
|
209
|
+
# 宣言が変わっていれば追従する (cron / lifecycle の drift 収束)
|
|
210
|
+
current = self._backup.get_backup_plan(BackupPlanId=plan_id)["BackupPlan"]
|
|
211
|
+
if not _plan_matches(current, document):
|
|
212
|
+
echo.log("Updating backup plan: %s" % plan.plan_name)
|
|
213
|
+
self._backup.update_backup_plan(BackupPlanId=plan_id, BackupPlan=document)
|
|
214
|
+
return plan_id
|
|
215
|
+
|
|
216
|
+
def _ensure_selection(
|
|
217
|
+
self,
|
|
218
|
+
plan: BackupPlanContext,
|
|
219
|
+
plan_id: str,
|
|
220
|
+
role_arn: str,
|
|
221
|
+
arns: list[str],
|
|
222
|
+
) -> None:
|
|
223
|
+
"""対象 DB の ARN を指した selection を確保する。
|
|
224
|
+
|
|
225
|
+
selection には更新 API が無いため、対象 ARN が変わっていたら
|
|
226
|
+
(cluster 再作成など) 作り直す。
|
|
227
|
+
"""
|
|
228
|
+
selections = self._backup.list_backup_selections(BackupPlanId=plan_id)
|
|
229
|
+
for item in selections["BackupSelectionsList"]:
|
|
230
|
+
if item["SelectionName"] != plan.plan_name:
|
|
231
|
+
continue
|
|
232
|
+
detail = self._backup.get_backup_selection(
|
|
233
|
+
BackupPlanId=plan_id, SelectionId=item["SelectionId"]
|
|
234
|
+
)
|
|
235
|
+
if sorted(detail["BackupSelection"].get("Resources", [])) == arns:
|
|
236
|
+
return
|
|
237
|
+
self._backup.delete_backup_selection(
|
|
238
|
+
BackupPlanId=plan_id, SelectionId=item["SelectionId"]
|
|
239
|
+
)
|
|
240
|
+
echo.log("Creating backup selection for %s" % plan.plan_name)
|
|
241
|
+
self._backup.create_backup_selection(
|
|
242
|
+
BackupPlanId=plan_id,
|
|
243
|
+
BackupSelection={
|
|
244
|
+
"SelectionName": plan.plan_name,
|
|
245
|
+
"IamRoleArn": role_arn,
|
|
246
|
+
"Resources": arns,
|
|
247
|
+
},
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def delete(self) -> None:
|
|
251
|
+
"""backup plan / selection を削除する。
|
|
252
|
+
|
|
253
|
+
recovery point と vault は消さない。バックアップ「設定」は stack の
|
|
254
|
+
付属物だが、バックアップ「データ」は stack より長生きさせるべきで、
|
|
255
|
+
cluster を消した後こそ必要になりうるため。
|
|
256
|
+
|
|
257
|
+
[backup] の宣言の有無に関わらず、stage に存在しうる全 plan 名を走査する
|
|
258
|
+
(宣言を外した後の destroy でも plan を残さないため)。backup 権限を
|
|
259
|
+
持たない deploy role でも destroy 自体は完遂させたいので、権限エラーは
|
|
260
|
+
警告に留める。
|
|
261
|
+
"""
|
|
262
|
+
for plan_name in self.context.cleanup_plan_names:
|
|
263
|
+
try:
|
|
264
|
+
plan_id = self._find_plan_id(plan_name)
|
|
265
|
+
except ClientError as e:
|
|
266
|
+
if e.response["Error"]["Code"] != "AccessDeniedException":
|
|
267
|
+
raise
|
|
268
|
+
echo.warning(
|
|
269
|
+
"backup plan を確認する権限がないため、削除をスキップしました"
|
|
270
|
+
" (backup:ListBackupPlans)。plan が残っている場合は手動で"
|
|
271
|
+
" 削除してください。"
|
|
272
|
+
)
|
|
273
|
+
return
|
|
274
|
+
if plan_id is None:
|
|
275
|
+
continue
|
|
276
|
+
selections = self._backup.list_backup_selections(BackupPlanId=plan_id)
|
|
277
|
+
for item in selections["BackupSelectionsList"]:
|
|
278
|
+
self._backup.delete_backup_selection(
|
|
279
|
+
BackupPlanId=plan_id, SelectionId=item["SelectionId"]
|
|
280
|
+
)
|
|
281
|
+
self._backup.delete_backup_plan(BackupPlanId=plan_id)
|
|
282
|
+
echo.log("Deleted backup plan: %s" % plan_name)
|
|
283
|
+
|
|
284
|
+
def list_recovery_points(self) -> list[dict] | None:
|
|
285
|
+
"""対象 DB の recovery point 一覧 (pocket-backup vault 内のもの)。
|
|
286
|
+
|
|
287
|
+
現存する cluster の ARN でしか引けない (削除済み cluster の分は対象外 =
|
|
288
|
+
console でしか消せない)。権限が無ければ None を返す (呼び出し側が案内を
|
|
289
|
+
スキップできるように、空リストと区別する)。
|
|
290
|
+
"""
|
|
291
|
+
points: list[dict] = []
|
|
292
|
+
try:
|
|
293
|
+
for arn in self.target_arns():
|
|
294
|
+
paginator = self._backup.get_paginator(
|
|
295
|
+
"list_recovery_points_by_resource"
|
|
296
|
+
)
|
|
297
|
+
for page in paginator.paginate(ResourceArn=arn):
|
|
298
|
+
points.extend(page["RecoveryPoints"])
|
|
299
|
+
except ClientError as e:
|
|
300
|
+
if e.response["Error"]["Code"] != "AccessDeniedException":
|
|
301
|
+
raise
|
|
302
|
+
return None
|
|
303
|
+
# pocket 管理の vault 分だけを扱う (--vault で利用者の vault に取った
|
|
304
|
+
# オンデマンド分は利用者の管理物とみなし、pocket からは消さない)
|
|
305
|
+
return [p for p in points if p.get("BackupVaultName") == BACKUP_VAULT_NAME]
|
|
306
|
+
|
|
307
|
+
def delete_recovery_points(self, points: list[dict]) -> int:
|
|
308
|
+
"""recovery point を削除して件数を返す。
|
|
309
|
+
|
|
310
|
+
[backup] deletable = true + 利用者の明示確認を経た経路 (cleanup CLI /
|
|
311
|
+
destroy の確認プロンプト) からのみ呼ぶこと。
|
|
312
|
+
"""
|
|
313
|
+
for point in points:
|
|
314
|
+
self._backup.delete_recovery_point(
|
|
315
|
+
BackupVaultName=point["BackupVaultName"],
|
|
316
|
+
RecoveryPointArn=point["RecoveryPointArn"],
|
|
317
|
+
)
|
|
318
|
+
if points:
|
|
319
|
+
echo.log("Deleted %d recovery point(s)." % len(points))
|
|
320
|
+
return len(points)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
import time
|
|
5
4
|
from functools import cached_property
|
|
6
5
|
from typing import TYPE_CHECKING
|
|
7
6
|
|
|
@@ -15,6 +14,11 @@ from pocket.secret_store import (
|
|
|
15
14
|
read_stored_value,
|
|
16
15
|
)
|
|
17
16
|
from pocket.utils import echo
|
|
17
|
+
from pocket_cli.resources.aws.backup_common import (
|
|
18
|
+
BACKUP_VAULT_NAME,
|
|
19
|
+
ensure_backup_role,
|
|
20
|
+
ensure_backup_vault,
|
|
21
|
+
)
|
|
18
22
|
from pocket_cli.resources.aws.poll import wait_until
|
|
19
23
|
|
|
20
24
|
if TYPE_CHECKING:
|
|
@@ -25,44 +29,13 @@ BACKUP_TERMINAL_STATES = {"COMPLETED", "FAILED", "ABORTED", "EXPIRED", "PARTIAL"
|
|
|
25
29
|
# restore job の終端状態
|
|
26
30
|
RESTORE_TERMINAL_STATES = {"COMPLETED", "FAILED", "ABORTED"}
|
|
27
31
|
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup",
|
|
36
|
-
"arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForRestores",
|
|
37
|
-
)
|
|
38
|
-
# backup plan の drift 判定で比較する項目。AWS が既定値で補完する項目
|
|
39
|
-
# (StartWindowMinutes / RuleId 等) を含めると毎 deploy で update が走るため、
|
|
40
|
-
# pocket が宣言した項目だけを見る
|
|
41
|
-
_PLAN_RULE_KEYS = (
|
|
42
|
-
"RuleName",
|
|
43
|
-
"TargetBackupVaultName",
|
|
44
|
-
"ScheduleExpression",
|
|
45
|
-
"ScheduleExpressionTimezone",
|
|
46
|
-
)
|
|
47
|
-
_PLAN_LIFECYCLE_KEYS = ("MoveToColdStorageAfterDays", "DeleteAfterDays")
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _plan_matches(current: dict, desired: dict) -> bool:
|
|
51
|
-
current_rules = current.get("Rules", [])
|
|
52
|
-
desired_rules = desired["Rules"]
|
|
53
|
-
if len(current_rules) != len(desired_rules):
|
|
54
|
-
return False
|
|
55
|
-
for cur, want in zip(current_rules, desired_rules, strict=True):
|
|
56
|
-
if any(cur.get(key) != want.get(key) for key in _PLAN_RULE_KEYS):
|
|
57
|
-
return False
|
|
58
|
-
cur_lifecycle = cur.get("Lifecycle", {})
|
|
59
|
-
want_lifecycle = want["Lifecycle"]
|
|
60
|
-
if any(
|
|
61
|
-
cur_lifecycle.get(key) != want_lifecycle.get(key)
|
|
62
|
-
for key in _PLAN_LIFECYCLE_KEYS
|
|
63
|
-
):
|
|
64
|
-
return False
|
|
65
|
-
return True
|
|
32
|
+
# オンデマンドバックアップで保持日数の指定が無く [backup] も未宣言なときの既定。
|
|
33
|
+
# AWS Backup の vault は既定ライフサイクルを持たないため、Lifecycle を渡さないと
|
|
34
|
+
# recovery point は無期限に残る。pocket にも deploy role にもデータ削除権限が無く
|
|
35
|
+
# (destroy も recovery point を残す)、console 作業でしか消せなくなるため、
|
|
36
|
+
# 「いつかは失効する」ことを既定で保証する。3 年は復元要件として十分長く、
|
|
37
|
+
# 意図せず消えるより増え続けるほうが困る、という判断
|
|
38
|
+
DEFAULT_ON_DEMAND_RETENTION_DAYS = 1095
|
|
66
39
|
|
|
67
40
|
|
|
68
41
|
class Dsql:
|
|
@@ -149,7 +122,7 @@ class Dsql:
|
|
|
149
122
|
"DSQL には自動バックアップ (PITR / スナップショット) がありません。"
|
|
150
123
|
)
|
|
151
124
|
echo.warning(
|
|
152
|
-
" 定期バックアップは [dsql
|
|
125
|
+
" 定期バックアップは [backup.dsql] を宣言してください。"
|
|
153
126
|
" 単発なら `pocket resource dsql backup` で取得できます。"
|
|
154
127
|
)
|
|
155
128
|
|
|
@@ -173,9 +146,6 @@ class Dsql:
|
|
|
173
146
|
def delete(self):
|
|
174
147
|
if not self.identifier:
|
|
175
148
|
return
|
|
176
|
-
# cluster より先に plan を消す (selection が消える cluster の ARN を
|
|
177
|
-
# 指したまま残ると、以後の backup job が失敗し続ける)
|
|
178
|
-
self.delete_backup_plan()
|
|
179
149
|
echo.log("Deleting DSQL cluster: %s" % self.identifier)
|
|
180
150
|
self._client.delete_cluster(identifier=self.identifier)
|
|
181
151
|
echo.log("Waiting for DSQL cluster deletion...")
|
|
@@ -186,128 +156,13 @@ class Dsql:
|
|
|
186
156
|
def ensure_post_deploy_state(self):
|
|
187
157
|
# create の有無に関わらず毎 deploy で publish を冪等に確保する。cluster
|
|
188
158
|
# 再作成や旧バージョンで deploy 済みの既存 cluster も常に実体を反映する
|
|
159
|
+
# (定期バックアップの plan / selection は Backup resource の責務)
|
|
189
160
|
self.publish_endpoint()
|
|
190
|
-
# plan の selection は cluster ARN を指すため、cluster 作成後に ensure する
|
|
191
|
-
self.ensure_backup_plan()
|
|
192
161
|
|
|
193
162
|
@property
|
|
194
|
-
def
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def ensure_backup_plan(self) -> None:
|
|
198
|
-
"""[dsql.backup] 宣言に沿って backup plan / selection を冪等に確保する。
|
|
199
|
-
|
|
200
|
-
DSQL は組み込みの自動バックアップを持たないため、宣言された stage では
|
|
201
|
-
pocket が plan まで provision する (native 属性で表現できる RDS の PITR
|
|
202
|
-
と違い、宣言だけでは何も起きないため)。未宣言なら何もしない。
|
|
203
|
-
"""
|
|
204
|
-
backup = self.context.backup
|
|
205
|
-
if backup is None or not self.arn:
|
|
206
|
-
return
|
|
207
|
-
self._ensure_backup_vault(BACKUP_VAULT_NAME)
|
|
208
|
-
role_arn = self._ensure_backup_role()
|
|
209
|
-
plan_id = self._ensure_plan(backup)
|
|
210
|
-
self._ensure_selection(plan_id, role_arn)
|
|
211
|
-
|
|
212
|
-
def _plan_document(self, backup) -> dict:
|
|
213
|
-
lifecycle: dict = {"DeleteAfterDays": backup.delete_after_days}
|
|
214
|
-
if backup.cold_storage_after_days:
|
|
215
|
-
lifecycle["MoveToColdStorageAfterDays"] = backup.cold_storage_after_days
|
|
216
|
-
return {
|
|
217
|
-
"BackupPlanName": self.backup_plan_name,
|
|
218
|
-
"Rules": [
|
|
219
|
-
{
|
|
220
|
-
"RuleName": "daily",
|
|
221
|
-
"TargetBackupVaultName": BACKUP_VAULT_NAME,
|
|
222
|
-
"ScheduleExpression": backup.schedule_expression,
|
|
223
|
-
"ScheduleExpressionTimezone": backup.timezone,
|
|
224
|
-
"Lifecycle": lifecycle,
|
|
225
|
-
}
|
|
226
|
-
],
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
def _find_plan_id(self) -> str | None:
|
|
230
|
-
paginator = self._backup.get_paginator("list_backup_plans")
|
|
231
|
-
for page in paginator.paginate():
|
|
232
|
-
for plan in page["BackupPlansList"]:
|
|
233
|
-
if plan["BackupPlanName"] == self.backup_plan_name:
|
|
234
|
-
return plan["BackupPlanId"]
|
|
235
|
-
return None
|
|
236
|
-
|
|
237
|
-
def _ensure_plan(self, backup) -> str:
|
|
238
|
-
document = self._plan_document(backup)
|
|
239
|
-
plan_id = self._find_plan_id()
|
|
240
|
-
if plan_id is None:
|
|
241
|
-
echo.log("Creating backup plan: %s" % self.backup_plan_name)
|
|
242
|
-
return self._backup.create_backup_plan(BackupPlan=document)["BackupPlanId"]
|
|
243
|
-
# 宣言が変わっていれば追従する (cron / lifecycle の drift 収束)
|
|
244
|
-
current = self._backup.get_backup_plan(BackupPlanId=plan_id)["BackupPlan"]
|
|
245
|
-
if not _plan_matches(current, document):
|
|
246
|
-
echo.log("Updating backup plan: %s" % self.backup_plan_name)
|
|
247
|
-
self._backup.update_backup_plan(BackupPlanId=plan_id, BackupPlan=document)
|
|
248
|
-
return plan_id
|
|
249
|
-
|
|
250
|
-
def _ensure_selection(self, plan_id: str, role_arn: str) -> None:
|
|
251
|
-
"""cluster ARN を対象にした selection を確保する。
|
|
252
|
-
|
|
253
|
-
selection には更新 API が無いため、対象 ARN が変わっていたら
|
|
254
|
-
(cluster 再作成など) 作り直す。
|
|
255
|
-
"""
|
|
256
|
-
if not self.arn:
|
|
257
|
-
return
|
|
258
|
-
selections = self._backup.list_backup_selections(BackupPlanId=plan_id)
|
|
259
|
-
for item in selections["BackupSelectionsList"]:
|
|
260
|
-
if item["SelectionName"] != self.backup_plan_name:
|
|
261
|
-
continue
|
|
262
|
-
detail = self._backup.get_backup_selection(
|
|
263
|
-
BackupPlanId=plan_id, SelectionId=item["SelectionId"]
|
|
264
|
-
)
|
|
265
|
-
if detail["BackupSelection"].get("Resources") == [self.arn]:
|
|
266
|
-
return
|
|
267
|
-
self._backup.delete_backup_selection(
|
|
268
|
-
BackupPlanId=plan_id, SelectionId=item["SelectionId"]
|
|
269
|
-
)
|
|
270
|
-
echo.log("Creating backup selection for %s" % self.context.tag_name)
|
|
271
|
-
self._backup.create_backup_selection(
|
|
272
|
-
BackupPlanId=plan_id,
|
|
273
|
-
BackupSelection={
|
|
274
|
-
"SelectionName": self.backup_plan_name,
|
|
275
|
-
"IamRoleArn": role_arn,
|
|
276
|
-
"Resources": [self.arn],
|
|
277
|
-
},
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
def delete_backup_plan(self) -> None:
|
|
281
|
-
"""backup plan / selection を削除する (cluster 削除との対称操作)。
|
|
282
|
-
|
|
283
|
-
recovery point と vault は消さない。バックアップ「設定」は stack の
|
|
284
|
-
付属物だが、バックアップ「データ」は stack より長生きさせるべきで、
|
|
285
|
-
cluster を消した後こそ必要になりうるため。
|
|
286
|
-
|
|
287
|
-
宣言の有無に関わらず走査する ([dsql.backup] を外した後の destroy でも
|
|
288
|
-
plan を残さないため)。backup 権限を持たない古い deploy role でも
|
|
289
|
-
destroy 自体は完遂させたいので、権限エラーは警告に留める。
|
|
290
|
-
"""
|
|
291
|
-
try:
|
|
292
|
-
plan_id = self._find_plan_id()
|
|
293
|
-
except ClientError as e:
|
|
294
|
-
if e.response["Error"]["Code"] != "AccessDeniedException":
|
|
295
|
-
raise
|
|
296
|
-
echo.warning(
|
|
297
|
-
"backup plan を確認する権限がないため、削除をスキップしました"
|
|
298
|
-
" (backup:ListBackupPlans)。plan が残っている場合は手動で"
|
|
299
|
-
" 削除してください。"
|
|
300
|
-
)
|
|
301
|
-
return
|
|
302
|
-
if plan_id is None:
|
|
303
|
-
return
|
|
304
|
-
selections = self._backup.list_backup_selections(BackupPlanId=plan_id)
|
|
305
|
-
for item in selections["BackupSelectionsList"]:
|
|
306
|
-
self._backup.delete_backup_selection(
|
|
307
|
-
BackupPlanId=plan_id, SelectionId=item["SelectionId"]
|
|
308
|
-
)
|
|
309
|
-
self._backup.delete_backup_plan(BackupPlanId=plan_id)
|
|
310
|
-
echo.log("Deleted backup plan: %s" % self.backup_plan_name)
|
|
163
|
+
def backup_target_arn(self) -> str | None:
|
|
164
|
+
"""[backup] の selection 対象 ARN (Backup resource が参照する)。"""
|
|
165
|
+
return self.arn
|
|
311
166
|
|
|
312
167
|
def publish_endpoint(self):
|
|
313
168
|
"""endpoint を stored user secret 正準パスへ publish する (作成記録)。
|
|
@@ -370,64 +225,43 @@ class Dsql:
|
|
|
370
225
|
"ResourceArn": self.arn,
|
|
371
226
|
"IamRoleArn": iam_role_arn,
|
|
372
227
|
}
|
|
373
|
-
|
|
374
|
-
|
|
228
|
+
resolved = self._resolve_retention_days(retention_days)
|
|
229
|
+
if resolved is not None:
|
|
230
|
+
params["Lifecycle"] = {"DeleteAfterDays": resolved}
|
|
375
231
|
res = self._backup.start_backup_job(**params)
|
|
376
232
|
return res["BackupJobId"]
|
|
377
233
|
|
|
378
|
-
def
|
|
379
|
-
|
|
380
|
-
# AWS Backup が存在しない vault への Describe に ResourceNotFoundException
|
|
381
|
-
# ではなく AccessDeniedException を返すため「未作成」を判定できない。
|
|
382
|
-
# CreateBackupVault は同名 vault があると AlreadyExistsException を返すので
|
|
383
|
-
# create を先に撃って握る (既存なら noop = 冪等)。
|
|
384
|
-
try:
|
|
385
|
-
self._backup.create_backup_vault(BackupVaultName=name)
|
|
386
|
-
except ClientError as e:
|
|
387
|
-
if e.response["Error"]["Code"] == "AlreadyExistsException":
|
|
388
|
-
return
|
|
389
|
-
raise
|
|
390
|
-
echo.log("Created backup vault: %s" % name)
|
|
391
|
-
|
|
392
|
-
def _ensure_backup_role(self) -> str:
|
|
393
|
-
"""AWS Backup サービスロールを冪等に ensure して ARN を返す。
|
|
234
|
+
def _resolve_retention_days(self, retention_days: int | None) -> int | None:
|
|
235
|
+
"""オンデマンドバックアップの保持日数を決める (None を返すと無期限)。
|
|
394
236
|
|
|
395
|
-
|
|
396
|
-
|
|
237
|
+
指定 > [backup.dsql] の最長階層 (monthly) > 既定 の順で解決する。
|
|
238
|
+
復元時の内部バックアップ (利用者が明示的に頼んでいない recovery point)
|
|
239
|
+
も start_backup を通るため同じ既定が効く。0 は「無期限」の明示指定。
|
|
397
240
|
"""
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
raise
|
|
403
|
-
echo.log("Creating AWS Backup service role: %s" % BACKUP_ROLE_NAME)
|
|
404
|
-
assume_role_policy = json.dumps(
|
|
405
|
-
{
|
|
406
|
-
"Version": "2012-10-17",
|
|
407
|
-
"Statement": [
|
|
408
|
-
{
|
|
409
|
-
"Effect": "Allow",
|
|
410
|
-
"Principal": {"Service": "backup.amazonaws.com"},
|
|
411
|
-
"Action": "sts:AssumeRole",
|
|
412
|
-
}
|
|
413
|
-
],
|
|
414
|
-
}
|
|
415
|
-
)
|
|
416
|
-
create_kwargs: dict = {
|
|
417
|
-
"RoleName": BACKUP_ROLE_NAME,
|
|
418
|
-
"AssumeRolePolicyDocument": assume_role_policy,
|
|
419
|
-
}
|
|
420
|
-
if self.context.permissions_boundary:
|
|
421
|
-
create_kwargs["PermissionsBoundary"] = self.context.permissions_boundary
|
|
422
|
-
role_arn: str = self._iam.create_role(**create_kwargs)["Role"]["Arn"]
|
|
423
|
-
for policy_arn in _BACKUP_ROLE_POLICIES:
|
|
424
|
-
self._iam.attach_role_policy(
|
|
425
|
-
RoleName=BACKUP_ROLE_NAME, PolicyArn=policy_arn
|
|
241
|
+
if retention_days == 0:
|
|
242
|
+
echo.warning(
|
|
243
|
+
"この recovery point は無期限保持です。"
|
|
244
|
+
"pocket からは削除できないため、消すには console 作業が必要です。"
|
|
426
245
|
)
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
246
|
+
return None
|
|
247
|
+
if retention_days is not None:
|
|
248
|
+
echo.log("Retention: %d days (--retention-days)" % retention_days)
|
|
249
|
+
return retention_days
|
|
250
|
+
backup = self.context.backup
|
|
251
|
+
if backup is not None:
|
|
252
|
+
echo.log(
|
|
253
|
+
"Retention: %d days (from [backup.dsql] monthly)"
|
|
254
|
+
% backup.monthly_delete_after_days
|
|
255
|
+
)
|
|
256
|
+
return backup.monthly_delete_after_days
|
|
257
|
+
echo.log("Retention: %d days (default)" % DEFAULT_ON_DEMAND_RETENTION_DAYS)
|
|
258
|
+
return DEFAULT_ON_DEMAND_RETENTION_DAYS
|
|
259
|
+
|
|
260
|
+
def _ensure_backup_vault(self, name: str) -> None:
|
|
261
|
+
ensure_backup_vault(self._backup, name)
|
|
262
|
+
|
|
263
|
+
def _ensure_backup_role(self) -> str:
|
|
264
|
+
return ensure_backup_role(self._iam, self.context.permissions_boundary)
|
|
431
265
|
|
|
432
266
|
def latest_recovery_point(self) -> dict | None:
|
|
433
267
|
"""現用クラスターの最新 recovery point (完了済み) を返す。"""
|
|
@@ -163,6 +163,19 @@ class Rds:
|
|
|
163
163
|
def database_name(self) -> str:
|
|
164
164
|
return self.context.database_name
|
|
165
165
|
|
|
166
|
+
@property
|
|
167
|
+
def backup_target_arn(self) -> str | None:
|
|
168
|
+
"""[backup] の selection 対象 ARN (Backup resource が参照する)。
|
|
169
|
+
|
|
170
|
+
managed = false の既存クラスタは対象外 (他 stack の管理物にバックアップ
|
|
171
|
+
設定を足さない)。
|
|
172
|
+
"""
|
|
173
|
+
if not self.context.managed:
|
|
174
|
+
return None
|
|
175
|
+
if self.cluster:
|
|
176
|
+
return self.cluster.get("DBClusterArn")
|
|
177
|
+
return None
|
|
178
|
+
|
|
166
179
|
@property
|
|
167
180
|
def status(self) -> ResourceStatus:
|
|
168
181
|
if not self.context.managed:
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "magic-pocket-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.28.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.
|
|
9
|
+
"magic-pocket>=0.28.0,<0.29",
|
|
10
10
|
"click>=8.1.7",
|
|
11
11
|
"jinja2>=3.1.3",
|
|
12
12
|
"python-on-whales>=0.68.0",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/builders/__init__.py
RENAMED
|
File without changes
|
{magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/builders/codebuild.py
RENAMED
|
File without changes
|
|
File without changes
|
{magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/builders/depot.py
RENAMED
|
File without changes
|
{magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/builders/docker.py
RENAMED
|
File without changes
|
|
File without changes
|
{magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/cloudformation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/lambdahandler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/resources/aws/stack_backed.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/templates/cloudformation/vpc.yaml
RENAMED
|
File without changes
|
{magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/templates/init/django-dotenv.env
RENAMED
|
File without changes
|
{magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/templates/init/django-settings.py
RENAMED
|
File without changes
|
{magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/templates/init/pocket.Dockerfile
RENAMED
|
File without changes
|
{magic_pocket_cli-0.26.0 → magic_pocket_cli-0.28.0}/pocket_cli/templates/init/pocket_simple.toml
RENAMED
|
File without changes
|