Skip to content
Open
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
20 changes: 20 additions & 0 deletions datadog_sync/model/monitors.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,23 @@ def connect_id(self, key: str, r_obj: Dict, resource_to_connect: str) -> Optiona
else:
# Use default connect_id method in base class when not handling special case for `query`
return super(Monitors, self).connect_id(key, r_obj, resource_to_connect)

def extract_source_ids(self, key: str, r_obj: Dict, resource_to_connect: str) -> Optional[List[str]]:
# Mirror of connect_id -- keep in sync when connect_id changes.
if key == "query" and r_obj.get("type") == "composite" and resource_to_connect != "service_level_objectives":
return re.findall("[0-9]+", r_obj[key])
elif key == "query" and resource_to_connect == "service_level_objectives" and r_obj.get("type") == "slo alert":
if res := re.search(r'(?:error_budget|burn_rate)\("(.*?)"\)\.', r_obj[key]):
return [res.group(1)]
return []
elif key == "query":
return []
elif key == "principals":
type_map = {"user": "users", "role": "roles", "team": "teams"}
return [
_id
for p in r_obj[key]
for _type, _id in [p.split(":", 1)]
if type_map.get(_type) == resource_to_connect
]
return super(Monitors, self).extract_source_ids(key, r_obj, resource_to_connect)
16 changes: 16 additions & 0 deletions datadog_sync/model/restriction_policies.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,19 @@ def connect_id(self, key: str, r_obj: Dict, resource_to_connect: str) -> Optiona
failed_connections.append(_id)

return failed_connections

def extract_source_ids(self, key: str, r_obj: Dict, resource_to_connect: str) -> Optional[List[str]]:
# Mirror of connect_id -- keep in sync when connect_id changes.
if key == "id":
_type, _id = r_obj[key].split(":", 1)
type_map = {"dashboard": "dashboards", "slo": "service_level_objectives", "notebook": "notebooks"}
return [_id] if type_map.get(_type) == resource_to_connect else []
elif key == "principals":
type_map = {"user": "users", "role": "roles", "team": "teams"}
return [
_id
for p in r_obj[key]
for _type, _id in [p.split(":", 1)]
if type_map.get(_type) == resource_to_connect
]
return super().extract_source_ids(key, r_obj, resource_to_connect)
14 changes: 14 additions & 0 deletions datadog_sync/model/service_level_objectives.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,17 @@ def connect_id(self, key: str, r_obj: Dict, resource_to_connect: str) -> Optiona
if not found:
failed_connections.append(_id)
return failed_connections

