Skip to content

Commit 5ed54ff

Browse files
hkajgmmeyer
authored andcommitted
Replace buggy image name extraction with get_identifier.
get_identifier looks for a datadog-id label in the container and falls back to the image name if it can't find it.
1 parent 179004c commit 5ed54ff

File tree

3 files changed

+140
-58
lines changed

3 files changed

+140
-58
lines changed

tests/core/test_service_discovery.py

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,30 +60,42 @@ def client_read(path):
6060
return TestServiceDiscovery.mock_tpls.get(image)[0][config_parts.index(config_part)]
6161

6262

63+
def issue_read(identifier):
64+
return TestServiceDiscovery.mock_tpls.get(identifier)
65+
66+
6367
class TestServiceDiscovery(unittest.TestCase):
6468
docker_container_inspect = {
6569
u'Id': u'69ff25598b2314d1cdb7752cc3a659fb1c1352b32546af4f1454321550e842c0',
66-
u'Image': u'6ffc02088cb870652eca9ccd4c4fb582f75b29af2879792ed09bb46fd1c898ef',
70+
u'Image': u'nginx',
6771
u'Name': u'/nginx',
6872
u'NetworkSettings': {u'IPAddress': u'172.17.0.21', u'Ports': {u'443/tcp': None, u'80/tcp': None}}
6973
}
74+
docker_container_inspect_with_label = {
75+
u'Id': u'69ff25598b2314d1cdb7752cc3a659fb1c1352b32546af4f1454321550e842c0',
76+
u'Image': u'nginx',
77+
u'Name': u'/nginx',
78+
u'NetworkSettings': {u'IPAddress': u'172.17.0.21', u'Ports': {u'443/tcp': None, u'80/tcp': None}},
79+
u'Labels': {'com.datadoghq.sd.check.id': 'custom-nginx'}
80+
}
7081
kubernetes_container_inspect = {
7182
u'Id': u'389dc8a4361f3d6c866e9e9a7b6972b26a31c589c4e2f097375d55656a070bc9',
72-
u'Image': u'de309495e6c7b2071bc60c0b7e4405b0d65e33e3a4b732ad77615d90452dd827',
83+
u'Image': u'foo',
7384
u'Name': u'/k8s_sentinel.38057ab9_redis-master_default_27b84e1e-a81c-11e5-8347-42010af00002_f70875a1',
7485
u'Config': {u'ExposedPorts': {u'6379/tcp': {}}},
7586
u'NetworkSettings': {u'IPAddress': u'', u'Ports': None}
7687
}
7788
malformed_container_inspect = {
7889
u'Id': u'69ff25598b2314d1cdb7752cc3a659fb1c1352b32546af4f1454321550e842c0',
79-
u'Image': u'6ffc02088cb870652eca9ccd4c4fb582f75b29af2879792ed09bb46fd1c898ef',
90+
u'Image': u'foo',
8091
u'Name': u'/nginx'
8192
}
8293
container_inspects = [
83-
# (inspect_dict, expected_ip, expected_port)
84-
(docker_container_inspect, '172.17.0.21', 'port', '443'),
85-
(kubernetes_container_inspect, None, 'port', '6379'), # arbitrarily defined in the mocked pod_list
86-
(malformed_container_inspect, None, 'port', KeyError)
94+
# (inspect_dict, expected_ip, expected_port, expected_ident)
95+
(docker_container_inspect, '172.17.0.21', ['80', '443'], 'nginx'),
96+
(docker_container_inspect_with_label, '172.17.0.21', ['80', '443'], 'custom-nginx'),
97+
(kubernetes_container_inspect, None, ['6379'], 'foo'), # arbitrarily defined in the mocked pod_list
98+
(malformed_container_inspect, None, KeyError, 'foo')
8799
]
88100

