magic-pocket-cli 0.28.2__tar.gz → 0.30.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 (80) hide show
  1. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/PKG-INFO +2 -2
  2. magic_pocket_cli-0.28.2/pocket_cli/cli/awscontainer_cli.py → magic_pocket_cli-0.30.0/pocket_cli/cli/container_cli.py +155 -125
  3. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/deploy_cli.py +40 -27
  4. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/destroy_cli.py +131 -99
  5. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/image_cli.py +11 -20
  6. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/main_cli.py +2 -2
  7. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/migrate_cli.py +34 -2
  8. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/removed_flags.py +1 -1
  9. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/runtime_config_cli.py +17 -14
  10. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/status_cli.py +8 -6
  11. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/store_url_helper.py +4 -4
  12. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/url_helper.py +1 -1
  13. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/django_cli.py +37 -19
  14. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/mediator.py +73 -24
  15. magic_pocket_cli-0.30.0/pocket_cli/migrations.py +225 -0
  16. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/aws/cloudformation.py +6 -6
  17. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/cloudfront.py +11 -12
  18. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/cloudfront_keys.py +4 -4
  19. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/cloudfront_waf.py +5 -5
  20. magic_pocket_cli-0.28.2/pocket_cli/resources/awscontainer.py → magic_pocket_cli-0.30.0/pocket_cli/resources/container.py +13 -11
  21. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/rds.py +1 -1
  22. magic_pocket_cli-0.28.2/pocket_cli/templates/cloudformation/awscontainer.yaml → magic_pocket_cli-0.30.0/pocket_cli/templates/cloudformation/container.yaml +17 -14
  23. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pyproject.toml +2 -2
  24. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/.gitignore +0 -0
  25. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/__init__.py +0 -0
  26. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/__init__.py +0 -0
  27. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/aws_auth.py +0 -0
  28. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/backup_cli.py +0 -0
  29. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/cloudfront_cli.py +0 -0
  30. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/cloudfront_keys_cli.py +0 -0
  31. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/cloudfront_waf_cli.py +0 -0
  32. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/dsql_cli.py +0 -0
  33. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/interaction.py +0 -0
  34. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/neon_cli.py +0 -0
  35. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/permissions_cli.py +0 -0
  36. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/rds_cli.py +0 -0
  37. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/resource_helper.py +0 -0
  38. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/s3_cli.py +0 -0
  39. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/tidb_cli.py +0 -0
  40. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/upstash_cli.py +0 -0
  41. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/vpc_cli.py +0 -0
  42. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/cli/waf_cli.py +0 -0
  43. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/__init__.py +0 -0
  44. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/aws/__init__.py +0 -0
  45. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/aws/backup_common.py +0 -0
  46. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/aws/builders/__init__.py +0 -0
  47. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/aws/builders/codebuild.py +0 -0
  48. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/aws/builders/context_check.py +0 -0
  49. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/aws/builders/depot.py +0 -0
  50. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/aws/builders/docker.py +0 -0
  51. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/aws/builders/dockerignore.py +0 -0
  52. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/aws/ecr.py +0 -0
  53. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/aws/efs.py +0 -0
  54. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/aws/lambdahandler.py +0 -0
  55. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/aws/poll.py +0 -0
  56. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/aws/s3_utils.py +0 -0
  57. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/aws/stack_backed.py +0 -0
  58. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/aws/state.py +0 -0
  59. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/backup.py +0 -0
  60. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/cloudfront_acm.py +0 -0
  61. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/dsql.py +0 -0
  62. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/neon.py +0 -0
  63. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/s3.py +0 -0
  64. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/tidb.py +0 -0
  65. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/upstash.py +0 -0
  66. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/resources/vpc.py +0 -0
  67. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/templates/cloudformation/cf_function_api_host.js +0 -0
  68. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/templates/cloudformation/cf_function_basic_auth_prelude.js +0 -0
  69. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/templates/cloudformation/cf_function_host_redirect_prelude.js +0 -0
  70. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/templates/cloudformation/cf_function_spa_auth.js +0 -0
  71. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/templates/cloudformation/cf_function_spa_fallback.js +0 -0
  72. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/templates/cloudformation/cloudfront.yaml +0 -0
  73. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/templates/cloudformation/cloudfront_acm.yaml +0 -0
  74. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/templates/cloudformation/cloudfront_keys.yaml +0 -0
  75. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/templates/cloudformation/cloudfront_waf.yaml +0 -0
  76. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/templates/cloudformation/vpc.yaml +0 -0
  77. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/templates/init/django-dotenv.env +0 -0
  78. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/templates/init/django-settings.py +0 -0
  79. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.0}/pocket_cli/templates/init/pocket.Dockerfile +0 -0
  80. {magic_pocket_cli-0.28.2 → magic_pocket_cli-0.30.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.28.2
3
+ Version: 0.30.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.29,>=0.28.2
10
+ Requires-Dist: magic-pocket<0.31,>=0.30.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
@@ -7,104 +7,128 @@ import click
7
7
  from botocore.exceptions import ClientError
8
8
 
9
9
  from pocket.context import Context
10
- from pocket.runtime import get_secrets
10
+ from pocket.runtime import get_secrets, resolve_container_name
11
11
  from pocket.utils import echo
12
12
  from pocket_cli.cli.destroy_cli import (
13
- _collect_awscontainer_targets,
14
- _destroy_awscontainer,
13
+ _collect_container_targets,
14
+ _destroy_containers,
15
15
  )
16
- from pocket_cli.cli.resource_helper import require_configured
17
16
  from pocket_cli.mediator import Mediator
18
- from pocket_cli.resources.awscontainer import AwsContainer
17
+ from pocket_cli.resources.container import Container
19
18
 
20
19
 
21
20
  @click.group()
22
- def awscontainer():
21
+ def container():
23
22
  pass
24
23
 
25
24
 
26
- def get_awscontainer_resource(stage):
25
+ def get_container_resource(stage, container_name: str | None = None):
26
+ """--container 指定 (単一 container なら省略可) の Container resource を返す。"""
27
27
  context = Context.from_toml(stage=stage)
28
- return AwsContainer(
29
- context=require_configured(
30
- context.awscontainer, "awscontainer is not configured for this stage"
31
- )
32
- )
28
+ if not context.container:
29
+ raise click.ClickException("container is not configured for this stage")
30
+ try:
31
+ name = resolve_container_name(context, container_name)
32
+ except RuntimeError as e:
33
+ raise click.ClickException(str(e)) from e
34
+ return Container(context.container[name])
35
+
36
+
37
+ _container_option = click.option(
38
+ "--container",
39
+ "container_name",
40
+ default=None,
41
+ help="対象 container 名 (1 つだけなら省略可)",
42
+ )
33
43
 
34
44
 
35
- @awscontainer.command()
45
+ @container.command()
36
46
  @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
37
- def yaml(stage):
38
- ac = get_awscontainer_resource(stage)
39
- print(ac.stack.yaml)
47
+ @_container_option
48
+ def yaml(stage, container_name):
49
+ c = get_container_resource(stage, container_name)
50
+ print(c.stack.yaml)
40
51
 
41
52
 
42
- @awscontainer.command()
53
+ @container.command()
43
54
  @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
44
- def yaml_diff(stage):
45
- ac = get_awscontainer_resource(stage)
46
- print(ac.stack.yaml_diff.to_json(indent=2))
55
+ @_container_option
56
+ def yaml_diff(stage, container_name):
57
+ c = get_container_resource(stage, container_name)
58
+ print(c.stack.yaml_diff.to_json(indent=2))
47
59
 
48
60
 
49
- @awscontainer.group()
61
+ @container.group()
50
62
  def secrets():
51
63
  pass
52
64
 
53
65
 
66
+ def _secrets_views(context: Context):
67
+ """(ラベル, SecretsContext) の一覧 (shared store + 各 container store)。"""
68
+ views = []
69
+ if context.secrets:
70
+ views.append(("shared", context.secrets))
71
+ for c_name in sorted(context.container):
72
+ sc = context.container[c_name].secrets
73
+ if sc:
74
+ views.append((c_name, sc))
75
+ return views
76
+
77
+
54
78
  @secrets.command("list")
55
79
  @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
56
80
  @click.option("--show-values", is_flag=True, default=False)
57
81
  def list_secrets(stage, show_values):
58
- ac = get_awscontainer_resource(stage)
59
- sc = ac.context.secrets
60
- if not sc:
82
+ context = Context.from_toml(stage=stage)
83
+ views = _secrets_views(context)
84
+ if not views:
61
85
  echo.warning("secrets is not configured for this stage")
62
86
  return
63
- for key, spec in sc.user.items():
64
- effective_store = spec.store or sc.store
65
- print("%s: %s (store=%s)" % (key, spec.name, effective_store))
66
- if show_values:
67
- if effective_store == "sm":
68
- client = boto3.client("secretsmanager", region_name=sc.region)
69
- value = client.get_secret_value(SecretId=spec.name)["SecretString"]
70
- else:
71
- client = boto3.client("ssm", region_name=sc.region)
72
- value = client.get_parameter(Name=spec.name, WithDecryption=True)[
73
- "Parameter"
74
- ]["Value"]
75
- print(" - " + value)
76
- for key, pocket_secret in sc.managed.items():
77
- status = "CREATED" if key in sc.pocket_store.secrets else "NOEXIST"
78
- print("%s: %s %s" % (key, pocket_secret.type, pocket_secret.options))
79
- print(" - " + status)
80
- if (status == "CREATED") and show_values:
81
- value = sc.pocket_store.secrets[key]
82
- if isinstance(value, str):
87
+ for label, sc in views:
88
+ echo.info("[%s] %s" % (label, sc.pocket_key))
89
+ for key, spec in sc.user.items():
90
+ effective_store = spec.store or sc.store
91
+ print("%s: %s (store=%s)" % (key, spec.name, effective_store))
92
+ if show_values:
93
+ if effective_store == "sm":
94
+ client = boto3.client("secretsmanager", region_name=sc.region)
95
+ value = client.get_secret_value(SecretId=spec.name)["SecretString"]
96
+ else:
97
+ client = boto3.client("ssm", region_name=sc.region)
98
+ value = client.get_parameter(Name=spec.name, WithDecryption=True)[
99
+ "Parameter"
100
+ ]["Value"]
83
101
  print(" - " + value)
84
- else:
85
- for k, v in value.items():
86
- print(f" - {k}: {v}")
102
+ for key, pocket_secret in sc.managed.items():
103
+ status = "CREATED" if key in sc.pocket_store.secrets else "NOEXIST"
104
+ print("%s: %s %s" % (key, pocket_secret.type, pocket_secret.options))
105
+ print(" - " + status)
106
+ if (status == "CREATED") and show_values:
107
+ value = sc.pocket_store.secrets[key]
108
+ if isinstance(value, str):
109
+ print(" - " + value)
110
+ else:
111
+ for k, v in value.items():
112
+ print(f" - {k}: {v}")
87
113
 
88
114
 
89
115
  @secrets.command()
90
116
  @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
91
117
  def create_pocket_managed(stage):
92
- ac = get_awscontainer_resource(stage)
93
- sc = ac.context.secrets
94
- if not sc:
118
+ context = Context.from_toml(stage=stage)
119
+ if not _secrets_views(context):
95
120
  echo.warning("secrets is not configured for this stage")
96
121
  return
97
- mediator = Mediator(Context.from_toml(stage=stage))
122
+ mediator = Mediator(context)
98
123
  mediator.create_pocket_managed_secrets()
99
124
 
100
125
 
101
- def _confirm_delete_pocket_managed_secrets(awscontainer: AwsContainer):
102
- sc = awscontainer.context.secrets
103
- if not sc:
104
- echo.warning("secrets is not configured")
105
- return
126
+ def _confirm_delete_pocket_managed_secrets(views):
106
127
  existing_secret_keys = [
107
- key for key in sc.managed.keys() if key in sc.pocket_store.secrets
128
+ "%s (%s)" % (key, label)
129
+ for label, sc in views
130
+ for key in sc.managed.keys()
131
+ if key in sc.pocket_store.secrets
108
132
  ]
109
133
  if not existing_secret_keys:
110
134
  echo.warning("No pocket managed secets are created yet.")
@@ -120,41 +144,46 @@ def _confirm_delete_pocket_managed_secrets(awscontainer: AwsContainer):
120
144
  @secrets.command()
121
145
  @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
122
146
  def delete_pocket_managed(stage):
123
- ac = get_awscontainer_resource(stage)
124
- _confirm_delete_pocket_managed_secrets(ac)
125
- if ac.context.secrets:
126
- ac.context.secrets.pocket_store.delete_secrets()
147
+ context = Context.from_toml(stage=stage)
148
+ views = _secrets_views(context)
149
+ if not views:
150
+ echo.warning("secrets is not configured")
151
+ return
152
+ _confirm_delete_pocket_managed_secrets(views)
153
+ for _label, sc in views:
154
+ sc.pocket_store.delete_secrets()
127
155
 
128
156
 
129
- @awscontainer.command()
157
+ @container.command()
130
158
  @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
131
- def create(stage):
132
- ac = get_awscontainer_resource(stage)
133
- if not ac.status == "NOEXIST":
159
+ @_container_option
160
+ def create(stage, container_name):
161
+ c = get_container_resource(stage, container_name)
162
+ if not c.status == "NOEXIST":
134
163
  echo.warning("AWS lambda container is already created.")
135
164
  else:
136
165
  mediator = Mediator(Context.from_toml(stage=stage))
137
- ac.create(mediator)
166
+ c.create(mediator)
138
167
  echo.success("Created: lambda")
139
168
 
140
169
 
141
- @awscontainer.command()
170
+ @container.command()
142
171
  @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
143
172
  @click.option("--with-secrets", is_flag=True, default=False)
144
173
  @click.option(
145
174
  "--yes", "-y", is_flag=True, default=False, help="確認プロンプトをスキップ"
146
175
  )
147
176
  def destroy(stage, with_secrets, yes):
148
- """AwsContainer 関連リソースを削除する。
177
+ """Container 関連リソース (全 container) を削除する。
149
178
 
150
179
  トップレベル `pocket destroy` と同じ実装を使う (共有 ECR の削除ガード /
151
180
  stack 削除完了待ち / CodeBuild / log group 掃除を含む)。
152
181
  """
153
182
  context = Context.from_toml(stage=stage)
154
- if not context.awscontainer:
155
- echo.warning("awscontainer is not configured for this stage")
183
+ if not context.container:
184
+ echo.warning("container is not configured for this stage")
156
185
  return
157
- targets = _collect_awscontainer_targets(context, with_secrets)
186
+ targets = _collect_container_targets(context, with_secrets)
158
187
  if not targets:
159
188
  echo.warning("削除対象のリソースが見つかりません。")
160
189
  return
@@ -164,49 +193,47 @@ def destroy(stage, with_secrets, yes):
164
193
  echo.danger("この操作は取り消せません!")
165
194
  if not yes:
166
195
  click.confirm(
167
- "stage '%s' の AwsContainer リソースを削除しますか?" % stage, abort=True
196
+ "stage '%s' の Container リソースを削除しますか?" % stage, abort=True
168
197
  )
169
- _destroy_awscontainer(context, with_secrets)
198
+ _destroy_containers(context, with_secrets)
170
199
 
171
200
 
172
- @awscontainer.command()
201
+ @container.command()
173
202
  @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
174
- def update(stage):
175
- ac = get_awscontainer_resource(stage)
176
- if ac.status == "NOEXIST":
203
+ @_container_option
204
+ def update(stage, container_name):
205
+ c = get_container_resource(stage, container_name)
206
+ if c.status == "NOEXIST":
177
207
  echo.warning("AWS lambda has not created yet.")
178
208
  return
179
- if ac.status == "FAILED":
209
+ if c.status == "FAILED":
180
210
  echo.danger("AWS lambda has failed. Please check console.")
181
211
  return
182
- if ac.status == "PROGRESS":
212
+ if c.status == "PROGRESS":
183
213
  echo.warning("AWS lambda is updating. Please wait.")
184
214
  return
185
215
  mediator = Mediator(Context.from_toml(stage=stage))
186
- ac.update(mediator)
216
+ c.update(mediator)
187
217
 
188
218
 
189
- @awscontainer.command()
219
+ @container.command()
190
220
  @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
191
- def status(stage):
192
- ac = get_awscontainer_resource(stage)
193
- if ac.status == "COMPLETED":
221
+ @_container_option
222
+ def status(stage, container_name):
223
+ c = get_container_resource(stage, container_name)
224
+ if c.status == "COMPLETED":
194
225
  echo.success("Container is working!!!")
195
- elif ac.status == "NOEXIST":
226
+ elif c.status == "NOEXIST":
196
227
  echo.warning("Container has not created yet.")
197
- elif ac.status == "FAILED":
228
+ elif c.status == "FAILED":
198
229
  echo.danger("Container has failed. Please check console.")
199
230
  else:
200
- echo.warning("Container stack status: %s" % ac.stack.status)
231
+ echo.warning("Container stack status: %s" % c.stack.status)
201
232
 
202
233
 
203
- def _resolve_lambda_target_handlers(
204
- context: Context, handler_name: str | None
205
- ) -> list[str]:
234
+ def _resolve_lambda_target_handlers(c_ctx, handler_name: str | None) -> list[str]:
206
235
  """reload-env / status-env の対象 handler を解決する。"""
207
- if context.awscontainer is None:
208
- raise RuntimeError("awscontainer is not configured")
209
- handlers = context.awscontainer.handlers
236
+ handlers = c_ctx.handlers
210
237
  if handler_name:
211
238
  if handler_name not in handlers:
212
239
  raise click.ClickException(
@@ -217,15 +244,6 @@ def _resolve_lambda_target_handlers(
217
244
  return list(handlers.keys())
218
245
 
219
246
 
220
- def _function_name(context: Context, handler_key: str) -> str:
221
- if context.awscontainer is None:
222
- raise RuntimeError("awscontainer is not configured")
223
- # deploy 側 (LambdaHandlerContext.function_name = resource_prefix + key) と同じ
224
- # 正準名を参照する。slug から再構成すると prefix_template / namespace
225
- # (既定 `pocket`) を取りこぼすため、handler context の値をそのまま使う。
226
- return context.awscontainer.handlers[handler_key].function_name
227
-
228
-
229
247
  def _fetch_lambda_env(client, function_name: str) -> dict[str, str]:
230
248
  """Lambda の現状 Environment.Variables を取得する。"""
231
249
  try:
@@ -240,12 +258,13 @@ def _fetch_lambda_env(client, function_name: str) -> dict[str, str]:
240
258
  return dict(config.get("Environment", {}).get("Variables", {}))
241
259
 
242
260
 
243
- @awscontainer.command("reload-env")
261
+ @container.command("reload-env")
244
262
  @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
263
+ @_container_option
245
264
  @click.option(
246
265
  "--handler", default=None, help="特定 handler のみ対象 (省略時は全 handler)"
247
266
  )
248
- def reload_env(stage, handler):
267
+ def reload_env(stage, container_name, handler):
249
268
  """SSM/Secrets Manager の最新値で Lambda env を即時更新する (CFn を介さない)。
250
269
 
251
270
  deploy 時の CFn snapshot を base に、secrets (managed + user) の最新値を
@@ -257,19 +276,24 @@ def reload_env(stage, handler):
257
276
  実体は CLI で直接更新、次 deploy で自己治癒)。
