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.
- azure_deploy_cli/__init__.py +44 -0
- azure_deploy_cli/_version.py +34 -0
- azure_deploy_cli/aca/aca_cli.py +518 -0
- azure_deploy_cli/aca/bash/aca-cert/create.sh +203 -0
- azure_deploy_cli/aca/bash/aca-cert/destroy.sh +44 -0
- azure_deploy_cli/aca/deploy_aca.py +794 -0
- azure_deploy_cli/aca/model.py +35 -0
- azure_deploy_cli/cli.py +66 -0
- azure_deploy_cli/identity/__init__.py +36 -0
- azure_deploy_cli/identity/group.py +84 -0
- azure_deploy_cli/identity/identity_cli.py +453 -0
- azure_deploy_cli/identity/managed_identity.py +177 -0
- azure_deploy_cli/identity/models.py +167 -0
- azure_deploy_cli/identity/py.typed +0 -0
- azure_deploy_cli/identity/role.py +338 -0
- azure_deploy_cli/identity/service_principal.py +268 -0
- azure_deploy_cli/py.typed +0 -0
- azure_deploy_cli/utils/__init__.py +0 -0
- azure_deploy_cli/utils/azure_cli.py +96 -0
- azure_deploy_cli/utils/docker.py +137 -0
- azure_deploy_cli/utils/env.py +108 -0
- azure_deploy_cli/utils/key_vault.py +11 -0
- azure_deploy_cli/utils/logging.py +125 -0
- azure_deploy_cli/utils/py.typed +0 -0
- azure_deploy_cli-0.1.6.dist-info/METADATA +678 -0
- azure_deploy_cli-0.1.6.dist-info/RECORD +30 -0
- azure_deploy_cli-0.1.6.dist-info/WHEEL +5 -0
- azure_deploy_cli-0.1.6.dist-info/entry_points.txt +3 -0
- azure_deploy_cli-0.1.6.dist-info/licenses/LICENSE +373 -0
- azure_deploy_cli-0.1.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Service principal lifecycle management."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..utils.azure_cli import get_subscription_and_tenant, run_command
|
|
8
|
+
from ..utils.logging import get_logger
|
|
9
|
+
from .models import SPAuthCredentials, SPAuthCredentialsWithSecret, SPCreateResult
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def list_cmd(sp_name: str) -> list[str]:
|
|
15
|
+
"""Build command to list service principals by name."""
|
|
16
|
+
return [
|
|
17
|
+
"az",
|
|
18
|
+
"ad",
|
|
19
|
+
"sp",
|
|
20
|
+
"list",
|
|
21
|
+
"--filter",
|
|
22
|
+
f"displayName eq '{sp_name}'",
|
|
23
|
+
"--output",
|
|
24
|
+
"json",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def exists_sp(sp_name: str) -> str | None:
|
|
29
|
+
"""
|
|
30
|
+
Check if service principal exists by name.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
sp_name: Display name of the service principal
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Object ID if found, None otherwise
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
ValueError: If multiple service principals with same name found
|
|
40
|
+
"""
|
|
41
|
+
result: list[dict[str, Any]] = run_command(list_cmd(sp_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 service principals found with name '{sp_name}'. "
|
|
49
|
+
"Please use a more specific name."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
sp_object_id: str = result[0]["id"]
|
|
53
|
+
return sp_object_id
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_sp(sp_name: str, subscription_id: str, tenant_id: str) -> SPCreateResult | None:
|
|
57
|
+
"""
|
|
58
|
+
Get existing service principal by name.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
sp_name: Display name of the service principal
|
|
62
|
+
subscription_id: Azure subscription ID
|
|
63
|
+
tenant_id: Azure tenant ID
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
SPCreateResult if found, None otherwise
|
|
67
|
+
"""
|
|
68
|
+
list_cmd_args: list[str] = list_cmd(sp_name)
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
existing_sps: list[dict[str, Any]] = run_command(list_cmd_args)
|
|
72
|
+
if existing_sps:
|
|
73
|
+
logger.warning(f"Service principal '{sp_name}' already exists")
|
|
74
|
+
sp = existing_sps[0]
|
|
75
|
+
object_id: str = sp.get("id", "")
|
|
76
|
+
app_id: str = sp.get("appId", "")
|
|
77
|
+
|
|
78
|
+
if not object_id or not app_id:
|
|
79
|
+
raise ValueError("Failed to load existing service principal details")
|
|
80
|
+
|
|
81
|
+
# Note: existing SP won't have clientSecret - caller must reset
|
|
82
|
+
# credentials
|
|
83
|
+
return SPCreateResult(
|
|
84
|
+
objectId=object_id,
|
|
85
|
+
authCredentials=SPAuthCredentials(
|
|
86
|
+
clientId=app_id,
|
|
87
|
+
subscriptionId=subscription_id,
|
|
88
|
+
tenantId=tenant_id,
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
except (subprocess.CalledProcessError, json.JSONDecodeError):
|
|
92
|
+
return None
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def create_sp(
|
|
97
|
+
sp_name: str,
|
|
98
|
+
skip_assignment: bool = True,
|
|
99
|
+
) -> SPCreateResult:
|
|
100
|
+
"""
|
|
101
|
+
Create a service principal if it doesn't already exist.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
sp_name: Name of the service principal to create
|
|
105
|
+
skip_assignment: Whether to skip role assignment during creation
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
SPCreateResult containing object ID and credentials
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
subprocess.CalledProcessError: If creation fails
|
|
112
|
+
ValueError: If service principal creation is unsuccessful
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
subscription_id, tenant_id = get_subscription_and_tenant()
|
|
116
|
+
|
|
117
|
+
logger.info(f"Checking if service principal '{sp_name}' exists")
|
|
118
|
+
result = get_sp(sp_name, subscription_id, tenant_id)
|
|
119
|
+
if result is not None:
|
|
120
|
+
logger.warning(f"Service principal '{sp_name}' already exists")
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
logger.info(f"Creating service principal '{sp_name}'")
|
|
124
|
+
create_cmd: list[str] = [
|
|
125
|
+
"az",
|
|
126
|
+
"ad",
|
|
127
|
+
"sp",
|
|
128
|
+
"create-for-rbac",
|
|
129
|
+
"--name",
|
|
130
|
+
sp_name,
|
|
131
|
+
"--output",
|
|
132
|
+
"json",
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
if skip_assignment:
|
|
136
|
+
create_cmd.insert(4, "--skip-assignment")
|
|
137
|
+
|
|
138
|
+
sp_output: dict[str, Any] = run_command(create_cmd)
|
|
139
|
+
get_sp_output = get_sp(sp_name, subscription_id, tenant_id)
|
|
140
|
+
if not get_sp_output:
|
|
141
|
+
raise ValueError("Failed to create service principal")
|
|
142
|
+
logger.info(f"{sp_output} {get_sp_output}")
|
|
143
|
+
|
|
144
|
+
object_id = get_sp_output.objectId
|
|
145
|
+
app_id = sp_output.get("appId", "")
|
|
146
|
+
client_secret = sp_output.get("password", "")
|
|
147
|
+
|
|
148
|
+
logger.success(f"Service principal '{sp_name}' created successfully")
|
|
149
|
+
|
|
150
|
+
return SPCreateResult(
|
|
151
|
+
objectId=object_id,
|
|
152
|
+
authCredentials=SPAuthCredentialsWithSecret(
|
|
153
|
+
clientId=app_id,
|
|
154
|
+
clientSecret=client_secret,
|
|
155
|
+
subscriptionId=subscription_id,
|
|
156
|
+
tenantId=tenant_id,
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
except subprocess.CalledProcessError as e:
|
|
161
|
+
logger.error(f"Failed to create service principal: {str(e)}")
|
|
162
|
+
raise
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def reset_sp_credentials(
|
|
166
|
+
sp_name: str,
|
|
167
|
+
credential_name: str = "Grant Scraper",
|
|
168
|
+
years: int = 2,
|
|
169
|
+
) -> SPAuthCredentialsWithSecret:
|
|
170
|
+
"""
|
|
171
|
+
Reset service principal credentials.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
sp_name: The display name of the service principal
|
|
175
|
+
credential_name: Display name for the credential
|
|
176
|
+
years: Number of years for the credential validity
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
AuthCredentialsWithSecret with clientId, clientSecret,
|
|
180
|
+
subscriptionId, tenantId
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
subprocess.CalledProcessError: If reset fails
|
|
184
|
+
ValueError: If service principal not found
|
|
185
|
+
"""
|
|
186
|
+
try:
|
|
187
|
+
subscription_id, tenant_id = get_subscription_and_tenant()
|
|
188
|
+
|
|
189
|
+
logger.critical(f"Looking up service principal '{sp_name}'")
|
|
190
|
+
|
|
191
|
+
# Get the app ID for the service principal by name
|
|
192
|
+
sp_result = get_sp(sp_name, subscription_id, tenant_id)
|
|
193
|
+
if not sp_result:
|
|
194
|
+
raise ValueError(f"Service principal '{sp_name}' not found")
|
|
195
|
+
|
|
196
|
+
client_id = sp_result.authCredentials.clientId
|
|
197
|
+
logger.info(f"Found service principal with app ID: {client_id}")
|
|
198
|
+
|
|
199
|
+
logger.critical(f"Resetting credentials for service principal '{sp_name}'")
|
|
200
|
+
|
|
201
|
+
reset_cmd: list[str] = [
|
|
202
|
+
"az",
|
|
203
|
+
"ad",
|
|
204
|
+
"sp",
|
|
205
|
+
"credential",
|
|
206
|
+
"reset",
|
|
207
|
+
"--id",
|
|
208
|
+
client_id,
|
|
209
|
+
"--display-name",
|
|
210
|
+
credential_name,
|
|
211
|
+
"--years",
|
|
212
|
+
str(years),
|
|
213
|
+
"--output",
|
|
214
|
+
"json",
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
result: dict[str, Any] = run_command(reset_cmd)
|
|
218
|
+
|
|
219
|
+
logger.success(f"Credentials reset for service principal '{sp_name}'")
|
|
220
|
+
|
|
221
|
+
return SPAuthCredentialsWithSecret(
|
|
222
|
+
clientId=client_id,
|
|
223
|
+
clientSecret=result.get("password", ""),
|
|
224
|
+
subscriptionId=subscription_id,
|
|
225
|
+
tenantId=tenant_id,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
except subprocess.CalledProcessError as e:
|
|
229
|
+
logger.error(f"Failed to reset service principal credentials: {str(e)}")
|
|
230
|
+
raise
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def delete_service_principal_by_name(sp_name: str) -> None:
|
|
234
|
+
"""
|
|
235
|
+
Delete a service principal by display name.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
sp_name: The display name of the service principal to delete
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
subprocess.CalledProcessError: If the delete operation fails
|
|
242
|
+
ValueError: If the service principal is not found
|
|
243
|
+
"""
|
|
244
|
+
try:
|
|
245
|
+
logger.info(f"Looking up service principal '{sp_name}'")
|
|
246
|
+
sp_object_id = exists_sp(sp_name)
|
|
247
|
+
if not sp_object_id:
|
|
248
|
+
logger.success(f"Service principal '{sp_name}' does not exist; nothing to delete")
|
|
249
|
+
return
|
|
250
|
+
logger.info(f"Found service principal with object ID: {sp_object_id}")
|
|
251
|
+
|
|
252
|
+
delete_cmd: list[str] = [
|
|
253
|
+
"az",
|
|
254
|
+
"ad",
|
|
255
|
+
"sp",
|
|
256
|
+
"delete",
|
|
257
|
+
"--id",
|
|
258
|
+
sp_object_id,
|
|
259
|
+
"--output",
|
|
260
|
+
"json",
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
run_command(delete_cmd)
|
|
264
|
+
logger.success(f"Service principal '{sp_name}' deleted successfully")
|
|
265
|
+
|
|
266
|
+
except subprocess.CalledProcessError as e:
|
|
267
|
+
logger.error(f"Failed to delete service principal: {str(e)}")
|
|
268
|
+
raise
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .logging import get_logger
|
|
7
|
+
|
|
8
|
+
logger = get_logger(__name__)
|
|
9
|
+
|
|
10
|
+
# Singleton credential instance
|
|
11
|
+
_credential = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run_command(command: list[str]) -> Any:
|
|
15
|
+
"""
|
|
16
|
+
Run an Azure CLI command and return the JSON output.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
command: List of command arguments starting with 'az'
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Parsed JSON output from the command
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
subprocess.CalledProcessError: If command fails
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
# Set environment to suppress Azure authentication logs
|
|
29
|
+
env = os.environ.copy()
|
|
30
|
+
env["AZURE_CORE_ONLY_SHOW_ERRORS"] = "True"
|
|
31
|
+
|
|
32
|
+
result = subprocess.run(
|
|
33
|
+
command,
|
|
34
|
+
capture_output=True,
|
|
35
|
+
text=True,
|
|
36
|
+
check=True,
|
|
37
|
+
env=env,
|
|
38
|
+
)
|
|
39
|
+
if result.stdout.strip() == "":
|
|
40
|
+
return {}
|
|
41
|
+
return json.loads(result.stdout)
|
|
42
|
+
except subprocess.CalledProcessError as e:
|
|
43
|
+
logger.error(f"Command failed: {' '.join(command)}")
|
|
44
|
+
logger.error(f"Error: {e.stderr}")
|
|
45
|
+
raise
|
|
46
|
+
except json.JSONDecodeError as e:
|
|
47
|
+
logger.error(f"Failed to parse JSON output: {e}")
|
|
48
|
+
return {}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_subscription_and_tenant() -> tuple[str, str]:
|
|
52
|
+
"""
|
|
53
|
+
Get subscription ID and tenant ID from Azure CLI.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Tuple of (subscription_id, tenant_id)
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ValueError: If subscription or tenant ID cannot be retrieved
|
|
60
|
+
"""
|
|
61
|
+
account_info = run_command(["az", "account", "show", "--output", "json"])
|
|
62
|
+
subscription_id: str = account_info.get("id", "")
|
|
63
|
+
tenant_id: str = account_info.get("tenantId", "")
|
|
64
|
+
|
|
65
|
+
if not subscription_id or not tenant_id:
|
|
66
|
+
raise ValueError("Failed to retrieve subscription or tenant information")
|
|
67
|
+
|
|
68
|
+
return subscription_id, tenant_id
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_credential(cache: bool = True):
|
|
72
|
+
"""
|
|
73
|
+
Get or create Azure CLI credential.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
cache: If True, returns cached credential singleton. If False,
|
|
77
|
+
creates new credential each time.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
AzureCliCredential instance
|
|
81
|
+
|
|
82
|
+
Note:
|
|
83
|
+
Using cached credentials (cache=True) is recommended for most use cases to avoid
|
|
84
|
+
repeated authentication overhead. Set cache=False only if you need isolated credentials
|
|
85
|
+
for testing or specific scenarios.
|
|
86
|
+
"""
|
|
87
|
+
from azure.identity import AzureCliCredential
|
|
88
|
+
|
|
89
|
+
global _credential
|
|
90
|
+
|
|
91
|
+
if cache:
|
|
92
|
+
if _credential is None:
|
|
93
|
+
_credential = AzureCliCredential()
|
|
94
|
+
return _credential
|
|
95
|
+
else:
|
|
96
|
+
return AzureCliCredential()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Docker utility functions for image operations."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def image_exists(full_image_name: str) -> bool:
|
|
8
|
+
"""
|
|
9
|
+
Check if a Docker image exists locally.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
full_image_name: Full image name including registry, repository, and tag
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
True if the image exists locally, False otherwise
|
|
16
|
+
"""
|
|
17
|
+
check_image_result = subprocess.run(
|
|
18
|
+
["docker", "image", "inspect", full_image_name],
|
|
19
|
+
capture_output=True,
|
|
20
|
+
text=True,
|
|
21
|
+
)
|
|
22
|
+
return check_image_result.returncode == 0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def push_image(full_image_name: str) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Push a Docker image to the registry.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
full_image_name: Full image name including registry, repository, and tag
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
RuntimeError: If the docker push command fails
|
|
34
|
+
"""
|
|
35
|
+
push_result = subprocess.run(
|
|
36
|
+
["docker", "push", full_image_name],
|
|
37
|
+
capture_output=True,
|
|
38
|
+
text=True,
|
|
39
|
+
)
|
|
40
|
+
if push_result.returncode != 0:
|
|
41
|
+
raise RuntimeError(f"Docker push failed {push_result.stderr}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def pull_image(full_image_name: str) -> None:
|
|
45
|
+
"""
|
|
46
|
+
Pull a Docker image from the registry.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
full_image_name: Full image name including registry, repository, and tag
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
RuntimeError: If the docker pull command fails
|
|
53
|
+
"""
|
|
54
|
+
pull_result = subprocess.run(
|
|
55
|
+
["docker", "pull", full_image_name],
|
|
56
|
+
capture_output=True,
|
|
57
|
+
text=True,
|
|
58
|
+
)
|
|
59
|
+
if pull_result.returncode != 0:
|
|
60
|
+
raise RuntimeError(f"Docker pull failed: {pull_result.stderr}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def tag_image(source_image: str, target_image: str) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Tag a Docker image locally.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
source_image: Full name of the source image
|
|
69
|
+
target_image: Full name of the target image
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
RuntimeError: If the docker tag command fails
|
|
73
|
+
"""
|
|
74
|
+
tag_result = subprocess.run(
|
|
75
|
+
["docker", "tag", source_image, target_image],
|
|
76
|
+
capture_output=True,
|
|
77
|
+
text=True,
|
|
78
|
+
)
|
|
79
|
+
if tag_result.returncode != 0:
|
|
80
|
+
raise RuntimeError(f"Docker tag failed: {tag_result.stderr}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def pull_retag_and_push_image(
|
|
84
|
+
source_full_image_name: str,
|
|
85
|
+
target_full_image_name: str,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Pull an existing image, retag it, and push to registry.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
source_full_image_name: Full name of the source image (registry/image:tag)
|
|
92
|
+
target_full_image_name: Full name of the target image (registry/image:new_tag)
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
RuntimeError: If the source image doesn't exist or operations fail
|
|
96
|
+
"""
|
|
97
|
+
if not image_exists(source_full_image_name):
|
|
98
|
+
pull_image(source_full_image_name)
|
|
99
|
+
|
|
100
|
+
tag_image(source_full_image_name, target_full_image_name)
|
|
101
|
+
push_image(target_full_image_name)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def build_and_push_image(
|
|
105
|
+
dockerfile: str,
|
|
106
|
+
full_image_name: str,
|
|
107
|
+
) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Build a Docker image using buildx and push to registry.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
dockerfile: Path to the Dockerfile
|
|
113
|
+
full_image_name: Full image name including registry, repository, and tag
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
RuntimeError: If the docker build and push command fails
|
|
117
|
+
"""
|
|
118
|
+
src_folder = str(Path(dockerfile).parent)
|
|
119
|
+
build_result = subprocess.run(
|
|
120
|
+
[
|
|
121
|
+
"docker",
|
|
122
|
+
"buildx",
|
|
123
|
+
"build",
|
|
124
|
+
"--platform",
|
|
125
|
+
"linux/amd64",
|
|
126
|
+
"-t",
|
|
127
|
+
full_image_name,
|
|
128
|
+
"-f",
|
|
129
|
+
dockerfile,
|
|
130
|
+
src_folder,
|
|
131
|
+
"--push",
|
|
132
|
+
],
|
|
133
|
+
capture_output=True,
|
|
134
|
+
text=True,
|
|
135
|
+
)
|
|
136
|
+
if build_result.returncode != 0:
|
|
137
|
+
raise RuntimeError(f"Docker build and push failed: {build_result.stderr}")
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Environment variable and credential file handling utilities."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from string import Template
|
|
5
|
+
|
|
6
|
+
from dotenv import dotenv_values
|
|
7
|
+
|
|
8
|
+
from .logging import get_logger
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def substitute_env_vars(value: str, env_vars: dict[str, str]) -> str:
|
|
14
|
+
"""
|
|
15
|
+
Substitute environment variables in a string using Template format.
|
|
16
|
+
|
|
17
|
+
All ${VAR_NAME} placeholders in the value must have corresponding entries in
|
|
18
|
+
env_vars, otherwise a KeyError is raised.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
value: String with ${VAR_NAME} placeholders
|
|
22
|
+
env_vars: Dictionary of variable values
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Fully substituted string with all variables replaced
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
KeyError: If any ${VAR_NAME} placeholders in value are not found in
|
|
29
|
+
env_vars
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
>>> env_vars = {
|
|
33
|
+
... "SUBSCRIPTION_ID": "12345678-1234-1234-1234-123456789012",
|
|
34
|
+
... "RESOURCE_GROUP": "my-rg",
|
|
35
|
+
... "OPENAI_RESOURCE_NAME": "my-openai",
|
|
36
|
+
... }
|
|
37
|
+
>>> scope = "/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/\
|
|
38
|
+
${RESOURCE_GROUP}/providers/Microsoft.CognitiveServices/accounts/\
|
|
39
|
+
${OPENAI_RESOURCE_NAME}"
|
|
40
|
+
>>> substitute_env_vars(scope, env_vars)
|
|
41
|
+
'/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/\
|
|
42
|
+
my-rg/providers/Microsoft.CognitiveServices/accounts/my-openai'
|
|
43
|
+
"""
|
|
44
|
+
template = Template(value)
|
|
45
|
+
result = template.substitute(env_vars)
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_env_vars_from_files(
|
|
50
|
+
env_file_paths: list[Path] | None,
|
|
51
|
+
) -> dict[str, str]:
|
|
52
|
+
"""
|
|
53
|
+
Load environment variables from multiple .env files using python-dotenv.
|
|
54
|
+
|
|
55
|
+
Files are loaded in order and merged, with later files overriding earlier
|
|
56
|
+
ones.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
env_file_paths: List of paths to .env files with KEY=VALUE format
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Merged dictionary of environment variables
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
FileNotFoundError: If any file doesn't exist
|
|
66
|
+
Exception: If file parsing fails
|
|
67
|
+
"""
|
|
68
|
+
if not env_file_paths:
|
|
69
|
+
return {}
|
|
70
|
+
|
|
71
|
+
merged_vars: dict[str, str | None] = {}
|
|
72
|
+
|
|
73
|
+
for env_file_path in env_file_paths:
|
|
74
|
+
env_vars = dotenv_values(env_file_path)
|
|
75
|
+
merged_vars.update(env_vars)
|
|
76
|
+
logger.info(f"Loaded {len(env_vars)} variables from {env_file_path}")
|
|
77
|
+
|
|
78
|
+
logger.info(f"Total merged: {len(merged_vars)} unique environment variables")
|
|
79
|
+
merged_vars_cleaned: dict[str, str] = {k: v for k, v in merged_vars.items() if v is not None}
|
|
80
|
+
return merged_vars_cleaned
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def add_var_to_env_file(
|
|
84
|
+
env_var_map: dict[str, str],
|
|
85
|
+
env_file_path: Path,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Add or update in an environment file.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
env_var_map: Dictionary of environment variable key-value pairs
|
|
92
|
+
env_file_path: Path to the environment file
|
|
93
|
+
"""
|
|
94
|
+
env_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
existing_content = ""
|
|
96
|
+
if env_file_path.exists():
|
|
97
|
+
with open(env_file_path) as f:
|
|
98
|
+
existing_content = f.read()
|
|
99
|
+
lines = [
|
|
100
|
+
line
|
|
101
|
+
for line in existing_content.split("\n")
|
|
102
|
+
if not any(line.startswith(f"{env_var_key}=") for env_var_key in env_var_map.keys())
|
|
103
|
+
]
|
|
104
|
+
with open(env_file_path, "w") as f:
|
|
105
|
+
f.write("\n".join(lines).rstrip())
|
|
106
|
+
for env_var_key, env_var_value in env_var_map.items():
|
|
107
|
+
env_var_line = f"{env_var_key}={env_var_value}\n"
|
|
108
|
+
f.write("\n" + env_var_line)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from azure.mgmt.keyvault import KeyVaultManagementClient
|
|
2
|
+
|
|
3
|
+
from ..utils.azure_cli import get_credential
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_key_vault_client(
|
|
7
|
+
subscription_id: str, resource_group: str, key_vault_name: str
|
|
8
|
+
) -> KeyVaultManagementClient:
|
|
9
|
+
credential = get_credential()
|
|
10
|
+
kv_client = KeyVaultManagementClient(credential, subscription_id)
|
|
11
|
+
return kv_client
|