1
+ from __future__ import annotations
1
2
import os
2
3
import threading
3
4
import time
5
+ from collections import defaultdict
4
6
from typing import List , Tuple , Dict
7
+ import uuid
5
8
6
- from assemblyline .odm .models .service import DockerConfig
9
+ from assemblyline .odm .models .service import DependencyConfig , DockerConfig
7
10
from .interface import ControllerInterface , ServiceControlError
8
11
9
- # How to identify the update volume as a whole, in a way that the underlying container system recognizes.
10
- FILE_UPDATE_VOLUME = os .environ .get ('FILE_UPDATE_VOLUME' , None )
11
-
12
12
# Where to find the update directory inside this container.
13
- FILE_UPDATE_DIRECTORY = os .environ .get ('FILE_UPDATE_DIRECTORY' , None )
14
13
INHERITED_VARIABLES = ['HTTP_PROXY' , 'HTTPS_PROXY' , 'NO_PROXY' , 'http_proxy' , 'https_proxy' , 'no_proxy' ]
15
14
16
15
# Every this many seconds, check that the services can actually reach the service server.
17
16
NETWORK_REFRESH_INTERVAL = 60 * 3
17
+ CHANGE_KEY_NAME = 'al_change_key'
18
18
19
19
20
20
class DockerController (ControllerInterface ):
21
21
"""A controller for *non* swarm mode docker."""
22
- def __init__ (self , logger , prefix = '' , labels = None , cpu_overallocation = 1 , memory_overallocation = 1 , log_level = "INFO" ):
22
+ def __init__ (self , logger , prefix = '' , labels : dict [ str , str ] = None , cpu_overallocation = 1 , memory_overallocation = 1 , log_level = "INFO" ):
23
23
"""
24
24
:param logger: A logger to report status and debug information.
25
25
:param prefix: A prefix used to distinguish containers launched by this controller.
@@ -32,9 +32,11 @@ def __init__(self, logger, prefix='', labels=None, cpu_overallocation=1, memory_
32
32
self .log = logger
33
33
self .log_level = log_level
34
34
self .global_mounts : List [Tuple [str , str ]] = []
35
+ self .core_mounts : List [Tuple [str , str ]] = []
35
36
self ._prefix : str = prefix
36
- self ._labels = labels
37
+ self ._labels : dict [ str , str ] = labels or {}
37
38
self .prune_lock = threading .Lock ()
39
+ self ._service_limited_env : dict [str , dict [str , str ]] = defaultdict (dict )
38
40
39
41
for network in self .client .networks .list (names = ['external' ]):
40
42
self .external_network = network
@@ -109,8 +111,8 @@ def _flush_containers(self):
109
111
110
112
def add_profile (self , profile , scale = 0 ):
111
113
"""Tell the controller about a service profile it needs to manage."""
112
- self ._pull_image (profile )
113
114
self ._profiles [profile .name ] = profile
115
+ self ._pull_image (profile )
114
116
115
117
def _start (self , service_name ):
116
118
"""Launch a docker container in a manner suitable for Assemblyline."""
@@ -124,15 +126,13 @@ def _start(self, service_name):
124
126
125
127
# Prepare the volumes and folders
126
128
volumes = {row [0 ]: {'bind' : row [1 ], 'mode' : 'ro' } for row in self .global_mounts }
127
- volumes [os .path .join (FILE_UPDATE_VOLUME , service_name )] = {'bind' : '/mount/updates/' , 'mode' : 'ro' }
128
- if not os .path .exists (os .path .join (FILE_UPDATE_DIRECTORY , service_name )):
129
- os .makedirs (os .path .join (FILE_UPDATE_DIRECTORY , service_name ), 0x777 )
130
129
131
130
# Define environment variables
132
131
env = [f'{ _e .name } ={ _e .value } ' for _e in cfg .environment ]
133
132
env += ['UPDATE_PATH=/mount/updates/' ]
134
133
env += [f'{ name } ={ os .environ [name ]} ' for name in INHERITED_VARIABLES if name in os .environ ]
135
134
env += [f'LOG_LEVEL={ self .log_level } ' ]
135
+ env += [f'{ _n } ={ _v } ' for _n , _v in self ._service_limited_env [service_name ].items ()]
136
136
137
137
container = self .client .containers .run (
138
138
image = cfg .image ,
@@ -152,7 +152,7 @@ def _start(self, service_name):
152
152
if cfg .allow_internet_access :
153
153
self .external_network .connect (container )
154
154
155
- def _start_container (self , name , labels , volumes , cfg : DockerConfig , network , hostname ):
155
+ def _start_container (self , service_name , name , labels , volumes , cfg : DockerConfig , network , hostname , core_container = False ):
156
156
"""Launch a docker container."""
157
157
# Take the port strings and convert them to a dictionary
158
158
ports = {}
@@ -174,9 +174,13 @@ def _start_container(self, name, labels, volumes, cfg: DockerConfig, network, ho
174
174
self .log .warning (f"Not sure how to parse port string { port_string } for container { name } not using it..." )
175
175
176
176
# Put together the environment variables
177
- env = [f'{ _e .name } ={ _e .value } ' for _e in cfg .environment ]
177
+ env = []
178
+ if core_container :
179
+ env += [f'{ _n } ={ _v } ' for _n , _v in os .environ .items ()
180
+ if any (term in _n for term in ['ELASTIC' , 'FILESTORE' , 'UI_SERVER' ])]
181
+ env += [f'{ _e .name } ={ _e .value } ' for _e in cfg .environment ]
178
182
env += [f'{ name } ={ os .environ [name ]} ' for name in INHERITED_VARIABLES if name in os .environ ]
179
- env += [f'LOG_LEVEL={ self .log_level } ' ]
183
+ env += [f'LOG_LEVEL={ self .log_level } ' , f'AL_SERVICE_NAME= { service_name } ' ]
180
184
181
185
container = self .client .containers .run (
182
186
image = cfg .image ,
@@ -192,8 +196,9 @@ def _start_container(self, name, labels, volumes, cfg: DockerConfig, network, ho
192
196
network = network ,
193
197
environment = env ,
194
198
detach = True ,
195
- ports = ports ,
199
+ # ports=ports,
196
200
)
201
+
197
202
if cfg .allow_internet_access :
198
203
self .external_network .connect (container , aliases = [hostname ])
199
204
@@ -324,16 +329,44 @@ def get_running_container_names(self):
324
329
out .append (container .name )
325
330
return out
326
331
327
- def start_stateful_container (self , service_name , container_name , spec , labels , mount_updates = False , change_key = '' ):
328
- volumes = {_n : {'bind' : _v .mount_path , 'mode' : 'rw' } for _n , _v in spec .volumes .items ()}
332
+ def start_stateful_container (self , service_name : str , container_name : str , spec : DependencyConfig ,
333
+ labels : dict [str , str ], change_key : str ):
334
+ import docker .errors
329
335
deployment_name = f'{ service_name } -dep-{ container_name } '
330
336
337
+ change_check = change_key + service_name + container_name + str (spec )
338
+
339
+ try :
340
+ old_container = self .client .containers .get (deployment_name )
341
+ instance_key = old_container .attrs ["Config" ]["Env" ]['AL_INSTANCE_KEY' ]
342
+ if old_container .labels .get (CHANGE_KEY_NAME ) == change_check and old_container .status == 'running' :
343
+ self ._service_limited_env [service_name ][f'{ container_name } _host' ] = deployment_name
344
+ self ._service_limited_env [service_name ][f'{ container_name } _key' ] = instance_key
345
+ if spec .container .ports :
346
+ self ._service_limited_env [service_name ][f'{ container_name } _port' ] = spec .container .ports [0 ]
347
+ return
348
+ else :
349
+ old_container .kill ()
350
+ except docker .errors .NotFound :
351
+ instance_key = uuid .uuid4 ().hex
352
+
353
+ volumes = {_n : {'bind' : _v .mount_path , 'mode' : 'rw' } for _n , _v in spec .volumes .items ()}
354
+ if spec .run_as_core :
355
+ volumes .update ({row [0 ]: {'bind' : row [1 ], 'mode' : 'ro' } for row in self .core_mounts })
356
+
331
357
all_labels = dict (self ._labels )
332
- all_labels .update ({'component' : service_name })
358
+ all_labels .update ({'component' : service_name , CHANGE_KEY_NAME : change_check })
333
359
all_labels .update (labels )
334
360
335
- self ._start_container (name = deployment_name , labels = all_labels , volumes = volumes , hostname = container_name ,
336
- cfg = spec .container , network = self ._get_network (service_name ).name )
361
+ spec .container .environment .append ({'name' : 'AL_INSTANCE_KEY' , 'value' : instance_key })
362
+
363
+ self ._service_limited_env [service_name ][f'{ container_name } _host' ] = deployment_name
364
+ self ._service_limited_env [service_name ][f'{ container_name } _key' ] = instance_key
365
+ if spec .container .ports :
366
+ self ._service_limited_env [service_name ][f'{ container_name } _port' ] = spec .container .ports [0 ]
367
+
368
+ self ._start_container (service_name = service_name , name = deployment_name , labels = all_labels , volumes = volumes , hostname = container_name ,
369
+ cfg = spec .container , core_container = spec .run_as_core , network = self ._get_network (service_name ).name )
337
370
338
371
def stop_containers (self , labels ):
339
372
label_strings = [f'{ name } ={ value } ' for name , value in labels .items ()]
@@ -368,6 +401,7 @@ def _pull_image(self, service):
368
401
369
402
This lets us override the auth_config on a per image basis.
370
403
"""
404
+ from docker .errors import ImageNotFound
371
405
# Split the image string into "[registry/]image_name" and "tag"
372
406
repository , _ , tag = service .container_config .image .rpartition (':' )
373
407
if '/' in tag :
@@ -385,4 +419,13 @@ def _pull_image(self, service):
385
419
'password' : service .container_config .registry_password
386
420
}
387
421
388
- self .client .images .pull (repository , tag , auth_config = auth_config )
422
+ try :
423
+ self .client .images .pull (repository , tag , auth_config = auth_config )
424
+ except ImageNotFound :
425
+ self .log .error (f"Couldn't pull image { repository } :{ tag } check authentication settings. "
426
+ "Will try to use local copy." )
427
+
428
+ try :
429
+ self .client .images .get (repository + ':' + tag )
430
+ except ImageNotFound :
431
+ self .log .error (f"Couldn't find local image { repository } :{ tag } " )
0 commit comments