258
277
  """
259
278
  context = Context.from_toml(stage=stage)
260
- if not context.awscontainer:
261
- raise click.ClickException("[awscontainer] が設定されていません")
279
+ if not context.container:
280
+ raise click.ClickException("[container.<name>] が設定されていません")
281
+ try:
282
+ c_name = resolve_container_name(context, container_name)
283
+ except RuntimeError as e:
284
+ raise click.ClickException(str(e)) from e
285
+ c_ctx = context.container[c_name]
262
286
 
263
- fresh_secrets = get_secrets(stage)
287
+ fresh_secrets = get_secrets(stage, container=c_name)
264
288
  if not fresh_secrets:
265
289
  echo.warning("secrets が宣言されていません。何もしません。")
266
290
  return
267
291
 
268
- lambda_client = boto3.client("lambda", region_name=context.awscontainer.region)
269
- targets = _resolve_lambda_target_handlers(context, handler)
292
+ lambda_client = boto3.client("lambda", region_name=c_ctx.region)
293
+ targets = _resolve_lambda_target_handlers(c_ctx, handler)
270
294
 
271
295
  for h_name in targets:
272
- function_name = _function_name(context, h_name)
296
+ function_name = c_ctx.handlers[h_name].function_name
273
297
  current = _fetch_lambda_env(lambda_client, function_name)
274
298
  new_env = {**current, **fresh_secrets}
275
299
  if new_env == current:
@@ -288,24 +312,30 @@ def reload_env(stage, handler):
288
312
  echo.log(" - %s" % k)
289
313
 
290
314
 
291
- @awscontainer.command("status-env")
315
+ @container.command("status-env")
292
316
  @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
317
+ @_container_option
293
318
  @click.option(
294
319
  "--handler", default=None, help="特定 handler のみ対象 (省略時は全 handler)"
295
320
  )
296
- def status_env(stage, handler):
321
+ def status_env(stage, container_name, handler):
297
322
  """Lambda の現在 env と SSM/SM 上の宣言値の drift を表示する。"""
298
323
  context = Context.from_toml(stage=stage)
299
- if not context.awscontainer:
300
- raise click.ClickException("[awscontainer] が設定されていません")
324
+ if not context.container:
325
+ raise click.ClickException("[container.<name>] が設定されていません")
326
+ try:
327
+ c_name = resolve_container_name(context, container_name)
328
+ except RuntimeError as e:
329
+ raise click.ClickException(str(e)) from e
330
+ c_ctx = context.container[c_name]
301
331
 
302
- fresh_secrets = get_secrets(stage)
303
- lambda_client = boto3.client("lambda", region_name=context.awscontainer.region)
304
- targets = _resolve_lambda_target_handlers(context, handler)
332
+ fresh_secrets = get_secrets(stage, container=c_name)
333
+ lambda_client = boto3.client("lambda", region_name=c_ctx.region)
334
+ targets = _resolve_lambda_target_handlers(c_ctx, handler)
305
335
 
306
336
  any_drift = False
307
337
  for h_name in targets:
308
- function_name = _function_name(context, h_name)
338
+ function_name = c_ctx.handlers[h_name].function_name
309
339
  current = _fetch_lambda_env(lambda_client, function_name)
310
340
  drift = [k for k in fresh_secrets if current.get(k) != fresh_secrets[k]]
311
341
  echo.info(
@@ -321,20 +351,20 @@ def status_env(stage, handler):
321
351
  any_drift = True
322
352
  if any_drift:
323
353
  echo.warning(
324
- "drift があります。"
325
- "`pocket resource awscontainer reload-env` で同期できます。"
354
+ "drift があります。`pocket resource container reload-env` で同期できます。"
326
355
  )
327
356
  else:
328
357
  echo.success("drift なし。Lambda env と secrets は同期されています。")
329
358
 
330
359
 
331
- @awscontainer.command()
360
+ @container.command()
332
361
  @click.option("--stage", envvar="POCKET_DEPLOY_STAGE", prompt=True)
362
+ @_container_option
333
363
  @click.option("--openpath")
334
- def url(stage, openpath):
335
- ac = get_awscontainer_resource(stage)
336
- if ac.status == "COMPLETED":
337
- if endpoint := ac.endpoints.get("wsgi"):
364
+ def url(stage, container_name, openpath):
365
+ c = get_container_resource(stage, container_name)
366
+ if c.status == "COMPLETED":
367
+ if endpoint := c.endpoints.get("wsgi"):
338
368
  echo.success(f"wsgi url: {endpoint}")
339
369
  if openpath:
340
370
  webbrowser.open(endpoint + "/" + openpath)
@@ -5,16 +5,17 @@ import click
5
5
 
6
6
  from pocket.context import Context, deploy_hash_report
7
7
  from pocket.utils import echo
8
+ from pocket_cli import migrations
8
9
  from pocket_cli.cli import interaction
9
10
  from pocket_cli.cli.removed_flags import removed_skip_check_existing
10
11
  from pocket_cli.mediator import Mediator
11
12
  from pocket_cli.resources.aws.state import StateStore, create_state_store
12
- from pocket_cli.resources.awscontainer import AwsContainer
13
13
  from pocket_cli.resources.backup import Backup
14
14
  from pocket_cli.resources.cloudfront import CloudFront
15
15
  from pocket_cli.resources.cloudfront_acm import CloudFrontAcm
16
16
  from pocket_cli.resources.cloudfront_keys import CloudFrontKeys
17
17
  from pocket_cli.resources.cloudfront_waf import CloudFrontWaf
18
+ from pocket_cli.resources.container import Container
18
19
  from pocket_cli.resources.dsql import Dsql
19
20
  from pocket_cli.resources.neon import Neon
20
21
  from pocket_cli.resources.rds import Rds
@@ -25,10 +26,12 @@ from pocket_cli.resources.vpc import Vpc
25
26
 
26
27
 
27
28
  def _append_infra_resources(resources, context: Context, state_bucket: str):
28
- """VPC / RDS / CloudFrontKeys / AwsContainer をまとめて追加"""
29
- if context.awscontainer and context.awscontainer.vpc:
30
- if context.awscontainer.vpc.manage:
31
- resources.append(Vpc(context.awscontainer.vpc))
29
+ """VPC / RDS / CloudFrontKeys / Container をまとめて追加"""
30
+ managed_vpc_names: set[str] = set()
31
+ for c_ctx in context.container.values():
32
+ if c_ctx.vpc and c_ctx.vpc.manage and c_ctx.vpc.name not in managed_vpc_names:
33
+ managed_vpc_names.add(c_ctx.vpc.name)
34
+ resources.append(Vpc(c_ctx.vpc))
32
35
  if context.dsql:
33
36
  resources.append(Dsql(context.dsql))
34
37
  if context.rds:
@@ -42,14 +45,14 @@ def _append_infra_resources(resources, context: Context, state_bucket: str):
42
45
  for _name, cf_ctx in context.cloudfront.items():
43
46
  if cf_ctx.signing_key:
44
47
  resources.append(CloudFrontKeys(cf_ctx))
45
- if context.awscontainer:
48
+ for c_name in sorted(context.container):
46
49
  resources.append(
47
- AwsContainer(
48
- context.awscontainer,
50
+ Container(
51
+ context.container[c_name],
49
52
  state_bucket=state_bucket,
50
53
  rds_context=context.rds,
51
54
  dsql_context=context.dsql,
52
- scheduler_context=context.scheduler,
55
+ scheduler_context=context.scheduler.get(c_name),
53
56
  )
54
57
  )
55
58
 
@@ -174,25 +177,30 @@ def deploy_resources(context: Context, *, state_bucket: str = ""):
174
177
  hook()
175
178
 
176
179
 
177
- def build_image(context: Context, *, tag: str) -> str:
178
- """awscontainer image を指定 tag で build & push する (deploy はしない)。
180
+ def build_image(context: Context, *, tag: str) -> list[str]:
181
+ """全 container image を指定 tag で build & push する (deploy はしない)。
179
182
 
180
183
  build once 用。codebuild backend は source upload に state bucket を要するため、
181
- deploy と同様に先に state bucket を確保してから build する。戻り値は ecr_name:tag。
184
+ deploy と同様に先に state bucket を確保してから build する。
185
+ 戻り値は ecr_name:tag のリスト (container 名順)。
182
186
  """
