From 5947d615b3f564e5d9aa50effe709156c399d328 Mon Sep 17 00:00:00 2001 From: NouemanKHAL Date: Mon, 9 Mar 2026 10:43:43 +0100 Subject: [PATCH 1/3] Fix nutanix categories format for clustermgmt API (#22836) * fix categories parsing for clusters and hosts + update fixtures with new categories * changelog --- nutanix/changelog.d/22836.fixed | 1 + .../nutanix/activity_monitor.py | 5 --- nutanix/datadog_checks/nutanix/check.py | 37 ++++++++++++++----- nutanix/tests/fixtures/categories.json | 2 +- nutanix/tests/fixtures/clusters.json | 6 ++- nutanix/tests/test_capacity_metrics.py | 9 +++++ nutanix/tests/test_clusters.py | 2 + nutanix/tests/test_hosts.py | 4 ++ nutanix/tests/test_resource_filters.py | 6 +-- 9 files changed, 52 insertions(+), 20 deletions(-) create mode 100644 nutanix/changelog.d/22836.fixed diff --git a/nutanix/changelog.d/22836.fixed b/nutanix/changelog.d/22836.fixed new file mode 100644 index 0000000000000..8f0457e25555c --- /dev/null +++ b/nutanix/changelog.d/22836.fixed @@ -0,0 +1 @@ +Fix categories collection for clusters diff --git a/nutanix/datadog_checks/nutanix/activity_monitor.py b/nutanix/datadog_checks/nutanix/activity_monitor.py index 7a62011b744ee..1866a0df2b146 100644 --- a/nutanix/datadog_checks/nutanix/activity_monitor.py +++ b/nutanix/datadog_checks/nutanix/activity_monitor.py @@ -379,9 +379,6 @@ def _process_audit(self, audit: dict) -> None: if entity_name := entity.get("name"): audit_tags.append(f"ntnx_affected_entity_name:{entity_name}") - # Add category tags from affected entity - audit_tags.extend(self.check.extract_category_tags(entity)) - audit_tags.append("ntnx_type:audit") self.check.event( @@ -513,7 +510,6 @@ def _process_task(self, task: dict) -> None: task_tags.append(f"ntnx_entity_type:{entity_type}") if entity_name := entity.get("name"): task_tags.append(f"ntnx_entity_name:{entity_name}") - task_tags.extend(self.check.extract_category_tags(entity)) # Enrich with rendered alert title when the entity is an alert if entity_type == "monitoring:serviceability:alert": @@ -567,7 +563,6 @@ def _add_source_entity_tags(self, tags: list[str], item: dict) -> None: if entity_type := source_entity.get("type"): if entity_name := source_entity.get("name"): tags.append(f"ntnx_{entity_type}_name:{entity_name}") - tags.extend(self.check.extract_category_tags(source_entity)) def _add_cluster_name_tag(self, tags: list[str], cluster_id: str | None, fallback_name: str | None = None) -> None: """Add cluster name tag from ID lookup, with optional fallback.""" diff --git a/nutanix/datadog_checks/nutanix/check.py b/nutanix/datadog_checks/nutanix/check.py index 98730d0b8fcbe..ee68dd298f3a4 100644 --- a/nutanix/datadog_checks/nutanix/check.py +++ b/nutanix/datadog_checks/nutanix/check.py @@ -108,16 +108,33 @@ def extract_category_tags(self, entity: dict) -> list[str]: categories = entity.get("categories") if categories: for c in categories: - category_id = c.get("extId") - if category_id and category_id in self.categories: - category = self.categories[category_id] - key = category.get("key") - value = category.get("value") - if key and value: - if self.prefix_category_tags: - tags.append(f"ntnx_{key}:{value}") - else: - tags.append(f"{key}:{value}") + if isinstance(c, dict): + category_id = c.get("extId") + elif isinstance(c, str): + category_id = c + else: + self.log.debug( + "Skipping unexpected category entry type=%s value=%r", + type(c), + c, + ) + continue + + if not category_id: + continue + + category = self.categories.get(category_id) + if not isinstance(category, dict): + continue + + key = category.get("key") + value = category.get("value") + + if key and value: + if self.prefix_category_tags: + tags.append(f"ntnx_{key}:{value}") + else: + tags.append(f"{key}:{value}") return tags def check(self, _): diff --git a/nutanix/tests/fixtures/categories.json b/nutanix/tests/fixtures/categories.json index d5faae63aba3e..19e0d4ccdeedd 100644 --- a/nutanix/tests/fixtures/categories.json +++ b/nutanix/tests/fixtures/categories.json @@ -915,4 +915,4 @@ "totalAvailableResults": 41 } } -] +] \ No newline at end of file diff --git a/nutanix/tests/fixtures/clusters.json b/nutanix/tests/fixtures/clusters.json index fccb99600effe..64cf053dff900 100644 --- a/nutanix/tests/fixtures/clusters.json +++ b/nutanix/tests/fixtures/clusters.json @@ -124,6 +124,7 @@ "currentClusterFaultTolerance": "CFT_1N_OR_1D", "desiredClusterFaultTolerance": "CFT_1N_OR_1D" }, + "operationMode": "NORMAL", "pulseStatus": { "$reserved": { "$fv": "v4.r1" @@ -207,7 +208,10 @@ "isSegmentationEnabled": false } }, - "upgradeStatus": "SUCCEEDED" + "upgradeStatus": "SUCCEEDED", + "categories": [ + "66ae6983-5a95-3f0b-9e5d-b5b603b5f2c0" + ] }, { "$reserved": { diff --git a/nutanix/tests/test_capacity_metrics.py b/nutanix/tests/test_capacity_metrics.py index ee087fffe480c..5bd4d4cd5a43e 100644 --- a/nutanix/tests/test_capacity_metrics.py +++ b/nutanix/tests/test_capacity_metrics.py @@ -119,6 +119,7 @@ def test_host_cpu_sockets(self, dd_run_check, aggregator, mock_instance, mock_ht dd_run_check(check) expected_tags = [ + 'Team:agent-integrations', 'ntnx_type:host', 'ntnx_cluster_name:datadog-nutanix-dev', 'ntnx_host_name:10-0-0-103-aws-us-east-1a', @@ -140,6 +141,7 @@ def test_host_cpu_cores(self, dd_run_check, aggregator, mock_instance, mock_http dd_run_check(check) expected_tags = [ + 'Team:agent-integrations', 'ntnx_type:host', 'ntnx_cluster_name:datadog-nutanix-dev', 'ntnx_host_name:10-0-0-103-aws-us-east-1a', @@ -161,6 +163,7 @@ def test_host_cpu_threads(self, dd_run_check, aggregator, mock_instance, mock_ht dd_run_check(check) expected_tags = [ + 'Team:agent-integrations', 'ntnx_type:host', 'ntnx_cluster_name:datadog-nutanix-dev', 'ntnx_host_name:10-0-0-103-aws-us-east-1a', @@ -182,6 +185,7 @@ def test_host_memory_bytes(self, dd_run_check, aggregator, mock_instance, mock_h dd_run_check(check) expected_tags = [ + 'Team:agent-integrations', 'ntnx_type:host', 'ntnx_cluster_name:datadog-nutanix-dev', 'ntnx_host_name:10-0-0-103-aws-us-east-1a', @@ -207,6 +211,7 @@ def test_cluster_cpu_total_cores(self, dd_run_check, aggregator, mock_instance, dd_run_check(check) expected_tags = [ + 'Team:agent-integrations', 'ntnx_cluster_name:datadog-nutanix-dev', 'nutanix', 'prism_central:10.0.0.197', @@ -221,6 +226,7 @@ def test_cluster_cpu_total_threads(self, dd_run_check, aggregator, mock_instance dd_run_check(check) expected_tags = [ + 'Team:agent-integrations', 'ntnx_cluster_name:datadog-nutanix-dev', 'nutanix', 'prism_central:10.0.0.197', @@ -235,6 +241,7 @@ def test_cluster_memory_total_bytes(self, dd_run_check, aggregator, mock_instanc dd_run_check(check) expected_tags = [ + 'Team:agent-integrations', 'ntnx_cluster_name:datadog-nutanix-dev', 'nutanix', 'prism_central:10.0.0.197', @@ -249,6 +256,7 @@ def test_cluster_vcpus_allocated(self, dd_run_check, aggregator, mock_instance, dd_run_check(check) expected_tags = [ + 'Team:agent-integrations', 'ntnx_cluster_name:datadog-nutanix-dev', 'nutanix', 'prism_central:10.0.0.197', @@ -263,6 +271,7 @@ def test_cluster_memory_allocated_bytes(self, dd_run_check, aggregator, mock_ins dd_run_check(check) expected_tags = [ + 'Team:agent-integrations', 'ntnx_cluster_name:datadog-nutanix-dev', 'nutanix', 'prism_central:10.0.0.197', diff --git a/nutanix/tests/test_clusters.py b/nutanix/tests/test_clusters.py index 2c79ac9a891b4..f0df6a1cb93f8 100644 --- a/nutanix/tests/test_clusters.py +++ b/nutanix/tests/test_clusters.py @@ -36,6 +36,7 @@ def test_cluster_metrics(dd_run_check, aggregator, mock_instance, mock_http_get) dd_run_check(check) expected_tags = [ + 'Team:agent-integrations', 'ntnx_cluster_name:datadog-nutanix-dev', 'nutanix', 'prism_central:10.0.0.197', @@ -54,6 +55,7 @@ def test_cluster_stats_metrics(dd_run_check, aggregator, mock_instance, mock_htt dd_run_check(check) expected_tags = [ + 'Team:agent-integrations', 'ntnx_cluster_name:datadog-nutanix-dev', 'nutanix', 'prism_central:10.0.0.197', diff --git a/nutanix/tests/test_hosts.py b/nutanix/tests/test_hosts.py index c6ed8508f8b6a..32d0bb8d16301 100644 --- a/nutanix/tests/test_hosts.py +++ b/nutanix/tests/test_hosts.py @@ -16,6 +16,7 @@ def test_host_metrics(dd_run_check, aggregator, mock_instance, mock_http_get): dd_run_check(check) expected_tags = [ + 'Team:agent-integrations', 'ntnx_type:host', 'ntnx_cluster_name:datadog-nutanix-dev', 'ntnx_host_name:10-0-0-103-aws-us-east-1a', @@ -34,6 +35,7 @@ def test_host_stats_metrics(dd_run_check, aggregator, mock_instance, mock_http_g dd_run_check(check) expected_tags = [ + 'Team:agent-integrations', 'ntnx_type:host', 'ntnx_cluster_name:datadog-nutanix-dev', 'ntnx_host_name:10-0-0-103-aws-us-east-1a', @@ -54,6 +56,7 @@ def test_host_status_metrics(dd_run_check, aggregator, mock_instance, mock_http_ dd_run_check(check) expected_tags = [ + 'Team:agent-integrations', 'ntnx_type:host', 'ntnx_cluster_name:datadog-nutanix-dev', 'ntnx_host_name:10-0-0-103-aws-us-east-1a', @@ -76,6 +79,7 @@ def test_external_tags_for_host(dd_run_check, aggregator, mock_instance, mock_ht '10-0-0-103-aws-us-east-1a', { 'nutanix': [ + 'Team:agent-integrations', 'ntnx_type:host', 'ntnx_cluster_name:datadog-nutanix-dev', 'ntnx_host_name:10-0-0-103-aws-us-east-1a', diff --git a/nutanix/tests/test_resource_filters.py b/nutanix/tests/test_resource_filters.py index 7f4d4a6dc6581..8d3a3a71d1779 100644 --- a/nutanix/tests/test_resource_filters.py +++ b/nutanix/tests/test_resource_filters.py @@ -21,7 +21,7 @@ def test_default_collects_only_on_vms_and_user_category_tags(dd_run_check, aggregator, mock_instance, mock_http_get): check = NutanixCheck('nutanix', {}, [mock_instance]) dd_run_check(check) - expected_tags = BASE_TAGS + ['ntnx_cluster_name:' + CLUSTER_NAME] + expected_tags = BASE_TAGS + ['Team:agent-integrations', 'ntnx_cluster_name:' + CLUSTER_NAME] aggregator.assert_metric("nutanix.cluster.count", value=1, tags=expected_tags) aggregator.assert_metric("nutanix.host.count", at_least=1) @@ -49,7 +49,7 @@ def test_include_cluster_by_id(dd_run_check, aggregator, mock_instance, mock_htt ] check = NutanixCheck('nutanix', {}, [mock_instance]) dd_run_check(check) - expected_tags = BASE_TAGS + ['ntnx_cluster_name:' + CLUSTER_NAME] + expected_tags = BASE_TAGS + ['Team:agent-integrations', 'ntnx_cluster_name:' + CLUSTER_NAME] aggregator.assert_metric("nutanix.cluster.count", value=1, tags=expected_tags) @@ -102,7 +102,7 @@ def test_multiple_include_patterns(dd_run_check, aggregator, mock_instance, mock ] check = NutanixCheck('nutanix', {}, [mock_instance]) dd_run_check(check) - expected_tags = BASE_TAGS + ['ntnx_cluster_name:' + CLUSTER_NAME] + expected_tags = BASE_TAGS + ['Team:agent-integrations', 'ntnx_cluster_name:' + CLUSTER_NAME] aggregator.assert_metric("nutanix.cluster.count", value=1, tags=expected_tags) From 46ce50875c6b4dc7af699f520818f11f24d1e38b Mon Sep 17 00:00:00 2001 From: Juanpe Araque Date: Mon, 9 Mar 2026 12:46:40 +0100 Subject: [PATCH 2/3] Bring back Dependabot cooldown on github actions (#22842) --- .github/dependabot.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3f70ce9a2d9fd..3dc0b4ef1855c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,6 +8,8 @@ updates: interval: "weekly" day: "monday" time: "06:00" + cooldown: + default-days: 7 # Group all actions updates together in the same PR groups: actions: @@ -23,7 +25,8 @@ updates: interval: "weekly" day: "monday" time: "06:00" - # Group all actions updates together in the same PR + cooldown: + default-days: 7 groups: actions: patterns: From a208a33bff3f1805b9a5b6c7b855853bbf0b03b3 Mon Sep 17 00:00:00 2001 From: Lucia Date: Mon, 9 Mar 2026 12:57:15 +0100 Subject: [PATCH 3/3] Add pattern matching for gitignore files in ddev size (#22838) * Add pattern matching to ignore gitignore files * Add changelog --- ddev/changelog.d/22838.fixed | 1 + ddev/src/ddev/cli/size/utils/common_funcs.py | 35 ++++++++++++++------ ddev/tests/size/test_common.py | 34 ++++++++++++++----- 3 files changed, 52 insertions(+), 18 deletions(-) create mode 100644 ddev/changelog.d/22838.fixed diff --git a/ddev/changelog.d/22838.fixed b/ddev/changelog.d/22838.fixed new file mode 100644 index 0000000000000..625180d52022a --- /dev/null +++ b/ddev/changelog.d/22838.fixed @@ -0,0 +1 @@ +Fixed `ddev size` ignore filtering to correctly interpret gitignore entries, ensuring excluded files are omitted from size calculations. \ No newline at end of file diff --git a/ddev/src/ddev/cli/size/utils/common_funcs.py b/ddev/src/ddev/cli/size/utils/common_funcs.py index 7a99807defa07..d607b3d02d85a 100644 --- a/ddev/src/ddev/cli/size/utils/common_funcs.py +++ b/ddev/src/ddev/cli/size/utils/common_funcs.py @@ -3,6 +3,7 @@ # Licensed under a 3-clause BSD style license (see LICENSE) from __future__ import annotations +import fnmatch import json import os import re @@ -160,7 +161,7 @@ def is_valid_integration_file( included_folder = "datadog_checks" + os.sep if git_ignore is None: - git_ignore = get_gitignore_files(repo_path) + git_ignore = get_gitignore_files(Path(repo_path)) # It is not an integration if path.startswith("."): return False @@ -171,20 +172,34 @@ def is_valid_integration_file( elif any(ignore in path for ignore in ignored_files): return False # This file is contained in .gitignore - elif any(ignore in path for ignore in git_ignore): + elif _matches_gitignore(path, git_ignore): return False else: return True -def get_gitignore_files(repo_path: str | Path) -> list[str]: - gitignore_path = os.path.join(repo_path, ".gitignore") - with open(gitignore_path, "r", encoding="utf-8") as file: - gitignore_content = file.read() - ignored_patterns = [ - line.strip() for line in gitignore_content.splitlines() if line.strip() and not line.startswith("#") - ] - return ignored_patterns +def _matches_gitignore(path: str, patterns: list[str]) -> bool: + parts = path.replace(os.sep, "/").split("/") + for pattern in patterns: + norm = pattern.rstrip("/") + if fnmatch.fnmatch(path, norm): + return True + if fnmatch.fnmatch(os.path.basename(path), norm): + return True + if any(fnmatch.fnmatch(part, norm) for part in parts): + return True + return False + + +def get_gitignore_files(repo_path: Path) -> list[str]: + gitignore_path = repo_path / ".gitignore" + if not gitignore_path.is_file(): + return [] + + with gitignore_path.open(mode="r", encoding="utf-8") as f: + lines = [line.strip() for line in f.read().splitlines() if line.strip() and not line.startswith("#")] + + return lines def convert_to_human_readable_size(size_bytes: float) -> str: diff --git a/ddev/tests/size/test_common.py b/ddev/tests/size/test_common.py index a392f5d222549..5480e84b968ca 100644 --- a/ddev/tests/size/test_common.py +++ b/ddev/tests/size/test_common.py @@ -7,6 +7,7 @@ import pytest from ddev.cli.size.utils.common_funcs import ( + _matches_gitignore, check_python_version, compress, convert_to_human_readable_size, @@ -115,14 +116,34 @@ def test_convert_to_human_readable_size(size_bytes, expected_string): pytest.param("__pycache__/file.py", False, id="pycache"), pytest.param("datadog_checks_dev/example.py", False, id="checks_dev"), pytest.param(".git/config", False, id="git"), + pytest.param("datadog_checks/module/cache.pyc", False, id="gitignore_glob_ext"), + pytest.param("datadog_checks/module/__pycache__/foo.py", False, id="gitignore_glob_dir"), ], ) def test_is_valid_integration_file(file_path, expected): repo_path = "fake_repo" - with patch("ddev.cli.size.utils.common_funcs.get_gitignore_files", return_value=set()): + gitignore_patterns = ["*.pyc", "__pycache__"] + with patch("ddev.cli.size.utils.common_funcs.get_gitignore_files", return_value=gitignore_patterns): assert is_valid_integration_file(to_native_path(file_path), repo_path) is expected +@pytest.mark.parametrize( + "path, patterns, expected", + [ + pytest.param("foo/bar/baz.pyc", ["*.pyc"], True, id="glob_extension_match"), + pytest.param("foo/bar/baz.py", ["*.pyc"], False, id="glob_extension_no_match"), + pytest.param("foo/__pycache__/module.py", ["__pycache__"], True, id="dir_segment_match"), + pytest.param("foo/bar/module.py", ["__pycache__"], False, id="dir_segment_no_match"), + pytest.param("foo/bar/notes.log", ["*.log"], True, id="glob_log_match"), + pytest.param("foo/bar/notes.txt", ["*.log"], False, id="glob_log_no_match"), + pytest.param("foo/bar/baz.py", ["*.pyc", "__pycache__", "*.log"], False, id="no_pattern_matches"), + pytest.param("foo/__pycache__/baz.pyc", ["*.pyc", "__pycache__"], True, id="multiple_patterns_first_matches"), + ], +) +def test_matches_gitignore(path, patterns, expected): + assert _matches_gitignore(to_native_path(path), patterns) is expected + + def test_get_dependencies_list(): file_content = "dependency1 @ https://example.com/dependency1/dependency1-1.1.1-.whl\ndependency2 @ https://example.com/dependency2/dependency2-1.1.1-.whl" mock_open_obj = mock_open(read_data=file_content) @@ -267,13 +288,10 @@ def test_check_version(py_version, expected): assert check_python_version("fake_repo", "integration1", py_version) is expected -def test_get_gitignore_files(): - mock_gitignore = f"__pycache__{os.sep}\n*.log\n" # Sample .gitignore file - repo_path = "fake_repo" - with patch("builtins.open", mock_open(read_data=mock_gitignore)): - with patch("ddev.cli.size.utils.common_funcs.os.path.exists", return_value=True): - ignored_patterns = get_gitignore_files(repo_path) - assert ignored_patterns == ["__pycache__" + os.sep, "*.log"] +def test_get_gitignore_files(tmp_path): + gitignore = tmp_path / ".gitignore" + gitignore.write_text(f"__pycache__{os.sep}\n*.log\n") + assert get_gitignore_files(tmp_path) == ["__pycache__" + os.sep, "*.log"] def test_compress():