azure-deploy-cli 0.1.6__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.
@@ -0,0 +1,35 @@
1
+ from dataclasses import dataclass
2
+
3
+ from azure.mgmt.keyvault import KeyVaultManagementClient
4
+
5
+ from ..identity.models import ManagedIdentity
6
+
7
+
8
+ @dataclass
9
+ class SecretKeyVaultConfig:
10
+ key_vault_client: KeyVaultManagementClient
11
+ key_vault_name: str
12
+ secret_names: list[str]
13
+ user_identity: ManagedIdentity
14
+
15
+
16
+ @dataclass
17
+ class RevisionDeploymentResult:
18
+ """Result of a revision deployment operation."""
19
+
20
+ revision_name: str
21
+ active: bool
22
+ health_state: str
23
+ provisioning_state: str
24
+ running_state: str
25
+ revision_url: str | None
26
+
27
+ @property
28
+ def is_healthy(self) -> bool:
29
+ """Check if the revision is healthy and active."""
30
+ return (
31
+ self.active
32
+ and self.health_state == "Healthy"
33
+ and self.provisioning_state == "Provisioned"
34
+ and self.running_state not in ("Stopped", "Degraded", "Failed")
35
+ )
@@ -0,0 +1,66 @@
1
+ import argparse
2
+ import sys
3
+
4
+ from .aca import aca_cli
5
+ from .identity import identity_cli
6
+ from .utils.logging import configure_logging, get_logger
7
+
8
+
9
+ def main() -> None:
10
+ """
11
+ Main CLI entry point
12
+
13
+ Routes to different tool namespaces:
14
+ - azid: Azure identity management (service principals, credentials, RBAC)
15
+ - azaca: Azure Container Apps management (identity and role setup)
16
+ """
17
+ parser = argparse.ArgumentParser(
18
+ description="Azure Deploy CLI",
19
+ prog="azd",
20
+ formatter_class=argparse.RawDescriptionHelpFormatter,
21
+ epilog="""
22
+ Namespaces:
23
+ azid Azure identity management (service principals, credentials, roles)
24
+ azaca Azure Container Apps management (identity and role setup)
25
+ """,
26
+ )
27
+
28
+ parser.add_argument(
29
+ "--log-level",
30
+ type=str,
31
+ choices=["debug", "info", "warning", "error", "critical", "notset"],
32
+ default="info",
33
+ help="Set the logging level.",
34
+ )
35
+
36
+ subparsers = parser.add_subparsers(dest="namespace", help="Tool namespace")
37
+
38
+ # Add identity namespace
39
+ identity_cli.add_commands(subparsers)
40
+
41
+ # Add ACA namespace
42
+ aca_cli.add_commands(subparsers)
43
+
44
+ args = parser.parse_args()
45
+
46
+ configure_logging(level=args.log_level)
47
+ logger = get_logger(__name__)
48
+
49
+ if not args.namespace:
50
+ parser.print_help()
51
+ sys.exit(1)
52
+
53
+ if not hasattr(args, "func"):
54
+ logger.error("No command specified")
55
+ parser.print_help()
56
+ sys.exit(1)
57
+
58
+ try:
59
+ args.func(args)
60
+ except Exception as e:
61
+ logger.error(f"Fatal error: {str(e)}")
62
+ sys.exit(1)
63
+
64
+
65
+ if __name__ == "__main__":
66
+ main()
@@ -0,0 +1,36 @@
1
+ import azure_deploy_cli.identity.identity_cli
2
+ from azure_deploy_cli.identity.managed_identity import (
3
+ create_or_get_user_identity,
4
+ delete_user_identity,
5
+ get_identity_principal_id,
6
+ )
7
+ from azure_deploy_cli.identity.models import (
8
+ AzureGroup,
9
+ ManagedIdentity,
10
+ RoleConfig,
11
+ RoleDefinition,
12
+ SPAuthCredentials,
13
+ SPAuthCredentialsWithSecret,
14
+ SPCreateResult,
15
+ )
16
+ from azure_deploy_cli.identity.role import assign_roles
17
+ from azure_deploy_cli.identity.service_principal import create_sp, reset_sp_credentials
18
+
19
+ __version__ = "0.1.0"
20
+
21
+ __all__ = [
22
+ "SPAuthCredentials",
23
+ "SPAuthCredentialsWithSecret",
24
+ "RoleConfig",
25
+ "RoleDefinition",
26
+ "SPCreateResult",
27
+ "ManagedIdentity",
28
+ "AzureGroup",
29
+ "assign_roles",
30
+ "create_sp",
31
+ "create_or_get_user_identity",
32
+ "delete_user_identity",
33
+ "get_identity_principal_id",
34
+ "identity_cli",
35
+ "reset_sp_credentials",
36
+ ]
@@ -0,0 +1,84 @@
1
+ """Azure AD Security Group lifecycle and role management."""
2
+
3
+ import json
4
+ import subprocess
5
+ from typing import Any
6
+
7
+ from ..utils.azure_cli import run_command
8
+ from ..utils.logging import get_logger
9
+ from .models import AzureGroup
10
+
11
+ logger = get_logger(__name__)
12
+
13
+
14
+ def list_cmd(group_name: str) -> list[str]:
15
+ """Build command to list security groups by name."""
16
+ return [
17
+ "az",
18
+ "ad",
19
+ "group",
20
+ "list",
21
+ "--display-name",
22
+ group_name,
23
+ "--output",
24
+ "json",
25
+ ]
26
+
27
+
28
+ def exists_group(group_name: str) -> str | None:
29
+ """
30
+ Check if security group exists by name.
31
+
32
+ Args:
33
+ group_name: Display name of the security group
34
+
35
+ Returns:
36
+ Object ID if found, None otherwise
37
+
38
+ Raises:
39
+ ValueError: If multiple groups with same name found
40
+ """
41
+ result: list[dict[str, Any]] = run_command(list_cmd(group_name))
42
+
43
+ if not result or len(result) == 0:
44
+ return None
45
+
46
+ if len(result) > 1:
47
+ raise ValueError(
48
+ f"Multiple security groups found with name '{group_name}'. "
49
+ "Please use a more specific name."
50
+ )
51
+
52
+ group_object_id: str = result[0]["id"]
53
+ return group_object_id
54
+
55
+
56
+ def get_group(group_name: str) -> AzureGroup | None:
57
+ """
58
+ Get existing security group by name.
59
+
60
+ Args:
61
+ group_name: Display name of the security group
62
+
63
+ Returns:
64
+ GroupAssignResult if found, None otherwise
65
+ """
66
+ list_cmd_args: list[str] = list_cmd(group_name)
67
+
68
+ try:
69
+ existing_groups: list[dict[str, Any]] = run_command(list_cmd_args)
70
+ if existing_groups:
71
+ logger.info(f"Found security group '{group_name}'")
72
+ group = existing_groups[0]
73
+ object_id: str = group.get("id", "")
74
+
75
+ if not object_id:
76
+ raise ValueError("Failed to load existing group details")
77
+
78
+ return AzureGroup(
79
+ objectId=object_id,
80
+ displayName=group_name,
81
+ )
82
+ except (subprocess.CalledProcessError, json.JSONDecodeError):
83
+ return None
84
+ return None
@@ -0,0 +1,453 @@
1
+ import argparse
2
+ import json
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from ..utils.azure_cli import run_command
9
+ from ..utils.env import add_var_to_env_file, load_env_vars_from_files
10
+ from ..utils.logging import get_logger
11
+ from .group import get_group
12
+ from .managed_identity import create_or_get_user_identity
13
+ from .models import SPAuthCredentialsWithSecret
14
+ from .role import assign_role_by_files
15
+ from .service_principal import (
16
+ create_sp,
17
+ delete_service_principal_by_name,
18
+ reset_sp_credentials,
19
+ )
20
+
21
+ logger = get_logger(__name__)
22
+
23
+
24
+ def _load_credentials(env_file: Path | None, cred_key: str) -> Any:
25
+ """
26
+ Load AZ_CREDENTIALS from env file or environment variable.
27
+
28
+ Args:
29
+ env_file: Optional path to env file
30
+
31
+ Returns:
32
+ Dictionary with credentials (clientId, clientSecret, subscriptionId,
33
+ tenantId)
34
+
35
+ Raises:
36
+ FileNotFoundError: If env file not found
37
+ ValueError: If credentials not found or invalid
38
+ json.JSONDecodeError: If AZ_CREDENTIALS JSON is invalid
39
+ """
40
+ credentials_json: str = ""
41
+
42
+ if env_file:
43
+ env_vars = load_env_vars_from_files([env_file])
44
+ c = env_vars.get(cred_key, "")
45
+ if c:
46
+ credentials_json = c
47
+
48
+ if not credentials_json:
49
+ credentials_json = os.environ.get(cred_key, "")
50
+
51
+ if not credentials_json:
52
+ raise ValueError(
53
+ f"Cannot find {cred_key} from --env-file or {cred_key} environment variable"
54
+ )
55
+
56
+ creds_dict = json.loads(credentials_json)
57
+ return creds_dict
58
+
59
+
60
+ def _save_credentials(
61
+ credentials: SPAuthCredentialsWithSecret, env_file: Path | None, cred_key: str
62
+ ) -> None:
63
+ logger.critical(f"Saving credentials to env file: {env_file}")
64
+
65
+ if not env_file:
66
+ logger.warning("No --env-file provided; credentials will not be saved to file")
67
+
68
+ if env_file:
69
+ if isinstance(credentials, SPAuthCredentialsWithSecret):
70
+ try:
71
+ logger.info(f"Updating credentials in '{env_file}'")
72
+ credentials_json = json.dumps(credentials.to_dict(), separators=(",", ":"))
73
+ add_var_to_env_file({cred_key: credentials_json}, env_file)
74
+ logger.success(f"Credentials saved to '{env_file}'")
75
+ except Exception as e:
76
+ raise Exception(f"Failed to save credentials: {str(e)}") from None
77
+ else:
78
+ logger.warning(
79
+ f"Credentials do not contain secret; cannot save to "
80
+ f"{env_file}. Only credentials with secrets can be "
81
+ f"persisted."
82
+ )
83
+ else:
84
+ logger.warning("No --env-file provided; credentials will not be saved to file")
85
+
86
+
87
+ def cli_create_and_assign(args: Any) -> None:
88
+ try:
89
+ sp_result = create_sp(args.sp_name, skip_assignment=True)
90
+
91
+ if args.reset_secrets and not isinstance(
92
+ sp_result.authCredentials, SPAuthCredentialsWithSecret
93
+ ):
94
+ try:
95
+ credentials = reset_sp_credentials(args.sp_name)
96
+ sp_result.authCredentials = credentials
97
+ except Exception as e:
98
+ raise Exception(f"Failed to reset secrets: {str(e)}") from None
99
+
100
+ _save_credentials(
101
+ sp_result.authCredentials,
102
+ args.env_file,
103
+ args.cred_key,
104
+ )
105
+
106
+ if args.print:
107
+ logger.critical(
108
+ f"Created credentials: {json.dumps(sp_result.authCredentials.to_dict(), indent=2)}",
109
+ )
110
+
111
+ assign_role_by_files(
112
+ sp_result.objectId,
113
+ args.roles_config,
114
+ args.env_vars_files,
115
+ )
116
+ logger.success("Service principal created and roles assigned successfully")
117
+ except Exception as e:
118
+ logger.error(str(e))
119
+ sys.exit(1)
120
+
121
+
122
+ def cli_reset_credentials(args: Any) -> None:
123
+ try:
124
+ credentials = reset_sp_credentials(args.sp_name)
125
+
126
+ _save_credentials(credentials, args.env_file, args.cred_key)
127
+
128
+ if args.print:
129
+ logger.info(
130
+ f"Created credentials: {json.dumps(credentials.to_dict(), indent=2)}",
131
+ )
132
+
133
+ logger.success("Credentials reset successfully")
134
+ except Exception as e:
135
+ logger.error(str(e))
136
+ sys.exit(1)
137
+
138
+
139
+ def cli_login(args: Any) -> None:
140
+ try:
141
+ credentials_dict = _load_credentials(args.env_file, args.cred_key)
142
+ credentials = SPAuthCredentialsWithSecret(**credentials_dict)
143
+
144
+ logger.info(f"Logging in with service principal {credentials.clientId}")
145
+
146
+ login_cmd: list[str] = [
147
+ "az",
148
+ "login",
149
+ "--service-principal",
150
+ "-u",
151
+ credentials.clientId,
152
+ "-p",
153
+ credentials.clientSecret,
154
+ "--tenant",
155
+ credentials.tenantId,
156
+ ]
157
+
158
+ run_command(login_cmd)
159
+ logger.success(f"Successfully logged in with service principal {credentials.clientId}")
160
+ except Exception as e:
161
+ logger.error(str(e))
162
+ sys.exit(1)
163
+
164
+
165
+ def cli_delete_service_principal(args: Any) -> None:
166
+ try:
167
+ delete_service_principal_by_name(args.sp_name)
168
+ except Exception as e:
169
+ logger.error(str(e))
170
+ sys.exit(1)
171
+
172
+
173
+ def cli_assign_roles_to_group(args: Any) -> None:
174
+ try:
175
+ group_result = get_group(args.group_name)
176
+
177
+ if group_result is None:
178
+ raise Exception(f"Security group '{args.group_name}' not found")
179
+
180
+ logger.success(
181
+ f"Found security group '{args.group_name}' with object ID: {group_result.objectId}"
182
+ )
183
+
184
+ assign_role_by_files(
185
+ group_result.objectId,
186
+ args.roles_config,
187
+ args.env_vars_files,
188
+ object_type="Group",
189
+ )
190
+
191
+ logger.success("Roles assigned to security group successfully")
192
+ except Exception as e:
193
+ logger.error(str(e))
194
+ sys.exit(1)
195
+
196
+
197
+ def cli_create_and_assign_managed_identity(args: Any) -> None:
198
+ try:
199
+ logger.info(f"Setting up managed identity '{args.identity_name}'...")
200
+ identity_result = create_or_get_user_identity(
201
+ args.identity_name,
202
+ args.resource_group,
203
+ args.location,
204
+ )
205
+ logger.success(f"Managed identity ready: {identity_result.resourceId}")
206
+
207
+ if args.roles_config:
208
+ assign_role_by_files(
209
+ identity_result.principalId,
210
+ args.roles_config,
211
+ args.env_vars_files,
212
+ )
213
+ logger.success("Roles assigned to managed identity successfully")
214
+ else:
215
+ logger.info("No role config provided; skipping role assignment")
216
+
217
+ logger.success(
218
+ f"Managed identity '{args.identity_name}' created and ready for use. "
219
+ f"Resource ID: {identity_result.resourceId}"
220
+ )
221
+ except Exception as e:
222
+ logger.error(str(e))
223
+ sys.exit(1)
224
+
225
+
226
+ def add_commands(subparsers: argparse._SubParsersAction) -> None:
227
+ azid_parser = subparsers.add_parser(
228
+ "azid",
229
+ help="Azure identity management (service principals, credentials, roles)",
230
+ description="Manage Azure service principals, credentials, and role assignments",
231
+ formatter_class=argparse.RawDescriptionHelpFormatter,
232
+ epilog="""
233
+ Examples:
234
+ # Create a new service principal and assign roles
235
+ cc azid create-sp-and-assign-roles \\
236
+ --sp-name my-sp \\
237
+ --roles-config roles-config.json \\
238
+ --env-vars-files .env.local \\
239
+ --env-file .env.credentials \\
240
+ --print
241
+
242
+ # Reset credentials for an existing service principal
243
+ cc azid reset-sp-credentials \\
244
+ --sp-name my-sp \\
245
+ --env-file .env.credentials \\
246
+ --print
247
+
248
+ # Assign roles to a security group
249
+ cc azid assign-roles-to-group \\
250
+ --group-name "My Security Team" \\
251
+ --roles-config roles-config.json \\
252
+ --env-vars-files .env.local
253
+
254
+ # Create user-assigned managed identity and assign roles
255
+ cc azid create-and-assign-managed-identity \\
256
+ --identity-name my-app-identity \\
257
+ --resource-group my-rg \\
258
+ --location eastus \\
259
+ --roles-config roles-config.json \\
260
+ --env-vars-files .env.local
261
+
262
+ # Login using stored credentials
263
+ cc azid login --env-file .env.credentials
264
+ """,
265
+ )
266
+
267
+ azid_subparsers = azid_parser.add_subparsers(
268
+ dest="azid_command", help="Identity command to execute"
269
+ )
270
+ azid_subparsers.required = True
271
+
272
+ create_parser = azid_subparsers.add_parser(
273
+ "create-sp-and-assign-roles",
274
+ help="Create a service principal and assign roles",
275
+ description=(
276
+ "Create a new service principal in Azure and assign it roles "
277
+ "based on a configuration file."
278
+ ),
279
+ )
280
+ create_parser.add_argument(
281
+ "--sp-name",
282
+ required=True,
283
+ help="Name of the service principal to create",
284
+ )
285
+ create_parser.add_argument(
286
+ "--roles-config",
287
+ required=True,
288
+ type=Path,
289
+ help="Path to roles-config.json file containing role definitions",
290
+ )
291
+ create_parser.add_argument(
292
+ "--env-vars-files",
293
+ required=True,
294
+ type=Path,
295
+ nargs="+",
296
+ help="Paths to .env files with environment variables (can specify multiple files)",
297
+ )
298
+ create_parser.add_argument(
299
+ "-f",
300
+ "--env-file",
301
+ type=Path,
302
+ help="Path to environment file to save credentials (optional)",
303
+ )
304
+ create_parser.add_argument(
305
+ "-k",
306
+ "--cred-key",
307
+ type=str,
308
+ default="AZ_CREDENTIALS",
309
+ help="Credential key used to save credentials (optional)",
310
+ )
311
+ create_parser.add_argument(
312
+ "--print",
313
+ action="store_true",
314
+ help="Print credentials to stdout in JSON format",
315
+ )
316
+ create_parser.add_argument(
317
+ "--reset-secrets",
318
+ action="store_true",
319
+ help="Reset secrets after creation if SP already exists (has no secret)",
320
+ )
321
+ create_parser.set_defaults(func=cli_create_and_assign)
322
+
323
+ reset_parser = azid_subparsers.add_parser(
324
+ "reset-sp-credentials",
325
+ help="Reset service principal credentials",
326
+ description="Reset (rotate) credentials for an existing service principal.",
327
+ )
328
+ reset_parser.add_argument(
329
+ "--sp-name",
330
+ required=True,
331
+ help="Display name of the service principal",
332
+ )
333
+ reset_parser.add_argument(
334
+ "-f",
335
+ "--env-file",
336
+ type=Path,
337
+ help="Path to environment file to save credentials (optional)",
338
+ )
339
+ reset_parser.add_argument(
340
+ "-k",
341
+ "--cred-key",
342
+ type=str,
343
+ default="AZ_CREDENTIALS",
344
+ help="Credential key used to save credentials (optional)",
345
+ )
346
+ reset_parser.add_argument(
347
+ "--print",
348
+ action="store_true",
349
+ help="Print credentials to stdout in JSON format",
350
+ )
351
+ reset_parser.set_defaults(func=cli_reset_credentials)
352
+
353
+ login_parser = azid_subparsers.add_parser(
354
+ "login",
355
+ help="Login using service principal credentials",
356
+ description="Authenticate with Azure using stored service principal credentials.",
357
+ )
358
+ login_parser.add_argument(
359
+ "-f",
360
+ "--env-file",
361
+ type=Path,
362
+ help=(
363
+ "Path to environment file containing AZ_CREDENTIALS "
364
+ "(optional, checks env vars if not provided)"
365
+ ),
366
+ )
367
+ login_parser.add_argument(
368
+ "-k",
369
+ "--cred-key",
370
+ type=str,
371
+ default="AZ_CREDENTIALS",
372
+ help="Credential key used to load credentials (optional)",
373
+ )
374
+ login_parser.set_defaults(func=cli_login)
375
+
376
+ delete_parser = azid_subparsers.add_parser(
377
+ "delete-sp",
378
+ help="Delete a service principal by name",
379
+ description="Delete a service principal from Azure by its display name.",
380
+ )
381
+ delete_parser.add_argument(
382
+ "--sp-name",
383
+ required=True,
384
+ help="Display name of the service principal to delete",
385
+ )
386
+ delete_parser.set_defaults(func=cli_delete_service_principal)
387
+
388
+ group_parser = azid_subparsers.add_parser(
389
+ "assign-roles-to-group",
390
+ help="Assign roles to a security group",
391
+ description=(
392
+ "Assign roles to an Azure AD security group based on a configuration file. "
393
+ "This is similar to create-sp-and-assign-roles but for security groups "
394
+ "instead of service principals."
395
+ ),
396
+ )
397
+ group_parser.add_argument(
398
+ "--group-name",
399
+ required=True,
400
+ help="Display name of the Azure AD security group",
401
+ )
402
+ group_parser.add_argument(
403
+ "--roles-config",
404
+ required=True,
405
+ type=Path,
406
+ help="Path to roles-config.json file containing role definitions",
407
+ )
408
+ group_parser.add_argument(
409
+ "--env-vars-files",
410
+ required=True,
411
+ type=Path,
412
+ nargs="+",
413
+ help="Paths to .env files with environment variables (can specify multiple files)",
414
+ )
415
+ group_parser.set_defaults(func=cli_assign_roles_to_group)
416
+
417
+ mi_parser = azid_subparsers.add_parser(
418
+ "create-and-assign-managed-identity",
419
+ help="Create a user-assigned managed identity and assign roles",
420
+ description=(
421
+ "Create a new user-assigned managed identity in Azure and optionally "
422
+ "assign it roles based on a configuration file."
423
+ ),
424
+ )
425
+ mi_parser.add_argument(
426
+ "--identity-name",
427
+ required=True,
428
+ help="Name of the user-assigned managed identity to create or retrieve",
429
+ )
430
+ mi_parser.add_argument(
431
+ "--resource-group",
432
+ required=True,
433
+ help="Azure resource group name where the identity will be created",
434
+ )
435
+ mi_parser.add_argument(
436
+ "--location",
437
+ required=True,
438
+ help="Azure region location for the identity (e.g., eastus, westus2)",
439
+ )
440
+ mi_parser.add_argument(
441
+ "--roles-config",
442
+ required=False,
443
+ type=Path,
444
+ help="Path to roles-config.json file containing role definitions (optional)",
445
+ )
446
+ mi_parser.add_argument(
447
+ "--env-vars-files",
448
+ required=False,
449
+ type=Path,
450
+ nargs="+",
451
+ help="Paths to .env files with environment variables (can specify multiple files)",
452
+ )
453
+ mi_parser.set_defaults(func=cli_create_and_assign_managed_identity)