183
- if context.awscontainer is None:
184
- raise click.ClickException("awscontainer がこの stage に設定されていません。")
187
+ if not context.container:
188
+ raise click.ClickException("container がこの stage に設定されていません。")
185
189
  state_store = _create_state_store(context)
186
190
  state_store.ensure_bucket()
187
- ac = AwsContainer(context.awscontainer, state_bucket=state_store.bucket_name)
188
- ac.build(tag)
189
- return f"{context.awscontainer.ecr_name}:{tag}"
191
+ targets = []
192
+ for c_name in sorted(context.container):
193
+ c_ctx = context.container[c_name]
194
+ echo.log("Building container '%s'..." % c_name)
195
+ Container(c_ctx, state_bucket=state_store.bucket_name).build(tag)
196
+ targets.append(f"{c_ctx.ecr_name}:{tag}")
197
+ return targets
190
198
 
191
199
 
192
200
  def _deploy_pipeline(context: Context, *, openpath=None, skip_frontend=False):
193
201
  """deploy / promote 共通のパイプライン本体。
194
202
 
195
- promote 時は context.awscontainer.promote_commit_hash が設定済みで、
203
+ promote 時は各 container の promote_commit_hash が設定済みで、
196
204
  deploy_init 内の image build が retag に置き換わる以外は deploy と同一。
197
205
  """
