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.
Files changed (70) hide show
  1. blackant/__init__.py +31 -0
  2. blackant/auth/__init__.py +10 -0
  3. blackant/auth/blackant_auth.py +518 -0
  4. blackant/auth/keycloak_manager.py +363 -0
  5. blackant/auth/request_id.py +52 -0
  6. blackant/auth/role_assignment.py +443 -0
  7. blackant/auth/tokens.py +57 -0
  8. blackant/client.py +400 -0
  9. blackant/config/__init__.py +0 -0
  10. blackant/config/docker_config.py +457 -0
  11. blackant/config/keycloak_admin_config.py +107 -0
  12. blackant/docker/__init__.py +12 -0
  13. blackant/docker/builder.py +616 -0
  14. blackant/docker/client.py +983 -0
  15. blackant/docker/dao.py +462 -0
  16. blackant/docker/registry.py +172 -0
  17. blackant/exceptions.py +111 -0
  18. blackant/http/__init__.py +8 -0
  19. blackant/http/client.py +125 -0
  20. blackant/patterns/__init__.py +1 -0
  21. blackant/patterns/singleton.py +20 -0
  22. blackant/services/__init__.py +10 -0
  23. blackant/services/dao.py +414 -0
  24. blackant/services/registry.py +635 -0
  25. blackant/utils/__init__.py +8 -0
  26. blackant/utils/initialization.py +32 -0
  27. blackant/utils/logging.py +337 -0
  28. blackant/utils/request_id.py +13 -0
  29. blackant/utils/store.py +50 -0
  30. blackant_sdk-1.0.2.dist-info/METADATA +117 -0
  31. blackant_sdk-1.0.2.dist-info/RECORD +70 -0
  32. blackant_sdk-1.0.2.dist-info/WHEEL +5 -0
  33. blackant_sdk-1.0.2.dist-info/top_level.txt +5 -0
  34. calculation/__init__.py +0 -0
  35. calculation/base.py +26 -0
  36. calculation/errors.py +2 -0
  37. calculation/impl/__init__.py +0 -0
  38. calculation/impl/my_calculation.py +144 -0
  39. calculation/impl/simple_calc.py +53 -0
  40. calculation/impl/test.py +1 -0
  41. calculation/impl/test_calc.py +36 -0
  42. calculation/loader.py +227 -0
  43. notifinations/__init__.py +8 -0
  44. notifinations/mail_sender.py +212 -0
  45. storage/__init__.py +0 -0
  46. storage/errors.py +10 -0
  47. storage/factory.py +26 -0
  48. storage/interface.py +19 -0
  49. storage/minio.py +106 -0
  50. task/__init__.py +0 -0
  51. task/dao.py +38 -0
  52. task/errors.py +10 -0
  53. task/log_adapter.py +11 -0
  54. task/parsers/__init__.py +0 -0
  55. task/parsers/base.py +13 -0
  56. task/parsers/callback.py +40 -0
  57. task/parsers/cmd_args.py +52 -0
  58. task/parsers/freetext.py +19 -0
  59. task/parsers/objects.py +50 -0
  60. task/parsers/request.py +56 -0
  61. task/resource.py +84 -0
  62. task/states/__init__.py +0 -0
  63. task/states/base.py +14 -0
  64. task/states/error.py +47 -0
  65. task/states/idle.py +12 -0
  66. task/states/ready.py +51 -0
  67. task/states/running.py +21 -0
  68. task/states/set_up.py +40 -0
  69. task/states/tear_down.py +29 -0
  70. task/task.py +358 -0
