Skip to content

Commit b63ef75

Browse files
authored
Add more fields for metadata collection (DataDog#24091)
* cherry pick * changelog * lint
1 parent 333acf7 commit b63ef75

4 files changed

Lines changed: 157 additions & 8 deletions

File tree

argocd/changelog.d/24091.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add more fields for metadata collection

argocd/datadog_checks/argocd/resources.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,17 +92,32 @@ def _scrub_url_credentials(text: str) -> str:
9292
return URL_CREDENTIALS_PATTERN.sub(r"\1", text)
9393

9494

95+
def _scrub_repo_urls(container: dict) -> None:
96+
"""Strip credentials from a source's repoURL and from any multi-source repoURLs, in place."""
97+
source = container.get("source")
98+
if isinstance(source, dict) and isinstance(source.get("repoURL"), str):
99+
source["repoURL"] = _strip_url_userinfo(source["repoURL"])
100+
for src in container.get("sources") or []:
101+
if isinstance(src, dict) and isinstance(src.get("repoURL"), str):
102+
src["repoURL"] = _strip_url_userinfo(src["repoURL"])
103+
104+
95105
def _sanitize_item(item: dict, resource_type: str) -> None:
96106
"""Strip embedded credentials from repo URLs and free-text messages in place before they ship."""
97107
if resource_type == "argocd_application":
98-
spec = item.get("spec") or {}
99-
source = spec.get("source")
100-
if isinstance(source, dict) and isinstance(source.get("repoURL"), str):
101-
source["repoURL"] = _strip_url_userinfo(source["repoURL"])
102-
for src in spec.get("sources") or []:
103-
if isinstance(src, dict) and isinstance(src.get("repoURL"), str):
104-
src["repoURL"] = _strip_url_userinfo(src["repoURL"])
105-
for condition in (item.get("status") or {}).get("conditions") or []:
108+
status = item.get("status") or {}
109+
_scrub_repo_urls(item.get("spec") or {})
110+
for entry in status.get("history") or []:
111+
if isinstance(entry, dict):
112+
_scrub_repo_urls(entry)
113+
operation_state = status.get("operationState")
114+
if isinstance(operation_state, dict) and isinstance(operation_state.get("message"), str):
115+
operation_state["message"] = _scrub_url_credentials(operation_state["message"])
116+
summary = status.get("summary") or {}
117+
external_urls = summary.get("externalURLs")
118+
if isinstance(external_urls, list):
119+
summary["externalURLs"] = [_strip_url_userinfo(u) if isinstance(u, str) else u for u in external_urls]
120+
for condition in status.get("conditions") or []:
106121
if isinstance(condition, dict) and isinstance(condition.get("message"), str):
107122
condition["message"] = _scrub_url_credentials(condition["message"])
108123
elif resource_type == "argocd_repository":

argocd/datadog_checks/argocd/resources_constants.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
"paths": (
2020
"metadata.name",
2121
"metadata.namespace",
22+
"metadata.uid",
23+
"metadata.creationTimestamp",
2224
"spec.project",
2325
"spec.source.repoURL",
2426
"spec.source.path",
@@ -37,23 +39,36 @@
3739
"status.sync.revision",
3840
"status.health.status",
3941
"status.health.message",
42+
"status.health.lastTransitionTime",
4043
"status.conditions[*].type",
4144
"status.conditions[*].message",
4245
"status.conditions[*].lastTransitionTime",
4346
"status.operationState.phase",
4447
"status.operationState.startedAt",
4548
"status.operationState.finishedAt",
49+
"status.operationState.message",
50+
"status.operationState.retryCount",
4651
"status.operationState.operation.initiatedBy.username",
4752
"status.operationState.operation.initiatedBy.automated",
4853
"status.sourceType",
4954
"status.reconciledAt",
5055
"status.summary.images[*]",
56+
"status.summary.externalURLs[*]",
5157
"status.history[*].id",
5258
"status.history[*].revision",
5359
"status.history[*].deployedAt",
5460
"status.history[*].deployStartedAt",
5561
"status.history[*].initiatedBy.username",
5662
"status.history[*].initiatedBy.automated",
63+
"status.history[*].source.repoURL",
64+
"status.history[*].source.path",
65+
"status.history[*].source.targetRevision",
66+
"status.history[*].source.chart",
67+
"status.history[*].sources[*].repoURL",
68+
"status.history[*].sources[*].path",
69+
"status.history[*].sources[*].targetRevision",
70+
"status.history[*].sources[*].chart",
71+
"status.history[*].revisions[*]",
5772
"status.resources[*].kind",
5873
"status.resources[*].name",
5974
"status.resources[*].namespace",
@@ -75,9 +90,12 @@
7590
"namespaces[*]",
7691
"connectionState.status",
7792
"connectionState.message",
93+
"connectionState.attemptedAt",
7894
"info.applicationsCount",
7995
"info.serverVersion",
8096
"info.cacheInfo.resourcesCount",
97+
"info.connectionState.status",
98+
"shard",
8199
),
82100
"map_paths": ("labels",),
83101
"annotation_keys": (),
@@ -91,6 +109,11 @@
91109
"project",
92110
"connectionState.status",
93111
"connectionState.message",
112+
"connectionState.attemptedAt",
113+
"insecure",
114+
"enableLfs",
115+
"enableOCI",
116+
"forceHttpBasicAuth",
94117
),
95118
"map_paths": (),
96119
"annotation_keys": (),