89101
# templates with variables already extracted
@@ -114,7 +126,12 @@ class TestServiceDiscovery(unittest.TestCase):
114126
[('check_2', {}, {"host": "%%host%%", "port": "%%port%%"})]),
115127
'bad_image_0': ((['invalid template']), []),
116128
'bad_image_1': (('invalid template'), []),
117-
'bad_image_2': (None, [])
129+
'bad_image_2': (None, []),
130+
'nginx': ('["nginx"]', '[{}]', '[{"host": "localhost"}]'),
131+
'nginx:latest': ('["nginx"]', '[{}]', '[{"host": "localhost", "tags": ["foo"]}]'),
132+
'custom-nginx': ('["nginx"]', '[{}]', '[{"host": "localhost"}]'),
133+
'repo/custom-nginx': ('["nginx"]', '[{}]', '[{"host": "localhost", "tags": ["bar"]}]'),
134+
'repo/dir:5000/custom-nginx:latest': ('["nginx"]', '[{}]', '[{"host": "local", "tags": ["foobar"]}]')
118135
}
119136

120137
bad_mock_templates = {
@@ -207,22 +224,22 @@ def test_get_host_address(self, mock_check_yaml, mock_get):
207224
mock_check_yaml.return_value = kubernetes_config
208225
mock_get.return_value = Response(pod_list)
209226

210-
for c_ins, tpl_var, expected_ip in ip_address_inspects:
227+
for c_ins, expected_ip, _, _ in self.container_inspects:
211228
with mock.patch.object(AbstractConfigStore, '__init__', return_value=None):
212229
with mock.patch('utils.dockerutil.DockerUtil.client', return_value=None):
213230
with mock.patch('utils.kubeutil.get_conf_path', return_value=None):
214231
sd_backend = get_sd_backend(agentConfig=self.auto_conf_agentConfig)
215-
self.assertEquals(sd_backend._get_host_address(c_ins, tpl_var), expected_ip)
232+
self.assertEqual(sd_backend._get_host(c_ins), expected_ip)
216233
clear_singletons(self.auto_conf_agentConfig)
217234

218235
def test_get_port(self):
219236
with mock.patch('utils.dockerutil.DockerUtil.client', return_value=None):
220-
for c_ins, _, var_tpl, expected_ports in self.container_inspects:
237+
for c_ins, _, expected_ports, _ in self.container_inspects:
221238
sd_backend = get_sd_backend(agentConfig=self.auto_conf_agentConfig)
222-
if isinstance(expected_ports, str):
223-
self.assertEquals(sd_backend._get_port(c_ins, var_tpl), expected_ports)
239+
if isinstance(expected_ports, list):
240+
self.assertEqual(sd_backend._get_ports(c_ins), expected_ports)
224241
else:
225-
self.assertRaises(expected_ports, sd_backend._get_port, c_ins, var_tpl)
242+
self.assertRaises(expected_ports, sd_backend._get_ports, c_ins)
226243
clear_singletons(self.auto_conf_agentConfig)
227244

228245
@mock.patch('docker.Client.inspect_container', side_effect=_get_container_inspect)
@@ -498,3 +515,29 @@ def test_get_check_tpls(self, mock_client_read):
498515
for image in invalid_config:
499516
tpl = self.mock_tpls.get(image)[1]
500517
self.assertEquals(tpl, config_store.get_check_tpls(image))
518+
519+
def test_get_config_id(self):
520+
"""Test get_config_id"""
521+
with mock.patch('utils.dockerutil.DockerUtil.client', return_value=None):
522+
for c_ins, _, _, expected_ident in self.container_inspects:
523+
sd_backend = get_sd_backend(agentConfig=self.auto_conf_agentConfig)
524+
self.assertEqual(
525+
sd_backend.get_config_id(c_ins.get('Image'), c_ins.get('Labels', {})),
526+
expected_ident)
527+
clear_singletons(self.auto_conf_agentConfig)
528+
529+
@mock.patch.object(AbstractConfigStore, '_issue_read', side_effect=issue_read)
530+
def test_read_config_from_store(self, issue_read):
531+
"""Test read_config_from_store"""
532+
valid_idents = [('nginx', 'nginx'), ('nginx:latest', 'nginx:latest'),
533+
('custom-nginx', 'custom-nginx'), ('custom-nginx:latest', 'custom-nginx'),
534+
('repo/custom-nginx:latest', 'custom-nginx'),
535+
('repo/dir:5000/custom-nginx:latest', 'repo/dir:5000/custom-nginx:latest')]
536+
invalid_idents = ['foo']
537+
config_store = get_config_store(self.auto_conf_agentConfig)
538+
for ident, expected_key in valid_idents:
539+
tpl = config_store.read_config_from_store(ident)
540+
# source is added after reading from the store
541+
self.assertEquals(tpl, ('template',) + self.mock_tpls.get(expected_key))
542+
for ident in invalid_idents:
543+
self.assertEquals(config_store.read_config_from_store(ident), [])

utils/service_discovery/abstract_config_store.py

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
CONFIG_FROM_FILE = 'YAML file'
2121
CONFIG_FROM_TEMPLATE = 'template'
2222
TRACE_CONFIG = 'trace_config' # used for tracing config load by service discovery
23+
CHECK_NAMES = 'check_names'
24+
INIT_CONFIGS = 'init_configs'
25+
INSTANCES = 'instances'
2326

2427

2528
class KeyNotFound(Exception):
@@ -59,7 +62,8 @@ def dump_directory(self, path, **kwargs):
5962
raise NotImplementedError()
6063

6164
def _get_auto_config(self, image_name):
62-
if image_name in self.auto_conf_images:
65+
# use the image name, ignore the tag
66+
if image_name.split(':')[0] in self.auto_conf_images:
6367
check_name = self.auto_conf_images[image_name]
6468

6569
# get the check class to verify it matches
@@ -75,35 +79,35 @@ def _get_auto_config(self, image_name):
7579

7680
return None
7781

78-
def get_check_tpls(self, image, **kwargs):
79-
"""Retrieve template configs for an image from the config_store or auto configuration."""
80-
# TODO: make mixing both sources possible
82+
def get_check_tpls(self, identifier, **kwargs):
83+
"""Retrieve template configs for an identifier from the config_store or auto configuration."""
8184
templates = []
8285
trace_config = kwargs.get(TRACE_CONFIG, False)
8386

8487
# this flag is used when no valid configuration store was provided
8588
# it makes the method skip directly to the auto_conf
8689
if kwargs.get('auto_conf') is True:
87-
auto_config = self._get_auto_config(image)
90+
# in auto config mode, identifier is the image name
91+
auto_config = self._get_auto_config(identifier)
8892
if auto_config is not None:
8993
source = CONFIG_FROM_AUTOCONF
9094
if trace_config:
9195
return [(source, auto_config)]
9296
return [auto_config]
9397
else:
94-
log.debug('No auto config was found for image %s, leaving it alone.' % image)
98+
log.debug('No auto config was found for image %s, leaving it alone.' % identifier)
9599
return []
96100
else:
97-
config = self.read_config_from_store(image)
101+
config = self.read_config_from_store(identifier)
98102
if config:
99103
source, check_names, init_config_tpls, instance_tpls = config
100104
else:
101105
return []
102106

103107
if len(check_names) != len(init_config_tpls) or len(check_names) != len(instance_tpls):
104108
log.error('Malformed configuration template: check_names, init_configs '
105-
'and instances are not all the same length. Image {0} '
106-
'will not be configured by the service discovery'.format(image))
109+
'and instances are not all the same length. Container with identifier {} '
110+
'will not be configured by the service discovery'.format(identifier))
107111
return []
108112