@@ -0,0 +1,983 @@
1
+ """BlackAnt Docker Client module.
2
+
3
+ High-level Docker client providing user-friendly API for Docker operations
4
+ through BlackAnt platform with automatic authentication.
5
+ """
6
+
7
+ import os
8
+ import json
9
+ from typing import Dict, Any, Optional, List
10
+
11
+ from ..auth.blackant_auth import BlackAntAuth
12
+ from ..auth.role_assignment import get_role_assignment_manager
13
+ from ..http.client import HTTPClient, HTTPConnectionError
14
+ from ..exceptions import BlackAntDockerError
15
+ from ..utils.logging import get_logger
16
+ from .builder import DockerImageBuilder
17
+
18
+
19
+ class BlackAntDockerClient:
20
+ """Docker client with BlackAnt authentication integration.
21
+
22
+ Provides high-level Docker operations through BlackAnt's HTTP API
23
+ with automatic Bearer token authentication. All requests are routed
24
+ through Nginx proxy which validates tokens with Keycloak.
25
+
26
+ This client uses the existing HTTPClient with persistent sessions,
27
+ connection pooling, and request ID tracking for efficient and
28
+ traceable API communication.
29
+
30
+ Args:
31
+ auth (BlackAntAuth): Authentication object with credentials.
32
+ base_url (str, optional): Base URL for BlackAnt API.
33
+
34
+ Examples:
35
+ >>> auth = BlackAntAuth(user="my_name", password="xxx")
36
+ >>> client = BlackAntDockerClient(auth=auth)
37
+ >>> print(client.version) # "18.9.3"
38
+ >>> containers = client.containers()
39
+ """
40
+
41
+ def \
42
+ __init__(self, auth: BlackAntAuth, base_url: Optional[str] = None):
43
+ """Initialize Docker client with authentication.
44
+
45
+ Args:
46
+ auth: BlackAntAuth object with user credentials.
47
+ base_url: Optional base URL, defaults to environment variable.
48
+ """
49
+ self.auth = auth
50
+ self.base_url = base_url or os.getenv(
51
+ "BLACKANT_BASE_URL",
52
+ "https://dev.blackant.app"
53
+ )
54
+
55
+ # Initialize HTTPClient with persistent session and connection pooling
56
+ self.http_client = HTTPClient(
57
+ base_url=self.base_url,
58
+ auth_token_store=auth.token_store # Share token store with auth
59
+ )
60
+
61
+ self.logger = get_logger("docker.client")
62
+
63
+ # Initialize Docker image builder with auth for remote daemon
64
+ self.image_builder = DockerImageBuilder(auth=self.auth)
65
+
66
+ # Ensure authentication on initialization
67
+ self.auth.get_token()
68
+ self.logger.info("BlackAntDockerClient initialized")
69
+
70
+ @property
71
+ def version(self) -> str:
72
+ """Get Docker daemon version.
73
+
74
+ Returns the Docker daemon version string through BlackAnt API.
75
+ This provides the Docker daemon version information through BlackAnt API.
76
+
77
+ Returns:
78
+ str: Docker version string (e.g., "18.9.3").
79
+
80
+ Raises:
81
+ BlackAntDockerError: If version fetch fails.
82
+ """
83
+ try:
84
+ response = self.http_client.send_request(
85
+ endpoint="/api/docker/version",
86
+ method="GET"
87
+ )
88
+ response.raise_for_status()
89
+
90
+ data = response.json()
91
+ version = data.get("version", data.get("Version", "unknown"))
92
+
93
+ self.logger.debug(f"Docker version: {version}")
94
+ return version
95
+
96
+ except HTTPConnectionError as error:
97
+ self.logger.error(f"Failed to fetch Docker version: {error}")
98
+ raise BlackAntDockerError(f"Could not fetch Docker version: {error}") from error
99
+ except Exception as error:
100
+ self.logger.error(f"Unexpected error fetching version: {error}")
101
+ raise BlackAntDockerError(f"Version fetch failed: {error}") from error
102
+
103
+ def containers(self, all_containers: bool = False) -> List[Dict[str, Any]]:
104
+ """List Docker containers.
105
+
106
+ Args:
107
+ all_containers: If True, show all containers. If False, only running.
108
+
109
+ Returns:
110
+ List of container dictionaries with details.
111
+
112
+ Raises:
113
+ BlackAntDockerError: If container list fails.
114
+ """
115
+ try:
116
+ params = {"all": str(all_containers).lower()} if all_containers else {}
117
+
118
+ response = self.http_client.send_request(
119
+ endpoint="/api/docker/v1.24/containers/json",
120
+ method="GET",
121
+ json=params if params else None
122
+ )
123
+ response.raise_for_status()
124
+
125
+ containers = response.json()
126
+ self.logger.debug(f"Found {len(containers)} containers")
127
+ return containers
128
+
129
+ except HTTPConnectionError as error:
130
+ self.logger.error(f"Failed to list containers: {error}")
131
+ raise BlackAntDockerError(f"Could not list containers: {error}") from error
132
+ except Exception as error:
133
+ self.logger.error(f"Unexpected error listing containers: {error}")
134
+ raise BlackAntDockerError(f"Container list failed: {error}") from error
135
+
136
+ def images(self) -> List[Dict[str, Any]]:
137
+ """List Docker images.
138
+
139
+ Returns:
140
+ List of image dictionaries with details.
141
+
142
+ Raises:
143
+ BlackAntDockerError: If image list fails.
144
+ """
145
+ try:
146
+ response = self.http_client.send_request(
147
+ endpoint="/api/docker/v1.24/images/json",
148
+ method="GET"
149
+ )
150
+ response.raise_for_status()
151
+
152
+ images = response.json()
153
+ self.logger.debug(f"Found {len(images)} images")
154
+ return images
155
+
156
+ except HTTPConnectionError as error:
157
+ self.logger.error(f"Failed to list images: {error}")
158
+ raise BlackAntDockerError(f"Could not list images: {error}") from error
159
+ except Exception as error:
160
+ self.logger.error(f"Unexpected error listing images: {error}")
161
+ raise BlackAntDockerError(f"Image list failed: {error}") from error
162
+
163
+ def services(self) -> List[Dict[str, Any]]:
164
+ """List Docker services (Swarm mode).
165
+
166
+ Returns:
167
+ List of service dictionaries with details.
168
+
169
+ Raises:
170
+ BlackAntDockerError: If service list fails.
171
+ """
172
+ try:
173
+ response = self.http_client.send_request(
174
+ endpoint="/api/docker/services",
175
+ method="GET"
176
+ )
177
+ response.raise_for_status()
178
+
179
+ services = response.json()
180
+ self.logger.debug(f"Found {len(services)} services")
181
+ return services
182
+
183
+ except HTTPConnectionError as error:
184
+ self.logger.error(f"Failed to list services: {error}")
185
+ raise BlackAntDockerError(f"Could not list services: {error}") from error
186
+ except Exception as error:
187
+ self.logger.error(f"Unexpected error listing services: {error}")
188
+ raise BlackAntDockerError(f"Service list failed: {error}") from error
189
+
190
+ def info(self) -> Dict[str, Any]:
191
+ """Get Docker daemon system information.
192
+
193
+ Returns:
194
+ Dictionary with Docker daemon info.
195
+
196
+ Raises:
197
+ BlackAntDockerError: If info fetch fails.
198
+ """
199
+ try:
200
+ response = self.http_client.send_request(
201
+ endpoint="/api/docker/info",
202
+ method="GET"
203
+ )
204
+ response.raise_for_status()
205
+
206
+ info = response.json()
207
+ self.logger.debug("Fetched Docker daemon info")
208
+ return info
209
+
210
+ except HTTPConnectionError as error:
211
+ self.logger.error(f"Failed to fetch Docker info: {error}")
212
+ raise BlackAntDockerError(f"Could not fetch Docker info: {error}") from error
213
+ except Exception as error:
214
+ self.logger.error(f"Unexpected error fetching info: {error}")
215
+ raise BlackAntDockerError(f"Info fetch failed: {error}") from error
216
+
217
+ def ping(self) -> bool:
218
+ """Ping Docker daemon to check connectivity.
219
+
220
+ Returns:
221
+ bool: True if Docker daemon is accessible.
222
+ """
223
+ try:
224
+ response = self.http_client.send_request(
225
+ endpoint="/api/docker/v1.24/_ping",
226
+ method="GET"
227
+ )
228
+ return response.status_code == 200
229
+
230
+ except (ConnectionError, TimeoutError, ValueError):
231
+ return False
232
+
233
+ def build_image(self, dockerfile_path: str, tag: str, context_path: str = ".",
234
+ build_args: Optional[Dict[str, str]] = None,
235
+ stream_logs: bool = True) -> Dict[str, Any]:
236
+ """Build Docker image from Dockerfile with streaming logs.
237
+
238
+ Args:
239
+ dockerfile_path: Path to Dockerfile relative to context.
240
+ tag: Image tag (e.g., "myapp:latest").
241
+ context_path: Build context directory path.
242
+ build_args: Build arguments dict.
243
+ stream_logs: Whether to stream build logs.
244
+
245
+ Returns:
246
+ Build result with logs and image ID.
247
+
248
+ Raises:
249
+ BlackAntDockerError: If build fails.
250
+ """
251
+ try:
252
+ import tarfile
253
+ import io
254
+ import base64
255
+
256
+ # Create tar archive from context
257
+ tar_buffer = io.BytesIO()
258
+ with tarfile.open(fileobj=tar_buffer, mode='w:gz') as tar:
259
+ tar.add(context_path, arcname='.')
260
+
261
+ tar_data = base64.b64encode(tar_buffer.getvalue()).decode()
262
+
263
+ build_data = {
264
+ "dockerfile": dockerfile_path,
265
+ "t": tag,
266
+ "buildargs": build_args or {},
267
+ "context": tar_data
268
+ }
269
+
270
+ response = self.http_client.send_request(
271
+ endpoint="/api/docker/v1.24/build",
272
+ method="POST",
273
+ json=build_data,
274
+ req_timeout=300.0
275
+ )
276
+ response.raise_for_status()
277
+
278
+ result = {"logs": [], "image_id": None, "success": False}
279
+
280
+ if stream_logs:
281
+ # Parse streaming response
282
+ for line in response.iter_lines():
283
+ if line:
284
+ try:
285
+ log_entry = line.decode().strip()
286
+ result["logs"].append(log_entry)
287
+ self.logger.info(f"Build: {log_entry}")
288
+ except Exception:
289
+ pass
290
+
291
+ build_response = response.json() if hasattr(response, 'json') else {}
292
+ result["image_id"] = build_response.get("Id", tag)
293
+ result["success"] = True
294
+
295
+ self.logger.info(f"Image built successfully: {tag}")
296
+ return result
297
+
298
+ except HTTPConnectionError as error:
299
+ self.logger.error(f"Failed to build image: {error}")
300
+ raise BlackAntDockerError(f"Could not build image: {error}") from error
301
+ except Exception as error:
302
+ self.logger.error(f"Unexpected error building image: {error}")
303
+ raise BlackAntDockerError(f"Image build failed: {error}") from error
304
+
305
+ def create_container(self, image: str, name: Optional[str] = None,
306
+ command: Optional[List[str]] = None,
307
+ environment: Optional[Dict[str, str]] = None,
308
+ ports: Optional[Dict[str, int]] = None,
309
+ volumes: Optional[Dict[str, str]] = None) -> str:
310
+ """Create a new container.
311
+
312
+ Args:
313
+ image: Docker image name/tag.
314
+ name: Container name (optional).
315
+ command: Command to run in container.
316
+ environment: Environment variables.
317
+ ports: Port mappings {container_port: host_port}.
318
+ volumes: Volume mappings {host_path: container_path}.
319
+
320
+ Returns:
321
+ Container ID.
322
+
323
+ Raises:
324
+ BlackAntDockerError: If container creation fails.
325
+ """
326
+ try:
327
+ create_data = {
328
+ "Image": image,
329
+ "Cmd": command or [],
330
+ "Env": [f"{k}={v}" for k, v in (environment or {}).items()],
331
+ "ExposedPorts": {f"{port}/tcp": {} for port in (ports or {}).keys()},
332
+ "HostConfig": {
333
+ "PortBindings": {
334
+ f"{container_port}/tcp": [{"HostPort": str(host_port)}]
335
+ for container_port, host_port in (ports or {}).items()
336
+ },
337
+ "Binds": [f"{host}:{container}" for host, container in (volumes or {}).items()]
338
+ }
339
+ }
340
+
341
+ if name:
342
+ create_data["Name"] = name
343
+
344
+ response = self.http_client.send_request(
345
+ endpoint="/api/docker/v1.24/containers/create",
346
+ method="POST",
347
+ json=create_data
348
+ )
349
+ response.raise_for_status()
350
+
351
+ result = response.json()
352
+ container_id = result.get("Id")
353
+
354
+ self.logger.info(f"Container created: {container_id}")
355
+ return container_id
356
+
357
+ except HTTPConnectionError as error:
358
+ self.logger.error(f"Failed to create container: {error}")
359
+ raise BlackAntDockerError(f"Could not create container: {error}") from error
360
+ except Exception as error:
361
+ self.logger.error(f"Unexpected error creating container: {error}")
362
+ raise BlackAntDockerError(f"Container creation failed: {error}") from error
363
+
364
+ def start_container(self, container_id: str) -> bool:
365
+ """Start a container.
366
+
367
+ Args:
368
+ container_id: Container ID or name.
369
+
370
+ Returns:
371
+ True if started successfully.
372
+
373
+ Raises:
374
+ BlackAntDockerError: If start fails.
375
+ """
376
+ try:
377
+ response = self.http_client.send_request(
378
+ endpoint=f"/api/docker/v1.24/containers/{container_id}/start",
379
+ method="POST"
380
+ )
381
+ response.raise_for_status()
382
+
383
+ self.logger.info(f"Container started: {container_id}")
384
+ return True
385
+
386
+ except HTTPConnectionError as error:
387
+ self.logger.error(f"Failed to start container: {error}")
388
+ raise BlackAntDockerError(f"Could not start container: {error}") from error
389
+ except Exception as error:
390
+ self.logger.error(f"Unexpected error starting container: {error}")
391
+ raise BlackAntDockerError(f"Container start failed: {error}") from error
392
+
393
+ def stop_container(self, container_id: str, timeout: int = 10) -> bool:
394
+ """Stop a container.
395
+
396
+ Args:
397
+ container_id: Container ID or name.
398
+ timeout: Seconds to wait before killing.
399
+
400
+ Returns:
401
+ True if stopped successfully.
402
+
403
+ Raises:
404
+ BlackAntDockerError: If stop fails.
405
+ """
406
+ try:
407
+ response = self.http_client.send_request(
408
+ endpoint=f"/api/docker/v1.24/containers/{container_id}/stop?t={timeout}",
409
+ method="POST"
410
+ )
411
+ response.raise_for_status()
412
+
413
+ self.logger.info(f"Container stopped: {container_id}")
414
+ return True
415
+
416
+ except HTTPConnectionError as error:
417
+ self.logger.error(f"Failed to stop container: {error}")
418
+ raise BlackAntDockerError(f"Could not stop container: {error}") from error
419
+ except Exception as error:
420
+ self.logger.error(f"Unexpected error stopping container: {error}")
421
+ raise BlackAntDockerError(f"Container stop failed: {error}") from error
422
+
423
+ def remove_container(self, container_id: str, force: bool = False) -> bool:
424
+ """Remove a container.
425
+
426
+ Args:
427
+ container_id: Container ID or name.
428
+ force: Force removal of running container.
429
+
430
+ Returns:
431
+ True if removed successfully.
432
+
433
+ Raises:
434
+ BlackAntDockerError: If removal fails.
435
+ """
436
+ try:
437
+ params = "?force=true" if force else ""
438
+ response = self.http_client.send_request(
439
+ endpoint=f"/api/docker/v1.24/containers/{container_id}{params}",
440
+ method="DELETE"
441
+ )
442
+ response.raise_for_status()
443
+
444
+ self.logger.info(f"Container removed: {container_id}")
445
+ return True
446
+
447
+ except HTTPConnectionError as error:
448
+ self.logger.error(f"Failed to remove container: {error}")
449
+ raise BlackAntDockerError(f"Could not remove container: {error}") from error
450
+ except Exception as error:
451
+ self.logger.error(f"Unexpected error removing container: {error}")
452
+ raise BlackAntDockerError(f"Container removal failed: {error}") from error
453
+
454
+ def pull_image(self, image: str, tag: str = "latest") -> Dict[str, Any]:
455
+ """Pull Docker image from registry.
456
+
457
+ Args:
458
+ image: Image name (e.g., "nginx").
459
+ tag: Image tag.
460
+
461
+ Returns:
462
+ Pull result with status.
463
+
464
+ Raises:
465
+ BlackAntDockerError: If pull fails.
466
+ """
467
+ try:
468
+ full_image = f"{image}:{tag}"
469
+ response = self.http_client.send_request(
470
+ endpoint=f"/api/docker/v1.24/images/create?fromImage={image}&tag={tag}",
471
+ method="POST",
472
+ req_timeout=300.0
473
+ )
474
+ response.raise_for_status()
475
+
476
+ self.logger.info(f"Image pulled: {full_image}")
477
+ return {"image": full_image, "success": True}
478
+
479
+ except HTTPConnectionError as error:
480
+ self.logger.error(f"Failed to pull image: {error}")
481
+ raise BlackAntDockerError(f"Could not pull image: {error}") from error
482
+ except Exception as error:
483
+ self.logger.error(f"Unexpected error pulling image: {error}")
484
+ raise BlackAntDockerError(f"Image pull failed: {error}") from error
485
+
486
+ def push_image(self, image: str, tag: str = "latest") -> Dict[str, Any]:
487
+ """Push Docker image to registry.
488
+
489
+ Args:
490
+ image: Image name.
491
+ tag: Image tag.
492
+
493
+ Returns:
494
+ Push result with status.
495
+
496
+ Raises:
497
+ BlackAntDockerError: If push fails.
498
+ """
499
+ try:
500
+ full_image = f"{image}:{tag}"
501
+ response = self.http_client.send_request(
502
+ endpoint=f"/api/docker/v1.24/images/{full_image}/push",
503
+ method="POST",
504
+ req_timeout=300.0
505
+ )
506
+ response.raise_for_status()
507
+
508
+ self.logger.info(f"Image pushed: {full_image}")
509
+ return {"image": full_image, "success": True}
510
+
511
+ except HTTPConnectionError as error:
512
+ self.logger.error(f"Failed to push image: {error}")
513
+ raise BlackAntDockerError(f"Could not push image: {error}") from error
514
+ except Exception as error:
515
+ self.logger.error(f"Unexpected error pushing image: {error}")
516
+ raise BlackAntDockerError(f"Image push failed: {error}") from error
517
+
518
+ def inspect_image(self, image: str) -> Dict[str, Any]:
519
+ """Inspect Docker image details.
520
+
521
+ Args:
522
+ image: Image name/ID.
523
+
524
+ Returns:
525
+ Image inspection data.
526
+
527
+ Raises:
528
+ BlackAntDockerError: If inspection fails.
529
+ """
530
+ try:
531
+ response = self.http_client.send_request(
532
+ endpoint=f"/api/docker/v1.24/images/{image}/json",
533
+ method="GET"
534
+ )
535
+ response.raise_for_status()
536
+
537
+ image_data = response.json()
538
+ self.logger.debug(f"Image inspected: {image}")
539
+ return image_data
540
+
541
+ except HTTPConnectionError as error:
542
+ self.logger.error(f"Failed to inspect image: {error}")
543
+ raise BlackAntDockerError(f"Could not inspect image: {error}") from error
544
+ except Exception as error:
545
+ self.logger.error(f"Unexpected error inspecting image: {error}")
546
+ raise BlackAntDockerError(f"Image inspection failed: {error}") from error
547
+
548
+ def get_container_logs(self, container_id: str, follow: bool = False,
549
+ tail: int = 100) -> str:
550
+ """Get container logs.
551
+
552
+ Args:
553
+ container_id: Container ID or name.
554
+ follow: Follow log output.
555
+ tail: Number of lines from end of logs.
556
+
557
+ Returns:
558
+ Container logs as string.
559
+
560
+ Raises:
561
+ BlackAntDockerError: If getting logs fails.
562
+ """
563
+ try:
564
+ params = f"?stdout=true&stderr=true&tail={tail}"
565
+ if follow:
566
+ params += "&follow=true"
567
+
568
+ response = self.http_client.send_request(
569
+ endpoint=f"/api/docker/v1.24/containers/{container_id}/logs{params}",
570
+ method="GET"
571
+ )
572
+ response.raise_for_status()
573
+
574
+ logs = response.text
575
+ self.logger.debug(f"Retrieved logs for container: {container_id}")
576
+ return logs
577
+
578
+ except HTTPConnectionError as error:
579
+ self.logger.error(f"Failed to get container logs: {error}")
580
+ raise BlackAntDockerError(f"Could not get logs: {error}") from error
581
+ except Exception as error:
582
+ self.logger.error(f"Unexpected error getting logs: {error}")
583
+ raise BlackAntDockerError(f"Get logs failed: {error}") from error
584
+
585
+ def get_events(self, since: Optional[str] = None, until: Optional[str] = None,
586
+ filters: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
587
+ """Get Docker events.
588
+
589
+ Args:
590
+ since: Timestamp to start from.
591
+ until: Timestamp to end at.
592
+ filters: Event filters (type, container, etc.).
593
+
594
+ Returns:
595
+ List of Docker events.
596
+
597
+ Raises:
598
+ BlackAntDockerError: If getting events fails.
599
+ """
600
+ try:
601
+ params = []
602
+ if since:
603
+ params.append(f"since={since}")
604
+ if until:
605
+ params.append(f"until={until}")
606
+ if filters:
607
+ filters_json = json.dumps(filters)
608
+ params.append(f"filters={filters_json}")
609
+
610
+ query_string = "&".join(params)
611
+ endpoint = "/api/docker/v1.24/events"
612
+ if query_string:
613
+ endpoint += f"?{query_string}"
614
+
615
+ response = self.http_client.send_request(
616
+ endpoint=endpoint,
617
+ method="GET",
618
+ req_timeout=30.0
619
+ )
620
+ response.raise_for_status()
621
+
622
+ # Parse JSONL response (one JSON object per line)
623
+ events = []
624
+ for line in response.text.split('\n'):
625
+ if line.strip():
626
+ try:
627
+ event = json.loads(line)
628
+ events.append(event)
629
+ except json.JSONDecodeError:
630
+ pass
631
+
632
+ self.logger.debug(f"Retrieved {len(events)} Docker events")
633
+ return events
634
+
635
+ except HTTPConnectionError as error:
636
+ self.logger.error(f"Failed to get Docker events: {error}")
637
+ raise BlackAntDockerError(f"Could not get events: {error}") from error
638
+ except Exception as error:
639
+ self.logger.error(f"Unexpected error getting events: {error}")
640
+ raise BlackAntDockerError(f"Get events failed: {error}") from error
641
+
642
+ def get_system_df(self) -> Dict[str, Any]:
643
+ """Get Docker system disk usage information.
644
+
645
+ Returns:
646
+ System disk usage data.
647
+
648
+ Raises:
649
+ BlackAntDockerError: If getting disk usage fails.
650
+ """
651
+ try:
652
+ response = self.http_client.send_request(
653
+ endpoint="/api/docker/v1.24/system/df",
654
+ method="GET"
655
+ )
656
+ response.raise_for_status()
657
+
658
+ df_data = response.json()
659
+ self.logger.debug("Retrieved Docker disk usage")
660
+ return df_data
661
+
662
+ except HTTPConnectionError as error:
663
+ self.logger.error(f"Failed to get disk usage: {error}")
664
+ raise BlackAntDockerError(f"Could not get disk usage: {error}") from error
665
+ except Exception as error:
666
+ self.logger.error(f"Unexpected error getting disk usage: {error}")
667
+ raise BlackAntDockerError(f"Get disk usage failed: {error}") from error
668
+
669
+ def inspect_container(self, container_id: str) -> Dict[str, Any]:
670
+ """Inspect container details.
671
+
672
+ Args:
673
+ container_id: Container ID or name.
674
+
675
+ Returns:
676
+ Container inspection data.
677
+
678
+ Raises:
679
+ BlackAntDockerError: If inspection fails.
680
+ """
681
+ try:
682
+ response = self.http_client.send_request(
683
+ endpoint=f"/api/docker/v1.24/containers/{container_id}/json",
684
+ method="GET"
685
+ )
686
+ response.raise_for_status()
687
+
688
+ container_data = response.json()
689
+ self.logger.debug(f"Container inspected: {container_id}")
690
+ return container_data
691
+
692
+ except HTTPConnectionError as error:
693
+ self.logger.error(f"Failed to inspect container: {error}")
694
+ raise BlackAntDockerError(f"Could not inspect container: {error}") from error
695
+ except Exception as error:
696
+ self.logger.error(f"Unexpected error inspecting container: {error}")
697
+ raise BlackAntDockerError(f"Container inspection failed: {error}") from error
698
+
699
+ def get_container_stats(self, container_id: str, stream: bool = False) -> Dict[str, Any]:
700
+ """Get container resource usage statistics.
701
+
702
+ Args:
703
+ container_id: Container ID or name.
704
+ stream: Whether to stream stats continuously.
705
+
706
+ Returns:
707
+ Container resource statistics.
708
+
709
+ Raises:
710
+ BlackAntDockerError: If getting stats fails.
711
+ """
712
+ try:
713
+ params = "?stream=false"
714
+ if stream:
715
+ params = "?stream=true"
716
+
717
+ response = self.http_client.send_request(
718
+ endpoint=f"/api/docker/v1.24/containers/{container_id}/stats{params}",
719
+ method="GET"
720
+ )
721
+ response.raise_for_status()
722
+
723
+ if stream:
724
+ # Return iterator for streaming stats
725
+ def stats_iterator():
726
+ for line in response.iter_lines():
727
+ if line:
728
+ try:
729
+ yield json.loads(line.decode())
730
+ except json.JSONDecodeError:
731
+ pass
732
+ return stats_iterator()
733
+ else:
734
+ stats = response.json()
735
+ self.logger.debug(f"Retrieved stats for container: {container_id}")
736
+ return stats
737
+
738
+ except HTTPConnectionError as error:
739
+ self.logger.error(f"Failed to get container stats: {error}")
740
+ raise BlackAntDockerError(f"Could not get stats: {error}") from error
741
+ except Exception as error:
742
+ self.logger.error(f"Unexpected error getting stats: {error}")
743
+ raise BlackAntDockerError(f"Get stats failed: {error}") from error
744
+
745
+ def prune_containers(self, filters: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
746
+ """Remove unused containers.
747
+
748
+ Args:
749
+ filters: Prune filters.
750
+
751
+ Returns:
752
+ Prune results with space reclaimed.
753
+
754
+ Raises:
755
+ BlackAntDockerError: If pruning fails.
756
+ """
757
+ try:
758
+ prune_data = {}
759
+ if filters:
760
+ prune_data["filters"] = filters
761
+
762
+ response = self.http_client.send_request(
763
+ endpoint="/api/docker/v1.24/containers/prune",
764
+ method="POST",
765
+ json=prune_data if prune_data else None
766
+ )
767
+ response.raise_for_status()
768
+
769
+ prune_result = response.json()
770
+ space_reclaimed = prune_result.get('SpaceReclaimed', 0)
771
+ self.logger.info(f"Containers pruned: {space_reclaimed} bytes reclaimed")
772
+ return prune_result
773
+
774
+ except HTTPConnectionError as error:
775
+ self.logger.error(f"Failed to prune containers: {error}")
776
+ raise BlackAntDockerError(f"Could not prune containers: {error}") from error
777
+ except Exception as error:
778
+ self.logger.error(f"Unexpected error pruning containers: {error}")
779
+ raise BlackAntDockerError(f"Container prune failed: {error}") from error
780
+
781
+ def prune_images(self, dangling_only: bool = True) -> Dict[str, Any]:
782
+ """Remove unused images.
783
+
784
+ Args:
785
+ dangling_only: Only remove dangling images.
786
+
787
+ Returns:
788
+ Prune results with space reclaimed.
789
+
790
+ Raises:
791
+ BlackAntDockerError: If pruning fails.
792
+ """
793
+ try:
794
+ filters = {}
795
+ if dangling_only:
796
+ filters["dangling"] = ["true"]
797
+
798
+ prune_data = {"filters": filters} if filters else {}
799
+
800
+ response = self.http_client.send_request(
801
+ endpoint="/api/docker/v1.24/images/prune",
802
+ method="POST",
803
+ json=prune_data if prune_data else None
804
+ )
805
+ response.raise_for_status()
806
+
807
+ prune_result = response.json()
808
+ space_reclaimed = prune_result.get('SpaceReclaimed', 0)
809
+ self.logger.info(f"Images pruned: {space_reclaimed} bytes reclaimed")
810
+ return prune_result
811
+
812
+ except HTTPConnectionError as error:
813
+ self.logger.error(f"Failed to prune images: {error}")
814
+ raise BlackAntDockerError(f"Could not prune images: {error}") from error
815
+ except Exception as error:
816
+ self.logger.error(f"Unexpected error pruning images: {error}")
817
+ raise BlackAntDockerError(f"Image prune failed: {error}") from error
818
+
819
+ def build_service(self,
820
+ service_name: str,
821
+ impl_path: str = "src/calculation/impl",
822
+ dockerfile_path: str = "Dockerfile",
823
+ tag: Optional[str] = None,
824
+ push: bool = True,
825
+ register_service: bool = True) -> Dict[str, Any]:
826
+ """Build és deploy service Docker image.
827
+
828
+ Ez az 5 core metódus egyike! Build Docker image a user implementation-ből,
829
+ upload registry-be, és regisztrálja a ServiceManager-ben.
830
+
831
+ Args:
832
+ service_name: Service név (alphanumeric + hyphens/underscores)
833
+ impl_path: User implementation mappa útvonala
834
+ dockerfile_path: Dockerfile útvonala (repository root-ból)
835
+ tag: Verzió tag (auto-generált ha None: v1.0.0-{timestamp})
836
+ push: Feltöltés env.blackant.app registry-be
837
+ register_service: Automatikus service regisztráció ServiceManager-ben
838
+
839
+ Returns:
840
+ dict: Build és deployment információk
841
+ {
842
+ "image_name": "env.blackant.app/systemdevelopers/service_name",
843
+ "tag": "v1.0.0-20250105-123456",
844
+ "full_image": "env.blackant.app/systemdevelopers/service_name:v1.0.0-20250105-123456",
845
+ "image_id": "sha256:...",
846
+ "size": 125000000,
847
+ "pushed": True,
848
+ "registry_url": "env.blackant.app",
849
+ "service_id": "uuid-from-servicemanager",
850
+ "service_registered": True
851
+ }
852
+
853
+ Raises:
854
+ BlackAntDockerError: Build, push vagy regisztrációs hiba esetén
855
+
856
+ Example:
857
+ >>> auth = BlackAntAuth(user="developer", password="xxx")
858
+ >>> client = BlackAntDockerClient(auth=auth)
859
+ >>> result = client.build_service(
860
+ ... service_name="my-calculation",
861
+ ... impl_path="src/calculation/impl",
862
+ ... tag="v1.0.0"
863
+ ... )
864
+ >>> print(result["full_image"])
865
+ env.blackant.app/systemdevelopers/my-calculation:v1.0.0
866
+ >>> print(result["service_id"])
867
+ abc-123-def-456
868
+ """
869
+
870
+ self.logger.info(f"Building service: {service_name}")
871
+
872
+ try:
873
+ # 1. Build Docker image local-ban
874
+ build_result = self.image_builder.build_service(
875
+ service_name=service_name,
876
+ impl_path=impl_path,
877
+ dockerfile_path=dockerfile_path,
878
+ tag=tag,
879
+ push_to_registry=push
880
+ )
881
+
882
+ self.logger.info(f"Image build completed: {build_result['full_image']}")
883
+
884
+ # 2. Role assignment after successful push (Balázs requirement)
885
+ role_assigned = False
886
+ if build_result.get("pushed", False):
887
+ try:
888
+ # Get user token for role assignment
889
+ user_token = self.auth.get_token() if self.auth else None
890
+ if user_token:
891
+ role_manager = get_role_assignment_manager()
892
+ role_assigned = role_manager.assign_role_after_publish(
893
+ user_token=user_token,
894
+ service_name=service_name,
895
+ metadata={
896
+ "image": build_result.get("full_image"),
897
+ "registry_url": build_result.get("registry_url")
898
+ }
899
+ )
900
+ if role_assigned:
901
+ self.logger.info(f"Role 'science_module' assigned after push for service: {service_name}")
902
+ else:
903
+ self.logger.warning(f"Role assignment failed for service: {service_name}")
904
+ else:
905
+ self.logger.warning("No user token available for role assignment")
906
+ except Exception as role_error:
907
+ self.logger.warning(f"Role assignment error (non-fatal): {role_error}")
908
+ # Continue - role assignment failure shouldn't block the build
909
+
910
+ # 3. Service registration preparation
911
+ service_registered = False
912
+ service_id = None
913
+
914
+ # 4. Service registration in ServiceManager (if requested and push successful)
915
+ if register_service and build_result.get("pushed", False):
916
+ try:
917
+ service_config = {
918
+ "name": service_name,
919
+ "type": "calculation",
920
+ "image": {
921
+ "container": "systemdevelopers", # Registry namespace fix
922
+ "name": service_name,
923
+ "tag": build_result["tag"]
924
+ },
925
+ "environments": {
926
+ "BLACKANT_ENV": "production",
927
+ "LOG_LEVEL": "INFO",
928
+ "SERVICE_NAME": service_name
929
+ },
930
+ "networks": ["blackant_network"],
931
+ "parent": None,
932
+ "resource_config": {
933
+ "use_gpu": False,
934
+ "cpu_limit": 1.0,
935
+ "ram_limit": "512M",
936
+ "disk_limit": "1G",
937
+ "node_label": None
938
+ }
939
+ }
940
+
941
+ self.logger.debug(f"Registering service with config: {service_config}")
942
+
943
+ # ServiceManager API hívás
944
+ response = self.http_client.send_request(
945
+ endpoint="/api/v1/services",
946
+ method="POST",
947
+ json=service_config
948
+ )
949
+
950
+ if response.status_code == 200 or response.status_code == 201:
951
+ response_data = response.json()
952
+ service_id = response_data.get("uuid") or response_data.get("id")
953
+ service_registered = True
954
+ self.logger.info(f"Service registered successfully: {service_name} -> {service_id}")
955
+ else:
956
+ self.logger.warning(f"Service registration failed: {response.status_code} - {response.text}")
957
+
958
+ except Exception as reg_error:
959
+ self.logger.warning(f"Service registration failed: {reg_error}")
960
+ # Folytatjuk annak ellenére, hogy a regisztráció nem sikerült
961
+
962
+ # 5. Collect complete result
963
+ result = {
964
+ **build_result, # All build results (image_name, tag, etc.)
965
+ "service_id": service_id,
966
+ "service_registered": service_registered,
967
+ "role_assigned": role_assigned,
968
+ "build_success": True
969
+ }
970
+
971
+ # 6. Success log
972
+ self.logger.info(f"Build service completed: {service_name}")
973
+ if role_assigned:
974
+ self.logger.info(f"Role 'science_module' assigned to user")
975
+ if service_registered:
976
+ self.logger.info(f"Service registered with ID: {service_id}")
977
+
978
+ return result
979
+
980
+ except Exception as e:
981
+ error_msg = f"Build service failed for {service_name}: {e}"
982
+ self.logger.error(error_msg)
983
+ raise BlackAntDockerError(error_msg) from e