azure-deploy-cli 0.1.11__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- azure_deploy_cli/_version.py +2 -2
- azure_deploy_cli/aca/aca_cli.py +41 -75
- azure_deploy_cli/aca/deploy_aca.py +111 -60
- azure_deploy_cli/aca/model.py +46 -0
- azure_deploy_cli/aca/yaml_loader.py +53 -0
- azure_deploy_cli/utils/docker.py +43 -33
- azure_deploy_cli/utils/logging.py +1 -1
- {azure_deploy_cli-0.1.11.dist-info → azure_deploy_cli-1.0.0.dist-info}/METADATA +62 -9
- {azure_deploy_cli-0.1.11.dist-info → azure_deploy_cli-1.0.0.dist-info}/RECORD +13 -12
- {azure_deploy_cli-0.1.11.dist-info → azure_deploy_cli-1.0.0.dist-info}/WHEEL +0 -0
- {azure_deploy_cli-0.1.11.dist-info → azure_deploy_cli-1.0.0.dist-info}/entry_points.txt +0 -0
- {azure_deploy_cli-0.1.11.dist-info → azure_deploy_cli-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {azure_deploy_cli-0.1.11.dist-info → azure_deploy_cli-1.0.0.dist-info}/top_level.txt +0 -0
azure_deploy_cli/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.
|
|
32
|
-
__version_tuple__ = version_tuple = (
|
|
31
|
+
__version__ = version = '1.0.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 0, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
azure_deploy_cli/aca/aca_cli.py
CHANGED
|
@@ -15,14 +15,13 @@ from ..utils.logging import get_logger
|
|
|
15
15
|
from .deploy_aca import (
|
|
16
16
|
SecretKeyVaultConfig,
|
|
17
17
|
bind_aca_managed_certificate,
|
|
18
|
-
build_acr_image,
|
|
19
18
|
create_container_app_env,
|
|
20
19
|
deploy_revision,
|
|
21
20
|
generate_revision_suffix,
|
|
22
|
-
get_aca_docker_image_name,
|
|
23
21
|
update_traffic_weights,
|
|
24
22
|
validate_revision_suffix_and_throw,
|
|
25
23
|
)
|
|
24
|
+
from .yaml_loader import ContainerAppConfig, load_app_config_yaml
|
|
26
25
|
|
|
27
26
|
logger = get_logger(__name__)
|
|
28
27
|
|
|
@@ -92,16 +91,16 @@ def _validate_cli_deploy(args: Any):
|
|
|
92
91
|
|
|
93
92
|
def cli_deploy(args: Any) -> None:
|
|
94
93
|
"""
|
|
95
|
-
|
|
96
|
-
Deploy Azure Container App revision without updating traffic.
|
|
94
|
+
Deploy Azure Container App revision from YAML configuration without updating traffic.
|
|
97
95
|
|
|
98
96
|
This command orchestrates:
|
|
99
|
-
1.
|
|
100
|
-
2.
|
|
101
|
-
3.
|
|
102
|
-
4.
|
|
103
|
-
5.
|
|
104
|
-
6.
|
|
97
|
+
1. Load container configuration from YAML
|
|
98
|
+
2. Create/get user-assigned managed identity (if specified)
|
|
99
|
+
3. Assign roles to the identity (if role config provided)
|
|
100
|
+
4. Build/push container images for all containers
|
|
101
|
+
5. Deploy new revision with 0% traffic
|
|
102
|
+
6. Verify revision activation and health
|
|
103
|
+
7. Output the revision name for use in traffic management
|
|
105
104
|
|
|
106
105
|
Args:
|
|
107
106
|
args: Parsed command line arguments
|
|
@@ -128,24 +127,9 @@ def cli_deploy(args: Any) -> None:
|
|
|
128
127
|
else generate_revision_suffix(stage=args.stage)
|
|
129
128
|
)
|
|
130
129
|
|
|
131
|
-
logger.critical("
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
registry_server=args.registry_server,
|
|
135
|
-
dockerfile=args.dockerfile,
|
|
136
|
-
full_image_name=get_aca_docker_image_name(
|
|
137
|
-
registry_server=args.registry_server,
|
|
138
|
-
image_name=args.image_name,
|
|
139
|
-
image_tag=revision_suffix,
|
|
140
|
-
),
|
|
141
|
-
source_full_image_name=get_aca_docker_image_name(
|
|
142
|
-
registry_server=args.registry_server,
|
|
143
|
-
image_name=args.image_name,
|
|
144
|
-
image_tag=args.existing_image_tag,
|
|
145
|
-
)
|
|
146
|
-
if args.existing_image_tag
|
|
147
|
-
else None,
|
|
148
|
-
)
|
|
130
|
+
logger.critical(f"Loading container configuration from '{args.container_config}'...")
|
|
131
|
+
app_config: ContainerAppConfig = load_app_config_yaml(args.container_config)
|
|
132
|
+
logger.critical(f"Loaded configuration with {len(app_config.containers)} container(s)")
|
|
149
133
|
|
|
150
134
|
logger.critical("Setting up managed identity and roles...")
|
|
151
135
|
user_identity = create_or_get_user_identity(
|
|
@@ -180,14 +164,13 @@ def cli_deploy(args: Any) -> None:
|
|
|
180
164
|
registry_server=args.registry_server,
|
|
181
165
|
registry_user=registry_user,
|
|
182
166
|
registry_pass_env_name=REGISTRY_PASS_SECRET_ENV_NAME,
|
|
183
|
-
image_name=args.image_name,
|
|
184
|
-
image_tag=revision_suffix,
|
|
185
167
|
revision_suffix=revision_suffix,
|
|
186
168
|
location=args.location,
|
|
187
169
|
stage=args.stage,
|
|
170
|
+
container_configs=app_config.containers,
|
|
188
171
|
target_port=args.target_port,
|
|
189
|
-
|
|
190
|
-
|
|
172
|
+
ingress_external=args.ingress_external,
|
|
173
|
+
ingress_transport=args.ingress_transport,
|
|
191
174
|
min_replicas=args.min_replicas,
|
|
192
175
|
max_replicas=args.max_replicas,
|
|
193
176
|
secret_key_vault_config=SecretKeyVaultConfig(
|
|
@@ -196,8 +179,6 @@ def cli_deploy(args: Any) -> None:
|
|
|
196
179
|
secret_names=args.env_var_secrets or [],
|
|
197
180
|
user_identity=user_identity,
|
|
198
181
|
),
|
|
199
|
-
env_var_names=args.env_vars,
|
|
200
|
-
existing_image_tag=args.existing_image_tag,
|
|
201
182
|
)
|
|
202
183
|
|
|
203
184
|
if args.custom_domains:
|
|
@@ -352,65 +333,57 @@ def add_commands(subparsers: argparse._SubParsersAction) -> None:
|
|
|
352
333
|
help="Container registry server.",
|
|
353
334
|
)
|
|
354
335
|
deploy_parser.add_argument(
|
|
355
|
-
"--
|
|
336
|
+
"--keyvault-name",
|
|
356
337
|
required=True,
|
|
357
338
|
type=str,
|
|
358
|
-
help="Name of the
|
|
339
|
+
help="Name of the Key Vault for storing secrets.",
|
|
359
340
|
)
|
|
341
|
+
|
|
360
342
|
deploy_parser.add_argument(
|
|
361
|
-
"--
|
|
362
|
-
required=
|
|
343
|
+
"--stage",
|
|
344
|
+
required=True,
|
|
363
345
|
type=str,
|
|
364
|
-
help=(
|
|
365
|
-
"Tag of an existing image to retag to the revision suffix. "
|
|
366
|
-
"If provided, the image with this tag will be pulled from the registry, "
|
|
367
|
-
"retagged to the revision suffix, and pushed. "
|
|
368
|
-
"If the image doesn't exist, deployment will fail."
|
|
369
|
-
),
|
|
346
|
+
help="Deployment stage label (e.g., staging, prod) used for revision naming.",
|
|
370
347
|
)
|
|
348
|
+
|
|
371
349
|
deploy_parser.add_argument(
|
|
372
350
|
"--target-port",
|
|
373
351
|
required=True,
|
|
374
352
|
type=int,
|
|
375
|
-
help="Target port for the container app.",
|
|
353
|
+
help="Target port for the container app ingress.",
|
|
376
354
|
)
|
|
355
|
+
|
|
377
356
|
deploy_parser.add_argument(
|
|
378
|
-
"--
|
|
379
|
-
required=
|
|
380
|
-
type=
|
|
381
|
-
|
|
357
|
+
"--ingress-external",
|
|
358
|
+
required=False,
|
|
359
|
+
type=bool,
|
|
360
|
+
default=True,
|
|
361
|
+
help="Whether ingress is external (default: True).",
|
|
382
362
|
)
|
|
363
|
+
|
|
383
364
|
deploy_parser.add_argument(
|
|
384
|
-
"--
|
|
385
|
-
required=
|
|
365
|
+
"--ingress-transport",
|
|
366
|
+
required=False,
|
|
386
367
|
type=str,
|
|
387
|
-
|
|
368
|
+
default="auto",
|
|
369
|
+
choices=["auto", "http", "http2", "tcp"],
|
|
370
|
+
help="Ingress transport protocol (default: auto).",
|
|
388
371
|
)
|
|
372
|
+
|
|
389
373
|
deploy_parser.add_argument(
|
|
390
374
|
"--min-replicas",
|
|
391
375
|
required=True,
|
|
392
376
|
type=int,
|
|
393
377
|
help="Minimum number of replicas for the container app.",
|
|
394
378
|
)
|
|
379
|
+
|
|
395
380
|
deploy_parser.add_argument(
|
|
396
381
|
"--max-replicas",
|
|
397
382
|
required=True,
|
|
398
383
|
type=int,
|
|
399
384
|
help="Maximum number of replicas for the container app.",
|
|
400
385
|
)
|
|
401
|
-
deploy_parser.add_argument(
|
|
402
|
-
"--keyvault-name",
|
|
403
|
-
required=True,
|
|
404
|
-
type=str,
|
|
405
|
-
help="Name of the Key Vault for storing secrets.",
|
|
406
|
-
)
|
|
407
386
|
|
|
408
|
-
deploy_parser.add_argument(
|
|
409
|
-
"--stage",
|
|
410
|
-
required=True,
|
|
411
|
-
type=str,
|
|
412
|
-
help="Deployment stage label (e.g., staging, prod) used for revision naming.",
|
|
413
|
-
)
|
|
414
387
|
deploy_parser.add_argument(
|
|
415
388
|
"--env-var-secrets",
|
|
416
389
|
required=False,
|
|
@@ -419,14 +392,6 @@ def add_commands(subparsers: argparse._SubParsersAction) -> None:
|
|
|
419
392
|
help="Space-separated names of environment variables to be stored as secrets in Key Vault.",
|
|
420
393
|
)
|
|
421
394
|
|
|
422
|
-
deploy_parser.add_argument(
|
|
423
|
-
"--env-vars",
|
|
424
|
-
required=False,
|
|
425
|
-
type=str,
|
|
426
|
-
nargs="+",
|
|
427
|
-
help="Space-separated names of environment variables to pass to the container.",
|
|
428
|
-
)
|
|
429
|
-
|
|
430
395
|
deploy_parser.add_argument(
|
|
431
396
|
"--role-config",
|
|
432
397
|
required=False,
|
|
@@ -458,10 +423,11 @@ def add_commands(subparsers: argparse._SubParsersAction) -> None:
|
|
|
458
423
|
)
|
|
459
424
|
|
|
460
425
|
deploy_parser.add_argument(
|
|
461
|
-
"--
|
|
426
|
+
"--container-config",
|
|
462
427
|
required=True,
|
|
463
|
-
type=
|
|
464
|
-
help="Path to
|
|
428
|
+
type=Path,
|
|
429
|
+
help="Path to YAML file containing container configurations "
|
|
430
|
+
"(includes image names, cpu, memory, env_vars, probes, ingress, and scale settings)",
|
|
465
431
|
)
|
|
466
432
|
|
|
467
433
|
deploy_parser.set_defaults(func=cli_deploy)
|
|
@@ -30,13 +30,15 @@ from azure.mgmt.appcontainers.models import (
|
|
|
30
30
|
TrafficWeight,
|
|
31
31
|
UserAssignedIdentity,
|
|
32
32
|
)
|
|
33
|
-
from azure.mgmt.appcontainers.models import
|
|
33
|
+
from azure.mgmt.appcontainers.models import (
|
|
34
|
+
Configuration as ContainerAppConfiguration,
|
|
35
|
+
)
|
|
34
36
|
from azure.mgmt.keyvault.models import SecretCreateOrUpdateParameters, SecretProperties
|
|
35
37
|
|
|
36
38
|
from ..identity.models import ManagedIdentity
|
|
37
39
|
from ..utils import docker
|
|
38
40
|
from ..utils.logging import get_logger
|
|
39
|
-
from .model import RevisionDeploymentResult, SecretKeyVaultConfig
|
|
41
|
+
from .model import ContainerConfig, RevisionDeploymentResult, SecretKeyVaultConfig
|
|
40
42
|
|
|
41
43
|
logger = get_logger(__name__)
|
|
42
44
|
|
|
@@ -195,6 +197,45 @@ def create_container_app_env(
|
|
|
195
197
|
return None
|
|
196
198
|
|
|
197
199
|
|
|
200
|
+
def build_container_images(
|
|
201
|
+
container_configs: list[ContainerConfig],
|
|
202
|
+
registry_server: str,
|
|
203
|
+
revision_suffix: str,
|
|
204
|
+
) -> list[str]:
|
|
205
|
+
image_names = []
|
|
206
|
+
|
|
207
|
+
for container_config in container_configs:
|
|
208
|
+
image_tag = revision_suffix
|
|
209
|
+
target_full_image_name = get_aca_docker_image_name(
|
|
210
|
+
registry_server, container_config.image_name, image_tag
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if container_config.existing_image_tag:
|
|
214
|
+
logger.info(
|
|
215
|
+
f"Retagging existing image '{container_config.image_name}:"
|
|
216
|
+
f"{container_config.existing_image_tag}' to '{image_tag}'..."
|
|
217
|
+
)
|
|
218
|
+
source_full_image_name = get_aca_docker_image_name(
|
|
219
|
+
registry_server, container_config.image_name, container_config.existing_image_tag
|
|
220
|
+
)
|
|
221
|
+
docker.pull_retag_and_push_image(source_full_image_name, target_full_image_name)
|
|
222
|
+
logger.success(f"Image retagged successfully to '{image_tag}'")
|
|
223
|
+
elif container_config.dockerfile:
|
|
224
|
+
logger.info(
|
|
225
|
+
f"Building image '{container_config.image_name}' from "
|
|
226
|
+
f"Dockerfile '{container_config.dockerfile}'..."
|
|
227
|
+
)
|
|
228
|
+
build_acr_image(
|
|
229
|
+
dockerfile=container_config.dockerfile,
|
|
230
|
+
full_image_name=target_full_image_name,
|
|
231
|
+
registry_server=registry_server,
|
|
232
|
+
)
|
|
233
|
+
logger.success("Image built successfully")
|
|
234
|
+
image_names.append(target_full_image_name)
|
|
235
|
+
|
|
236
|
+
return image_names
|
|
237
|
+
|
|
238
|
+
|
|
198
239
|
def deploy_revision(
|
|
199
240
|
client: ContainerAppsAPIClient,
|
|
200
241
|
subscription_id: str,
|
|
@@ -205,84 +246,106 @@ def deploy_revision(
|
|
|
205
246
|
registry_server: str,
|
|
206
247
|
registry_user: str,
|
|
207
248
|
registry_pass_env_name: str,
|
|
208
|
-
|
|
209
|
-
image_tag: str,
|
|
249
|
+
revision_suffix: str,
|
|
210
250
|
location: str,
|
|
211
251
|
stage: str,
|
|
252
|
+
container_configs: list[ContainerConfig], # list[ContainerConfig] from yaml_loader
|
|
212
253
|
target_port: int,
|
|
213
|
-
|
|
214
|
-
|
|
254
|
+
ingress_external: bool,
|
|
255
|
+
ingress_transport: str,
|
|
215
256
|
min_replicas: int,
|
|
216
257
|
max_replicas: int,
|
|
217
258
|
secret_key_vault_config: SecretKeyVaultConfig,
|
|
218
|
-
env_var_names: list[str],
|
|
219
|
-
revision_suffix: str,
|
|
220
|
-
existing_image_tag: str | None = None,
|
|
221
259
|
) -> RevisionDeploymentResult:
|
|
222
260
|
"""
|
|
223
|
-
Deploy a new revision without updating traffic weights.
|
|
261
|
+
Deploy a new revision with multiple containers without updating traffic weights.
|
|
224
262
|
|
|
225
263
|
This function creates a new revision with existing traffic preserved and checks
|
|
226
264
|
if the activation succeeds. Returns revision information for use in subsequent
|
|
227
265
|
traffic management operations.
|
|
228
266
|
|
|
229
|
-
The image_tag parameter should be the revision suffix when building images with
|
|
230
|
-
revision-specific tags for rollback support.
|
|
231
|
-
|
|
232
267
|
Args:
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
268
|
+
client: ContainerAppsAPIClient instance
|
|
269
|
+
subscription_id: Azure subscription ID
|
|
270
|
+
resource_group: Resource group name
|
|
271
|
+
container_app_env: Managed environment for the container app
|
|
272
|
+
user_identity: User-assigned managed identity
|
|
273
|
+
container_app_name: Name of the container app
|
|
274
|
+
registry_server: Container registry server URL
|
|
275
|
+
registry_user: Registry username
|
|
276
|
+
registry_pass_env_name: Name of the registry password environment variable
|
|
277
|
+
revision_suffix: Revision suffix for naming
|
|
278
|
+
location: Azure location
|
|
279
|
+
stage: Deployment stage label
|
|
280
|
+
container_configs: List of ContainerConfig objects from YAML
|
|
281
|
+
target_port: Target port for ingress
|
|
282
|
+
ingress_external: Whether ingress is external
|
|
283
|
+
ingress_transport: Ingress transport protocol
|
|
284
|
+
min_replicas: Minimum number of replicas
|
|
285
|
+
max_replicas: Maximum number of replicas
|
|
286
|
+
secret_key_vault_config: Key Vault configuration for secrets
|
|
239
287
|
|
|
240
288
|
Returns:
|
|
241
289
|
RevisionDeploymentResult with revision name and status information
|
|
242
290
|
|
|
243
291
|
Raises:
|
|
244
|
-
RuntimeError: If the deployment fails or
|
|
245
|
-
but the image doesn't exist
|
|
292
|
+
RuntimeError: If the deployment fails or image operations fail
|
|
246
293
|
"""
|
|
294
|
+
validate_revision_suffix_and_throw(revision_suffix, stage)
|
|
295
|
+
|
|
247
296
|
logger.info(f"Deploying new revision for Container App '{container_app_name}'...")
|
|
297
|
+
logger.info(f"Building and deploying {len(container_configs)} container(s)...")
|
|
298
|
+
|
|
248
299
|
secret_key_vault_config.secret_names.append(registry_pass_env_name)
|
|
249
|
-
secrets,
|
|
300
|
+
secrets, env_vars_dict = _prepare_secrets_and_env_vars(
|
|
250
301
|
secret_config=secret_key_vault_config,
|
|
251
302
|
subscription_id=subscription_id,
|
|
252
|
-
env_var_names=
|
|
303
|
+
env_var_names=[
|
|
304
|
+
env_var
|
|
305
|
+
for container_config in container_configs
|
|
306
|
+
for env_var in container_config.env_vars
|
|
307
|
+
],
|
|
253
308
|
resource_group=resource_group,
|
|
254
309
|
)
|
|
255
310
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
#
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
311
|
+
full_image_names = build_container_images(container_configs, registry_server, revision_suffix)
|
|
312
|
+
if len(full_image_names) != len(container_configs):
|
|
313
|
+
raise RuntimeError("Mismatch in number of built images and container configurations.")
|
|
314
|
+
|
|
315
|
+
# prepare container definitions
|
|
316
|
+
containers: list[Container] = []
|
|
317
|
+
for target_full_image_name, container_config in zip(
|
|
318
|
+
full_image_names, container_configs, strict=True
|
|
319
|
+
):
|
|
320
|
+
container_env_vars = [
|
|
321
|
+
env_var for env_var in env_vars_dict if env_var.name in container_config.env_vars
|
|
322
|
+
]
|
|
323
|
+
containers.append(
|
|
324
|
+
Container(
|
|
325
|
+
image=target_full_image_name,
|
|
326
|
+
name=container_config.name,
|
|
327
|
+
env=container_env_vars,
|
|
328
|
+
resources=ContainerResources(
|
|
329
|
+
cpu=container_config.cpu, memory=container_config.memory
|
|
330
|
+
),
|
|
331
|
+
probes=container_config.probes,
|
|
332
|
+
)
|
|
265
333
|
)
|
|
266
|
-
target_full_image_name = get_aca_docker_image_name(registry_server, image_name, image_tag)
|
|
267
|
-
|
|
268
|
-
try:
|
|
269
|
-
docker.pull_retag_and_push_image(source_full_image_name, target_full_image_name)
|
|
270
|
-
except RuntimeError as e:
|
|
271
|
-
logger.error(f"Failed to retag image from '{existing_image_tag}' to '{image_tag}': {e}")
|
|
272
|
-
raise RuntimeError(
|
|
273
|
-
f"Image with tag '{existing_image_tag}' does not exist or retagging failed. "
|
|
274
|
-
f"Please ensure the source image exists in the registry."
|
|
275
|
-
) from e
|
|
276
334
|
|
|
335
|
+
# prepare ingress with existing traffic weights
|
|
277
336
|
existing_app = _get_container_app(client, resource_group, container_app_name)
|
|
278
337
|
existing_traffic_weights = None
|
|
279
338
|
if existing_app and existing_app.configuration and existing_app.configuration.ingress:
|
|
280
339
|
existing_traffic_weights = existing_app.configuration.ingress.traffic
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
340
|
+
ingress: Ingress = Ingress(
|
|
341
|
+
external=ingress_external,
|
|
342
|
+
target_port=target_port,
|
|
343
|
+
transport=ingress_transport,
|
|
344
|
+
traffic=existing_traffic_weights, # Preserve existing traffic
|
|
285
345
|
)
|
|
346
|
+
|
|
347
|
+
revision_name = generate_revision_name(container_app_name, revision_suffix, stage)
|
|
348
|
+
logger.info(f"Deploying revision '{revision_name}' with existing traffic preserved")
|
|
286
349
|
poller = client.container_apps.begin_create_or_update(
|
|
287
350
|
resource_group_name=resource_group,
|
|
288
351
|
container_app_name=container_app_name,
|
|
@@ -290,12 +353,7 @@ def deploy_revision(
|
|
|
290
353
|
location=location,
|
|
291
354
|
environment_id=container_app_env.id,
|
|
292
355
|
configuration=ContainerAppConfiguration(
|
|
293
|
-
ingress=
|
|
294
|
-
external=True,
|
|
295
|
-
target_port=target_port,
|
|
296
|
-
transport="auto",
|
|
297
|
-
traffic=existing_traffic_weights, # Preserve existing traffic
|
|
298
|
-
),
|
|
356
|
+
ingress=ingress,
|
|
299
357
|
registries=[
|
|
300
358
|
RegistryCredentials(
|
|
301
359
|
server=registry_server,
|
|
@@ -308,14 +366,7 @@ def deploy_revision(
|
|
|
308
366
|
),
|
|
309
367
|
template=Template(
|
|
310
368
|
revision_suffix=revision_suffix,
|
|
311
|
-
containers=
|
|
312
|
-
Container(
|
|
313
|
-
image=full_image_name,
|
|
314
|
-
name=container_app_name,
|
|
315
|
-
env=env_vars,
|
|
316
|
-
resources=ContainerResources(cpu=cpu, memory=f"{memory}Gi"),
|
|
317
|
-
)
|
|
318
|
-
],
|
|
369
|
+
containers=containers,
|
|
319
370
|
scale=Scale(min_replicas=min_replicas, max_replicas=max_replicas),
|
|
320
371
|
),
|
|
321
372
|
identity=ManagedServiceIdentity(
|
azure_deploy_cli/aca/model.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
+
from typing import Any
|
|
2
3
|
|
|
4
|
+
from azure.mgmt.appcontainers.models import ContainerAppProbe
|
|
3
5
|
from azure.mgmt.keyvault import KeyVaultManagementClient
|
|
6
|
+
from pydantic import BaseModel, Field, field_validator
|
|
4
7
|
|
|
5
8
|
from ..identity.models import ManagedIdentity
|
|
6
9
|
|
|
@@ -33,3 +36,46 @@ class RevisionDeploymentResult:
|
|
|
33
36
|
and self.provisioning_state == "Provisioned"
|
|
34
37
|
and self.running_state not in ("Stopped", "Degraded", "Failed")
|
|
35
38
|
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ContainerConfig(BaseModel):
|
|
42
|
+
"""Configuration for a single container from YAML."""
|
|
43
|
+
|
|
44
|
+
name: str
|
|
45
|
+
image_name: str = Field(..., description="Just the image name, no registry or tag")
|
|
46
|
+
cpu: float
|
|
47
|
+
memory: str
|
|
48
|
+
env_vars: list[str] = Field(
|
|
49
|
+
default_factory=list, description="List of environment variable names to load"
|
|
50
|
+
)
|
|
51
|
+
probes: list[ContainerAppProbe] | None = Field(
|
|
52
|
+
default=None, description="List of probe configurations"
|
|
53
|
+
)
|
|
54
|
+
existing_image_tag: str | None = Field(default=None, description="Optional tag to retag from")
|
|
55
|
+
dockerfile: str | None = Field(default=None, description="Optional dockerfile path")
|
|
56
|
+
|
|
57
|
+
def post_init(self):
|
|
58
|
+
if not (self.dockerfile or self.existing_image_tag):
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"Container '{self.name}' must have either 'dockerfile' "
|
|
61
|
+
f"or 'existing_image_tag' specified"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
@field_validator("probes", mode="before")
|
|
65
|
+
@classmethod
|
|
66
|
+
def parse_probes(cls, v: list[dict[str, Any]] | None) -> list[ContainerAppProbe] | None:
|
|
67
|
+
"""Parse probe dictionaries to ContainerAppProbe objects."""
|
|
68
|
+
if v is None:
|
|
69
|
+
return None
|
|
70
|
+
return [ContainerAppProbe(**probe_data) for probe_data in v]
|
|
71
|
+
|
|
72
|
+
class Config:
|
|
73
|
+
"""Pydantic configuration for ContainerConfig."""
|
|
74
|
+
|
|
75
|
+
arbitrary_types_allowed = True
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ContainerAppConfig(BaseModel):
|
|
79
|
+
containers: list[ContainerConfig] = Field(
|
|
80
|
+
..., min_length=1, description="List of container configurations"
|
|
81
|
+
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
|
|
6
|
+
from .model import ContainerAppConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_app_config_yaml(yaml_path: Path) -> ContainerAppConfig:
|
|
10
|
+
"""
|
|
11
|
+
Load container configurations from YAML file using Pydantic for validation.
|
|
12
|
+
|
|
13
|
+
The YAML should have the following structure:
|
|
14
|
+
```yaml
|
|
15
|
+
containers:
|
|
16
|
+
- name: my-app
|
|
17
|
+
image_name: my-image # Just the image name
|
|
18
|
+
cpu: 0.5
|
|
19
|
+
memory: "1.0Gi"
|
|
20
|
+
env_vars: # List of env var names to load from environment
|
|
21
|
+
- ENV_VAR1
|
|
22
|
+
- ENV_VAR2
|
|
23
|
+
dockerfile: ./Dockerfile # optional
|
|
24
|
+
existing_image_tag: v1.0 # optional
|
|
25
|
+
probes: # optional - use snake_case keys
|
|
26
|
+
- type: Liveness
|
|
27
|
+
http_get:
|
|
28
|
+
path: /health
|
|
29
|
+
port: 8080
|
|
30
|
+
initial_delay_seconds: 10
|
|
31
|
+
period_seconds: 30
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
yaml_path: Path to the YAML configuration file
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
List of ContainerConfig instances
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ValueError: If YAML structure is invalid or validation fails
|
|
42
|
+
"""
|
|
43
|
+
with open(yaml_path) as f:
|
|
44
|
+
data: dict[str, Any] = yaml.safe_load(f)
|
|
45
|
+
|
|
46
|
+
if not data:
|
|
47
|
+
raise ValueError("YAML file is empty")
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
app_config = ContainerAppConfig(**data)
|
|
51
|
+
return app_config
|
|
52
|
+
except Exception as e:
|
|
53
|
+
raise ValueError(f"Invalid YAML configuration: {e}") from e
|
azure_deploy_cli/utils/docker.py
CHANGED
|
@@ -3,6 +3,34 @@
|
|
|
3
3
|
import subprocess
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
+
from .logging import get_logger
|
|
7
|
+
|
|
8
|
+
logger = get_logger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _run_and_stream(cmd: list[str], show_output: bool = True) -> int:
|
|
12
|
+
"""Run a command and stream output to stderr in real-time.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
cmd: Command and arguments to run
|
|
16
|
+
show_output: Whether to display output to stderr
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
The return code of the process
|
|
20
|
+
"""
|
|
21
|
+
process = subprocess.Popen(
|
|
22
|
+
cmd,
|
|
23
|
+
stdout=subprocess.PIPE,
|
|
24
|
+
stderr=subprocess.STDOUT,
|
|
25
|
+
text=True,
|
|
26
|
+
)
|
|
27
|
+
if process.stdout is not None:
|
|
28
|
+
for line in iter(process.stdout.readline, ""):
|
|
29
|
+
if line and show_output:
|
|
30
|
+
logger.info(line.rstrip("\n"))
|
|
31
|
+
process.stdout.close()
|
|
32
|
+
return process.wait()
|
|
33
|
+
|
|
6
34
|
|
|
7
35
|
def image_exists(full_image_name: str) -> bool:
|
|
8
36
|
"""
|
|
@@ -14,12 +42,8 @@ def image_exists(full_image_name: str) -> bool:
|
|
|
14
42
|
Returns:
|
|
15
43
|
True if the image exists locally, False otherwise
|
|
16
44
|
"""
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
capture_output=True,
|
|
20
|
-
text=True,
|
|
21
|
-
)
|
|
22
|
-
return check_image_result.returncode == 0
|
|
45
|
+
returncode = _run_and_stream(["docker", "image", "inspect", full_image_name], show_output=False)
|
|
46
|
+
return returncode == 0
|
|
23
47
|
|
|
24
48
|
|
|
25
49
|
def push_image(full_image_name: str) -> None:
|
|
@@ -32,13 +56,9 @@ def push_image(full_image_name: str) -> None:
|
|
|
32
56
|
Raises:
|
|
33
57
|
RuntimeError: If the docker push command fails
|
|
34
58
|
"""
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
text=True,
|
|
39
|
-
)
|
|
40
|
-
if push_result.returncode != 0:
|
|
41
|
-
raise RuntimeError(f"Docker push failed {push_result.stderr}")
|
|
59
|
+
returncode = _run_and_stream(["docker", "push", full_image_name])
|
|
60
|
+
if returncode != 0:
|
|
61
|
+
raise RuntimeError("Docker push failed")
|
|
42
62
|
|
|
43
63
|
|
|
44
64
|
def pull_image(full_image_name: str) -> None:
|
|
@@ -51,13 +71,9 @@ def pull_image(full_image_name: str) -> None:
|
|
|
51
71
|
Raises:
|
|
52
72
|
RuntimeError: If the docker pull command fails
|
|
53
73
|
"""
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
text=True,
|
|
58
|
-
)
|
|
59
|
-
if pull_result.returncode != 0:
|
|
60
|
-
raise RuntimeError(f"Docker pull failed: {pull_result.stderr}")
|
|
74
|
+
returncode = _run_and_stream(["docker", "pull", full_image_name])
|
|
75
|
+
if returncode != 0:
|
|
76
|
+
raise RuntimeError("Docker pull failed")
|
|
61
77
|
|
|
62
78
|
|
|
63
79
|
def tag_image(source_image: str, target_image: str) -> None:
|
|
@@ -71,13 +87,9 @@ def tag_image(source_image: str, target_image: str) -> None:
|
|
|
71
87
|
Raises:
|
|
72
88
|
RuntimeError: If the docker tag command fails
|
|
73
89
|
"""
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
text=True,
|
|
78
|
-
)
|
|
79
|
-
if tag_result.returncode != 0:
|
|
80
|
-
raise RuntimeError(f"Docker tag failed: {tag_result.stderr}")
|
|
90
|
+
returncode = _run_and_stream(["docker", "tag", source_image, target_image])
|
|
91
|
+
if returncode != 0:
|
|
92
|
+
raise RuntimeError("Docker tag failed")
|
|
81
93
|
|
|
82
94
|
|
|
83
95
|
def pull_retag_and_push_image(
|
|
@@ -116,7 +128,7 @@ def build_and_push_image(
|
|
|
116
128
|
RuntimeError: If the docker build and push command fails
|
|
117
129
|
"""
|
|
118
130
|
src_folder = str(Path(dockerfile).parent)
|
|
119
|
-
|
|
131
|
+
returncode = _run_and_stream(
|
|
120
132
|
[
|
|
121
133
|
"docker",
|
|
122
134
|
"buildx",
|
|
@@ -129,9 +141,7 @@ def build_and_push_image(
|
|
|
129
141
|
dockerfile,
|
|
130
142
|
src_folder,
|
|
131
143
|
"--push",
|
|
132
|
-
]
|
|
133
|
-
capture_output=True,
|
|
134
|
-
text=True,
|
|
144
|
+
]
|
|
135
145
|
)
|
|
136
|
-
if
|
|
137
|
-
raise RuntimeError(
|
|
146
|
+
if returncode != 0:
|
|
147
|
+
raise RuntimeError("Docker build and push failed")
|
|
@@ -88,7 +88,7 @@ def configure_logging(level: str = "info") -> None:
|
|
|
88
88
|
stderr_handler = logging.StreamHandler(sys.stderr)
|
|
89
89
|
stderr_handler.setLevel(log_level)
|
|
90
90
|
stderr_handler.addFilter(lambda record: record.levelno != STDOUT_LEVEL)
|
|
91
|
-
stderr_handler.setFormatter(ColoredFormatter("%(message)s"))
|
|
91
|
+
stderr_handler.setFormatter(ColoredFormatter("%(asctime)s %(message)s"))
|
|
92
92
|
root_logger.addHandler(stderr_handler)
|
|
93
93
|
|
|
94
94
|
# Handler for stdout (only for STDOUT level)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: azure-deploy-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Python CLI for Azure deployment automation - identity, roles, and container apps management
|
|
5
5
|
Author-email: decewei <celinew1221@gmail.com>
|
|
6
6
|
License: Mozilla Public License Version 2.0
|
|
@@ -399,6 +399,7 @@ Requires-Dist: azure-mgmt-keyvault
|
|
|
399
399
|
Requires-Dist: azure-mgmt-msi
|
|
400
400
|
Requires-Dist: python-dotenv>=1.0.0
|
|
401
401
|
Requires-Dist: pydantic>=2.0.0
|
|
402
|
+
Requires-Dist: pyyaml>=6.0
|
|
402
403
|
Provides-Extra: dev
|
|
403
404
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
404
405
|
Requires-Dist: mypy>=1.7.0; extra == "dev"
|
|
@@ -409,6 +410,7 @@ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
|
409
410
|
Requires-Dist: commitizen>=3.0.0; extra == "dev"
|
|
410
411
|
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
|
|
411
412
|
Requires-Dist: types-setuptools; extra == "dev"
|
|
413
|
+
Requires-Dist: types-PyYAML; extra == "dev"
|
|
412
414
|
Dynamic: license-file
|
|
413
415
|
|
|
414
416
|
# Azure Deploy CLI
|
|
@@ -459,11 +461,11 @@ pip install -e /path/to/scripts
|
|
|
459
461
|
|
|
460
462
|
### Azure Container Apps (ACA) Deployment
|
|
461
463
|
|
|
462
|
-
The ACA deployment process is split into two stages for better control:
|
|
464
|
+
The ACA deployment process uses YAML configuration for containers and is split into two stages for better control:
|
|
463
465
|
|
|
464
466
|
#### Stage 1: Deploy Revision
|
|
465
467
|
|
|
466
|
-
Deploy a new container revision without affecting traffic:
|
|
468
|
+
Deploy a new container revision from YAML configuration without affecting traffic:
|
|
467
469
|
|
|
468
470
|
```bash
|
|
469
471
|
azd azaca deploy \
|
|
@@ -474,25 +476,76 @@ azd azaca deploy \
|
|
|
474
476
|
--user-assigned-identity-name my-identity \
|
|
475
477
|
--container-app my-app \
|
|
476
478
|
--registry-server myregistry.azurecr.io \
|
|
477
|
-
--image-name my-image \
|
|
478
479
|
--stage prod \
|
|
479
|
-
--target-port
|
|
480
|
-
--cpu 0.5 \
|
|
481
|
-
--memory 1.0 \
|
|
480
|
+
--target-port 8080 \
|
|
482
481
|
--min-replicas 1 \
|
|
483
482
|
--max-replicas 10 \
|
|
484
483
|
--keyvault-name my-keyvault \
|
|
485
|
-
--
|
|
486
|
-
--env-vars ENV_VAR1 ENV_VAR2 \
|
|
484
|
+
--container-config ./container-config.yaml \
|
|
487
485
|
--env-var-secrets SECRET1 SECRET2
|
|
488
486
|
```
|
|
489
487
|
|
|
490
488
|
This command:
|
|
491
489
|
|
|
490
|
+
- Loads container configurations from YAML file
|
|
491
|
+
- Builds/pushes container images for all containers
|
|
492
492
|
- Creates or updates a new revision with 0% traffic
|
|
493
|
+
- Supports multiple containers with independent configurations
|
|
493
494
|
- Verifies the revision is healthy and active
|
|
494
495
|
- Outputs the revision name for use in traffic management
|
|
495
496
|
|
|
497
|
+
**Container Configuration YAML:**
|
|
498
|
+
|
|
499
|
+
The `--container-config` file specifies container settings including images, resources, environment variables, and health probes:
|
|
500
|
+
|
|
501
|
+
```yaml
|
|
502
|
+
containers:
|
|
503
|
+
- name: my-app
|
|
504
|
+
image_name: my-image
|
|
505
|
+
cpu: 0.5
|
|
506
|
+
memory: "1.0Gi"
|
|
507
|
+
env_vars:
|
|
508
|
+
- ENV_VAR1
|
|
509
|
+
- ENV_VAR2
|
|
510
|
+
# relative to the directory which command will run fromm
|
|
511
|
+
dockerfile: ./Dockerfile
|
|
512
|
+
probes:
|
|
513
|
+
- type: Liveness
|
|
514
|
+
http_get:
|
|
515
|
+
path: /health
|
|
516
|
+
port: 8080
|
|
517
|
+
initial_delay_seconds: 10
|
|
518
|
+
period_seconds: 30
|
|
519
|
+
- type: Readiness
|
|
520
|
+
http_get:
|
|
521
|
+
path: /ready
|
|
522
|
+
port: 8080
|
|
523
|
+
initial_delay_seconds: 5
|
|
524
|
+
period_seconds: 10
|
|
525
|
+
|
|
526
|
+
- name: sidecar
|
|
527
|
+
image_name: sidecar-image
|
|
528
|
+
cpu: 0.25
|
|
529
|
+
memory: "0.5Gi"
|
|
530
|
+
env_vars:
|
|
531
|
+
- SIDECAR_CONFIG
|
|
532
|
+
existing_image_tag: v1.0.0 # Optional: retag from existing image
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
**Configuration Fields:**
|
|
536
|
+
|
|
537
|
+
- `containers` (required): List of container configurations
|
|
538
|
+
- `name`: Container name (required)
|
|
539
|
+
- `image_name`: Image name without registry/tag (required)
|
|
540
|
+
- `cpu`: CPU allocation (required, e.g., 0.5)
|
|
541
|
+
- `memory`: Memory allocation (required, e.g., "1.0Gi")
|
|
542
|
+
- `env_vars`: List of environment variable names to load (optional)
|
|
543
|
+
- `dockerfile`: Path to Dockerfile for building (required if existing_image_tag not provided)
|
|
544
|
+
- `existing_image_tag`: Tag to retag from instead of building (required if dockerfile not provided)
|
|
545
|
+
- `probes`: List of health probes (optional)
|
|
546
|
+
|
|
547
|
+
**Note:** Ingress configuration (target port) and scaling parameters (min/max replicas) are specified via CLI arguments, not in the YAML file.
|
|
548
|
+
|
|
496
549
|
#### Stage 2: Update Traffic Weights
|
|
497
550
|
|
|
498
551
|
Update traffic distribution and deactivate old revisions:
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
azure_deploy_cli/__init__.py,sha256=Xj8ZMqmqKWc2hhR8rtSETkOmkvrFgh020MYVB_yHp4s,1134
|
|
2
|
-
azure_deploy_cli/_version.py,sha256=
|
|
2
|
+
azure_deploy_cli/_version.py,sha256=vLA4ITz09S-S435nq6yTF6l3qiSz6w4euS1rOxXgd1M,704
|
|
3
3
|
azure_deploy_cli/cli.py,sha256=I1hXskorvbryjNg0XXdqLjdBuQTPKgZeW03tLHlLxzk,1663
|
|
4
4
|
azure_deploy_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
azure_deploy_cli/aca/aca_cli.py,sha256=
|
|
6
|
-
azure_deploy_cli/aca/deploy_aca.py,sha256=
|
|
7
|
-
azure_deploy_cli/aca/model.py,sha256
|
|
5
|
+
azure_deploy_cli/aca/aca_cli.py,sha256=_6c6l7iH5W2Prue3JHSHBQL12WGs9twGivmsZ9h8STU,15944
|
|
6
|
+
azure_deploy_cli/aca/deploy_aca.py,sha256=puuQgP6j-0CJ9rRUFs_yBLiZkEm9bVlQUpRoSon52ac,30303
|
|
7
|
+
azure_deploy_cli/aca/model.py,sha256=rCIVd9WF89g7r7AOZoCPTG6siMaZwSXNR_RzqmKRQEQ,2581
|
|
8
|
+
azure_deploy_cli/aca/yaml_loader.py,sha256=awCJLx88j8ErLm-tLIdV8mFtlSQqGezIqugU0v4m9BI,1413
|
|
8
9
|
azure_deploy_cli/aca/bash/aca-cert/create.sh,sha256=QOPm7b1cTguBDJ2T6Nm65rMLlG1_ccfTcev00IQ-CI0,6485
|
|
9
10
|
azure_deploy_cli/aca/bash/aca-cert/destroy.sh,sha256=iao35H_c4YnZHLDpKaYeyd3I7RhHKJzSAUbNovUdcFg,1171
|
|
10
11
|
azure_deploy_cli/identity/__init__.py,sha256=6s9SoMMhHjMNWNyjkQxWhp2cTbGru3bmrF56-8nCftw,919
|
|
@@ -17,14 +18,14 @@ azure_deploy_cli/identity/role.py,sha256=WlSJmn54Urar58x4HmJUC0adbeuSTPyW-0lGodv
|
|
|
17
18
|
azure_deploy_cli/identity/service_principal.py,sha256=IDBKVVTQP4PBso0Cv0gcApexKEdXUlKEQ3sMr4OjC1U,7958
|
|
18
19
|
azure_deploy_cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
20
|
azure_deploy_cli/utils/azure_cli.py,sha256=mQXHedduPShY6NqG4JePijWJnK3alCMxrTjqHCn2MaM,2576
|
|
20
|
-
azure_deploy_cli/utils/docker.py,sha256=
|
|
21
|
+
azure_deploy_cli/utils/docker.py,sha256=S-X5YkuxiC6TEphwrXlM8Hk_Ik-i_eTnmAs6ymvN4Z8,3965
|
|
21
22
|
azure_deploy_cli/utils/env.py,sha256=Qoc0mlrEcyCGk6TLl0AJNfuvhdBuok_93EW2g7Clpu8,3451
|
|
22
23
|
azure_deploy_cli/utils/key_vault.py,sha256=9JYE1pmoPhggU7TZXod_qmu8TgeVmdM0OEIVHPYtPvM,354
|
|
23
|
-
azure_deploy_cli/utils/logging.py,sha256=
|
|
24
|
+
azure_deploy_cli/utils/logging.py,sha256=NxrhhNGnS0SWReKbwJ56A8LYLmjRd77ZBVOVQe9bgQE,3813
|
|
24
25
|
azure_deploy_cli/utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
-
azure_deploy_cli-0.
|
|
26
|
-
azure_deploy_cli-0.
|
|
27
|
-
azure_deploy_cli-0.
|
|
28
|
-
azure_deploy_cli-0.
|
|
29
|
-
azure_deploy_cli-0.
|
|
30
|
-
azure_deploy_cli-0.
|
|
26
|
+
azure_deploy_cli-1.0.0.dist-info/licenses/LICENSE,sha256=Pz2eACSxkhsGfW9_iN60pgy-enjnbGTj8df8O3ebnQQ,16726
|
|
27
|
+
azure_deploy_cli-1.0.0.dist-info/METADATA,sha256=cQtDCD1kYILhA56bgSEAoeYHsZ-HqXlpsx1hB3zGdag,30298
|
|
28
|
+
azure_deploy_cli-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
29
|
+
azure_deploy_cli-1.0.0.dist-info/entry_points.txt,sha256=vuN79v5YJtDi5tF3rCd5qVvCGAMEQMImRHq64B-y-KQ,91
|
|
30
|
+
azure_deploy_cli-1.0.0.dist-info/top_level.txt,sha256=luw_MJJWFd2vFwCtx3wbmihlkCdlZxk1NZou1eToBtU,17
|
|
31
|
+
azure_deploy_cli-1.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|