109113
for idx, c_name in enumerate(check_names):
@@ -113,45 +117,72 @@ def get_check_tpls(self, image, **kwargs):
113117
templates.append((c_name, init_config_tpls[idx], instance_tpls[idx]))
114118
return templates
115119

116-
def read_config_from_store(self, image):
120+
def read_config_from_store(self, identifier):
117121
"""Try to read from the config store, falls back to auto-config in case of failure."""
118122
try:
119-
check_names = json.loads(
120-
self.client_read(path.join(self.sd_template_dir, image, 'check_names').lstrip('/')))
121-
init_config_tpls = json.loads(
122-
self.client_read(path.join(self.sd_template_dir, image, 'init_configs').lstrip('/')))
123-
instance_tpls = json.loads(
124-
self.client_read(path.join(self.sd_template_dir, image, 'instances').lstrip('/')))
125-
source = CONFIG_FROM_TEMPLATE
126-
except (KeyNotFound, TimeoutError, json.JSONDecodeError) as ex:
127-
# first case is kind of expected, it means that no template was provided for this container
128-
if isinstance(ex, KeyNotFound):
129-
log.debug("Could not find directory {0} in the config store, "
130-
"trying to auto-configure a check...".format(image))
123+
try:
124+
res = self._issue_read(identifier)
125+
except KeyNotFound:
126+
log.debug("Could not find directory {} in the config store, "
127+
"trying to convert to the old format.".format(identifier))
128+
legacy_ident = identifier.split(':')[0].split('/')[-1]
129+
res = self._issue_read(legacy_ident)
130+
131+
if res and len(res) == 3:
132+
source = CONFIG_FROM_TEMPLATE
133+
check_names, init_config_tpls, instance_tpls = res
134+
else:
135+
log.debug("Could not find directory {} in the config store, "
136+
"trying to convert to the old format...".format(identifier))
137+
legacy_ident = identifier.split(':')[0].split('/')[-1]
138+
res = self._issue_read(legacy_ident)
139+
if res and len(res) == 3:
140+
source = CONFIG_FROM_TEMPLATE
141+
check_names, init_config_tpls, instance_tpls = res
142+
else:
143+
raise KeyError
144+
except (KeyError, KeyNotFound, TimeoutError, json.JSONDecodeError) as ex:
145+
# this is kind of expected, it means that no template was provided for this container
146+
if isinstance(ex, KeyError) or isinstance(ex, KeyNotFound):
147+
log.debug("Could not find directory {} in the config store, "
148+
"trying to auto-configure a check...".format(identifier))
131149
# this case is not expected, the agent can't reach the config store
132-
elif isinstance(ex, TimeoutError):
150+
if isinstance(ex, TimeoutError):
133151
log.warning("Connection to the config backend timed out. Is it reachable?\n"
134-
"Trying to auto-configure a check for the image %s..." % image)
152+
"Trying to auto-configure a check for the container with ident %s." % identifier)
135153
# the template is reachable but invalid
136154
elif isinstance(ex, json.JSONDecodeError):
137-
log.error('Could not decode the JSON configuration template. '
138-
'Trying to auto-configure a check for the image %s...' % image)
139-
# In any case cases we try to read from auto-config templates
140-
auto_config = self._get_auto_config(image)
155+
log.error('Could not decode the JSON configuration template '
156+
'for the container with ident %s...' % identifier)
157+
return []
158+
# try to read from auto-config templates
159+
auto_config = self._get_auto_config(identifier)
141160
if auto_config is not None:
142161
# create list-format config based on an autoconf template
143162
check_names, init_config_tpls, instance_tpls = map(lambda x: [x], auto_config)
144163
source = CONFIG_FROM_AUTOCONF
145164
else:
146-
log.debug('No auto config was found for image %s, leaving it alone.' % image)
165+
log.debug('No config was found for container with ident %s, leaving it alone.' % identifier)
147166
return []
148167
except Exception as ex:
149168
log.warning(
150169
'Fetching the value for {0} in the config store failed, this check '
151-
'will not be configured by the service discovery. Error: {1}'.format(image, str(ex)))
170+
'will not be configured by the service discovery. Error: {1}'.format(identifier, str(ex)))
152171
return []
153172
return source, check_names, init_config_tpls, instance_tpls
154173

174+
def _issue_read(self, identifier):
175+
try:
176+
check_names = json.loads(
177+
self.client_read(path.join(self.sd_template_dir, identifier, CHECK_NAMES).lstrip('/')))
178+
init_config_tpls = json.loads(
179+
self.client_read(path.join(self.sd_template_dir, identifier, INIT_CONFIGS).lstrip('/')))
180+
instance_tpls = json.loads(
181+
self.client_read(path.join(self.sd_template_dir, identifier, INSTANCES).lstrip('/')))
182+
return [check_names, init_config_tpls, instance_tpls]
183+
except KeyError:
184+
return None
185+
155186
def crawl_config_template(self):
156187
"""Return whether or not configuration templates have changed since the previous crawl"""
157188
try:

0 commit comments

Comments
 (0)