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
blackant/exceptions.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""BlackAnt SDK exception classes.
|
|
2
|
+
|
|
3
|
+
Central exception hierarchy for all BlackAnt SDK errors.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BlackAntException(Exception):
|
|
8
|
+
"""Base exception for all BlackAnt SDK errors.
|
|
9
|
+
|
|
10
|
+
All SDK-specific exceptions inherit from this base class
|
|
11
|
+
to allow catching all SDK errors with a single handler.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BlackAntAuthenticationError(BlackAntException):
|
|
16
|
+
"""Authentication and authorization related errors.
|
|
17
|
+
|
|
18
|
+
Raised when:
|
|
19
|
+
- Login credentials are invalid
|
|
20
|
+
- Keycloak authentication fails
|
|
21
|
+
- Bearer token is missing or expired
|
|
22
|
+
- Authorization is denied
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BlackAntDockerError(BlackAntException):
|
|
27
|
+
"""Docker operation related errors.
|
|
28
|
+
|
|
29
|
+
Raised when:
|
|
30
|
+
- Docker API calls fail
|
|
31
|
+
- Container operations encounter issues
|
|
32
|
+
- Image operations fail
|
|
33
|
+
- Registry communication errors
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BlackAntConnectionError(BlackAntException):
|
|
38
|
+
"""Network and connection related errors.
|
|
39
|
+
|
|
40
|
+
Raised when:
|
|
41
|
+
- HTTP requests timeout
|
|
42
|
+
- Network connection fails
|
|
43
|
+
- API endpoints are unreachable
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class BlackAntConfigurationError(BlackAntException):
|
|
48
|
+
"""Configuration related errors.
|
|
49
|
+
|
|
50
|
+
Raised when:
|
|
51
|
+
- Required environment variables are missing
|
|
52
|
+
- Configuration files are invalid
|
|
53
|
+
- Service configuration is incorrect
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# =============================================================================
|
|
58
|
+
# Keycloak & Role Assignment Exceptions
|
|
59
|
+
# =============================================================================
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class KeycloakError(BlackAntException):
|
|
63
|
+
"""Base exception for Keycloak-related errors.
|
|
64
|
+
|
|
65
|
+
All Keycloak-specific exceptions inherit from this base class
|
|
66
|
+
to allow catching all Keycloak errors with a single handler.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class KeycloakConnectionError(KeycloakError):
|
|
71
|
+
"""Raised when connection to Keycloak server fails.
|
|
72
|
+
|
|
73
|
+
This can occur due to:
|
|
74
|
+
- Network connectivity issues
|
|
75
|
+
- Incorrect server URL
|
|
76
|
+
- Firewall blocking connection
|
|
77
|
+
- Keycloak server being down
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class KeycloakAuthenticationError(KeycloakError):
|
|
82
|
+
"""Raised when Keycloak authentication fails.
|
|
83
|
+
|
|
84
|
+
This can occur due to:
|
|
85
|
+
- Invalid service account credentials
|
|
86
|
+
- Expired client secret
|
|
87
|
+
- Incorrect client ID
|
|
88
|
+
- Insufficient permissions for service account
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class RoleAssignmentError(KeycloakError):
|
|
93
|
+
"""Raised when role assignment operation fails.
|
|
94
|
+
|
|
95
|
+
This can occur due to:
|
|
96
|
+
- Role does not exist in realm
|
|
97
|
+
- User does not exist
|
|
98
|
+
- Insufficient permissions to assign role
|
|
99
|
+
- Keycloak API error during assignment
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TokenValidationError(BlackAntException):
|
|
104
|
+
"""Raised when JWT token validation fails.
|
|
105
|
+
|
|
106
|
+
This can occur due to:
|
|
107
|
+
- Invalid token format
|
|
108
|
+
- Missing required claims (sub, etc.)
|
|
109
|
+
- Expired token
|
|
110
|
+
- Token signature validation failure
|
|
111
|
+
"""
|
blackant/http/client.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""BlackAnt SDK HTTP communication module.
|
|
2
|
+
|
|
3
|
+
HTTP client with Bearer token authentication, request ID tracking,
|
|
4
|
+
and integration with auth store modules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from requests.adapters import HTTPAdapter
|
|
9
|
+
|
|
10
|
+
from ..auth.tokens import AuthTokenStore
|
|
11
|
+
from ..auth.request_id import RequestIdStore
|
|
12
|
+
from ..utils.logging import get_logger, set_request_id, clear_request_id
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HTTPConnectionError(Exception):
|
|
16
|
+
"""HTTP connection specific exception.
|
|
17
|
+
|
|
18
|
+
Raised when HTTP requests fail due to connection issues,
|
|
19
|
+
timeouts, or network problems.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HTTPClient: # pylint: disable=too-few-public-methods
|
|
24
|
+
"""HTTP client for BlackAnt SDK API communication.
|
|
25
|
+
|
|
26
|
+
Provides HTTP request functionality with automatic Bearer token injection,
|
|
27
|
+
request ID tracking, and base URL management.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
base_url (str): Base URL for API endpoints.
|
|
31
|
+
auth_token_store (AuthTokenStore, optional): Token storage instance.
|
|
32
|
+
request_id_store (RequestIdStore, optional): Request ID storage instance.
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
>>> client = HTTPClient("https://api.blackant.app")
|
|
36
|
+
>>> client.auth_token_store.user_token = "your_token"
|
|
37
|
+
>>> response = client.send_request("/api/v1/services", "GET")
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, base_url, auth_token_store=None, request_id_store=None):
|
|
41
|
+
"""Initialize HTTP client.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
base_url (str): Base URL for API endpoints.
|
|
45
|
+
auth_token_store (AuthTokenStore, optional): Token storage.
|
|
46
|
+
request_id_store (RequestIdStore, optional): Request ID storage.
|
|
47
|
+
"""
|
|
48
|
+
self.base_url = base_url.rstrip("/")
|
|
49
|
+
self.auth_token_store = auth_token_store or AuthTokenStore()
|
|
50
|
+
self.request_id_store = request_id_store or RequestIdStore()
|
|
51
|
+
self.logger = get_logger("http")
|
|
52
|
+
|
|
53
|
+
self.session = requests.Session()
|
|
54
|
+
adapter = HTTPAdapter(pool_connections=10, pool_maxsize=20, max_retries=0)
|
|
55
|
+
self.session.mount("https://", adapter)
|
|
56
|
+
self.session.mount("http://", adapter)
|
|
57
|
+
|
|
58
|
+
def send_request( # pylint: disable=too-many-arguments
|
|
59
|
+
self,
|
|
60
|
+
endpoint,
|
|
61
|
+
method,
|
|
62
|
+
json=None,
|
|
63
|
+
anonymous=False,
|
|
64
|
+
is_admin=False,
|
|
65
|
+
req_timeout=60.0,
|
|
66
|
+
token=None,
|
|
67
|
+
):
|
|
68
|
+
"""Send HTTP request to the configured API endpoint.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
endpoint (str): API endpoint path.
|
|
72
|
+
method (str): HTTP method.
|
|
73
|
+
json (dict, optional): JSON data for request body.
|
|
74
|
+
anonymous (bool): Skip Bearer token authentication.
|
|
75
|
+
is_admin (bool): Use admin token instead of user token.
|
|
76
|
+
req_timeout (float): Request timeout in seconds.
|
|
77
|
+
token (str, optional): Custom Bearer token.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
requests.Response: HTTP response object.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
HTTPConnectionError: When connection fails or times out.
|
|
84
|
+
"""
|
|
85
|
+
request_id = self.request_id_store.generate_id()
|
|
86
|
+
set_request_id(request_id)
|
|
87
|
+
|
|
88
|
+
url = f"{self.base_url}/{endpoint.lstrip('/')}"
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
req = requests.Request(method, url, json=json)
|
|
92
|
+
prepped = self.session.prepare_request(req)
|
|
93
|
+
|
|
94
|
+
if not anonymous:
|
|
95
|
+
if token:
|
|
96
|
+
prepped.headers["Authorization"] = f"Bearer {token}"
|
|
97
|
+
else:
|
|
98
|
+
auth_token = (
|
|
99
|
+
self.auth_token_store.admin_token if is_admin
|
|
100
|
+
else self.auth_token_store.user_token
|
|
101
|
+
)
|
|
102
|
+
if auth_token:
|
|
103
|
+
prepped.headers["Authorization"] = f"Bearer {auth_token}"
|
|
104
|
+
|
|
105
|
+
prepped.headers[self.request_id_store.header_name] = request_id
|
|
106
|
+
|
|
107
|
+
# Determine if we should verify SSL (skip for localhost/dev)
|
|
108
|
+
verify_ssl = not (
|
|
109
|
+
"localhost" in self.base_url or
|
|
110
|
+
"127.0.0.1" in self.base_url or
|
|
111
|
+
"http://" in url or # HTTP doesn't need SSL verification
|
|
112
|
+
"dev.blackant.app" in self.base_url # Dev environment self-signed cert
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
response = self.session.send(prepped, timeout=req_timeout, verify=verify_ssl)
|
|
116
|
+
return response
|
|
117
|
+
|
|
118
|
+
except requests.exceptions.ConnectionError as exc:
|
|
119
|
+
self.logger.error(f"Connection error: {exc}")
|
|
120
|
+
raise HTTPConnectionError from exc
|
|
121
|
+
except requests.exceptions.Timeout as exc:
|
|
122
|
+
self.logger.error(f"Request time-out error: {exc}")
|
|
123
|
+
raise HTTPConnectionError from exc
|
|
124
|
+
finally:
|
|
125
|
+
clear_request_id()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Pattern implementations for BlackAnt SDK."""
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Singleton pattern implementation for BlackAnt SDK."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Singleton(type):
|
|
7
|
+
"""Singleton metaclass implementation.
|
|
8
|
+
|
|
9
|
+
Thread-safe singleton pattern using metaclass approach.
|
|
10
|
+
Ensures only one instance per class exists across the application.
|
|
11
|
+
"""
|
|
12
|
+
_instances = {}
|
|
13
|
+
_lock = threading.Lock()
|
|
14
|
+
|
|
15
|
+
def __call__(cls, *args, **kwargs):
|
|
16
|
+
if cls not in cls._instances:
|
|
17
|
+
with cls._lock:
|
|
18
|
+
if cls not in cls._instances:
|
|
19
|
+
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
|
20
|
+
return cls._instances[cls]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""BlackAnt Services module.
|
|
2
|
+
|
|
3
|
+
Service registry and lifecycle management components with both
|
|
4
|
+
HTTP API (external) and Docker SDK (internal) support.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .registry import ServiceRegistry
|
|
8
|
+
from .dao import ServiceManagerDAO
|
|
9
|
+
|
|
10
|
+
__all__ = ["ServiceRegistry", "ServiceManagerDAO"]
|
blackant/services/dao.py
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"""ServiceManager DAO - Docker SDK objektum szintű service kezelés.
|
|
2
|
+
|
|
3
|
+
High-level Docker service operations using Docker SDK objects directly,
|
|
4
|
+
following the BlackAnt "Docker as Object" design pattern.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
import random
|
|
10
|
+
from typing import Optional, List, Dict, Any, Union
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
|
|
13
|
+
import docker
|
|
14
|
+
from docker.models.services import Service as DockerServiceObject
|
|
15
|
+
from docker.models.containers import Container as DockerContainer
|
|
16
|
+
from docker.errors import DockerException, NotFound, APIError
|
|
17
|
+
|
|
18
|
+
from ..utils.logging import get_logger
|
|
19
|
+
from ..exceptions import BlackAntDockerError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ServiceConfig:
|
|
24
|
+
"""Service configuration data class for ServiceManagerDAO."""
|
|
25
|
+
|
|
26
|
+
name: str = field(default="")
|
|
27
|
+
image: str = field(default="")
|
|
28
|
+
command: Optional[List[str]] = field(default=None)
|
|
29
|
+
environments: Dict[str, str] = field(default_factory=dict)
|
|
30
|
+
networks: List[str] = field(default_factory=list)
|
|
31
|
+
labels: Dict[str, str] = field(default_factory=dict)
|
|
32
|
+
constraints: List[str] = field(default_factory=list)
|
|
33
|
+
replicas: int = field(default=1)
|
|
34
|
+
cpu_limit: Optional[int] = field(default=None) # nanocpus
|
|
35
|
+
memory_limit: Optional[int] = field(default=None) # bytes
|
|
36
|
+
ports: Dict[str, int] = field(default_factory=dict)
|
|
37
|
+
mounts: List[Dict[str, str]] = field(default_factory=list)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ServiceManagerDAO:
|
|
41
|
+
"""Docker Services objektum szintű kezelése.
|
|
42
|
+
|
|
43
|
+
Provides high-level service management operations using Docker SDK
|
|
44
|
+
objects directly instead of HTTP requests. This follows the BlackAnt
|
|
45
|
+
"Docker as Object" pattern for internal server-side operations.
|
|
46
|
+
|
|
47
|
+
Examples:
|
|
48
|
+
>>> dao = ServiceManagerDAO()
|
|
49
|
+
>>> config = ServiceConfig(name="my-service", image="nginx:latest")
|
|
50
|
+
>>> service = dao.create_service(config)
|
|
51
|
+
>>> services = dao.list_services()
|
|
52
|
+
>>> dao.remove_service(service.id)
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, docker_host: Optional[str] = None):
|
|
56
|
+
"""ServiceManager DAO inicializálása Docker client-tel.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
docker_host: Optional Docker daemon host URL
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
BlackAntDockerError: Ha a Docker client inicializálás sikertelen
|
|
63
|
+
"""
|
|
64
|
+
self.logger = get_logger("service-manager-dao")
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
if docker_host:
|
|
68
|
+
self.__docker_client = docker.DockerClient(base_url=docker_host)
|
|
69
|
+
else:
|
|
70
|
+
self.__docker_client = docker.from_env()
|
|
71
|
+
|
|
72
|
+
# Test connection
|
|
73
|
+
self.__docker_client.ping()
|
|
74
|
+
self.logger.info("ServiceManagerDAO initialized successfully")
|
|
75
|
+
|
|
76
|
+
except DockerException as docker_error:
|
|
77
|
+
error_msg = f"Docker client initialization failed: {docker_error}"
|
|
78
|
+
self.logger.error(error_msg)
|
|
79
|
+
raise BlackAntDockerError(error_msg) from docker_error
|
|
80
|
+
|
|
81
|
+
def create_service(self, service_config: Union[ServiceConfig, Dict[str, Any]]) -> DockerServiceObject:
|
|
82
|
+
"""Szolgáltatás létrehozása Docker SDK-val.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
service_config: ServiceConfig object or configuration dictionary
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
DockerServiceObject: Létrehozott Docker service objektum
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
BlackAntDockerError: Docker API hiba esetén
|
|
92
|
+
"""
|
|
93
|
+
# Convert dict to ServiceConfig if needed
|
|
94
|
+
if isinstance(service_config, dict):
|
|
95
|
+
config = ServiceConfig(**service_config)
|
|
96
|
+
else:
|
|
97
|
+
config = service_config
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
# Prepare service creation parameters
|
|
101
|
+
create_params = {
|
|
102
|
+
'name': config.name,
|
|
103
|
+
'image': config.image,
|
|
104
|
+
'env': [f"{k}={v}" for k, v in config.environments.items()],
|
|
105
|
+
'networks': config.networks,
|
|
106
|
+
'labels': config.labels,
|
|
107
|
+
'mode': docker.types.ServiceMode('replicated', config.replicas)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Add command if specified
|
|
111
|
+
if config.command:
|
|
112
|
+
create_params['command'] = config.command
|
|
113
|
+
|
|
114
|
+
# Add resource constraints if specified
|
|
115
|
+
if config.cpu_limit or config.memory_limit:
|
|
116
|
+
resources = docker.types.Resources(
|
|
117
|
+
cpu_limit=config.cpu_limit,
|
|
118
|
+
mem_limit=config.memory_limit
|
|
119
|
+
)
|
|
120
|
+
create_params['resources'] = resources
|
|
121
|
+
|
|
122
|
+
# Add placement constraints
|
|
123
|
+
if config.constraints:
|
|
124
|
+
create_params['constraints'] = config.constraints
|
|
125
|
+
|
|
126
|
+
# Add port mappings if specified
|
|
127
|
+
if config.ports:
|
|
128
|
+
endpoint_spec = docker.types.EndpointSpec(
|
|
129
|
+
ports={port: target for port, target in config.ports.items()}
|
|
130
|
+
)
|
|
131
|
+
create_params['endpoint_spec'] = endpoint_spec
|
|
132
|
+
|
|
133
|
+
# Add mounts if specified
|
|
134
|
+
if config.mounts:
|
|
135
|
+
mounts = []
|
|
136
|
+
for mount_config in config.mounts:
|
|
137
|
+
mount = docker.types.Mount(
|
|
138
|
+
target=mount_config['target'],
|
|
139
|
+
source=mount_config['source'],
|
|
140
|
+
type=mount_config.get('type', 'bind'),
|
|
141
|
+
read_only=mount_config.get('read_only', False)
|
|
142
|
+
)
|
|
143
|
+
mounts.append(mount)
|
|
144
|
+
create_params['mounts'] = mounts
|
|
145
|
+
|
|
146
|
+
# Create service using Docker SDK
|
|
147
|
+
service = self.__docker_client.services.create(**create_params)
|
|
148
|
+
|
|
149
|
+
self.logger.info(f"Service created successfully: {service.name} (ID: {service.id})")
|
|
150
|
+
return service
|
|
151
|
+
|
|
152
|
+
except APIError as api_error:
|
|
153
|
+
error_msg = f"Service creation failed for '{config.name}': {api_error}"
|
|
154
|
+
self.logger.error(error_msg)
|
|
155
|
+
raise BlackAntDockerError(error_msg) from api_error
|
|
156
|
+
except Exception as error:
|
|
157
|
+
error_msg = f"Unexpected error creating service '{config.name}': {error}"
|
|
158
|
+
self.logger.error(error_msg)
|
|
159
|
+
raise BlackAntDockerError(error_msg) from error
|
|
160
|
+
|
|
161
|
+
def get_service(self, service_id: str) -> Optional[DockerServiceObject]:
|
|
162
|
+
"""Szolgáltatás lekérése ID vagy név alapján.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
service_id: Service ID vagy név
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
DockerServiceObject vagy None ha nem található
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
BlackAntDockerError: Docker API hiba esetén (kivéve NotFound)
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
service = self.__docker_client.services.get(service_id)
|
|
175
|
+
self.logger.debug(f"Service found: {service_id}")
|
|
176
|
+
return service
|
|
177
|
+
|
|
178
|
+
except NotFound:
|
|
179
|
+
self.logger.warning(f"Service not found: {service_id}")
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
except APIError as api_error:
|
|
183
|
+
error_msg = f"Error getting service '{service_id}': {api_error}"
|
|
184
|
+
self.logger.error(error_msg)
|
|
185
|
+
raise BlackAntDockerError(error_msg) from api_error
|
|
186
|
+
|
|
187
|
+
def list_services(self, filters: Optional[Dict[str, Any]] = None) -> List[DockerServiceObject]:
|
|
188
|
+
"""Szolgáltatások listázása.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
filters: Docker API szűrők (pl. {'label': 'env=prod'})
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
List[DockerServiceObject]: Service objektumok listája
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
BlackAntDockerError: Docker API hiba esetén
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
services = self.__docker_client.services.list(filters=filters)
|
|
201
|
+
self.logger.debug(f"Found {len(services)} services")
|
|
202
|
+
return services
|
|
203
|
+
|
|
204
|
+
except APIError as api_error:
|
|
205
|
+
error_msg = f"Error listing services: {api_error}"
|
|
206
|
+
self.logger.error(error_msg)
|
|
207
|
+
raise BlackAntDockerError(error_msg) from api_error
|
|
208
|
+
|
|
209
|
+
def remove_service(self, service_id: str) -> bool:
|
|
210
|
+
"""Szolgáltatás eltávolítása.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
service_id: Service ID vagy név
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
bool: True ha sikeresen eltávolítva, False ha nem található
|
|
217
|
+
|
|
218
|
+
Raises:
|
|
219
|
+
BlackAntDockerError: Docker API hiba esetén (kivéve NotFound)
|
|
220
|
+
"""
|
|
221
|
+
try:
|
|
222
|
+
service = self.get_service(service_id)
|
|
223
|
+
if service:
|
|
224
|
+
service.remove()
|
|
225
|
+
self.logger.info(f"Service removed successfully: {service_id}")
|
|
226
|
+
return True
|
|
227
|
+
else:
|
|
228
|
+
self.logger.warning(f"Service not found for removal: {service_id}")
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
except APIError as api_error:
|
|
232
|
+
error_msg = f"Error removing service '{service_id}': {api_error}"
|
|
233
|
+
self.logger.error(error_msg)
|
|
234
|
+
raise BlackAntDockerError(error_msg) from api_error
|
|
235
|
+
|
|
236
|
+
def update_service(self, service_id: str, **kwargs) -> Optional[DockerServiceObject]:
|
|
237
|
+
"""Szolgáltatás frissítése.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
service_id: Service ID vagy név
|
|
241
|
+
**kwargs: Frissítendő paraméterek (image, env, labels, stb.)
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
DockerServiceObject vagy None ha nem található
|
|
245
|
+
|
|
246
|
+
Raises:
|
|
247
|
+
BlackAntDockerError: Docker API hiba esetén
|
|
248
|
+
"""
|
|
249
|
+
try:
|
|
250
|
+
service = self.get_service(service_id)
|
|
251
|
+
if service:
|
|
252
|
+
# Refresh service object to get latest version
|
|
253
|
+
service.reload()
|
|
254
|
+
|
|
255
|
+
# Update service with provided parameters
|
|
256
|
+
service.update(**kwargs)
|
|
257
|
+
|
|
258
|
+
self.logger.info(f"Service updated successfully: {service_id}")
|
|
259
|
+
return service
|
|
260
|
+
else:
|
|
261
|
+
self.logger.warning(f"Service not found for update: {service_id}")
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
except APIError as api_error:
|
|
265
|
+
error_msg = f"Error updating service '{service_id}': {api_error}"
|
|
266
|
+
self.logger.error(error_msg)
|
|
267
|
+
raise BlackAntDockerError(error_msg) from api_error
|
|
268
|
+
|
|
269
|
+
def scale_service(self, service_id: str, replicas: int) -> Optional[DockerServiceObject]:
|
|
270
|
+
"""Szolgáltatás méretezése (replicas számának változtatása).
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
service_id: Service ID vagy név
|
|
274
|
+
replicas: Új replicas szám
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
DockerServiceObject vagy None ha nem található
|
|
278
|
+
|
|
279
|
+
Raises:
|
|
280
|
+
BlackAntDockerError: Docker API hiba esetén
|
|
281
|
+
"""
|
|
282
|
+
try:
|
|
283
|
+
service = self.get_service(service_id)
|
|
284
|
+
if service:
|
|
285
|
+
# Scale service by updating replica count
|
|
286
|
+
mode = docker.types.ServiceMode('replicated', replicas)
|
|
287
|
+
service.update(mode=mode)
|
|
288
|
+
|
|
289
|
+
self.logger.info(f"Service scaled successfully: {service_id} -> {replicas} replicas")
|
|
290
|
+
return service
|
|
291
|
+
else:
|
|
292
|
+
self.logger.warning(f"Service not found for scaling: {service_id}")
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
except APIError as api_error:
|
|
296
|
+
error_msg = f"Error scaling service '{service_id}': {api_error}"
|
|
297
|
+
self.logger.error(error_msg)
|
|
298
|
+
raise BlackAntDockerError(error_msg) from api_error
|
|
299
|
+
|
|
300
|
+
def get_service_logs(self, service_id: str, **kwargs) -> str:
|
|
301
|
+
"""Szolgáltatás naplóinak lekérése.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
service_id: Service ID vagy név
|
|
305
|
+
**kwargs: Log opciók (tail, since, follow, stb.)
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
str: Service logs
|
|
309
|
+
|
|
310
|
+
Raises:
|
|
311
|
+
BlackAntDockerError: Ha a service nem található vagy API hiba
|
|
312
|
+
"""
|
|
313
|
+
try:
|
|
314
|
+
service = self.get_service(service_id)
|
|
315
|
+
if service:
|
|
316
|
+
logs = service.logs(**kwargs)
|
|
317
|
+
# Convert bytes to string if needed
|
|
318
|
+
if isinstance(logs, bytes):
|
|
319
|
+
logs = logs.decode('utf-8')
|
|
320
|
+
|
|
321
|
+
self.logger.debug(f"Retrieved logs for service: {service_id}")
|
|
322
|
+
return logs
|
|
323
|
+
else:
|
|
324
|
+
raise BlackAntDockerError(f"Service not found: {service_id}")
|
|
325
|
+
|
|
326
|
+
except APIError as api_error:
|
|
327
|
+
error_msg = f"Error getting logs for service '{service_id}': {api_error}"
|
|
328
|
+
self.logger.error(error_msg)
|
|
329
|
+
raise BlackAntDockerError(error_msg) from api_error
|
|
330
|
+
|
|
331
|
+
def is_service_running(self, service_id: str) -> bool:
|
|
332
|
+
"""Ellenőrzi, hogy a szolgáltatásnak van-e futó task-ja.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
service_id: Service ID vagy név
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
bool: True ha van futó task, False egyébként
|
|
339
|
+
"""
|
|
340
|
+
try:
|
|
341
|
+
service = self.get_service(service_id)
|
|
342
|
+
if service:
|
|
343
|
+
# Check if service has any running tasks
|
|
344
|
+
tasks = service.tasks()
|
|
345
|
+
for task in tasks:
|
|
346
|
+
if task.get('Status', {}).get('State') == 'running':
|
|
347
|
+
return True
|
|
348
|
+
return False
|
|
349
|
+
else:
|
|
350
|
+
return False
|
|
351
|
+
|
|
352
|
+
except Exception as error:
|
|
353
|
+
self.logger.error(f"Error checking service status '{service_id}': {error}")
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
def wait_for_service_ready(self, service_id: str, timeout: int = 300) -> bool:
|
|
357
|
+
"""Várakozás amíg a szolgáltatás futó állapotba kerül.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
service_id: Service ID vagy név
|
|
361
|
+
timeout: Maximum várakozási idő másodpercekben
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
bool: True ha a szolgáltatás futó állapotba került, False timeout esetén
|
|
365
|
+
"""
|
|
366
|
+
self.logger.info(f"Waiting for service to become ready: {service_id}")
|
|
367
|
+
|
|
368
|
+
start_time = time.time()
|
|
369
|
+
while time.time() - start_time < timeout:
|
|
370
|
+
if self.is_service_running(service_id):
|
|
371
|
+
self.logger.info(f"Service is ready: {service_id}")
|
|
372
|
+
return True
|
|
373
|
+
|
|
374
|
+
# Random sleep to avoid overwhelming the API
|
|
375
|
+
sleep_time = random.uniform(0.5, 2.0)
|
|
376
|
+
time.sleep(sleep_time)
|
|
377
|
+
|
|
378
|
+
self.logger.warning(f"Service did not become ready within {timeout}s: {service_id}")
|
|
379
|
+
return False
|
|
380
|
+
|
|
381
|
+
def get_service_tasks(self, service_id: str) -> List[Dict[str, Any]]:
|
|
382
|
+
"""Szolgáltatás task-jainak lekérése.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
service_id: Service ID vagy név
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
List[Dict]: Service task információk listája
|
|
389
|
+
|
|
390
|
+
Raises:
|
|
391
|
+
BlackAntDockerError: Ha a service nem található vagy API hiba
|
|
392
|
+
"""
|
|
393
|
+
try:
|
|
394
|
+
service = self.get_service(service_id)
|
|
395
|
+
if service:
|
|
396
|
+
tasks = service.tasks()
|
|
397
|
+
self.logger.debug(f"Found {len(tasks)} tasks for service: {service_id}")
|
|
398
|
+
return tasks
|
|
399
|
+
else:
|
|
400
|
+
raise BlackAntDockerError(f"Service not found: {service_id}")
|
|
401
|
+
|
|
402
|
+
except APIError as api_error:
|
|
403
|
+
error_msg = f"Error getting tasks for service '{service_id}': {api_error}"
|
|
404
|
+
self.logger.error(error_msg)
|
|
405
|
+
raise BlackAntDockerError(error_msg) from api_error
|
|
406
|
+
|
|
407
|
+
def __enter__(self):
|
|
408
|
+
"""Context manager entry."""
|
|
409
|
+
return self
|
|
410
|
+
|
|
411
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
412
|
+
"""Context manager exit - cleanup resources."""
|
|
413
|
+
if hasattr(self.__docker_client, "close"):
|
|
414
|
+
self.__docker_client.close()
|