4343from rich .console import Console
4444from rich .table import Table
4545
46+ from airbyte_cdk .cli .airbyte_cdk .exceptions import ConnectorSecretWithNoValidVersionsError
4647from airbyte_cdk .utils .connector_paths import (
4748 resolve_connector_name ,
4849 resolve_connector_name_and_directory ,
@@ -131,24 +132,46 @@ def fetch(
131132 )
132133 # Fetch and write secrets
133134 secret_count = 0
135+ exceptions = []
136+
134137 for secret in secrets :
135138 secret_file_path = _get_secret_filepath (
136139 secrets_dir = secrets_dir ,
137140 secret = secret ,
138141 )
139- _write_secret_file (
140- secret = secret ,
141- client = client ,
142- file_path = secret_file_path ,
142+ try :
143+ _write_secret_file (
144+ secret = secret ,
145+ client = client ,
146+ file_path = secret_file_path ,
147+ connector_name = connector_name ,
148+ gcp_project_id = gcp_project_id ,
149+ )
150+ click .echo (f"Secret written to: { secret_file_path .absolute ()!s} " , err = True )
151+ secret_count += 1
152+ except ConnectorSecretWithNoValidVersionsError as e :
153+ exceptions .append (e )
154+ click .echo (
155+ f"Failed to retrieve secret '{ e .secret_name } ': No enabled version found" , err = True
156+ )
157+
158+ if secret_count == 0 and not exceptions :
159+ click .echo (
160+ f"No secrets found for connector: '{ connector_name } '" ,
161+ err = True ,
143162 )
144- click .echo (f"Secret written to: { secret_file_path .absolute ()!s} " , err = True )
145- secret_count += 1
146163
147- if secret_count == 0 :
164+ if exceptions :
165+ error_message = f"Failed to retrieve { len (exceptions )} secret(s)"
148166 click .echo (
149- f"No secrets found for connector: '{ connector_name } '" ,
167+ style (
168+ error_message ,
169+ fg = "red" ,
170+ ),
150171 err = True ,
151172 )
173+ if secret_count == 0 :
174+ raise exceptions [0 ]
152175
153176 if not print_ci_secrets_masks :
154177 return
@@ -230,9 +253,8 @@ def list_(
230253 table .add_column ("Created" , justify = "left" , style = "blue" , overflow = "fold" )
231254 for secret in secrets :
232255 full_secret_name = secret .name
233- secret_name = full_secret_name .split ("/secrets/" )[- 1 ] # Removes project prefix
234- # E.g. https://console.cloud.google.com/security/secret-manager/secret/SECRET_SOURCE-SHOPIFY__CREDS/versions?hl=en&project=<gcp_project_id>
235- secret_url = f"https://console.cloud.google.com/security/secret-manager/secret/{ secret_name } /versions?hl=en&project={ gcp_project_id } "
256+ secret_name = _extract_secret_name (full_secret_name )
257+ secret_url = _get_secret_url (secret_name , gcp_project_id )
236258 table .add_row (
237259 f"[link={ secret_url } ]{ secret_name } [/link]" ,
238260 "\n " .join ([f"{ k } ={ v } " for k , v in secret .labels .items ()]),
@@ -242,6 +264,43 @@ def list_(
242264 console .print (table )
243265
244266
267+ def _extract_secret_name (secret_name : str ) -> str :
268+ """Extract the secret name from a fully qualified secret path.
269+
270+ Handles different formats of secret names:
271+ - Full path: "projects/project-id/secrets/SECRET_NAME"
272+ - Already extracted: "SECRET_NAME"
273+
274+ Args:
275+ secret_name: The secret name or path
276+
277+ Returns:
278+ str: The extracted secret name without project prefix
279+ """
280+ if "/secrets/" in secret_name :
281+ return secret_name .split ("/secrets/" )[- 1 ]
282+ return secret_name
283+
284+
285+ def _get_secret_url (secret_name : str , gcp_project_id : str ) -> str :
286+ """Generate a URL for a secret in the GCP Secret Manager console.
287+
288+ Note: This URL itself does not contain secrets or sensitive information.
289+ The URL itself is only useful for valid logged-in users of the project, and it
290+ safe to print this URL in logs.
291+
292+ Args:
293+ secret_name: The name of the secret in GCP.
294+ gcp_project_id: The GCP project ID.
295+
296+ Returns:
297+ str: URL to the secret in the GCP console
298+ """
299+ # Ensure we have just the secret name without the project prefix
300+ secret_name = _extract_secret_name (secret_name )
301+ return f"https://console.cloud.google.com/security/secret-manager/secret/{ secret_name } /versions?hl=en&project={ gcp_project_id } "
302+
303+
245304def _fetch_secret_handles (
246305 connector_name : str ,
247306 gcp_project_id : str = AIRBYTE_INTERNAL_GCP_PROJECT ,
@@ -272,9 +331,44 @@ def _write_secret_file(
272331 secret : "Secret" , # type: ignore
273332 client : "secretmanager.SecretManagerServiceClient" , # type: ignore
274333 file_path : Path ,
334+ connector_name : str ,
335+ gcp_project_id : str ,
275336) -> None :
276- version_name = f"{ secret .name } /versions/latest"
277- response = client .access_secret_version (name = version_name )
337+ """Write the most recent enabled version of a secret to a file.
338+
339+ Lists all enabled versions of the secret and selects the most recent one.
340+ Raises ConnectorSecretWithNoValidVersionsError if no enabled versions are found.
341+
342+ Args:
343+ secret: The secret to write to a file
344+ client: The Secret Manager client
345+ file_path: The path to write the secret to
346+ connector_name: The name of the connector
347+ gcp_project_id: The GCP project ID
348+
349+ Raises:
350+ ConnectorSecretWithNoValidVersionsError: If no enabled version is found
351+ """
352+ # List all enabled versions of the secret.
353+ response = client .list_secret_versions (
354+ request = {"parent" : secret .name , "filter" : "state:ENABLED" }
355+ )
356+
357+ # The API returns versions pre-sorted in descending order, with the
358+ # 0th item being the latest version.
359+ versions = list (response )
360+
361+ if not versions :
362+ secret_name = _extract_secret_name (secret .name )
363+ raise ConnectorSecretWithNoValidVersionsError (
364+ connector_name = connector_name ,
365+ secret_name = secret_name ,
366+ gcp_project_id = gcp_project_id ,
367+ )
368+
369+ enabled_version = versions [0 ]
370+
371+ response = client .access_secret_version (name = enabled_version .name )
278372 file_path .write_text (response .payload .data .decode ("UTF-8" ))
279373 file_path .chmod (0o600 ) # default to owner read/write only
280374
0 commit comments