Skip to content

Commit bcf5d9f

Browse files
authored
Merge branch 'aj/feat/add-standard-tests-cli' into devin/1744841809-add-build-command
2 parents 598d454 + 1b47886 commit bcf5d9f

18 files changed

Lines changed: 218 additions & 5685 deletions

File tree

airbyte_cdk/cli/airbyte_cdk/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,12 @@
4444
from airbyte_cdk.cli.airbyte_cdk._connector import connector_cli_group
4545
from airbyte_cdk.cli.airbyte_cdk._image import image_cli_group
4646
from airbyte_cdk.cli.airbyte_cdk._manifest import manifest_cli_group
47+
from airbyte_cdk.cli.airbyte_cdk._secrets import secrets_cli_group
4748
from airbyte_cdk.cli.airbyte_cdk._version import print_version
4849

4950

5051
@click.group(
51-
help=__doc__.replace("\n", "\n\n"), # Workaround to format help text correctly
52+
help=__doc__.replace("\n", "\n\n"), # Render docstring as help text (markdown)
5253
invoke_without_command=True,
5354
)
5455
@click.option(
@@ -78,6 +79,7 @@ def cli(
7879
cli.add_command(connector_cli_group)
7980
cli.add_command(manifest_cli_group)
8081
cli.add_command(image_cli_group)
82+
cli.add_command(secrets_cli_group)
8183

8284

8385
if __name__ == "__main__":

airbyte_cdk/cli/airbyte_cdk/_connector.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,10 @@
8888
'''
8989

9090

91-
@click.group(name="connector")
91+
@click.group(
92+
name="connector",
93+
help=__doc__.replace("\n", "\n\n"), # Render docstring as help text (markdown)
94+
)
9295
def connector_cli_group() -> None:
9396
"""Connector related commands."""
9497
pass

airbyte_cdk/cli/airbyte_cdk/_image.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@
4747
)
4848

4949

50-
@click.group(name="image")
50+
@click.group(
51+
name="image",
52+
help=__doc__.replace("\n", "\n\n"), # Render docstring as help text (markdown)
53+
)
5154
def image_cli_group() -> None:
5255
"""Commands for working with connector Docker images."""
5356

airbyte_cdk/cli/airbyte_cdk/_manifest.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2-
"""Manifest related commands."""
2+
"""Manifest related commands.
33
4-
import click
4+
Coming soon.
55
6+
This module is planned to provide a command line interface (CLI) for validating
7+
Airbyte CDK manifests.
8+
"""
69

7-
@click.group(name="manifest")
10+
import rich_click as click
11+
12+
13+
@click.group(
14+
name="manifest",
15+
help=__doc__.replace("\n", "\n\n"), # Render docstring as help text (markdown)
16+
)
817
def manifest_cli_group() -> None:
918
"""Manifest related commands."""
1019
pass
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2+
"""Secret management commands.
3+
4+
This module provides commands for managing secrets for Airbyte connectors.
5+
6+
Usage:
7+
airbyte-cdk secrets fetch --connector-name source-github
8+
airbyte-cdk secrets fetch --connector-directory /path/to/connector
9+
airbyte-cdk secrets fetch # Run from within a connector directory
10+
11+
Usage without pre-installing (stateless):
12+
pipx run airbyte-cdk secrets fetch ...
13+
uvx airbyte-cdk secrets fetch ...
14+
15+
The 'fetch' command retrieves secrets from Google Secret Manager based on connector
16+
labels and writes them to the connector's `secrets` directory.
17+
"""
18+
19+
import json
20+
import os
21+
from pathlib import Path
22+
23+
import rich_click as click
24+
25+
from airbyte_cdk.cli.airbyte_cdk._util import resolve_connector_name_and_directory
26+
27+
AIRBYTE_INTERNAL_GCP_PROJECT = "dataline-integration-testing"
28+
CONNECTOR_LABEL = "connector"
29+
30+
31+
@click.group(
32+
name="secrets",
33+
help=__doc__.replace("\n", "\n\n"), # Render docstring as help text (markdown)
34+
)
35+
def secrets_cli_group() -> None:
36+
"""Secret management commands."""
37+
pass
38+
39+
40+
@secrets_cli_group.command()
41+
@click.option(
42+
"--connector-name",
43+
type=str,
44+
help="Name of the connector to fetch secrets for. Ignored if --connector-directory is provided.",
45+
)
46+
@click.option(
47+
"--connector-directory",
48+
type=click.Path(exists=True, file_okay=False, path_type=Path),
49+
help="Path to the connector directory.",
50+
)
51+
@click.option(
52+
"--gcp-project-id",
53+
type=str,
54+
default=AIRBYTE_INTERNAL_GCP_PROJECT,
55+
help=f"GCP project ID. Defaults to '{AIRBYTE_INTERNAL_GCP_PROJECT}'.",
56+
)
57+
def fetch(
58+
connector_name: str | None = None,
59+
connector_directory: Path | None = None,
60+
gcp_project_id: str = AIRBYTE_INTERNAL_GCP_PROJECT,
61+
) -> None:
62+
"""Fetch secrets for a connector from Google Secret Manager.
63+
64+
This command fetches secrets for a connector from Google Secret Manager and writes them
65+
to the connector's secrets directory.
66+
67+
If no connector name or directory is provided, we will look within the current working
68+
directory. If the current working directory is not a connector directory (e.g. starting
69+
with 'source-') and no connector name or path is provided, the process will fail.
70+
"""
71+
try:
72+
from google.cloud import secretmanager_v1 as secretmanager
73+
except ImportError:
74+
raise ImportError(
75+
"google-cloud-secret-manager package is required for Secret Manager integration. "
76+
"Install it with 'pip install airbyte-cdk[dev]' "
77+
"or 'pip install google-cloud-secret-manager'."
78+
)
79+
80+
click.echo("Fetching secrets...")
81+
82+
# Resolve connector name/directory
83+
try:
84+
connector_name, connector_directory = resolve_connector_name_and_directory(
85+
connector_name=connector_name,
86+
connector_directory=connector_directory,
87+
)
88+
except FileNotFoundError as e:
89+
raise FileNotFoundError(
90+
f"Could not find connector directory for '{connector_name}'. "
91+
"Please provide the --connector-directory option with the path to the connector. "
92+
"Note: This command requires either running from within a connector directory, "
93+
"being in the airbyte monorepo, or explicitly providing the connector directory path."
94+
) from e
95+
except ValueError as e:
96+
raise ValueError(str(e))
97+
98+
# Create secrets directory if it doesn't exist
99+
secrets_dir = connector_directory / "secrets"
100+
secrets_dir.mkdir(parents=True, exist_ok=True)
101+
102+
gitignore_path = secrets_dir / ".gitignore"
103+
gitignore_path.write_text("*")
104+
105+
# Get GSM client
106+
credentials_json = os.environ.get("GCP_GSM_CREDENTIALS")
107+
if not credentials_json:
108+
raise ValueError(
109+
"No Google Cloud credentials found. Please set the GCP_GSM_CREDENTIALS environment variable."
110+
)
111+
112+
client = secretmanager.SecretManagerServiceClient.from_service_account_info(
113+
json.loads(credentials_json)
114+
)
115+
116+
# List all secrets with the connector label
117+
parent = f"projects/{gcp_project_id}"
118+
filter_string = f"labels.{CONNECTOR_LABEL}={connector_name}"
119+
secrets = client.list_secrets(
120+
request=secretmanager.ListSecretsRequest(
121+
parent=parent,
122+
filter=filter_string,
123+
)
124+
)
125+
126+
# Fetch and write secrets
127+
secret_count = 0
128+
for secret in secrets:
129+
secret_name = secret.name
130+
version_name = f"{secret_name}/versions/latest"
131+
response = client.access_secret_version(name=version_name)
132+
payload = response.payload.data.decode("UTF-8")
133+
134+
filename_base = "config" # Default filename
135+
if secret.labels and "filename" in secret.labels:
136+
filename_base = secret.labels["filename"]
137+
138+
secret_file_path = secrets_dir / f"{filename_base}.json"
139+
secret_file_path.write_text(payload)
140+
secret_file_path.chmod(0o600) # default to owner read/write only
141+
click.echo(f"Secret written to: {secret_file_path.absolute()!s}")
142+
secret_count += 1
143+
144+
if secret_count == 0:
145+
click.echo(f"No secrets found for connector: {connector_name}")
146+
147+
148+
__all__ = [
149+
"secrets_cli_group",
150+
]

airbyte_cdk/sources/declarative/requesters/query_properties/property_chunking.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def get_request_property_chunks(
5555
# todo: Add ability to specify parameter delimiter representation and take into account in property_field_size
5656
property_field_size = (
5757
len(property_field)
58-
+ 1 # The +1 represents the extra character for the delimiter in between properties
58+
+ 3 # The +3 represents the extra characters for encoding the delimiter in between properties
5959
if self.property_limit_type == PropertyLimitType.characters
6060
else 1
6161
)

airbyte_cdk/test/declarative/__init__.py

Lines changed: 0 additions & 6 deletions
This file was deleted.

airbyte_cdk/test/declarative/models/__init__.py

Whitespace-only changes.

airbyte_cdk/test/declarative/models/scenario.py

Lines changed: 0 additions & 74 deletions
This file was deleted.

airbyte_cdk/test/declarative/utils/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)