blackant-sdk 1.0.2__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.
- blackant/__init__.py +31 -0
- blackant/auth/__init__.py +10 -0
- blackant/auth/blackant_auth.py +518 -0
- blackant/auth/keycloak_manager.py +363 -0
- blackant/auth/request_id.py +52 -0
- blackant/auth/role_assignment.py +443 -0
- blackant/auth/tokens.py +57 -0
- blackant/client.py +400 -0
- blackant/config/__init__.py +0 -0
- blackant/config/docker_config.py +457 -0
- blackant/config/keycloak_admin_config.py +107 -0
- blackant/docker/__init__.py +12 -0
- blackant/docker/builder.py +616 -0
- blackant/docker/client.py +983 -0
- blackant/docker/dao.py +462 -0
- blackant/docker/registry.py +172 -0
- blackant/exceptions.py +111 -0
- blackant/http/__init__.py +8 -0
- blackant/http/client.py +125 -0
- blackant/patterns/__init__.py +1 -0
- blackant/patterns/singleton.py +20 -0
- blackant/services/__init__.py +10 -0
- blackant/services/dao.py +414 -0
- blackant/services/registry.py +635 -0
- blackant/utils/__init__.py +8 -0
- blackant/utils/initialization.py +32 -0
- blackant/utils/logging.py +337 -0
- blackant/utils/request_id.py +13 -0
- blackant/utils/store.py +50 -0
- blackant_sdk-1.0.2.dist-info/METADATA +117 -0
- blackant_sdk-1.0.2.dist-info/RECORD +70 -0
- blackant_sdk-1.0.2.dist-info/WHEEL +5 -0
- blackant_sdk-1.0.2.dist-info/top_level.txt +5 -0
- calculation/__init__.py +0 -0
- calculation/base.py +26 -0
- calculation/errors.py +2 -0
- calculation/impl/__init__.py +0 -0
- calculation/impl/my_calculation.py +144 -0
- calculation/impl/simple_calc.py +53 -0
- calculation/impl/test.py +1 -0
- calculation/impl/test_calc.py +36 -0
- calculation/loader.py +227 -0
- notifinations/__init__.py +8 -0
- notifinations/mail_sender.py +212 -0
- storage/__init__.py +0 -0
- storage/errors.py +10 -0
- storage/factory.py +26 -0
- storage/interface.py +19 -0
- storage/minio.py +106 -0
- task/__init__.py +0 -0
- task/dao.py +38 -0
- task/errors.py +10 -0
- task/log_adapter.py +11 -0
- task/parsers/__init__.py +0 -0
- task/parsers/base.py +13 -0
- task/parsers/callback.py +40 -0
- task/parsers/cmd_args.py +52 -0
- task/parsers/freetext.py +19 -0
- task/parsers/objects.py +50 -0
- task/parsers/request.py +56 -0
- task/resource.py +84 -0
- task/states/__init__.py +0 -0
- task/states/base.py +14 -0
- task/states/error.py +47 -0
- task/states/idle.py +12 -0
- task/states/ready.py +51 -0
- task/states/running.py +21 -0
- task/states/set_up.py +40 -0
- task/states/tear_down.py +29 -0
- task/task.py +358 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
"""Docker image builder module for BlackAnt SDK."""
|
|
2
|
+
|
|
3
|
+
import docker
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional, Dict, Any, Iterator
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
from blackant.utils.logging import get_logger
|
|
12
|
+
from blackant.exceptions import BlackAntDockerError
|
|
13
|
+
from blackant.config.docker_config import get_docker_config
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DockerImageBuilder:
|
|
17
|
+
"""Remote Docker image builder class for BlackAnt SDK.
|
|
18
|
+
|
|
19
|
+
Implements remote Docker build using BlackAnt infrastructure as per
|
|
20
|
+
the architecture specification. Supports both nginx proxy (recommended)
|
|
21
|
+
and direct TLS connection to remote Docker daemon.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, auth=None, registry_url: str = None):
|
|
25
|
+
"""Initialize remote Docker builder.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
auth: BlackAntAuth instance for JWT token authentication
|
|
29
|
+
registry_url: Docker registry URL (if None, loaded from config)
|
|
30
|
+
"""
|
|
31
|
+
self.logger = get_logger("docker-builder")
|
|
32
|
+
self.config = get_docker_config()
|
|
33
|
+
self.auth = auth
|
|
34
|
+
self.registry_url = registry_url or self.config.registry.url
|
|
35
|
+
|
|
36
|
+
# Initialize remote Docker client
|
|
37
|
+
self._init_remote_docker_client()
|
|
38
|
+
|
|
39
|
+
def _init_remote_docker_client(self):
|
|
40
|
+
"""Initialize Docker client with remote daemon connection.
|
|
41
|
+
|
|
42
|
+
Supports two modes:
|
|
43
|
+
1. Nginx proxy with JWT token authentication (recommended)
|
|
44
|
+
2. Direct TLS connection to Docker daemon
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
BlackAntDockerError: If remote Docker connection fails
|
|
48
|
+
"""
|
|
49
|
+
base_url = self.config.daemon.base_url
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
# Mode 1: Nginx proxy with JWT token authentication
|
|
53
|
+
if self.config.daemon.use_nginx_proxy:
|
|
54
|
+
# IMPORTANT: Create APIClient with explicit version to bypass auto-detection
|
|
55
|
+
# Auto-detection calls version() during __init__() which requires JWT auth
|
|
56
|
+
from docker import APIClient
|
|
57
|
+
|
|
58
|
+
# Get JWT token if available
|
|
59
|
+
token = None
|
|
60
|
+
if self.auth:
|
|
61
|
+
token = self.auth.get_token()
|
|
62
|
+
if not token:
|
|
63
|
+
self.logger.warning("No JWT token available for Docker daemon authentication")
|
|
64
|
+
|
|
65
|
+
# Check if we should disable SSL verification (for dev environments)
|
|
66
|
+
should_disable_ssl_verify = (
|
|
67
|
+
"dev.blackant.app" in base_url or
|
|
68
|
+
"localhost" in base_url or
|
|
69
|
+
"127.0.0.1" in base_url
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Disable SSL warnings for dev environments
|
|
73
|
+
if should_disable_ssl_verify:
|
|
74
|
+
import urllib3
|
|
75
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
76
|
+
self.logger.debug(f"SSL verification disabled for dev environment: {base_url}")
|
|
77
|
+
|
|
78
|
+
# Create TLS config for HTTPS connections in dev environment
|
|
79
|
+
tls_config = None
|
|
80
|
+
if "https://" in base_url and should_disable_ssl_verify:
|
|
81
|
+
from docker import tls
|
|
82
|
+
tls_config = tls.TLSConfig(verify=False)
|
|
83
|
+
self.logger.debug("TLS config created with SSL verification disabled")
|
|
84
|
+
|
|
85
|
+
# Create APIClient with explicit API version (bypasses auto-detection)
|
|
86
|
+
api_client = APIClient(
|
|
87
|
+
base_url=base_url,
|
|
88
|
+
timeout=self.config.daemon.timeout,
|
|
89
|
+
version='1.47', # Explicit version prevents auth-required version() call during init
|
|
90
|
+
tls=tls_config # SSL verification disabled for dev environments
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Add JWT token for subsequent API calls
|
|
94
|
+
if token:
|
|
95
|
+
api_client.headers.update({
|
|
96
|
+
'Authorization': f'Bearer {token}'
|
|
97
|
+
})
|
|
98
|
+
self.logger.debug("JWT token injected into API client headers")
|
|
99
|
+
|
|
100
|
+
# Wrap APIClient in a DockerClient for high-level operations
|
|
101
|
+
# Use from_env() as dummy then replace api attribute
|
|
102
|
+
self.docker_client = object.__new__(docker.DockerClient)
|
|
103
|
+
self.docker_client.api = api_client
|
|
104
|
+
|
|
105
|
+
# Initialize collections manually
|
|
106
|
+
from docker.models.containers import ContainerCollection
|
|
107
|
+
from docker.models.images import ImageCollection
|
|
108
|
+
from docker.models.networks import NetworkCollection
|
|
109
|
+
from docker.models.volumes import VolumeCollection
|
|
110
|
+
from docker.models.services import ServiceCollection
|
|
111
|
+
|
|
112
|
+
object.__setattr__(self.docker_client, '_containers', ContainerCollection(client=self.docker_client))
|
|
113
|
+
object.__setattr__(self.docker_client, '_images', ImageCollection(client=self.docker_client))
|
|
114
|
+
object.__setattr__(self.docker_client, '_networks', NetworkCollection(client=self.docker_client))
|
|
115
|
+
object.__setattr__(self.docker_client, '_volumes', VolumeCollection(client=self.docker_client))
|
|
116
|
+
object.__setattr__(self.docker_client, '_services', ServiceCollection(client=self.docker_client))
|
|
117
|
+
|
|
118
|
+
if token:
|
|
119
|
+
self.logger.info(f"Remote Docker client initialized via nginx proxy with JWT: {base_url}")
|
|
120
|
+
else:
|
|
121
|
+
self.logger.info(f"Remote Docker client initialized via nginx proxy (no auth): {base_url}")
|
|
122
|
+
|
|
123
|
+
# Mode 2: Direct TLS connection
|
|
124
|
+
else:
|
|
125
|
+
tls_config = self.config.daemon.get_tls_config()
|
|
126
|
+
|
|
127
|
+
self.docker_client = docker.DockerClient(
|
|
128
|
+
base_url=base_url,
|
|
129
|
+
tls=tls_config,
|
|
130
|
+
timeout=self.config.daemon.timeout
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
self.logger.info(f"Remote Docker client initialized with TLS: {base_url}")
|
|
134
|
+
|
|
135
|
+
# Test connection to remote Docker daemon
|
|
136
|
+
try:
|
|
137
|
+
version_info = self.docker_client.version()
|
|
138
|
+
self.logger.info(f"Connected to remote Docker daemon version: {version_info.get('Version', 'unknown')}")
|
|
139
|
+
self.logger.debug(f"Docker API version: {version_info.get('ApiVersion', 'unknown')}")
|
|
140
|
+
except Exception as ping_error:
|
|
141
|
+
self.logger.warning(f"Could not verify remote Docker connection: {ping_error}")
|
|
142
|
+
self.logger.warning("Continuing anyway - build operations might still work")
|
|
143
|
+
|
|
144
|
+
except Exception as e:
|
|
145
|
+
self.logger.error(f"Failed to initialize remote Docker client: {e}")
|
|
146
|
+
raise BlackAntDockerError(f"Remote Docker client initialization failed: {e}")
|
|
147
|
+
|
|
148
|
+
def build_service(self,
|
|
149
|
+
service_name: str,
|
|
150
|
+
impl_path: str = "src/calculation/impl",
|
|
151
|
+
dockerfile_path: str = "Dockerfile",
|
|
152
|
+
tag: Optional[str] = None,
|
|
153
|
+
push_to_registry: bool = True) -> Dict[str, Any]:
|
|
154
|
+
"""Build and push service Docker image on REMOTE Docker daemon.
|
|
155
|
+
|
|
156
|
+
The build context is automatically uploaded to the remote Docker daemon
|
|
157
|
+
by docker-py SDK as a tar.gz archive. The build executes on BlackAnt
|
|
158
|
+
infrastructure, ensuring the source code never leaves the platform.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
service_name: Service name
|
|
162
|
+
impl_path: Implementation folder path (will be uploaded)
|
|
163
|
+
dockerfile_path: Dockerfile path (will be uploaded)
|
|
164
|
+
tag: Image tag (default: timestamp)
|
|
165
|
+
push_to_registry: Push to registry
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
dict: Build result information
|
|
169
|
+
{
|
|
170
|
+
"image_name": "env.blackant.app/systemdevelopers/service_name",
|
|
171
|
+
"tag": "v1.0.0-20250105-123456",
|
|
172
|
+
"image_id": "sha256:...",
|
|
173
|
+
"size": 125000000,
|
|
174
|
+
"pushed": True,
|
|
175
|
+
"registry_url": "env.blackant.app",
|
|
176
|
+
"build_location": "remote",
|
|
177
|
+
"remote_daemon": "http://localhost"
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
Raises:
|
|
181
|
+
BlackAntDockerError: Build or push error
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
# 1. Generate tag if not provided
|
|
185
|
+
if not tag:
|
|
186
|
+
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
187
|
+
tag = f"v1.0.0-{timestamp}"
|
|
188
|
+
|
|
189
|
+
# 2. Build full image name using config
|
|
190
|
+
full_image = self.config.get_full_image_name(service_name, tag)
|
|
191
|
+
|
|
192
|
+
self.logger.info(f"Building image on REMOTE Docker daemon: {full_image}")
|
|
193
|
+
self.logger.info(f"Remote daemon: {self.config.daemon.base_url}")
|
|
194
|
+
|
|
195
|
+
# 3. Input validation
|
|
196
|
+
self._validate_build_inputs(service_name, impl_path, dockerfile_path)
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
# 4. Prepare build context
|
|
200
|
+
build_context = self._prepare_build_context(impl_path, dockerfile_path)
|
|
201
|
+
|
|
202
|
+
# 5. Build Docker image
|
|
203
|
+
image, build_logs = self._build_image(full_image, build_context, service_name)
|
|
204
|
+
|
|
205
|
+
# 6. Push to registry
|
|
206
|
+
pushed = False
|
|
207
|
+
if push_to_registry:
|
|
208
|
+
pushed = self._push_to_registry(full_image)
|
|
209
|
+
|
|
210
|
+
# 7. Cleanup local build context
|
|
211
|
+
try:
|
|
212
|
+
shutil.rmtree(build_context, ignore_errors=True)
|
|
213
|
+
self.logger.debug("Local build context cleaned up")
|
|
214
|
+
except Exception as cleanup_error:
|
|
215
|
+
self.logger.warning(f"Failed to cleanup build context: {cleanup_error}")
|
|
216
|
+
|
|
217
|
+
# 8. Assemble result
|
|
218
|
+
result = {
|
|
219
|
+
"image_name": f"{self.registry_url}/{self.config.registry.namespace}/{service_name}",
|
|
220
|
+
"tag": tag,
|
|
221
|
+
"full_image": full_image,
|
|
222
|
+
"image_id": image.id,
|
|
223
|
+
"size": image.attrs.get('Size', 0),
|
|
224
|
+
"pushed": pushed,
|
|
225
|
+
"registry_url": self.registry_url,
|
|
226
|
+
"created": image.attrs.get('Created'),
|
|
227
|
+
"labels": image.labels or {},
|
|
228
|
+
"build_location": "remote", # Indicates remote build
|
|
229
|
+
"remote_daemon": self.config.daemon.base_url, # Remote daemon URL
|
|
230
|
+
"use_nginx_proxy": self.config.daemon.use_nginx_proxy # Connection mode
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
self.logger.info(f"Remote build completed successfully: {service_name}")
|
|
234
|
+
return result
|
|
235
|
+
|
|
236
|
+
except docker.errors.BuildError as e:
|
|
237
|
+
error_msg = f"Docker build failed for {service_name}: {e}"
|
|
238
|
+
self.logger.error(error_msg)
|
|
239
|
+
raise BlackAntDockerError(error_msg)
|
|
240
|
+
except Exception as e:
|
|
241
|
+
error_msg = f"Unexpected error during build of {service_name}: {e}"
|
|
242
|
+
self.logger.error(error_msg)
|
|
243
|
+
raise BlackAntDockerError(error_msg)
|
|
244
|
+
|
|
245
|
+
def _validate_build_inputs(self, service_name: str, impl_path: str, dockerfile_path: str) -> None:
|
|
246
|
+
"""Validate build input parameters.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
service_name: Service name
|
|
250
|
+
impl_path: Implementation path
|
|
251
|
+
dockerfile_path: Dockerfile path
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
BlackAntDockerError: Validation error
|
|
255
|
+
"""
|
|
256
|
+
if not service_name or not service_name.strip():
|
|
257
|
+
raise BlackAntDockerError("Service name cannot be empty")
|
|
258
|
+
|
|
259
|
+
if not service_name.replace('-', '').replace('_', '').isalnum():
|
|
260
|
+
raise BlackAntDockerError("Service name must contain only alphanumeric characters, hyphens, and underscores")
|
|
261
|
+
|
|
262
|
+
impl_path_obj = Path(impl_path)
|
|
263
|
+
if not impl_path_obj.exists():
|
|
264
|
+
raise BlackAntDockerError(f"Implementation path does not exist: {impl_path}")
|
|
265
|
+
|
|
266
|
+
if not impl_path_obj.is_dir():
|
|
267
|
+
raise BlackAntDockerError(f"Implementation path is not a directory: {impl_path}")
|
|
268
|
+
|
|
269
|
+
dockerfile_path_obj = Path(dockerfile_path)
|
|
270
|
+
if not dockerfile_path_obj.exists():
|
|
271
|
+
raise BlackAntDockerError(f"Dockerfile does not exist: {dockerfile_path}")
|
|
272
|
+
|
|
273
|
+
if not dockerfile_path_obj.is_file():
|
|
274
|
+
raise BlackAntDockerError(f"Dockerfile path is not a file: {dockerfile_path}")
|
|
275
|
+
|
|
276
|
+
def _prepare_build_context(self, impl_path: str, dockerfile_path: str) -> str:
|
|
277
|
+
"""Prepare build context.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
impl_path: Implementation folder
|
|
281
|
+
dockerfile_path: Dockerfile path
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
str: Temporary build context path
|
|
285
|
+
|
|
286
|
+
Raises:
|
|
287
|
+
BlackAntDockerError: Context preparation error
|
|
288
|
+
"""
|
|
289
|
+
try:
|
|
290
|
+
# Create temporary build context
|
|
291
|
+
temp_context = tempfile.mkdtemp(prefix="blackant_build_")
|
|
292
|
+
context_path = Path(temp_context)
|
|
293
|
+
|
|
294
|
+
self.logger.debug(f"Created build context: {context_path}")
|
|
295
|
+
|
|
296
|
+
# Determine project root (where Dockerfile is located)
|
|
297
|
+
dockerfile_src = Path(dockerfile_path).resolve()
|
|
298
|
+
project_root = dockerfile_src.parent
|
|
299
|
+
|
|
300
|
+
# Copy Dockerfile
|
|
301
|
+
dockerfile_dst = context_path / "Dockerfile"
|
|
302
|
+
shutil.copy2(dockerfile_src, dockerfile_dst)
|
|
303
|
+
|
|
304
|
+
# Detect build mode: full project or simple build
|
|
305
|
+
src_path = project_root / "src"
|
|
306
|
+
has_src_directory = src_path.exists() and src_path.is_dir()
|
|
307
|
+
|
|
308
|
+
if has_src_directory:
|
|
309
|
+
# FULL PROJECT MODE (e.g., test_full_state_machine_e2e.py)
|
|
310
|
+
# Copy entire src/ directory - contains everything including impl
|
|
311
|
+
shutil.copytree(src_path, context_path / "src", dirs_exist_ok=True)
|
|
312
|
+
self.logger.debug("Full project mode: Copied src/ directory")
|
|
313
|
+
|
|
314
|
+
# Copy project-level files
|
|
315
|
+
requirements_files = ["requirements.txt", "requirements.debug.txt"]
|
|
316
|
+
for req_file in requirements_files:
|
|
317
|
+
req_src = project_root / req_file
|
|
318
|
+
if req_src.exists():
|
|
319
|
+
shutil.copy2(req_src, context_path / req_file)
|
|
320
|
+
self.logger.debug(f"Copied {req_file}")
|
|
321
|
+
|
|
322
|
+
entrypoint_src = project_root / "docker-entrypoint.sh"
|
|
323
|
+
if entrypoint_src.exists():
|
|
324
|
+
shutil.copy2(entrypoint_src, context_path / "docker-entrypoint.sh")
|
|
325
|
+
self.logger.debug("Copied docker-entrypoint.sh")
|
|
326
|
+
|
|
327
|
+
else:
|
|
328
|
+
# SIMPLE BUILD MODE (e.g., test_remote_docker_build.py)
|
|
329
|
+
# Copy only impl/ directory - minimal build context
|
|
330
|
+
impl_src = Path(impl_path).resolve()
|
|
331
|
+
impl_dst = context_path / "impl"
|
|
332
|
+
if impl_src.exists() and impl_src.is_dir():
|
|
333
|
+
shutil.copytree(impl_src, impl_dst, dirs_exist_ok=True)
|
|
334
|
+
self.logger.debug(f"Simple build mode: Copied impl/ from {impl_src}")
|
|
335
|
+
else:
|
|
336
|
+
raise BlackAntDockerError(f"Implementation path not found: {impl_path}")
|
|
337
|
+
|
|
338
|
+
# Create .dockerignore file if it doesn't exist
|
|
339
|
+
dockerignore_path = context_path / ".dockerignore"
|
|
340
|
+
if not dockerignore_path.exists():
|
|
341
|
+
dockerignore_content = """
|
|
342
|
+
__pycache__/
|
|
343
|
+
*.pyc
|
|
344
|
+
*.pyo
|
|
345
|
+
*.pyd
|
|
346
|
+
.git/
|
|
347
|
+
.gitignore
|
|
348
|
+
*.md
|
|
349
|
+
.pytest_cache/
|
|
350
|
+
.coverage
|
|
351
|
+
"""
|
|
352
|
+
dockerignore_path.write_text(dockerignore_content.strip())
|
|
353
|
+
|
|
354
|
+
self.logger.debug(f"Build context prepared with {len(list(context_path.rglob('*')))} files")
|
|
355
|
+
return str(context_path)
|
|
356
|
+
|
|
357
|
+
except Exception as e:
|
|
358
|
+
error_msg = f"Failed to prepare build context: {e}"
|
|
359
|
+
self.logger.error(error_msg)
|
|
360
|
+
raise BlackAntDockerError(error_msg)
|
|
361
|
+
|
|
362
|
+
def _build_image(self, full_image: str, build_context: str, service_name: str) -> tuple:
|
|
363
|
+
"""Execute Docker image build on REMOTE daemon.
|
|
364
|
+
|
|
365
|
+
The docker-py SDK automatically handles:
|
|
366
|
+
1. Creating tar.gz archive from build_context path
|
|
367
|
+
2. Uploading to remote Docker daemon via configured connection
|
|
368
|
+
3. Executing build on remote infrastructure
|
|
369
|
+
4. Streaming build logs back to client
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
full_image: Full image name with tag
|
|
373
|
+
build_context: LOCAL build context path (will be uploaded)
|
|
374
|
+
service_name: Service name
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
tuple: (image object, build logs)
|
|
378
|
+
|
|
379
|
+
Raises:
|
|
380
|
+
docker.errors.BuildError: Build error
|
|
381
|
+
"""
|
|
382
|
+
self.logger.info(f"Starting REMOTE Docker build for {service_name}")
|
|
383
|
+
self.logger.info(f"Uploading build context to remote daemon...")
|
|
384
|
+
|
|
385
|
+
# Build arguments using config
|
|
386
|
+
build_args = self.config.get_build_args(service_name, {
|
|
387
|
+
"BUILD_DATE": datetime.now().isoformat(),
|
|
388
|
+
"CALCULATION_NAME": service_name # Required by Dockerfile
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
# Build labels using config
|
|
392
|
+
labels = self.config.get_build_labels(service_name)
|
|
393
|
+
|
|
394
|
+
# Execute REMOTE Docker build
|
|
395
|
+
# The docker-py SDK automatically:
|
|
396
|
+
# - Creates tar.gz archive from build_context
|
|
397
|
+
# - Uploads to remote Docker daemon
|
|
398
|
+
# - Executes build remotely
|
|
399
|
+
# - Streams logs back
|
|
400
|
+
image, build_logs = self.docker_client.images.build(
|
|
401
|
+
path=build_context, # LOCAL path - SDK uploads it automatically!
|
|
402
|
+
tag=full_image,
|
|
403
|
+
rm=self.config.build.rm,
|
|
404
|
+
forcerm=self.config.build.forcerm,
|
|
405
|
+
pull=self.config.build.pull,
|
|
406
|
+
buildargs=build_args,
|
|
407
|
+
labels=labels,
|
|
408
|
+
timeout=self.config.build.timeout,
|
|
409
|
+
platform="linux/amd64" # Ensure consistent platform
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Process build logs
|
|
413
|
+
for log in build_logs:
|
|
414
|
+
if 'stream' in log:
|
|
415
|
+
log_line = log['stream'].strip()
|
|
416
|
+
if log_line:
|
|
417
|
+
self.logger.debug(f"Remote build: {log_line}")
|
|
418
|
+
elif 'error' in log:
|
|
419
|
+
self.logger.error(f"Remote build error: {log['error']}")
|
|
420
|
+
|
|
421
|
+
self.logger.info(f"Remote image built successfully: {image.id[:12]}")
|
|
422
|
+
return image, build_logs
|
|
423
|
+
|
|
424
|
+
def _push_to_registry(self, image_name: str) -> bool:
|
|
425
|
+
"""Push image to Docker registry.
|
|
426
|
+
|
|
427
|
+
If SDK Services is configured, delegates push to sdk-services backend.
|
|
428
|
+
Otherwise, pushes directly using Docker client with local credentials.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
image_name: Full image name with tag
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
bool: Successful push
|
|
435
|
+
"""
|
|
436
|
+
# Check if SDK Services should handle the push
|
|
437
|
+
if self.config.sdk_services.use_for_registry and self.config.sdk_services.url:
|
|
438
|
+
return self._push_via_sdk_services(image_name)
|
|
439
|
+
|
|
440
|
+
# Direct push via Docker client
|
|
441
|
+
return self._push_direct(image_name)
|
|
442
|
+
|
|
443
|
+
def _push_via_sdk_services(self, image_name: str) -> bool:
|
|
444
|
+
"""Push image via SDK Services endpoint.
|
|
445
|
+
|
|
446
|
+
Delegates push to sdk-services backend, keeping registry credentials server-side.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
image_name: Full image name with tag
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
bool: Successful push
|
|
453
|
+
"""
|
|
454
|
+
try:
|
|
455
|
+
import requests
|
|
456
|
+
|
|
457
|
+
push_url = self.config.sdk_services.registry_push_url
|
|
458
|
+
self.logger.info(f"Pushing image via SDK Services: {push_url}")
|
|
459
|
+
|
|
460
|
+
# Get auth token if available
|
|
461
|
+
headers = {"Content-Type": "application/json"}
|
|
462
|
+
if self.auth:
|
|
463
|
+
token = self.auth.get_token()
|
|
464
|
+
if token:
|
|
465
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
466
|
+
|
|
467
|
+
# Call SDK Services endpoint
|
|
468
|
+
response = requests.post(
|
|
469
|
+
push_url,
|
|
470
|
+
json={"image_name": image_name},
|
|
471
|
+
headers=headers,
|
|
472
|
+
timeout=self.config.sdk_services.timeout,
|
|
473
|
+
verify=False # For dev environments with self-signed certs
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
if response.status_code == 200:
|
|
477
|
+
result = response.json()
|
|
478
|
+
if result.get("success"):
|
|
479
|
+
self.logger.info(f"Image pushed via SDK Services: {image_name}")
|
|
480
|
+
return True
|
|
481
|
+
else:
|
|
482
|
+
error_msg = result.get("message", "Unknown error")
|
|
483
|
+
self.logger.error(f"SDK Services push failed: {error_msg}")
|
|
484
|
+
return False
|
|
485
|
+
else:
|
|
486
|
+
self.logger.error(f"SDK Services push failed with status {response.status_code}")
|
|
487
|
+
return False
|
|
488
|
+
|
|
489
|
+
except Exception as e:
|
|
490
|
+
self.logger.error(f"SDK Services push error: {e}")
|
|
491
|
+
return False
|
|
492
|
+
|
|
493
|
+
def _push_direct(self, image_name: str) -> bool:
|
|
494
|
+
"""Push image directly via Docker client.
|
|
495
|
+
|
|
496
|
+
Uses local registry credentials from configuration.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
image_name: Full image name with tag
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
bool: Successful push
|
|
503
|
+
"""
|
|
504
|
+
try:
|
|
505
|
+
self.logger.info(f"Pushing image to registry: {image_name}")
|
|
506
|
+
|
|
507
|
+
# Registry login if credentials available
|
|
508
|
+
credentials = self.config.get_registry_credentials()
|
|
509
|
+
if credentials:
|
|
510
|
+
self.logger.debug(f"Logging in to registry: {self.registry_url}")
|
|
511
|
+
self.docker_client.login(**credentials)
|
|
512
|
+
|
|
513
|
+
# Execute push
|
|
514
|
+
push_response = self.docker_client.images.push(
|
|
515
|
+
image_name,
|
|
516
|
+
stream=True,
|
|
517
|
+
decode=True
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
# Process push logs
|
|
521
|
+
for line in push_response:
|
|
522
|
+
if 'status' in line:
|
|
523
|
+
status = line['status']
|
|
524
|
+
if 'id' in line:
|
|
525
|
+
self.logger.debug(f"Push {line['id']}: {status}")
|
|
526
|
+
else:
|
|
527
|
+
self.logger.debug(f"Push: {status}")
|
|
528
|
+
|
|
529
|
+
if 'error' in line:
|
|
530
|
+
error_msg = f"Push error: {line['error']}"
|
|
531
|
+
self.logger.error(error_msg)
|
|
532
|
+
raise BlackAntDockerError(error_msg)
|
|
533
|
+
|
|
534
|
+
if 'errorDetail' in line:
|
|
535
|
+
error_detail = line['errorDetail'].get('message', 'Unknown error')
|
|
536
|
+
error_msg = f"Push error detail: {error_detail}"
|
|
537
|
+
self.logger.error(error_msg)
|
|
538
|
+
raise BlackAntDockerError(error_msg)
|
|
539
|
+
|
|
540
|
+
self.logger.info(f"Image pushed successfully: {image_name}")
|
|
541
|
+
return True
|
|
542
|
+
|
|
543
|
+
except docker.errors.APIError as e:
|
|
544
|
+
error_msg = f"Docker API error during push: {e}"
|
|
545
|
+
self.logger.error(error_msg)
|
|
546
|
+
return False
|
|
547
|
+
except Exception as e:
|
|
548
|
+
error_msg = f"Unexpected error during push: {e}"
|
|
549
|
+
self.logger.error(error_msg)
|
|
550
|
+
return False
|
|
551
|
+
|
|
552
|
+
def list_local_images(self, service_prefix: str = None) -> list:
|
|
553
|
+
"""List local Docker images.
|
|
554
|
+
|
|
555
|
+
Args:
|
|
556
|
+
service_prefix: Service name prefix for filtering
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
list: List of image information
|
|
560
|
+
"""
|
|
561
|
+
try:
|
|
562
|
+
images = self.docker_client.images.list()
|
|
563
|
+
result = []
|
|
564
|
+
|
|
565
|
+
for image in images:
|
|
566
|
+
for tag in image.tags:
|
|
567
|
+
if service_prefix and service_prefix not in tag:
|
|
568
|
+
continue
|
|
569
|
+
|
|
570
|
+
result.append({
|
|
571
|
+
"id": image.id,
|
|
572
|
+
"tag": tag,
|
|
573
|
+
"created": image.attrs.get('Created'),
|
|
574
|
+
"size": image.attrs.get('Size', 0),
|
|
575
|
+
"labels": image.labels or {}
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
return result
|
|
579
|
+
|
|
580
|
+
except Exception as e:
|
|
581
|
+
self.logger.error(f"Failed to list local images: {e}")
|
|
582
|
+
return []
|
|
583
|
+
|
|
584
|
+
def remove_image(self, image_name: str, force: bool = False) -> bool:
|
|
585
|
+
"""Remove Docker image.
|
|
586
|
+
|
|
587
|
+
Args:
|
|
588
|
+
image_name: Image name or ID
|
|
589
|
+
force: Force removal
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
bool: Successful removal
|
|
593
|
+
"""
|
|
594
|
+
try:
|
|
595
|
+
self.docker_client.images.remove(image_name, force=force)
|
|
596
|
+
self.logger.info(f"Image removed: {image_name}")
|
|
597
|
+
return True
|
|
598
|
+
|
|
599
|
+
except docker.errors.ImageNotFound:
|
|
600
|
+
self.logger.warning(f"Image not found: {image_name}")
|
|
601
|
+
return False
|
|
602
|
+
except Exception as e:
|
|
603
|
+
self.logger.error(f"Failed to remove image {image_name}: {e}")
|
|
604
|
+
return False
|
|
605
|
+
|
|
606
|
+
def __enter__(self):
|
|
607
|
+
"""Context manager entry."""
|
|
608
|
+
return self
|
|
609
|
+
|
|
610
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
611
|
+
"""Context manager exit - cleanup resources."""
|
|
612
|
+
try:
|
|
613
|
+
if hasattr(self, 'docker_client'):
|
|
614
|
+
self.docker_client.close()
|
|
615
|
+
except Exception as e:
|
|
616
|
+
self.logger.warning(f"Error closing Docker client: {e}")
|