Skip to content

Commit 8d9bcaa

Browse files
acrylJonnycursoragentcubic-dev-ai[bot]
authored
feat(ingest/airbyte): opt in to inactive connection ingestion (#19062)
Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
1 parent 7253263 commit 8d9bcaa

7 files changed

Lines changed: 133 additions & 11 deletions

File tree

metadata-ingestion/docs/sources/airbyte/airbyte_post.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ Module behavior is constrained by source APIs, permissions, and metadata exposed
2323

2424
If ingestion fails, validate credentials, permissions, connectivity, and scope filters first. Then review ingestion logs for source-specific errors and adjust configuration accordingly.
2525

26+
#### Missing Connections
27+
28+
By default only enabled Airbyte connections are ingested. Disabled connections are
29+
skipped with no warning. If a connection is missing from DataHub, check whether it
30+
is disabled in Airbyte (or whether its Public API `status` is `"inactive"`). To
31+
ingest disabled connections as well, set `include_inactive_connections: true`.
32+
2633
#### Authentication Errors
2734

2835
Verify that your OAuth2 client credentials are correct and have not expired. For OSS deployments, confirm the API is reachable at the `/api/public/v1` path prefix.

metadata-ingestion/docs/sources/airbyte/airbyte_recipe.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,4 @@ source:
6464
sink:
6565
type: datahub-rest
6666
config:
67-
server: http://localhost:8080
67+
server: http://localhost:8080

metadata-ingestion/src/datahub/ingestion/source/airbyte/client.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,10 @@ def list_workspaces(
267267
return [AirbyteWorkspacePartial.model_validate(w) for w in workspaces_data]
268268

269269
def list_connections(
270-
self, workspace_id: str, pattern: Optional[AllowDenyPattern] = None
270+
self,
271+
workspace_id: str,
272+
pattern: Optional[AllowDenyPattern] = None,
273+
include_inactive: bool = False,
271274
) -> List[AirbyteConnectionPartial]:
272275
self._check_auth_before_request()
273276
params = {API_QUERY_WORKSPACE_ID: workspace_id}
@@ -279,16 +282,17 @@ def list_connections(
279282
)
280283
)
281284

282-
active_connections = [
283-
conn
284-
for conn in connections
285-
if conn.get(API_FIELD_STATUS) != API_STATUS_INACTIVE
286-
]
285+
if not include_inactive:
286+
connections = [
287+
conn
288+
for conn in connections
289+
if conn.get(API_FIELD_STATUS) != API_STATUS_INACTIVE
290+
]
287291

288292
if pattern:
289-
active_connections = apply_pattern(active_connections, pattern)
293+
connections = apply_pattern(connections, pattern)
290294

291-
return [AirbyteConnectionPartial.model_validate(c) for c in active_connections]
295+
return [AirbyteConnectionPartial.model_validate(c) for c in connections]
292296

293297
def list_jobs(
294298
self,

metadata-ingestion/src/datahub/ingestion/source/airbyte/config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,15 @@ class AirbyteSourceConfig(
263263
"Use this to override platform details for specific destinations.",
264264
)
265265

266+
include_inactive_connections: bool = Field(
267+
default=False,
268+
description=(
269+
"Also ingest connections that are disabled in Airbyte. "
270+
"By default, connections reported as inactive are skipped with no "
271+
"warning, even if they still appear healthy in the Airbyte UI."
272+
),
273+
)
274+
266275
include_statuses: bool = Field(
267276
default=True,
268277
description="Whether to ingest run statuses",

metadata-ingestion/src/datahub/ingestion/source/airbyte/source.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ def _get_pipelines(self) -> Iterable[AirbytePipelineInfo]:
281281
for connection in self.client.list_connections(
282282
workspace.workspace_id,
283283
pattern=self.source_config.connection_pattern,
284+
include_inactive=self.source_config.include_inactive_connections,
284285
):
285286
try:
286287
if (

metadata-ingestion/tests/unit/airbyte/test_airbyte_client.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,36 @@ def test_list_connections(self, mock_paginate_results):
185185
result_key="data",
186186
)
187187

188+
@patch("datahub.ingestion.source.airbyte.client.AirbyteOSSClient._paginate_results")
189+
def test_list_connections_skips_inactive_by_default(self, mock_paginate_results):
190+
mock_paginate_results.return_value = [
191+
{
192+
"connectionId": "active-id",
193+
"name": "Active Connection",
194+
"sourceId": "source-id-1",
195+
"destinationId": "destination-id-1",
196+
"status": "active",
197+
},
198+
{
199+
"connectionId": "inactive-id",
200+
"name": "Inactive Connection",
201+
"sourceId": "source-id-2",
202+
"destinationId": "destination-id-2",
203+
"status": "inactive",
204+
},
205+
]
206+
config = AirbyteClientConfig(
207+
deployment_type=AirbyteDeploymentType.OPEN_SOURCE,
208+
host_port="http://localhost:8000",
209+
)
210+
client = AirbyteOSSClient(config)
211+
212+
connections = client.list_connections("workspace-id-1")
213+
assert [c.connection_id for c in connections] == ["active-id"]
214+
215+
connections = client.list_connections("workspace-id-1", include_inactive=True)
216+
assert [c.connection_id for c in connections] == ["active-id", "inactive-id"]
217+
188218
@patch("datahub.ingestion.source.airbyte.client.AirbyteOSSClient._make_request")
189219
def test_http_error_handling(self, mock_make_request):
190220
mock_make_request.side_effect = requests.exceptions.HTTPError(
@@ -226,7 +256,9 @@ def list_sources(self, workspace_id, pattern=None):
226256
def list_destinations(self, workspace_id, pattern=None):
227257
return []
228258

229-
def list_connections(self, workspace_id, pattern=None):
259+
def list_connections(
260+
self, workspace_id, pattern=None, include_inactive=False
261+
):
230262
return []
231263

232264
class IncompleteClient(AirbyteBaseClient):

metadata-ingestion/tests/unit/airbyte/test_source_workspaces.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ def test_get_pipelines(mock_create_client, mock_ctx, mock_client):
122122

123123
mock_client.list_workspaces.assert_called_once()
124124
mock_client.list_connections.assert_called_once_with(
125-
"workspace-1", pattern=AllowDenyPattern.allow_all()
125+
"workspace-1",
126+
pattern=AllowDenyPattern.allow_all(),
127+
include_inactive=False,
126128
)
127129
mock_client.get_connection.assert_called_once_with("connection-1")
128130
mock_client.get_source.assert_called_once_with("source-1")
@@ -191,6 +193,73 @@ def test_get_pipelines_with_filters(mock_create_client, mock_ctx, mock_client):
191193
assert len(pipelines) == 0
192194

193195

196+
@patch("datahub.ingestion.source.airbyte.source.create_airbyte_client")
197+
def test_get_pipelines_include_inactive_connections(
198+
mock_create_client, mock_ctx, mock_client
199+
):
200+
mock_create_client.return_value = mock_client
201+
config = AirbyteSourceConfig(
202+
deployment_type=AirbyteDeploymentType.OPEN_SOURCE,
203+
host_port="http://localhost:8000",
204+
platform_instance="test-instance",
205+
include_inactive_connections=True,
206+
)
207+
source = AirbyteSource(config, mock_ctx)
208+
209+
workspace = AirbyteWorkspacePartial(
210+
workspace_id="workspace-1",
211+
name="Test Workspace",
212+
)
213+
connection = AirbyteConnectionPartial(
214+
connection_id="inactive-connection-1",
215+
name="Disabled Connection",
216+
source_id="source-1",
217+
destination_id="destination-1",
218+
status="inactive",
219+
)
220+
source_model = AirbyteSourcePartial(
221+
source_id="source-1",
222+
name="Test Source",
223+
source_type="postgres",
224+
source_definition_id="source-def-1",
225+
workspace_id="workspace-1",
226+
configuration={"host": "localhost", "port": 5432},
227+
)
228+
destination = AirbyteDestinationPartial(
229+
destination_id="destination-1",
230+
name="Test Destination",
231+
destination_type="postgres",
232+
destination_definition_id="dest-def-1",
233+
workspace_id="workspace-1",
234+
configuration={"host": "localhost", "port": 5432},
235+
)
236+
237+
mock_client.list_workspaces.return_value = [workspace]
238+
mock_client.list_connections.return_value = [connection]
239+
mock_client.get_connection.return_value = connection
240+
mock_client.get_source.return_value = source_model
241+
mock_client.get_destination.return_value = destination
242+
243+
pipelines = list(source._get_pipelines())
244+
245+
assert len(pipelines) == 1
246+
assert isinstance(pipelines[0], AirbytePipelineInfo)
247+
assert pipelines[0].workspace.workspace_id == "workspace-1"
248+
assert pipelines[0].connection.connection_id == "inactive-connection-1"
249+
assert pipelines[0].connection.status == "inactive"
250+
assert pipelines[0].source.source_id == "source-1"
251+
assert pipelines[0].destination.destination_id == "destination-1"
252+
253+
mock_client.list_connections.assert_called_once_with(
254+
"workspace-1",
255+
pattern=AllowDenyPattern.allow_all(),
256+
include_inactive=True,
257+
)
258+
mock_client.get_connection.assert_called_once_with("inactive-connection-1")
259+
mock_client.get_source.assert_called_once_with("source-1")
260+
mock_client.get_destination.assert_called_once_with("destination-1")
261+
262+
194263
@patch("datahub.ingestion.source.airbyte.source.create_airbyte_client")
195264
@patch(
196265
"datahub.ingestion.source.airbyte.source.AirbyteSource._create_lineage_workunits"

0 commit comments

Comments
 (0)