Skip to content

Commit 05ed0b3

Browse files
feat: add secrets fetch command to CDK CLI
Co-Authored-By: Aaron <AJ> Steers <[email protected]>
1 parent 0cc217e commit 05ed0b3

File tree

3 files changed

+135
-0
lines changed

3 files changed

+135
-0
lines changed

airbyte_cdk/cli/airbyte_cdk/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
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

@@ -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__":
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2+
"""Secret management commands."""
3+
4+
import json
5+
import os
6+
from pathlib import Path
7+
from typing import Optional
8+
9+
import rich_click as click
10+
11+
from airbyte_cdk.test.standard_tests.test_resources import find_connector_root_from_name
12+
13+
AIRBYTE_INTERNAL_GCP_PROJECT = "dataline-integration-testing"
14+
CONNECTOR_LABEL = "connector"
15+
16+
17+
@click.group(name="secrets")
18+
def secrets_cli_group() -> None:
19+
"""Secret management commands."""
20+
pass
21+
22+
23+
@secrets_cli_group.command()
24+
@click.option(
25+
"--connector-name",
26+
type=str,
27+
help="Name of the connector to fetch secrets for. Ignored if --connector-directory is provided.",
28+
)
29+
@click.option(
30+
"--connector-directory",
31+
type=click.Path(exists=True, file_okay=False, path_type=Path),
32+
help="Path to the connector directory.",
33+
)
34+
@click.option(
35+
"--project",
36+
type=str,
37+
default=AIRBYTE_INTERNAL_GCP_PROJECT,
38+
help=f"GCP project ID. Defaults to '{AIRBYTE_INTERNAL_GCP_PROJECT}'.",
39+
)
40+
def fetch(
41+
connector_name: Optional[str] = None,
42+
connector_directory: Optional[Path] = None,
43+
project: str = AIRBYTE_INTERNAL_GCP_PROJECT,
44+
) -> None:
45+
"""Fetch secrets for a connector from Google Secret Manager.
46+
47+
This command fetches secrets for a connector from Google Secret Manager and writes them
48+
to the connector's secrets directory.
49+
50+
If no connector name or directory is provided, we will look within the current working
51+
directory. If the current working directory is not a connector directory (e.g. starting
52+
with 'source-') and no connector name or path is provided, the process will fail.
53+
"""
54+
try:
55+
from google.cloud import secretmanager_v1 as secretmanager
56+
except ImportError:
57+
raise ImportError(
58+
"google-cloud-secret-manager package is required for Secret Manager integration. "
59+
"Install it with 'pip install airbyte-cdk[dev]' or 'pip install google-cloud-secret-manager'."
60+
)
61+
62+
click.echo("Fetching secrets...")
63+
64+
# Resolve connector name/directory
65+
if not connector_name and not connector_directory:
66+
cwd = Path().resolve().absolute()
67+
if cwd.name.startswith("source-") or cwd.name.startswith("destination-"):
68+
connector_name = cwd.name
69+
connector_directory = cwd
70+
else:
71+
raise ValueError(
72+
"Either connector_name or connector_directory must be provided if not "
73+
"running from a connector directory."
74+
)
75+
76+
if connector_directory:
77+
connector_directory = connector_directory.resolve().absolute()
78+
if not connector_name:
79+
connector_name = connector_directory.name
80+
elif connector_name:
81+
connector_directory = find_connector_root_from_name(connector_name)
82+
else:
83+
raise ValueError("Either connector_name or connector_directory must be provided.")
84+
85+
# Create secrets directory if it doesn't exist
86+
secrets_dir = connector_directory / "secrets"
87+
secrets_dir.mkdir(parents=True, exist_ok=True)
88+
89+
# Get GSM client
90+
credentials_json = os.environ.get("GCP_GSM_CREDENTIALS")
91+
if not credentials_json:
92+
raise ValueError(
93+
"No Google Cloud credentials found. Please set the GCP_GSM_CREDENTIALS environment variable."
94+
)
95+
96+
client = secretmanager.SecretManagerServiceClient.from_service_account_info(
97+
json.loads(credentials_json)
98+
)
99+
100+
# List all secrets with the connector label
101+
parent = f"projects/{project}"
102+
filter_string = f"labels.{CONNECTOR_LABEL}={connector_name}"
103+
secrets = client.list_secrets(request=secretmanager.ListSecretsRequest(
104+
parent=parent,
105+
filter=filter_string,
106+
))
107+
108+
# Fetch and write secrets
109+
secret_count = 0
110+
for secret in secrets:
111+
secret_name = secret.name
112+
version_name = f"{secret_name}/versions/latest"
113+
response = client.access_secret_version(name=version_name)
114+
payload = response.payload.data.decode("UTF-8")
115+
116+
filename_base = "config" # Default filename
117+
if secret.labels and "filename" in secret.labels:
118+
filename_base = secret.labels["filename"]
119+
120+
secret_file_path = secrets_dir / f"{filename_base}.json"
121+
secret_file_path.write_text(payload)
122+
click.echo(f"Secret written to: {secret_file_path}")
123+
secret_count += 1
124+
125+
if secret_count == 0:
126+
click.echo(f"No secrets found for connector: {connector_name}")
127+
128+
129+
__all__ = [
130+
"secrets_cli_group",
131+
]

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ whenever = "^0.6.16"
8888
click = "^8.1.8"
8989

9090
[tool.poetry.group.dev.dependencies]
91+
google-cloud-secret-manager = { version = "^2.17.0", optional = true }
9192
freezegun = "*"
9293
mypy = "*"
9394
asyncio = "3.4.3"
@@ -110,6 +111,7 @@ types-cachetools = "^5.5.0.20240820"
110111
deptry = "^0.23.0"
111112

112113
[tool.poetry.extras]
114+
dev = ["google-cloud-secret-manager"]
113115
file-based = ["avro", "fastavro", "pyarrow", "unstructured", "pdf2image", "pdfminer.six", "unstructured.pytesseract", "pytesseract", "markdown", "python-calamine", "python-snappy"]
114116
vector-db-based = ["langchain", "openai", "cohere", "tiktoken"]
115117
sql = ["sqlalchemy"]

0 commit comments

Comments
 (0)