airbyte-internal-ops 0.1.3__py3-none-any.whl → 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {airbyte_internal_ops-0.1.3.dist-info → airbyte_internal_ops-0.1.5.dist-info}/METADATA +8 -5
- {airbyte_internal_ops-0.1.3.dist-info → airbyte_internal_ops-0.1.5.dist-info}/RECORD +35 -15
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_pipelines/airbyte_ci/connectors/test/steps/common.py +1 -1
- airbyte_ops_mcp/airbyte_repo/list_connectors.py +44 -4
- airbyte_ops_mcp/airbyte_repo/utils.py +5 -3
- airbyte_ops_mcp/cli/cloud.py +317 -47
- airbyte_ops_mcp/cli/repo.py +15 -0
- airbyte_ops_mcp/cloud_admin/connection_config.py +131 -0
- airbyte_ops_mcp/live_tests/__init__.py +16 -0
- airbyte_ops_mcp/live_tests/_connection_retriever/__init__.py +35 -0
- airbyte_ops_mcp/live_tests/_connection_retriever/audit_logging.py +88 -0
- airbyte_ops_mcp/live_tests/_connection_retriever/consts.py +33 -0
- airbyte_ops_mcp/live_tests/_connection_retriever/db_access.py +82 -0
- airbyte_ops_mcp/live_tests/_connection_retriever/retrieval.py +391 -0
- airbyte_ops_mcp/live_tests/_connection_retriever/secrets_resolution.py +130 -0
- airbyte_ops_mcp/live_tests/config.py +190 -0
- airbyte_ops_mcp/live_tests/connection_fetcher.py +159 -2
- airbyte_ops_mcp/live_tests/connection_secret_retriever.py +173 -0
- airbyte_ops_mcp/live_tests/evaluation_modes.py +45 -0
- airbyte_ops_mcp/live_tests/http_metrics.py +81 -0
- airbyte_ops_mcp/live_tests/message_cache/__init__.py +15 -0
- airbyte_ops_mcp/live_tests/message_cache/duckdb_cache.py +415 -0
- airbyte_ops_mcp/live_tests/obfuscation.py +126 -0
- airbyte_ops_mcp/live_tests/regression/__init__.py +29 -0
- airbyte_ops_mcp/live_tests/regression/comparators.py +466 -0
- airbyte_ops_mcp/live_tests/schema_generation.py +154 -0
- airbyte_ops_mcp/live_tests/validation/__init__.py +43 -0
- airbyte_ops_mcp/live_tests/validation/catalog_validators.py +389 -0
- airbyte_ops_mcp/live_tests/validation/record_validators.py +227 -0
- airbyte_ops_mcp/mcp/_mcp_utils.py +3 -0
- airbyte_ops_mcp/mcp/live_tests.py +515 -0
- airbyte_ops_mcp/mcp/server.py +3 -0
- airbyte_ops_mcp/mcp/server_info.py +2 -2
- {airbyte_internal_ops-0.1.3.dist-info → airbyte_internal_ops-0.1.5.dist-info}/WHEEL +0 -0
- {airbyte_internal_ops-0.1.3.dist-info → airbyte_internal_ops-0.1.5.dist-info}/entry_points.txt +0 -0
airbyte_ops_mcp/cli/cloud.py
CHANGED
|
@@ -7,6 +7,7 @@ Commands:
|
|
|
7
7
|
airbyte-ops cloud connector clear-version-override - Clear connector version override
|
|
8
8
|
airbyte-ops cloud connector live-test - Run live validation tests on a connector
|
|
9
9
|
airbyte-ops cloud connector regression-test - Run regression tests comparing connector versions
|
|
10
|
+
airbyte-ops cloud connector fetch-connection-config - Fetch connection config to local file
|
|
10
11
|
"""
|
|
11
12
|
|
|
12
13
|
from __future__ import annotations
|
|
@@ -15,11 +16,20 @@ import json
|
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
from typing import Annotated, Literal
|
|
17
18
|
|
|
19
|
+
from airbyte_cdk.models.connector_metadata import MetadataFile
|
|
20
|
+
from airbyte_cdk.utils.connector_paths import find_connector_root_from_name
|
|
21
|
+
from airbyte_cdk.utils.docker import build_connector_image, verify_docker_installation
|
|
18
22
|
from airbyte_protocol.models import ConfiguredAirbyteCatalog
|
|
19
23
|
from cyclopts import App, Parameter
|
|
20
24
|
|
|
21
25
|
from airbyte_ops_mcp.cli._base import app
|
|
22
|
-
from airbyte_ops_mcp.cli._shared import
|
|
26
|
+
from airbyte_ops_mcp.cli._shared import (
|
|
27
|
+
exit_with_error,
|
|
28
|
+
print_error,
|
|
29
|
+
print_json,
|
|
30
|
+
print_success,
|
|
31
|
+
)
|
|
32
|
+
from airbyte_ops_mcp.cloud_admin.connection_config import fetch_connection_config
|
|
23
33
|
from airbyte_ops_mcp.live_tests.ci_output import (
|
|
24
34
|
generate_regression_report,
|
|
25
35
|
get_report_summary,
|
|
@@ -52,6 +62,9 @@ from airbyte_ops_mcp.mcp.cloud_connector_versions import (
|
|
|
52
62
|
set_cloud_connector_version_override,
|
|
53
63
|
)
|
|
54
64
|
|
|
65
|
+
# Path to connectors directory within the airbyte repo
|
|
66
|
+
CONNECTORS_SUBDIR = Path("airbyte-integrations") / "connectors"
|
|
67
|
+
|
|
55
68
|
# Create the cloud sub-app
|
|
56
69
|
cloud_app = App(name="cloud", help="Airbyte Cloud operations.")
|
|
57
70
|
app.command(cloud_app)
|
|
@@ -250,14 +263,83 @@ def _run_connector_command(
|
|
|
250
263
|
}
|
|
251
264
|
|
|
252
265
|
|
|
266
|
+
def _build_connector_image_from_source(
|
|
267
|
+
connector_name: str,
|
|
268
|
+
repo_root: Path | None = None,
|
|
269
|
+
tag: str = "dev",
|
|
270
|
+
) -> str | None:
|
|
271
|
+
"""Build a connector image from source code.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
connector_name: Name of the connector (e.g., 'source-pokeapi').
|
|
275
|
+
repo_root: Optional path to the airbyte repo root. If not provided,
|
|
276
|
+
will attempt to auto-detect from current directory.
|
|
277
|
+
tag: Tag to apply to the built image (default: 'dev').
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
The full image name with tag if successful, None if build fails.
|
|
281
|
+
"""
|
|
282
|
+
if not verify_docker_installation():
|
|
283
|
+
print_error("Docker is not installed or not running")
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
connector_directory = find_connector_root_from_name(connector_name)
|
|
288
|
+
except FileNotFoundError:
|
|
289
|
+
if repo_root:
|
|
290
|
+
connector_directory = repo_root / CONNECTORS_SUBDIR / connector_name
|
|
291
|
+
if not connector_directory.exists():
|
|
292
|
+
print_error(f"Connector directory not found: {connector_directory}")
|
|
293
|
+
return None
|
|
294
|
+
else:
|
|
295
|
+
print_error(
|
|
296
|
+
f"Could not find connector '{connector_name}'. "
|
|
297
|
+
"Try providing --repo-root to specify the airbyte repo location."
|
|
298
|
+
)
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
metadata_file_path = connector_directory / "metadata.yaml"
|
|
302
|
+
if not metadata_file_path.exists():
|
|
303
|
+
print_error(f"metadata.yaml not found at {metadata_file_path}")
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
metadata = MetadataFile.from_file(metadata_file_path)
|
|
307
|
+
print_success(f"Building image for connector: {connector_name}")
|
|
308
|
+
|
|
309
|
+
built_image = build_connector_image(
|
|
310
|
+
connector_name=connector_name,
|
|
311
|
+
connector_directory=connector_directory,
|
|
312
|
+
metadata=metadata,
|
|
313
|
+
tag=tag,
|
|
314
|
+
no_verify=False,
|
|
315
|
+
)
|
|
316
|
+
print_success(f"Successfully built image: {built_image}")
|
|
317
|
+
return built_image
|
|
318
|
+
|
|
319
|
+
|
|
253
320
|
@connector_app.command(name="live-test")
|
|
254
321
|
def live_test(
|
|
255
322
|
connector_image: Annotated[
|
|
256
|
-
str,
|
|
323
|
+
str | None,
|
|
257
324
|
Parameter(
|
|
258
|
-
help="Full connector image name with tag (e.g., airbyte/source-github:1.0.0)."
|
|
325
|
+
help="Full connector image name with tag (e.g., airbyte/source-github:1.0.0). "
|
|
326
|
+
"Optional if connector_name or connection_id is provided."
|
|
259
327
|
),
|
|
260
|
-
],
|
|
328
|
+
] = None,
|
|
329
|
+
connector_name: Annotated[
|
|
330
|
+
str | None,
|
|
331
|
+
Parameter(
|
|
332
|
+
help="Connector name to build from source (e.g., 'source-pokeapi'). "
|
|
333
|
+
"If provided, builds the image locally with tag 'dev'."
|
|
334
|
+
),
|
|
335
|
+
] = None,
|
|
336
|
+
repo_root: Annotated[
|
|
337
|
+
str | None,
|
|
338
|
+
Parameter(
|
|
339
|
+
help="Path to the airbyte repo root. Required if connector_name is provided "
|
|
340
|
+
"and the repo cannot be auto-detected."
|
|
341
|
+
),
|
|
342
|
+
] = None,
|
|
261
343
|
command: Annotated[
|
|
262
344
|
Literal["spec", "check", "discover", "read"],
|
|
263
345
|
Parameter(help="The Airbyte command to run."),
|
|
@@ -266,7 +348,8 @@ def live_test(
|
|
|
266
348
|
str | None,
|
|
267
349
|
Parameter(
|
|
268
350
|
help="Airbyte Cloud connection ID to fetch config/catalog from. "
|
|
269
|
-
"Mutually exclusive with config-path/catalog-path."
|
|
351
|
+
"Mutually exclusive with config-path/catalog-path. "
|
|
352
|
+
"If provided, connector_image can be auto-detected."
|
|
270
353
|
),
|
|
271
354
|
] = None,
|
|
272
355
|
config_path: Annotated[
|
|
@@ -292,6 +375,11 @@ def live_test(
|
|
|
292
375
|
and validates the output. Results are written to the output directory and
|
|
293
376
|
to GitHub Actions outputs if running in CI.
|
|
294
377
|
|
|
378
|
+
You can provide the connector image in three ways:
|
|
379
|
+
1. --connector-image: Use a pre-built image from Docker registry
|
|
380
|
+
2. --connector-name: Build the image locally from source code
|
|
381
|
+
3. --connection-id: Auto-detect from an Airbyte Cloud connection
|
|
382
|
+
|
|
295
383
|
You can provide config/catalog either via file paths OR via a connection_id
|
|
296
384
|
that fetches them from Airbyte Cloud.
|
|
297
385
|
"""
|
|
@@ -300,26 +388,41 @@ def live_test(
|
|
|
300
388
|
|
|
301
389
|
cmd = Command(command)
|
|
302
390
|
|
|
303
|
-
if not ensure_image_available(connector_image):
|
|
304
|
-
print_error(f"Failed to pull connector image: {connector_image}")
|
|
305
|
-
write_github_output("success", False)
|
|
306
|
-
write_github_output("error", f"Failed to pull image: {connector_image}")
|
|
307
|
-
return
|
|
308
|
-
|
|
309
391
|
config_file: Path | None = None
|
|
310
392
|
catalog_file: Path | None = None
|
|
311
393
|
state_file = Path(state_path) if state_path else None
|
|
394
|
+
resolved_connector_image: str | None = connector_image
|
|
395
|
+
|
|
396
|
+
# If connector_name is provided, build the image from source
|
|
397
|
+
if connector_name:
|
|
398
|
+
if connector_image:
|
|
399
|
+
write_github_output("success", False)
|
|
400
|
+
write_github_output(
|
|
401
|
+
"error", "Cannot specify both connector_image and connector_name"
|
|
402
|
+
)
|
|
403
|
+
exit_with_error("Cannot specify both connector_image and connector_name")
|
|
404
|
+
|
|
405
|
+
repo_root_path = Path(repo_root) if repo_root else None
|
|
406
|
+
built_image = _build_connector_image_from_source(
|
|
407
|
+
connector_name=connector_name,
|
|
408
|
+
repo_root=repo_root_path,
|
|
409
|
+
tag="dev",
|
|
410
|
+
)
|
|
411
|
+
if not built_image:
|
|
412
|
+
write_github_output("success", False)
|
|
413
|
+
write_github_output("error", f"Failed to build image for {connector_name}")
|
|
414
|
+
exit_with_error(f"Failed to build image for {connector_name}")
|
|
415
|
+
resolved_connector_image = built_image
|
|
312
416
|
|
|
313
417
|
if connection_id:
|
|
314
418
|
if config_path or catalog_path:
|
|
315
|
-
print_error(
|
|
316
|
-
"Cannot specify both connection_id and config_path/catalog_path"
|
|
317
|
-
)
|
|
318
419
|
write_github_output("success", False)
|
|
319
420
|
write_github_output(
|
|
320
421
|
"error", "Cannot specify both connection_id and file paths"
|
|
321
422
|
)
|
|
322
|
-
|
|
423
|
+
exit_with_error(
|
|
424
|
+
"Cannot specify both connection_id and config_path/catalog_path"
|
|
425
|
+
)
|
|
323
426
|
|
|
324
427
|
print_success(f"Fetching config/catalog from connection: {connection_id}")
|
|
325
428
|
connection_data = fetch_connection_data(connection_id)
|
|
@@ -330,12 +433,35 @@ def live_test(
|
|
|
330
433
|
f"Fetched config for source: {connection_data.source_name} "
|
|
331
434
|
f"with {len(connection_data.stream_names)} streams"
|
|
332
435
|
)
|
|
436
|
+
|
|
437
|
+
if not resolved_connector_image and connection_data.connector_image:
|
|
438
|
+
resolved_connector_image = connection_data.connector_image
|
|
439
|
+
print_success(f"Auto-detected connector image: {resolved_connector_image}")
|
|
333
440
|
else:
|
|
334
441
|
config_file = Path(config_path) if config_path else None
|
|
335
442
|
catalog_file = Path(catalog_path) if catalog_path else None
|
|
336
443
|
|
|
444
|
+
if not resolved_connector_image:
|
|
445
|
+
write_github_output("success", False)
|
|
446
|
+
write_github_output("error", "Missing connector image")
|
|
447
|
+
exit_with_error(
|
|
448
|
+
"You must provide one of the following: a connector_image, a connector_name, "
|
|
449
|
+
"or a connection_id for a connection that has an associated connector image. "
|
|
450
|
+
"If using connection_id, ensure the connection has a connector image configured."
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
# If connector_name was provided, we just built the image locally and it is already
|
|
454
|
+
# available in Docker, so we skip the image availability check/pull. Only try to pull
|
|
455
|
+
# if we didn't just build it (i.e., using a pre-built image from registry).
|
|
456
|
+
if not connector_name and not ensure_image_available(resolved_connector_image):
|
|
457
|
+
write_github_output("success", False)
|
|
458
|
+
write_github_output(
|
|
459
|
+
"error", f"Failed to pull image: {resolved_connector_image}"
|
|
460
|
+
)
|
|
461
|
+
exit_with_error(f"Failed to pull connector image: {resolved_connector_image}")
|
|
462
|
+
|
|
337
463
|
result = _run_connector_command(
|
|
338
|
-
connector_image=
|
|
464
|
+
connector_image=resolved_connector_image,
|
|
339
465
|
command=cmd,
|
|
340
466
|
output_dir=output_path,
|
|
341
467
|
target_or_control=TargetOrControl.TARGET,
|
|
@@ -349,14 +475,14 @@ def live_test(
|
|
|
349
475
|
write_github_outputs(
|
|
350
476
|
{
|
|
351
477
|
"success": result["success"],
|
|
352
|
-
"connector":
|
|
478
|
+
"connector": resolved_connector_image,
|
|
353
479
|
"command": command,
|
|
354
480
|
"exit_code": result["exit_code"],
|
|
355
481
|
}
|
|
356
482
|
)
|
|
357
483
|
|
|
358
484
|
write_test_summary(
|
|
359
|
-
connector_image=
|
|
485
|
+
connector_image=resolved_connector_image,
|
|
360
486
|
test_type="live-test",
|
|
361
487
|
success=result["success"],
|
|
362
488
|
results={
|
|
@@ -367,9 +493,9 @@ def live_test(
|
|
|
367
493
|
)
|
|
368
494
|
|
|
369
495
|
if result["success"]:
|
|
370
|
-
print_success(f"Live test passed for {
|
|
496
|
+
print_success(f"Live test passed for {resolved_connector_image}")
|
|
371
497
|
else:
|
|
372
|
-
|
|
498
|
+
exit_with_error(f"Live test failed for {resolved_connector_image}")
|
|
373
499
|
|
|
374
500
|
|
|
375
501
|
def _run_with_optional_http_metrics(
|
|
@@ -451,17 +577,33 @@ def _run_with_optional_http_metrics(
|
|
|
451
577
|
@connector_app.command(name="regression-test")
|
|
452
578
|
def regression_test(
|
|
453
579
|
target_image: Annotated[
|
|
454
|
-
str,
|
|
580
|
+
str | None,
|
|
455
581
|
Parameter(
|
|
456
|
-
help="Target connector image (new version) with tag (e.g., airbyte/source-github:2.0.0)."
|
|
582
|
+
help="Target connector image (new version) with tag (e.g., airbyte/source-github:2.0.0). "
|
|
583
|
+
"Optional if connector_name is provided."
|
|
457
584
|
),
|
|
458
|
-
],
|
|
585
|
+
] = None,
|
|
459
586
|
control_image: Annotated[
|
|
460
|
-
str,
|
|
587
|
+
str | None,
|
|
461
588
|
Parameter(
|
|
462
|
-
help="Control connector image (baseline version) with tag (e.g., airbyte/source-github:1.0.0)."
|
|
589
|
+
help="Control connector image (baseline version) with tag (e.g., airbyte/source-github:1.0.0). "
|
|
590
|
+
"Optional if connection_id is provided (auto-detected from connection)."
|
|
463
591
|
),
|
|
464
|
-
],
|
|
592
|
+
] = None,
|
|
593
|
+
connector_name: Annotated[
|
|
594
|
+
str | None,
|
|
595
|
+
Parameter(
|
|
596
|
+
help="Connector name to build target image from source (e.g., 'source-pokeapi'). "
|
|
597
|
+
"If provided, builds the target image locally with tag 'dev'."
|
|
598
|
+
),
|
|
599
|
+
] = None,
|
|
600
|
+
repo_root: Annotated[
|
|
601
|
+
str | None,
|
|
602
|
+
Parameter(
|
|
603
|
+
help="Path to the airbyte repo root. Required if connector_name is provided "
|
|
604
|
+
"and the repo cannot be auto-detected."
|
|
605
|
+
),
|
|
606
|
+
] = None,
|
|
465
607
|
command: Annotated[
|
|
466
608
|
Literal["spec", "check", "discover", "read"],
|
|
467
609
|
Parameter(help="The Airbyte command to run."),
|
|
@@ -470,7 +612,8 @@ def regression_test(
|
|
|
470
612
|
str | None,
|
|
471
613
|
Parameter(
|
|
472
614
|
help="Airbyte Cloud connection ID to fetch config/catalog from. "
|
|
473
|
-
"Mutually exclusive with config-path/catalog-path."
|
|
615
|
+
"Mutually exclusive with config-path/catalog-path. "
|
|
616
|
+
"If provided, control_image can be auto-detected."
|
|
474
617
|
),
|
|
475
618
|
] = None,
|
|
476
619
|
config_path: Annotated[
|
|
@@ -506,6 +649,14 @@ def regression_test(
|
|
|
506
649
|
Results are written to the output directory and to GitHub Actions outputs
|
|
507
650
|
if running in CI.
|
|
508
651
|
|
|
652
|
+
You can provide the target image in two ways:
|
|
653
|
+
1. --target-image: Use a pre-built image from Docker registry
|
|
654
|
+
2. --connector-name: Build the target image locally from source code
|
|
655
|
+
|
|
656
|
+
You can provide the control image in two ways:
|
|
657
|
+
1. --control-image: Use a pre-built image from Docker registry
|
|
658
|
+
2. --connection-id: Auto-detect from an Airbyte Cloud connection
|
|
659
|
+
|
|
509
660
|
You can provide config/catalog either via file paths OR via a connection_id
|
|
510
661
|
that fetches them from Airbyte Cloud.
|
|
511
662
|
"""
|
|
@@ -514,27 +665,42 @@ def regression_test(
|
|
|
514
665
|
|
|
515
666
|
cmd = Command(command)
|
|
516
667
|
|
|
517
|
-
for image in [target_image, control_image]:
|
|
518
|
-
if not ensure_image_available(image):
|
|
519
|
-
print_error(f"Failed to pull connector image: {image}")
|
|
520
|
-
write_github_output("success", False)
|
|
521
|
-
write_github_output("error", f"Failed to pull image: {image}")
|
|
522
|
-
return
|
|
523
|
-
|
|
524
668
|
config_file: Path | None = None
|
|
525
669
|
catalog_file: Path | None = None
|
|
526
670
|
state_file = Path(state_path) if state_path else None
|
|
671
|
+
resolved_target_image: str | None = target_image
|
|
672
|
+
resolved_control_image: str | None = control_image
|
|
673
|
+
|
|
674
|
+
# If connector_name is provided, build the target image from source
|
|
675
|
+
if connector_name:
|
|
676
|
+
if target_image:
|
|
677
|
+
write_github_output("success", False)
|
|
678
|
+
write_github_output(
|
|
679
|
+
"error", "Cannot specify both target_image and connector_name"
|
|
680
|
+
)
|
|
681
|
+
exit_with_error("Cannot specify both target_image and connector_name")
|
|
682
|
+
|
|
683
|
+
repo_root_path = Path(repo_root) if repo_root else None
|
|
684
|
+
built_image = _build_connector_image_from_source(
|
|
685
|
+
connector_name=connector_name,
|
|
686
|
+
repo_root=repo_root_path,
|
|
687
|
+
tag="dev",
|
|
688
|
+
)
|
|
689
|
+
if not built_image:
|
|
690
|
+
write_github_output("success", False)
|
|
691
|
+
write_github_output("error", f"Failed to build image for {connector_name}")
|
|
692
|
+
exit_with_error(f"Failed to build image for {connector_name}")
|
|
693
|
+
resolved_target_image = built_image
|
|
527
694
|
|
|
528
695
|
if connection_id:
|
|
529
696
|
if config_path or catalog_path:
|
|
530
|
-
print_error(
|
|
531
|
-
"Cannot specify both connection_id and config_path/catalog_path"
|
|
532
|
-
)
|
|
533
697
|
write_github_output("success", False)
|
|
534
698
|
write_github_output(
|
|
535
699
|
"error", "Cannot specify both connection_id and file paths"
|
|
536
700
|
)
|
|
537
|
-
|
|
701
|
+
exit_with_error(
|
|
702
|
+
"Cannot specify both connection_id and config_path/catalog_path"
|
|
703
|
+
)
|
|
538
704
|
|
|
539
705
|
print_success(f"Fetching config/catalog from connection: {connection_id}")
|
|
540
706
|
connection_data = fetch_connection_data(connection_id)
|
|
@@ -545,15 +711,53 @@ def regression_test(
|
|
|
545
711
|
f"Fetched config for source: {connection_data.source_name} "
|
|
546
712
|
f"with {len(connection_data.stream_names)} streams"
|
|
547
713
|
)
|
|
714
|
+
|
|
715
|
+
# Auto-detect control_image from connection if not provided
|
|
716
|
+
if not resolved_control_image and connection_data.connector_image:
|
|
717
|
+
resolved_control_image = connection_data.connector_image
|
|
718
|
+
print_success(f"Auto-detected control image: {resolved_control_image}")
|
|
548
719
|
else:
|
|
549
720
|
config_file = Path(config_path) if config_path else None
|
|
550
721
|
catalog_file = Path(catalog_path) if catalog_path else None
|
|
551
722
|
|
|
723
|
+
# Validate that we have both images
|
|
724
|
+
if not resolved_target_image:
|
|
725
|
+
write_github_output("success", False)
|
|
726
|
+
write_github_output("error", "No target image specified")
|
|
727
|
+
exit_with_error(
|
|
728
|
+
"You must provide one of the following: a target_image or a connector_name "
|
|
729
|
+
"to build the target image from source."
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
if not resolved_control_image:
|
|
733
|
+
write_github_output("success", False)
|
|
734
|
+
write_github_output("error", "No control image specified")
|
|
735
|
+
exit_with_error(
|
|
736
|
+
"You must provide one of the following: a control_image or a connection_id "
|
|
737
|
+
"for a connection that has an associated connector image."
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
# Pull images if they weren't just built locally
|
|
741
|
+
# If connector_name was provided, we just built the target image locally
|
|
742
|
+
if not connector_name and not ensure_image_available(resolved_target_image):
|
|
743
|
+
write_github_output("success", False)
|
|
744
|
+
write_github_output("error", f"Failed to pull image: {resolved_target_image}")
|
|
745
|
+
exit_with_error(
|
|
746
|
+
f"Failed to pull target connector image: {resolved_target_image}"
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
if not ensure_image_available(resolved_control_image):
|
|
750
|
+
write_github_output("success", False)
|
|
751
|
+
write_github_output("error", f"Failed to pull image: {resolved_control_image}")
|
|
752
|
+
exit_with_error(
|
|
753
|
+
f"Failed to pull control connector image: {resolved_control_image}"
|
|
754
|
+
)
|
|
755
|
+
|
|
552
756
|
target_output = output_path / "target"
|
|
553
757
|
control_output = output_path / "control"
|
|
554
758
|
|
|
555
759
|
target_result = _run_with_optional_http_metrics(
|
|
556
|
-
connector_image=
|
|
760
|
+
connector_image=resolved_target_image,
|
|
557
761
|
command=cmd,
|
|
558
762
|
output_dir=target_output,
|
|
559
763
|
target_or_control=TargetOrControl.TARGET,
|
|
@@ -564,7 +768,7 @@ def regression_test(
|
|
|
564
768
|
)
|
|
565
769
|
|
|
566
770
|
control_result = _run_with_optional_http_metrics(
|
|
567
|
-
connector_image=
|
|
771
|
+
connector_image=resolved_control_image,
|
|
568
772
|
command=cmd,
|
|
569
773
|
output_dir=control_output,
|
|
570
774
|
target_or_control=TargetOrControl.CONTROL,
|
|
@@ -589,8 +793,8 @@ def regression_test(
|
|
|
589
793
|
write_github_outputs(
|
|
590
794
|
{
|
|
591
795
|
"success": both_succeeded and not regression_detected,
|
|
592
|
-
"target_image":
|
|
593
|
-
"control_image":
|
|
796
|
+
"target_image": resolved_target_image,
|
|
797
|
+
"control_image": resolved_control_image,
|
|
594
798
|
"command": command,
|
|
595
799
|
"target_exit_code": target_result["exit_code"],
|
|
596
800
|
"control_exit_code": control_result["exit_code"],
|
|
@@ -601,8 +805,8 @@ def regression_test(
|
|
|
601
805
|
write_json_output("regression_report", combined_result)
|
|
602
806
|
|
|
603
807
|
report_path = generate_regression_report(
|
|
604
|
-
target_image=
|
|
605
|
-
control_image=
|
|
808
|
+
target_image=resolved_target_image,
|
|
809
|
+
control_image=resolved_control_image,
|
|
606
810
|
command=command,
|
|
607
811
|
target_result=target_result,
|
|
608
812
|
control_result=control_result,
|
|
@@ -614,8 +818,74 @@ def regression_test(
|
|
|
614
818
|
write_github_summary(summary)
|
|
615
819
|
|
|
616
820
|
if regression_detected:
|
|
617
|
-
|
|
821
|
+
exit_with_error(
|
|
822
|
+
f"Regression detected between {resolved_target_image} and {resolved_control_image}"
|
|
823
|
+
)
|
|
618
824
|
elif both_succeeded:
|
|
619
|
-
print_success(
|
|
825
|
+
print_success(
|
|
826
|
+
f"Regression test passed for {resolved_target_image} vs {resolved_control_image}"
|
|
827
|
+
)
|
|
828
|
+
else:
|
|
829
|
+
exit_with_error(
|
|
830
|
+
f"Both versions failed for {resolved_target_image} vs {resolved_control_image}"
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
@connector_app.command(name="fetch-connection-config")
|
|
835
|
+
def fetch_connection_config_cmd(
|
|
836
|
+
connection_id: Annotated[
|
|
837
|
+
str,
|
|
838
|
+
Parameter(help="The UUID of the Airbyte Cloud connection."),
|
|
839
|
+
],
|
|
840
|
+
output_path: Annotated[
|
|
841
|
+
str | None,
|
|
842
|
+
Parameter(
|
|
843
|
+
help="Path to output file or directory. "
|
|
844
|
+
"If directory, writes connection-<id>-config.json. "
|
|
845
|
+
"Default: ./connection-<id>-config.json"
|
|
846
|
+
),
|
|
847
|
+
] = None,
|
|
848
|
+
with_secrets: Annotated[
|
|
849
|
+
bool,
|
|
850
|
+
Parameter(
|
|
851
|
+
name="--with-secrets",
|
|
852
|
+
negative="--no-secrets",
|
|
853
|
+
help="If set, fetches unmasked secrets from the internal database. "
|
|
854
|
+
"Requires GCP_PROD_DB_ACCESS_CREDENTIALS env var or `gcloud auth application-default login`. "
|
|
855
|
+
"Must be used with --oc-issue-url.",
|
|
856
|
+
),
|
|
857
|
+
] = False,
|
|
858
|
+
oc_issue_url: Annotated[
|
|
859
|
+
str | None,
|
|
860
|
+
Parameter(
|
|
861
|
+
help="OC issue URL for audit logging. Required when using --with-secrets."
|
|
862
|
+
),
|
|
863
|
+
] = None,
|
|
864
|
+
) -> None:
|
|
865
|
+
"""Fetch connection configuration from Airbyte Cloud to a local file.
|
|
866
|
+
|
|
867
|
+
This command retrieves the source configuration for a given connection ID
|
|
868
|
+
and writes it to a local JSON file.
|
|
869
|
+
|
|
870
|
+
Requires authentication via AIRBYTE_CLOUD_CLIENT_ID and
|
|
871
|
+
AIRBYTE_CLOUD_CLIENT_SECRET environment variables.
|
|
872
|
+
|
|
873
|
+
When --with-secrets is specified, the command fetches unmasked secrets from
|
|
874
|
+
the internal database using the connection-retriever. This additionally requires:
|
|
875
|
+
- An OC issue URL for audit logging (--oc-issue-url)
|
|
876
|
+
- GCP credentials via `GCP_PROD_DB_ACCESS_CREDENTIALS` env var or `gcloud auth application-default login`
|
|
877
|
+
- If `CI=true`: expects `cloud-sql-proxy` running on localhost, or
|
|
878
|
+
direct network access to the Cloud SQL instance.
|
|
879
|
+
"""
|
|
880
|
+
path = Path(output_path) if output_path else None
|
|
881
|
+
result = fetch_connection_config(
|
|
882
|
+
connection_id=connection_id,
|
|
883
|
+
output_path=path,
|
|
884
|
+
with_secrets=with_secrets,
|
|
885
|
+
oc_issue_url=oc_issue_url,
|
|
886
|
+
)
|
|
887
|
+
if result.success:
|
|
888
|
+
print_success(result.message)
|
|
620
889
|
else:
|
|
621
|
-
print_error(
|
|
890
|
+
print_error(result.message)
|
|
891
|
+
print_json(result.model_dump())
|
airbyte_ops_mcp/cli/repo.py
CHANGED
|
@@ -27,6 +27,7 @@ from airbyte_ops_mcp.airbyte_repo.list_connectors import (
|
|
|
27
27
|
CONNECTOR_PATH_PREFIX,
|
|
28
28
|
METADATA_FILE_NAME,
|
|
29
29
|
_detect_connector_language,
|
|
30
|
+
get_connectors_with_local_cdk,
|
|
30
31
|
)
|
|
31
32
|
from airbyte_ops_mcp.cli._base import app
|
|
32
33
|
from airbyte_ops_mcp.cli._shared import exit_with_error, print_json
|
|
@@ -160,6 +161,15 @@ def list_connectors(
|
|
|
160
161
|
bool,
|
|
161
162
|
Parameter(help="Include only modified connectors (requires PR context)."),
|
|
162
163
|
] = False,
|
|
164
|
+
local_cdk: Annotated[
|
|
165
|
+
bool,
|
|
166
|
+
Parameter(
|
|
167
|
+
help=(
|
|
168
|
+
"Include connectors using local CDK reference. "
|
|
169
|
+
"When combined with --modified-only, adds local-CDK connectors to the modified set."
|
|
170
|
+
)
|
|
171
|
+
),
|
|
172
|
+
] = False,
|
|
163
173
|
language: Annotated[
|
|
164
174
|
list[str] | None,
|
|
165
175
|
Parameter(help="Languages to include (python, java, low-code, manifest-only)."),
|
|
@@ -286,6 +296,11 @@ def list_connectors(
|
|
|
286
296
|
connectors = list(result.connectors)
|
|
287
297
|
repo_path_obj = Path(repo_path)
|
|
288
298
|
|
|
299
|
+
# Add connectors with local CDK reference if --local-cdk flag is set
|
|
300
|
+
if local_cdk:
|
|
301
|
+
local_cdk_connectors = get_connectors_with_local_cdk(repo_path)
|
|
302
|
+
connectors = sorted(set(connectors) | local_cdk_connectors)
|
|
303
|
+
|
|
289
304
|
# Apply connector type filter
|
|
290
305
|
if connector_type_filter:
|
|
291
306
|
connectors = [
|