198
206
  # DEPLOY_HASH の解決結果を deploy 時に 1 回可視化する (env 伝播漏れで
@@ -206,6 +214,9 @@ def _deploy_pipeline(context: Context, *, openpath=None, skip_frontend=False):
206
214
  state_bucket = state_store.bucket_name
207
215
  deploy_init_resources(context, state_bucket=state_bucket)
208
216
  deploy_resources(context, state_bucket=state_bucket)
217
+ # リリース跨ぎ移行の掃除フェーズ (旧配置の削除は deploy 成功後にしか
218
+ # できないため、cloudfront 切替完了後のここで毎回呼ぶ。冪等)
219
+ migrations.run_deploy_cleanup(context)
209
220
  upload_managed_assets(context)
210
221
  if not skip_frontend:
211
222
  deploy_frontend(context)
@@ -254,9 +265,10 @@ def promote(stage: str, commit_hash, openpath, skip_frontend, yes):
254
265
  interaction.set_assume_yes(yes)
255
266
  check_aws_credentials()
256
267
  context = Context.from_toml(stage=stage)
257
- if context.awscontainer is None:
258
- raise click.ClickException("awscontainer がこの stage に設定されていません。")
259
- context.awscontainer.promote_commit_hash = commit_hash
268
+ if not context.container:
269
+ raise click.ClickException("container がこの stage に設定されていません。")
270
+ for c_ctx in context.container.values():
271
+ c_ctx.promote_commit_hash = commit_hash
260
272
  _deploy_pipeline(context, openpath=openpath, skip_frontend=skip_frontend)
261
273
 
262
274
 
@@ -276,10 +288,11 @@ def _get_deploy_url(context: Context) -> str | None:
276
288
  if domain:
277
289
  return f"https://{domain}"
278
290
 
279
- # フォールバック: API Gateway
280
- if context.awscontainer:
281
- ac = AwsContainer(context.awscontainer)
282
- endpoint = ac.endpoints.get("wsgi")
283
- if endpoint:
284
- return endpoint
291
+ # フォールバック: API Gateway (container 名順で最初に見つかった endpoint)
292
+ for c_name in sorted(context.container):
293
+ endpoints = Container(context.container[c_name]).endpoints
294
+ if "wsgi" in endpoints:
295
+ return endpoints["wsgi"]
296
+ if endpoints:
297
+ return next(iter(endpoints.values()))
285
298
  return None