Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update dashboard validation for Manifest V2 #10398

Merged
merged 2 commits into from
Oct 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions datadog_checks_dev/datadog_checks/dev/tooling/manifest_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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
Comment on lines +27 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where are these used?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/DataDog/integrations-core/pull/10398/files#diff-e5ca44e5d121dd8240c03bc3064b8c0cc2a3f84fb899aa11c3516328bc635838R129

In the other file, by adding these enums here, it avoids an extra import statement which will also be helpful in other use cases


@staticmethod
def load_manifest(check):
"""
Expand Down Expand Up @@ -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')
Expand All @@ -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']

Expand All @@ -93,10 +109,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)
Comment on lines +114 to +115
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we using this get_path anywhere?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, but at one point in my PR I did - thought it would be useful to have so I left it.


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 ''

Expand Down