1616labels and writes them to the connector's `secrets` directory.
1717"""
1818
19+ from __future__ import annotations
20+
1921import json
2022import os
2123from pathlib import Path
24+ from types import ModuleType
2225
2326import rich_click as click
27+ from click import style
28+ from rich .console import Console
29+ from rich .table import Table
2430
25- from airbyte_cdk .cli .airbyte_cdk ._util import resolve_connector_name_and_directory
31+ from airbyte_cdk .cli .airbyte_cdk ._util import (
32+ resolve_connector_name ,
33+ resolve_connector_name_and_directory ,
34+ )
2635
2736AIRBYTE_INTERNAL_GCP_PROJECT = "dataline-integration-testing"
2837CONNECTOR_LABEL = "connector"
2938
3039
40+ secretmanager : ModuleType | None
41+ Secret : type | None
42+ try :
43+ from google .cloud import secretmanager_v1 as secretmanager
44+ from google .cloud .secretmanager_v1 import Secret
45+ except ImportError :
46+ # If the package is not installed, we will raise an error in the CLI command.
47+ secretmanager = None
48+ Secret = None
49+
50+
3151@click .group (
3252 name = "secrets" ,
33- help = __doc__ .replace ("\n " , "\n \n " ), # Render docstring as help text (markdown)
53+ help = __doc__ .replace ("\n " , "\n \n " ), # Render docstring as help text (markdown) # type: ignore
3454)
3555def secrets_cli_group () -> None :
3656 """Secret management commands."""
@@ -68,18 +88,151 @@ def fetch(
6888 directory. If the current working directory is not a connector directory (e.g. starting
6989 with 'source-') and no connector name or path is provided, the process will fail.
7090 """
71- try :
72- from google .cloud import secretmanager_v1 as secretmanager
73- except ImportError :
91+ click .echo ("Fetching secrets..." )
92+
93+ client = _get_gsm_secrets_client ()
94+ connector_name , connector_directory = resolve_connector_name_and_directory (
95+ connector_name = connector_name ,
96+ connector_directory = connector_directory ,
97+ )
98+ secrets_dir = _get_secrets_dir (
99+ connector_directory = connector_directory ,
100+ connector_name = connector_name ,
101+ ensure_exists = True ,
102+ )
103+ secrets = _fetch_secret_handles (
104+ connector_name = connector_name ,
105+ gcp_project_id = gcp_project_id ,
106+ )
107+ # Fetch and write secrets
108+ secret_count = 0
109+ for secret in secrets :
110+ secret_file_path = _get_secret_filepath (
111+ secrets_dir = secrets_dir ,
112+ secret = secret ,
113+ )
114+ secret_file_path .write_text (_get_secret_value (secret = secret , client = client ))
115+ secret_file_path .chmod (0o600 ) # default to owner read/write only
116+ click .echo (f"Secret written to: { secret_file_path .absolute ()!s} " )
117+ secret_count += 1
118+
119+ if secret_count == 0 :
120+ click .echo (
121+ f"No secrets found for connector: '{ connector_name } '" ,
122+ err = True ,
123+ )
124+
125+
126+ @secrets_cli_group .command ("list" )
127+ @click .option (
128+ "--connector-name" ,
129+ type = str ,
130+ help = "Name of the connector to fetch secrets for. Ignored if --connector-directory is provided." ,
131+ )
132+ @click .option (
133+ "--connector-directory" ,
134+ type = click .Path (exists = True , file_okay = False , path_type = Path ),
135+ help = "Path to the connector directory." ,
136+ )
137+ @click .option (
138+ "--gcp-project-id" ,
139+ type = str ,
140+ default = AIRBYTE_INTERNAL_GCP_PROJECT ,
141+ help = f"GCP project ID. Defaults to '{ AIRBYTE_INTERNAL_GCP_PROJECT } '." ,
142+ )
143+ def list_ (
144+ connector_name : str | None = None ,
145+ connector_directory : Path | None = None ,
146+ gcp_project_id : str = AIRBYTE_INTERNAL_GCP_PROJECT ,
147+ ) -> None :
148+ """List secrets for a connector from Google Secret Manager.
149+
150+ This command fetches secrets for a connector from Google Secret Manager and prints
151+ them as a table.
152+
153+ If no connector name or directory is provided, we will look within the current working
154+ directory. If the current working directory is not a connector directory (e.g. starting
155+ with 'source-') and no connector name or path is provided, the process will fail.
156+ """
157+ click .echo ("Fetching secrets..." )
158+
159+ connector_name = connector_name or resolve_connector_name (
160+ connector_directory = connector_directory or Path ().resolve ().absolute (),
161+ )
162+ secrets = _fetch_secret_handles (
163+ connector_name = connector_name ,
164+ gcp_project_id = gcp_project_id ,
165+ )
166+
167+ if not secrets :
168+ click .echo (
169+ f"No secrets found for connector: '{ connector_name } '" ,
170+ err = True ,
171+ )
172+ return
173+ # print a rich table with the secrets
174+ click .echo (
175+ style (
176+ f"Secrets for connector '{ connector_name } ' in project '{ gcp_project_id } ':" ,
177+ fg = "green" ,
178+ )
179+ )
180+
181+ console = Console ()
182+ table = Table (title = f"'{ connector_name } ' Secrets" )
183+ table .add_column ("Name" , justify = "left" , style = "cyan" , overflow = "fold" )
184+ table .add_column ("Labels" , justify = "left" , style = "magenta" , overflow = "fold" )
185+ table .add_column ("Last Updated" , justify = "left" , style = "blue" , overflow = "fold" )
186+ for secret in secrets :
187+ table .add_row (
188+ secret .name .split ("/secrets/" )[- 1 ],
189+ str (secret .create_time ),
190+ "\n " .join ([f"{ k } ={ v } " for k , v in secret .labels .items ()]),
191+ )
192+
193+ console .print (table )
194+
195+
196+ def _fetch_secret_handles (
197+ connector_name : str ,
198+ gcp_project_id : str = AIRBYTE_INTERNAL_GCP_PROJECT ,
199+ ) -> list ["Secret" ]: # type: ignore
200+ """Fetch secrets from Google Secret Manager."""
201+ if not secretmanager :
74202 raise ImportError (
75203 "google-cloud-secret-manager package is required for Secret Manager integration. "
76204 "Install it with 'pip install airbyte-cdk[dev]' "
77205 "or 'pip install google-cloud-secret-manager'."
78206 )
79207
80- click .echo ("Fetching secrets..." )
208+ client = _get_gsm_secrets_client ()
209+
210+ # List all secrets with the connector label
211+ parent = f"projects/{ gcp_project_id } "
212+ filter_string = f"labels.{ CONNECTOR_LABEL } ={ connector_name } "
213+ secrets = client .list_secrets (
214+ request = secretmanager .ListSecretsRequest (
215+ parent = parent ,
216+ filter = filter_string ,
217+ )
218+ )
219+ return [s for s in secrets ]
220+
221+
222+ def _get_secret_value (
223+ secret : "Secret" , # type: ignore
224+ client : "secretmanager.SecretManagerServiceClient" , # type: ignore
225+ ) -> str :
226+ version_name = f"{ secret .name } /versions/latest"
227+ response = client .access_secret_version (name = version_name )
228+ return response .payload .data .decode ("UTF-8" )
229+
81230
82- # Resolve connector name/directory
231+ def _get_secrets_dir (
232+ connector_directory : Path ,
233+ connector_name : str ,
234+ ensure_exists : bool = True ,
235+ ) -> Path :
83236 try :
84237 connector_name , connector_directory = resolve_connector_name_and_directory (
85238 connector_name = connector_name ,
@@ -95,56 +248,44 @@ def fetch(
95248 except ValueError as e :
96249 raise ValueError (str (e ))
97250
98- # Create secrets directory if it doesn't exist
99251 secrets_dir = connector_directory / "secrets"
100- secrets_dir .mkdir (parents = True , exist_ok = True )
252+ if ensure_exists :
253+ secrets_dir .mkdir (parents = True , exist_ok = True )
101254
102- gitignore_path = secrets_dir / ".gitignore"
103- gitignore_path .write_text ("*" )
255+ gitignore_path = secrets_dir / ".gitignore"
256+ if not gitignore_path .exists ():
257+ gitignore_path .write_text ("*" )
104258
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- )
259+ return secrets_dir
111260
112- client = secretmanager .SecretManagerServiceClient .from_service_account_info (
113- json .loads (credentials_json )
114- )
115261
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- )
262+ def _get_secret_filepath (
263+ secrets_dir : Path ,
264+ secret : Secret , # type: ignore
265+ ) -> Path :
266+ """Get the file path for a secret based on its labels."""
267+ if secret .labels and "filename" in secret .labels :
268+ return secrets_dir / f"{ secret .labels ['filename' ]} .json"
125269
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" )
270+ return secrets_dir / "config.json" # Default filename
133271
134- filename_base = "config" # Default filename
135- if secret .labels and "filename" in secret .labels :
136- filename_base = secret .labels ["filename" ]
137272
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 } " )
273+ def _get_gsm_secrets_client () -> "secretmanager.SecretManagerServiceClient" : # type: ignore
274+ """Get the Google Secret Manager client."""
275+ if not secretmanager :
276+ raise ImportError (
277+ "google-cloud-secret-manager package is required for Secret Manager integration. "
278+ "Install it with 'pip install airbyte-cdk[dev]' "
279+ "or 'pip install google-cloud-secret-manager'."
280+ )
146281
282+ credentials_json = os .environ .get ("GCP_GSM_CREDENTIALS" )
283+ if not credentials_json :
284+ raise ValueError (
285+ "No Google Cloud credentials found. "
286+ "Please set the `GCP_GSM_CREDENTIALS` environment variable."
287+ )
147288
148- __all__ = [
149- "secrets_cli_group" ,
150- ]
289+ return secretmanager . SecretManagerServiceClient . from_service_account_info (
290+ json . loads ( credentials_json )
291+ )
0 commit comments