def extract_source_ids(self, key: str, r_obj: Dict, resource_to_connect: str) -> Optional[List[str]]:
# Mirror of connect_id -- keep in sync when connect_id changes.
# connect_id checks each monitor_id against monitors destination state first,
# then falls back to synthetics_tests using suffix match on composite keys
# ('{public_id}#{monitor_id}'). For source discovery, exclude IDs that are
# synthetics monitor IDs to prevent false ("monitors", id) misses.
if key != "monitor_ids":
return super().extract_source_ids(key, r_obj, resource_to_connect)
ids = [str(obj) for obj in r_obj[key]]
if resource_to_connect == "monitors":
synthetics = self.config.state.source["synthetics_tests"]
return [_id for _id in ids if not any(k.endswith("#" + _id) for k in synthetics)]
return super().extract_source_ids(key, r_obj, resource_to_connect)
50 changes: 43 additions & 7 deletions datadog_sync/model/synthetics_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class SyntheticsTests(BaseResource):
"rum_applications": ["options.rumSettings.applicationId"],
"synthetics_mobile_applications": [
"options.mobileApplication.applicationId",
"options.mobileApplication.referenceId",
],
"synthetics_mobile_applications_versions": [
"mobileApplicationsVersions",
Expand Down Expand Up @@ -90,7 +91,10 @@ class SyntheticsTests(BaseResource):
network_base_path: str = "/api/v2/synthetics/tests/network"
network_delete_path: str = "/api/v2/synthetics/tests/bulk-delete"
get_params = {"include_metadata": "true"}
versions: List = []

def __init__(self, config):
super().__init__(config)
self.versions: Optional[List[Dict]] = None

@staticmethod
def _unwrap_network_response(resp: Dict) -> Dict:
Expand Down Expand Up @@ -144,13 +148,18 @@ async def _delete_test(self, client: CustomClient, test_type: str, public_id: st
body = {"public_ids": [public_id]}
await client.post(self.resource_config.base_path + "/delete", body)

async def _ensure_mobile_versions_loaded(self, client: CustomClient) -> List[Dict]:
if self.versions is None:
versions_resource = SyntheticsMobileApplicationsVersions(self.config)
self.versions = await versions_resource.get_resources(client)
return self.versions

async def get_resources(self, client: CustomClient) -> List[Dict]:
resp = await client.get(
self.resource_config.base_path,
params=self.get_params,
)
versions = SyntheticsMobileApplicationsVersions(self.config)
self.versions = await versions.get_resources(client)
await self._ensure_mobile_versions_loaded(client)
return resp["tests"]

async def import_resource(self, _id: Optional[str] = None, resource: Optional[Dict] = None) -> Tuple[str, Dict]:
Expand Down Expand Up @@ -197,9 +206,10 @@ async def import_resource(self, _id: Optional[str] = None, resource: Optional[Di
self.mobile_test_path.format(_id),
params=self.get_params,
)
mobile_versions = await self._ensure_mobile_versions_loaded(source_client)
versions = [
i["id"]
for i in self.versions
for i in mobile_versions
if i["application_id"] == resource["options"]["mobileApplication"]["applicationId"]
]
resource["mobileApplicationsVersions"] = list(set(versions))
Expand Down Expand Up @@ -407,11 +417,37 @@ def connect_id(self, key: str, r_obj: Dict, resource_to_connect: str) -> Optiona
else:
failed_connections.append(_id)
return failed_connections
elif resource_to_connect == "synthetics_mobile_applications" and key == "referenceId":
# referenceId is an application ID only when referenceType is "latest".
if r_obj.get("referenceType") == "latest":
return super(SyntheticsTests, self).connect_id(key, r_obj, resource_to_connect)
return []
elif resource_to_connect == "synthetics_mobile_applications_versions" and key == "referenceId":
# When referenceType is "latest", referenceId contains the application ID, not a version ID.
# Connect it against synthetics_mobile_applications instead.
# referenceId is a version ID only when referenceType is not "latest".
if r_obj.get("referenceType") == "latest":
return super(SyntheticsTests, self).connect_id(key, r_obj, "synthetics_mobile_applications")
return []
return super(SyntheticsTests, self).connect_id(key, r_obj, resource_to_connect)
else:
return super(SyntheticsTests, self).connect_id(key, r_obj, resource_to_connect)

def extract_source_ids(self, key: str, r_obj: Dict, resource_to_connect: str) -> Optional[List[str]]:
# Mirror of connect_id -- keep in sync when connect_id changes.
# Only synthetics_private_locations and mobile application versions need special handling.
# rum_applications, synthetics_tests (subtests), synthetics_global_variables, roles, and
# synthetics_mobile_applications (applicationId key) all use plain IDs at the leaf —
# base extract_source_ids handles them. For synthetics_tests subtests, _dep_in_source_state
# handles composite key prefix matching ('{public_id}#{monitor_id}' keys).
if resource_to_connect == "synthetics_private_locations":
pl = self.config.resources["synthetics_private_locations"]
return [str(_id) for _id in r_obj[key] if pl.pl_id_regex.match(str(_id))]
elif resource_to_connect == "synthetics_mobile_applications" and key == "referenceId":
# referenceId is an application ID only when referenceType is "latest".
if r_obj.get("referenceType") == "latest":
return super(SyntheticsTests, self).extract_source_ids(key, r_obj, resource_to_connect)
return []
elif resource_to_connect == "synthetics_mobile_applications_versions" and key == "referenceId":
# referenceId is a version ID only when referenceType is not "latest".
if r_obj.get("referenceType") == "latest":
return []
return super(SyntheticsTests, self).extract_source_ids(key, r_obj, resource_to_connect)
return super(SyntheticsTests, self).extract_source_ids(key, r_obj, resource_to_connect)
8 changes: 8 additions & 0 deletions datadog_sync/model/team_memberships.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,11 @@ def connect_id(self, key: str, r_obj: Dict, resource_to_connect: str) -> Optiona
else:
failed_connections.append(_id)
return failed_connections

def extract_source_ids(self, key: str, r_obj: Dict, resource_to_connect: str) -> Optional[List[str]]:
# Mirror of connect_id -- keep in sync when connect_id changes.
_type = r_obj["type"]
type_map = {"users": "users", "team": "teams"}
if type_map.get(_type) == resource_to_connect:
return [r_obj["id"]]
return []
14 changes: 14 additions & 0 deletions datadog_sync/utils/base_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,20 @@ def connect_id(self, key: str, r_obj: Dict, resource_to_connect: str) -> Optiona

return failed_connections

def extract_source_ids(self, key: str, r_obj: Dict, resource_to_connect: str) -> Optional[List[str]]:
"""Extract dependency IDs referenced at r_obj[key] for resource_to_connect.

Source-only: does NOT check destination state, does NOT mutate r_obj.
Override in subclasses with custom connect_id logic (regex, prefix
parsing, type dispatch, etc.).
Mirror of connect_id -- keep in sync when connect_id changes.
"""
if not r_obj.get(key):
return None
if isinstance(r_obj[key], list):
return [str(v) for v in r_obj[key]]
return [str(r_obj[key])]

def connect_resources(self, _id: str, resource: Dict) -> None:
if not self.resource_config.resource_connections:
return
Expand Down
Loading
Loading