argocd/tests/test_resources.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
APPLICATION_INCLUDE,
1515
CLUSTER_INCLUDE,
1616
GENRESOURCES_API_UP_METRIC,
17+
REPOSITORY_INCLUDE,
1718
)
1819
from datadog_checks.dev.http import MockResponse
1920

@@ -124,6 +125,115 @@ def test_application_include_contains_kubernetes_resource_identity_for_automatic
124125
} <= set(APPLICATION_INCLUDE["paths"])
125126

126127

128+
def test_application_include_adds_pilot_metadata_and_state_fields():
129+
assert {
130+
"metadata.uid",
131+
"metadata.creationTimestamp",
132+
"status.health.lastTransitionTime",
133+
"status.operationState.retryCount",
134+
} <= set(APPLICATION_INCLUDE["paths"])
135+
136+
137+
def test_application_include_contains_deployment_history_and_operation_fields():
138+
assert {
139+
"status.operationState.message",
140+
"status.summary.externalURLs[*]",
141+
"status.history[*].source.repoURL",
142+
"status.history[*].source.path",
143+
"status.history[*].sources[*].repoURL",
144+
"status.history[*].revisions[*]",
145+
} <= set(APPLICATION_INCLUDE["paths"])
146+
147+
148+
def test_collect_scrubs_credentials_from_operation_state_message(mock_http_response_per_endpoint):
149+
app = _application("broken")
150+
app["status"]["operationState"] = {
151+
"phase": "Failed",
152+
"message": "sync failed: https://oauth2:t0ken@github.com/org/repo: auth required",
153+
}
154+
mock_http_response_per_endpoint(
155+
{
156+
APPLICATIONS_URL: [_items_response([app])],
157+
CLUSTERS_URL: [_items_response([])],
158+
REPOSITORIES_URL: [_items_response([])],
159+
}
160+
)
161+
check = _check()
162+
163+
with patch.object(check, "submit_generic_resource") as submit:
164+
check._resource_collector.collect()
165+
166+
app_call = next(c for c in submit.call_args_list if c.kwargs["type"] == "argocd_application")
167+
message = app_call.kwargs["fields"]["status"]["operationState"]["message"]
168+
assert "t0ken" not in message
169+
assert "https://github.com/org/repo" in message
170+
171+
172+
def test_collect_strips_credentials_from_external_urls(mock_http_response_per_endpoint):
173+
app = _application("web")
174+
app["status"]["summary"] = {"externalURLs": ["https://user:t0ken@app.example.com"]}
175+
mock_http_response_per_endpoint(
176+
{
177+
APPLICATIONS_URL: [_items_response([app])],
178+
CLUSTERS_URL: [_items_response([])],
179+
REPOSITORIES_URL: [_items_response([])],
180+
}
181+
)
182+
check = _check()
183+
184+
with patch.object(check, "submit_generic_resource") as submit:
185+
check._resource_collector.collect()
186+
187+
app_call = next(c for c in submit.call_args_list if c.kwargs["type"] == "argocd_application")
188+
assert app_call.kwargs["fields"]["status"]["summary"]["externalURLs"] == ["https://app.example.com"]
189+
190+
191+
def test_real_helper_ships_multisource_history_and_scrubs_its_repo_urls(aggregator, mock_http_response_per_endpoint):
192+
# Runs the real helper: proves nested [*] (history[*].sources[*]) projects AND history repoURLs are scrubbed.
193+
app = _application("checkout")
194+
app["status"]["history"] = [
195+
{
196+
"id": 1,
197+
"source": {"repoURL": "https://oauth2:t0ken@github.com/org/repo", "path": "guestbook"},
198+
"sources": [{"repoURL": "https://oauth2:t0ken@github.com/org/multi", "path": "base"}],
199+
}
200+
]
201+
mock_http_response_per_endpoint(
202+
{
203+
APPLICATIONS_URL: [_items_response([app])],
204+
CLUSTERS_URL: [_items_response([])],
205+
REPOSITORIES_URL: [_items_response([])],
206+
}
207+
)
208+
check = _check()
209+
210+
check._resource_collector.collect()
211+
212+
payloads = aggregator.get_event_platform_events("genresources", parse_json=False)
213+
blob = b"".join(p if isinstance(p, bytes) else p.encode() for p in payloads)
214+
assert b"guestbook" in blob # single-source history field projected
215+
assert b"base" in blob # multi-source history field projected (nested [*] works)
216+
assert b"t0ken" not in blob # both history repoURLs scrubbed before ship
217+
218+
219+
def test_cluster_include_contains_connection_and_shard_fields():
220+
assert {
221+
"connectionState.attemptedAt",
222+
"info.connectionState.status",
223+
"shard",
224+
} <= set(CLUSTER_INCLUDE["paths"])
225+
226+
227+
def test_repository_include_contains_connection_and_capability_flags():
228+
assert {
229+
"connectionState.attemptedAt",
230+
"insecure",
231+
"enableLfs",
232+
"enableOCI",
233+
"forceHttpBasicAuth",
234+
} <= set(REPOSITORY_INCLUDE["paths"])
235+
236+
127237
def test_application_key_uses_app_identity_not_destination(mock_http_response_per_endpoint):
128238
apps = [
129239
_application("web", namespace="team-a", cluster="https://remote", dest_namespace="prod"),

0 commit comments

Comments
 (0)