diff --git a/datadog_checks_dev/datadog_checks/dev/tooling/commands/validate/dashboards.py b/datadog_checks_dev/datadog_checks/dev/tooling/commands/validate/dashboards.py index 283bbae407673..f6ce8126ceb60 100644 --- a/datadog_checks_dev/datadog_checks/dev/tooling/commands/validate/dashboards.py +++ b/datadog_checks_dev/datadog_checks/dev/tooling/commands/validate/dashboards.py @@ -6,11 +6,13 @@ import click +from ....fs import write_file from ....utils import read_file from ...annotations import annotate_display_queue, annotate_error +from ...manifest_utils import Manifest from ...testing import process_checks_option from ...utils import complete_valid_checks, get_assets_from_manifest, get_manifest_file -from ..console import CONTEXT_SETTINGS, abort, echo_failure, echo_info, echo_success +from ..console import CONTEXT_SETTINGS, abort, echo_debug, echo_failure, echo_info, echo_success REQUIRED_ATTRIBUTES = {"description", "template_variables", "widgets"} DASHBOARD_ONLY_FIELDS = {"layout_type", "title", "created_at"} @@ -30,9 +32,38 @@ def _is_dashboard_format(payload): return False +def check_widgets(decoded, filename, app_uuid, fix, file_fixed, file_failed, display_queue): + """Recursively check the decoded dashboard object for widget references and validate the app_id inside.""" + for widget in decoded.get('widgets', []): + + if widget.get('definition', {}).get('widgets'): + decoded = {'widgets': widget['definition']['widgets']} + file_fixed, file_failed = check_widgets( + decoded, filename, app_uuid, fix, file_fixed, file_failed, display_queue + ) + + widget_app_uuid = widget.get('definition', {}).get('app_id') + if widget_app_uuid and widget_app_uuid != app_uuid: + if fix: + widget['definition']['app_id'] = app_uuid + file_fixed = True + continue + else: + file_failed = True + msg = ( + f" {filename} widget {widget['id']} does not contain correct app_uuid: " + f"{widget_app_uuid} should be {app_uuid}" + ) + display_queue.append( + (echo_failure, msg), + ) + return file_fixed, file_failed + + @click.command('dashboards', context_settings=CONTEXT_SETTINGS, short_help='Validate dashboard definition JSON files') @click.argument('check', autocompletion=complete_valid_checks, required=False) -def dashboards(check): +@click.option('--fix', is_flag=True, help='Attempt to fix errors') +def dashboards(check, fix): """Validate all Dashboard definition files. If `check` is specified, only the check will be validated, if check value is 'changed' will only apply to changed @@ -45,11 +76,15 @@ def dashboards(check): echo_info(f"Validating Dashboard definition files for {len(checks)} checks...") for check_name in checks: + echo_debug(f"Validating Dashboard definition files for {check_name} check...") display_queue = [] file_failed = False + file_fixed = False - dashboard_relative_locations, invalid_files = get_assets_from_manifest(check_name, 'dashboards') + manifest = Manifest.load_manifest(check_name) manifest_file = get_manifest_file(check_name) + + dashboard_relative_locations, invalid_files = get_assets_from_manifest(check_name, 'dashboards') for invalid in invalid_files: message = f'{invalid} does not exist' echo_info(f'{check_name}... ', nl=False) @@ -90,6 +125,21 @@ def dashboards(check): (echo_failure, f' {dashboard_filename} is not using the new /dashboard payload format.'), ) + # check app_id for Manifest V2 dashboards + if manifest.version == Manifest.V2: + echo_debug(f'Validating app dashboard {dashboard_filename} ..') + app_uuid = manifest.get_app_uuid() + + file_fixed, file_failed = check_widgets( + decoded, dashboard_filename, app_uuid, fix, file_fixed, file_failed, display_queue + ) + + if fix and file_fixed: + new_dashboard = f"{json.dumps(decoded, indent=2, separators=(',', ': '))}\n" + write_file(dashboard_file, new_dashboard) + echo_info(f"{dashboard_file}... ", nl=False) + echo_success("FIXED") + if file_failed: failed_checks += 1 # Display detailed info if file is invalid diff --git a/datadog_checks_dev/datadog_checks/dev/tooling/manifest_utils.py b/datadog_checks_dev/datadog_checks/dev/tooling/manifest_utils.py index 5904706c67e86..ee79559f55fbc 100644 --- a/datadog_checks_dev/datadog_checks/dev/tooling/manifest_utils.py +++ b/datadog_checks_dev/datadog_checks/dev/tooling/manifest_utils.py @@ -7,6 +7,7 @@ from .commands.console import abort from .constants import get_root from .datastructures import JSONDict +from .manifest_validator.constants import V1, V2 from .utils import load_manifest NON_INTEGRATION_PATHS = [ @@ -23,6 +24,10 @@ class Manifest: This also supports the case of querying file information about the Agent """ + # add version constants to avoid extra imports + V1 = V1 + V2 = V2 + @staticmethod def load_manifest(check): """ @@ -50,6 +55,7 @@ class Agent: def __init__(self, check_name, manifest_json): self._check_name = check_name self._manifest_json = manifest_json + self.version = None def get_config_spec(self): return os.path.join(get_root(), 'pkg', 'config', 'conf_spec.yaml') @@ -64,10 +70,20 @@ class ManifestV1: def __init__(self, check_name, manifest_json): self._check_name = check_name self._manifest_json = JSONDict(manifest_json) + self.version = V1 + + def get_path(self, path): + return self._manifest_json.get(path) def get_display_name(self): return self._manifest_json['display_name'] + def get_app_id(self): + return None + + def get_app_uuid(self): + return None + def get_metric_prefix(self): return self._manifest_json['metric_prefix'] @@ -100,10 +116,20 @@ class ManifestV2: def __init__(self, check_name, manifest_json): self._check_name = check_name self._manifest_json = JSONDict(manifest_json) + self.version = V2 + + def get_path(self, path): + return self._manifest_json.get(path) def get_display_name(self): return self._manifest_json.get_path("/assets/integration/source_type_name") + def get_app_id(self): + return self._manifest_json['app_id'] + + def get_app_uuid(self): + return self._manifest_json['app_uuid'] + def get_metric_prefix(self): return self._manifest_json.get_path("/assets/integration/metrics/prefix") or ''