Skip to content

Commit e1466e4

Browse files
add agent_check template for publishing platform integrations
1 parent ef41af5 commit e1466e4

File tree

20 files changed

+449
-16
lines changed

20 files changed

+449
-16
lines changed

datadog_checks_dev/datadog_checks/dev/tooling/commands/create.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99

1010
from ...fs import resolve_path
1111
from ..constants import get_root
12-
from ..create import construct_template_fields, create_template_files, get_valid_templates
12+
from ..create import (
13+
construct_template_fields,
14+
create_template_files,
15+
get_valid_templates,
16+
prefill_template_fields_for_check_only,
17+
)
1318
from ..utils import kebab_case_name, normalize_package_name
1419
from .console import CONTEXT_SETTINGS, abort, echo_info, echo_success, echo_warning
1520

@@ -171,27 +176,36 @@ def create(ctx, name, integration_type, location, non_interactive, quiet, dry_ru
171176
if integration_type == 'snmp_tile':
172177
integration_dir_name = 'snmp_' + integration_dir_name
173178
integration_dir = os.path.join(root, integration_dir_name)
174-
if os.path.exists(integration_dir):
175-
abort(f'Path `{integration_dir}` already exists!')
179+
# check-only is designed to already have content in it
180+
if integration_type == 'check_only':
181+
if not os.path.exists(os.path.join(integration_dir, "manifest.json")):
182+
abort(f"Expected {integration_dir}/manifest.json to exist")
183+
else:
184+
if os.path.exists(integration_dir):
185+
abort(f'Path `{integration_dir}` already exists!')
176186

177187
template_fields = {'manifest_version': '1.0.0', "today": date.today()}
188+
template_fields.update(prefill_template_fields_for_check_only(integration_dir_name))
178189
if non_interactive and repo_choice != 'core':
179190
abort(f'Cannot use non-interactive mode with repo_choice: {repo_choice}')
180191

181192
if not non_interactive and not dry_run:
182193
if repo_choice not in ['core', 'integrations-internal-core']:
183-
support_email = click.prompt('Email used for support requests')
184-
template_fields['email'] = support_email
194+
prompt_and_update_if_missing(template_fields, 'email', 'Email used for support requests')
195+
support_email = template_fields['email']
196+
# support_email = click.prompt('Email used for support requests')
197+
# template_fields['email'] = support_email
185198
template_fields['email_packages'] = template_fields['email']
186199
if repo_choice == 'extras':
187200
template_fields['author'] = click.prompt('Your name')
188201

189202
if repo_choice == 'marketplace':
190-
author_name = click.prompt('Your Company Name')
191-
homepage = click.prompt('The product or company homepage')
192-
sales_email = click.prompt('Email used for subscription notifications')
193-
194-
template_fields['author'] = author_name
203+
prompt_and_update_if_missing(template_fields, 'author_name', 'Your Company Name')
204+
prompt_and_update_if_missing(template_fields, 'homepage', 'The product or company homepage')
205+
prompt_and_update_if_missing(template_fields, 'sales_email', 'Email used for subscription notifications')
206+
author_name = template_fields['author_name']
207+
sales_email = template_fields['sales_email']
208+
homepage = template_fields['homepage']
195209

196210
eula = 'assets/eula.pdf'
197211
template_fields['terms'] = f'\n "terms": {{\n "eula": "{eula}"\n }},'
@@ -211,24 +225,24 @@ def create(ctx, name, integration_type, location, non_interactive, quiet, dry_ru
211225
template_fields[
212226
'author_info'
213227
] = """
214-
"author": {
228+
"author": {
215229
"support_email": "help@datadoghq.com",
216230
"name": "Datadog",
217231
"homepage": "https://www.datadoghq.com",
218232
"sales_email": "info@datadoghq.com"
219-
}"""
233+
}"""
220234
else:
221235
prompt_and_update_if_missing(template_fields, 'email', 'Email used for support requests')
222236
prompt_and_update_if_missing(template_fields, 'author', 'Your name')
223237
template_fields[
224238
'author_info'
225239
] = f"""
226-
"author": {{
240+
"author": {{
227241
"support_email": "{template_fields['email']}",
228242
"name": "{template_fields['author']}",
229243
"homepage": "",
230244
"sales_email": ""
231-
}}"""
245+
}}"""
232246
template_fields['terms'] = ''
233247
template_fields['integration_id'] = kebab_case_name(name)
234248
template_fields['package_url'] = (

datadog_checks_dev/datadog_checks/dev/tooling/constants.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,16 @@
9797
[9]: https://docs.datadoghq.com/help/
9898
"""
9999

100+
CHECK_ONLY_LINKS = """\
101+
[1]: **LINK_TO_INTEGRATION_SITE**
102+
[2]: https://app.datadoghq.com/account/settings/agent/latest
103+
[3]: https://docs.datadoghq.com/agent/kubernetes/integrations/
104+
[4]: https://github.com/DataDog/{repository}/blob/master/{name}/datadog_checks/{name}/data/conf.yaml.example
105+
[5]: https://docs.datadoghq.com/agent/guide/agent-commands/#start-stop-and-restart-the-agent
106+
[6]: https://docs.datadoghq.com/agent/guide/agent-commands/#agent-status-and-information
107+
[9]: https://docs.datadoghq.com/help/
108+
"""
109+
100110
LOGS_LINKS = """\
101111
[1]: https://docs.datadoghq.com/help/
102112
[2]: https://app.datadoghq.com/account/settings/agent/latest
@@ -132,6 +142,7 @@
132142

133143
integration_type_links = {
134144
'check': CHECK_LINKS,
145+
'check_only': CHECK_ONLY_LINKS,
135146
'logs': LOGS_LINKS,
136147
'jmx': JMX_LINKS,
137148
'snmp_tile': SNMP_TILE_LINKS,

datadog_checks_dev/datadog_checks/dev/tooling/create.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# (C) Datadog, Inc. 2018-present
22
# All rights reserved
33
# Licensed under a 3-clause BSD style license (see LICENSE)
4+
import json
45
import os
56
from datetime import datetime
67
from operator import attrgetter
@@ -52,6 +53,25 @@ def get_valid_templates():
5253
return sorted(templates, key=attrgetter('name'))
5354

5455

56+
def prefill_template_fields_for_check_only(normalized_integration_name: str) -> dict:
57+
manifest_dict = {}
58+
manifest_path = os.path.join(normalized_integration_name, 'manifest.json')
59+
if not os.path.exists(manifest_path):
60+
raise ValueError(f"Expected manifest to exist at {manifest_path}")
61+
with open(f'{normalized_integration_name}/manifest.json', 'r') as manifest:
62+
manifest_dict = json.loads(manifest.read())
63+
author = manifest_dict.get("author", {}).get("name")
64+
return {
65+
'author': author,
66+
# Both author and author_name are used depending on if marketplace or not
67+
'author_name': author,
68+
'check_name': normalize_package_name(f"{author}_{normalized_integration_name}"),
69+
'email': manifest_dict.get("author", {}).get("support_email"),
70+
'homepage': manifest_dict.get("author", {}).get("homepage"),
71+
'sales_email': manifest_dict.get("author", {}).get("sales_email"),
72+
}
73+
74+
5575
def construct_template_fields(integration_name, repo_choice, integration_type, **kwargs):
5676
normalized_integration_name = normalize_package_name(integration_name)
5777
check_name_kebab = kebab_case_name(integration_name)
@@ -71,7 +91,17 @@ def construct_template_fields(integration_name, repo_choice, integration_type, *
7191
4. Upload the build artifact to any host with an Agent and
7292
run `datadog-agent integration install -w
7393
path/to/{normalized_integration_name}/dist/<ARTIFACT_NAME>.whl`."""
74-
94+
if integration_type == 'check_only':
95+
# check_name, author, email come from kwargs due to prefill
96+
check_name = ''
97+
author = ''
98+
email = ''
99+
email_packages = ''
100+
install_info = third_party_install_info
101+
# Static fields
102+
license_header = ''
103+
support_type = 'partner'
104+
integration_links = ''
75105
if repo_choice == 'core':
76106
check_name = normalized_integration_name
77107
author = 'Datadog'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Initial Release
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{license_header}
2+
__path__ = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{license_header}
2+
__version__ = '{starting_version}'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{license_header}
2+
from .__about__ import __version__
3+
from .check import {check_class}
4+
5+
__all__ = ['__version__', '{check_class}']
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
{license_header}
2+
from typing import Any # noqa: F401
3+
4+
from datadog_checks.base import AgentCheck # noqa: F401
5+
6+
# from datadog_checks.base.utils.db import QueryManager
7+
# from requests.exceptions import ConnectionError, HTTPError, InvalidURL, Timeout
8+
# from json import JSONDecodeError
9+
10+
11+
class {check_class}(AgentCheck):
12+
13+
# This will be the prefix of every metric and service check the integration sends
14+
__NAMESPACE__ = '{check_name}'
15+
16+
def __init__(self, name, init_config, instances):
17+
super({check_class}, self).__init__(name, init_config, instances)
18+
19+
# Use self.instance to read the check configuration
20+
# self.url = self.instance.get("url")
21+
22+
# If the check is going to perform SQL queries you should define a query manager here.
23+
# More info at
24+
# https://datadoghq.dev/integrations-core/base/databases/#datadog_checks.base.utils.db.core.QueryManager
25+
# sample_query = {{
26+
# "name": "sample",
27+
# "query": "SELECT * FROM sample_table",
28+
# "columns": [
29+
# {{"name": "metric", "type": "gauge"}}
30+
# ],
31+
# }}
32+
# self._query_manager = QueryManager(self, self.execute_query, queries=[sample_query])
33+
# self.check_initializations.append(self._query_manager.compile_queries)
34+
35+
def check(self, _):
36+
# type: (Any) -> None
37+
# The following are useful bits of code to help new users get started.
38+
39+
# Perform HTTP Requests with our HTTP wrapper.
40+
# More info at https://datadoghq.dev/integrations-core/base/http/
41+
# try:
42+
# response = self.http.get(self.url)
43+
# response.raise_for_status()
44+
# response_json = response.json()
45+
46+
# except Timeout as e:
47+
# self.service_check(
48+
# "can_connect",
49+
# AgentCheck.CRITICAL,
50+
# message="Request timeout: {{}}, {{}}".format(self.url, e),
51+
# )
52+
# raise
53+
54+
# except (HTTPError, InvalidURL, ConnectionError) as e:
55+
# self.service_check(
56+
# "can_connect",
57+
# AgentCheck.CRITICAL,
58+
# message="Request failed: {{}}, {{}}".format(self.url, e),
59+
# )
60+
# raise
61+
62+
# except JSONDecodeError as e:
63+
# self.service_check(
64+
# "can_connect",
65+
# AgentCheck.CRITICAL,
66+
# message="JSON Parse failed: {{}}, {{}}".format(self.url, e),
67+
# )
68+
# raise
69+
70+
# except ValueError as e:
71+
# self.service_check(
72+
# "can_connect", AgentCheck.CRITICAL, message=str(e)
73+
# )
74+
# raise
75+
76+
# This is how you submit metrics
77+
# There are different types of metrics that you can submit (gauge, event).
78+
# More info at https://datadoghq.dev/integrations-core/base/api/#datadog_checks.base.checks.base.AgentCheck
79+
# self.gauge("test", 1.23, tags=['foo:bar'])
80+
81+
# Perform database queries using the Query Manager
82+
# self._query_manager.execute()
83+
84+
# This is how you use the persistent cache. This cache file based and persists across agent restarts.
85+
# If you need an in-memory cache that is persisted across runs
86+
# You can define a dictionary in the __init__ method.
87+
# self.write_persistent_cache("key", "value")
88+
# value = self.read_persistent_cache("key")
89+
90+
# If your check ran successfully, you can send the status.
91+
# More info at
92+
# https://datadoghq.dev/integrations-core/base/api/#datadog_checks.base.checks.base.AgentCheck.service_check
93+
# self.service_check("can_connect", AgentCheck.OK)
94+
95+
# If it didn't then it should send a critical service check
96+
self.service_check("can_connect", AgentCheck.CRITICAL)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{license_header}
2+
3+
{documentation}
4+
5+
from .instance import InstanceConfig
6+
from .shared import SharedConfig
7+
8+
9+
class ConfigMixin:
10+
_config_model_instance: InstanceConfig
11+
_config_model_shared: SharedConfig
12+
13+
@property
14+
def config(self) -> InstanceConfig:
15+
return self._config_model_instance
16+
17+
@property
18+
def shared_config(self) -> SharedConfig:
19+
return self._config_model_shared
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{license_header}
2+
3+
{documentation}
4+
5+
def instance_empty_default_hostname():
6+
return False
7+
8+
9+
def instance_min_collection_interval():
10+
return 15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{license_header}
2+
3+
{documentation}
4+
5+
from __future__ import annotations
6+
7+
from typing import Optional
8+
9+
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
10+
11+
from datadog_checks.base.utils.functions import identity
12+
from datadog_checks.base.utils.models import validation
13+
14+
from . import defaults, validators
15+
16+
17+
class InstanceConfig(BaseModel):
18+
model_config = ConfigDict(
19+
validate_default=True,
20+
arbitrary_types_allowed=True,
21+
frozen=True,
22+
)
23+
empty_default_hostname: Optional[bool] = None
24+
min_collection_interval: Optional[float] = None
25+
service: Optional[str] = None
26+
tags: Optional[tuple[str, ...]] = None
27+
28+
@model_validator(mode='before')
29+
def _initial_validation(cls, values):
30+
return validation.core.initialize_config(getattr(validators, 'initialize_instance', identity)(values))
31+
32+
@field_validator('*', mode='before')
33+
def _validate(cls, value, info):
34+
field = cls.model_fields[info.field_name]
35+
field_name = field.alias or info.field_name
36+
if field_name in info.context['configured_fields']:
37+
value = getattr(validators, f'instance_{{info.field_name}}', identity)(value, field=field)
38+
else:
39+
value = getattr(defaults, f'instance_{{info.field_name}}', lambda: value)()
40+
41+
return validation.utils.make_immutable(value)
42+
43+
@model_validator(mode='after')
44+
def _final_validation(cls, model):
45+
return validation.core.check_model(getattr(validators, 'check_instance', identity)(model))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{license_header}
2+
3+
{documentation}
4+
5+
from __future__ import annotations
6+
7+
from typing import Optional
8+
9+
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
10+
11+
from datadog_checks.base.utils.functions import identity
12+
from datadog_checks.base.utils.models import validation
13+
14+
from . import defaults, validators
15+
16+
17+
class SharedConfig(BaseModel):
18+
model_config = ConfigDict(
19+
validate_default=True,
20+
arbitrary_types_allowed=True,
21+
frozen=True,
22+
)
23+
service: Optional[str] = None
24+
25+
@model_validator(mode='before')
26+
def _initial_validation(cls, values):
27+
return validation.core.initialize_config(getattr(validators, 'initialize_shared', identity)(values))
28+
29+
@field_validator('*', mode='before')
30+
def _validate(cls, value, info):
31+
field = cls.model_fields[info.field_name]
32+
field_name = field.alias or info.field_name
33+
if field_name in info.context['configured_fields']:
34+
value = getattr(validators, f'shared_{{info.field_name}}', identity)(value, field=field)
35+
else:
36+
value = getattr(defaults, f'shared_{{info.field_name}}', lambda: value)()
37+
38+
return validation.utils.make_immutable(value)
39+
40+
@model_validator(mode='after')
41+
def _final_validation(cls, model):
42+
return validation.core.check_model(getattr(validators, 'check_shared', identity)(model))

0 commit comments

Comments
 (0)