dataflow-core 2.1.14__py3-none-any.whl → 2.1.15rc1__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.
@@ -3,6 +3,7 @@ import os
3
3
  from .interface import SecretManager
4
4
  from .providers.aws_manager import AWSSecretsManager
5
5
  from .providers.azure_manager import AzureKeyVault
6
+ from .providers.gcp_manager import GCPSecretsManager
6
7
  from ..configuration import ConfigurationManager
7
8
 
8
9
  # A custom exception for clear error messages
@@ -17,10 +18,6 @@ def get_secret_manager() -> SecretManager:
17
18
  to determine which cloud provider's secret manager to instantiate.
18
19
  """
19
20
  try:
20
- # dataflow_config = None
21
- # if os.getenv('HOSTNAME'):
22
- # dataflow_config = ConfigurationManager('/dataflow/app/auth_config/dataflow_auth.cfg')
23
- # else:
24
21
  dataflow_config = ConfigurationManager('/dataflow/app/config/dataflow.cfg')
25
22
  except Exception as e:
26
23
  raise SecretProviderError(
@@ -49,11 +46,20 @@ def get_secret_manager() -> SecretManager:
49
46
  )
50
47
  return AzureKeyVault(vault_url=vault_url)
51
48
 
52
- # You can easily add more providers here in the future
53
- # elif provider == "gcp":
54
- # return GCPSecretManager()
49
+ elif provider == "gcp":
50
+ project_id = dataflow_config.get_config_value('cloudProvider', 'gcp_project_id')
51
+ region = dataflow_config.get_config_value('cloudProvider', 'gcp_region')
52
+ if not project_id:
53
+ raise SecretProviderError(
54
+ "GCP_PROJECT_ID must be set when using the GCP provider."
55
+ )
56
+ if not region:
57
+ raise SecretProviderError(
58
+ "GCP_REGION must be set when using the GCP provider."
59
+ )
60
+ return GCPSecretsManager(project_id=project_id, region=region)
55
61
 
56
62
  else:
57
63
  raise SecretProviderError(
58
- f"Unsupported secret provider: '{provider}'. Supported providers are: aws, azure."
64
+ f"Unsupported secret provider: '{provider}'. Supported providers are: aws, azure and gcp"
59
65
  )
@@ -0,0 +1,332 @@
1
+ import os, base64, json, atexit
2
+ from pathlib import Path
3
+ from google.cloud import secretmanager
4
+ from google.cloud.secretmanager_v1 import SecretManagerServiceClient
5
+ from google.cloud.secretmanager_v1.types import Secret, SecretPayload
6
+ from google.api_core.exceptions import (
7
+ AlreadyExists,
8
+ NotFound,
9
+ PermissionDenied,
10
+ Forbidden,
11
+ ResourceExhausted,
12
+ InvalidArgument,
13
+ FailedPrecondition
14
+ )
15
+ import json
16
+ from ..interface import SecretManager
17
+ from ...utils.exceptions import (
18
+ SecretNotFoundException,
19
+ SecretAlreadyExistsException,
20
+ SecretManagerAuthException,
21
+ SecretManagerServiceException
22
+ )
23
+
24
+ def _setup_gcp_credentials():
25
+ """Setup GCP credentials from base64 encoded JSON environment variable"""
26
+
27
+ # Only run if GOOGLE_APPLICATION_CREDENTIALS is not already set
28
+ if os.getenv('GOOGLE_APPLICATION_CREDENTIALS'):
29
+ return
30
+
31
+ # Get base64 encoded JSON credentials
32
+ encoded_json = os.getenv('GOOGLE_APPLICATION_CREDENTIALS_JSON')
33
+
34
+ if encoded_json:
35
+ try:
36
+ # Decode base64 to JSON string
37
+ json_credentials = base64.b64decode(encoded_json).decode('utf-8')
38
+
39
+ # Validate it's valid JSON
40
+ json.loads(json_credentials) # Just to validate
41
+
42
+ # Create credentials file in home directory
43
+ home_dir = Path.home()
44
+ credentials_dir = home_dir / '.gcp'
45
+ credentials_dir.mkdir(exist_ok=True) # Create .gcp directory if it doesn't exist
46
+
47
+ credentials_path = credentials_dir / 'credentials.json'
48
+
49
+ # Write JSON string to credentials file
50
+ with open(credentials_path, 'w') as f:
51
+ f.write(json_credentials)
52
+
53
+ # Set the standard Google environment variable that the SDK looks for
54
+ os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = str(credentials_path)
55
+
56
+ # Clean up file on exit
57
+ atexit.register(lambda: credentials_path.unlink() if credentials_path.exists() else None)
58
+
59
+ print(f"GCP credentials decoded and configured at {credentials_path}")
60
+
61
+ except Exception as e:
62
+ print(f"Error setting up GCP credentials: {e}")
63
+
64
+
65
+ class GCPSecretsManager(SecretManager):
66
+ """Google Cloud Platform Secrets Manager implementation."""
67
+
68
+ def __init__(self, project_id: str, region: str):
69
+ """Initialize the GCP Secret Manager client.
70
+
71
+ Args:
72
+ project_id: The GCP project ID where secrets will be stored.
73
+ region: The GCP region where secrets will be stored.
74
+ """
75
+ self.project_id = project_id
76
+ self.region = region
77
+ try:
78
+ _setup_gcp_credentials()
79
+ self.client = secretmanager.SecretManagerServiceClient()
80
+ except PermissionDenied as e:
81
+ raise SecretManagerAuthException("initialize_gcp_client", original_error=str(e))
82
+ except Exception as e:
83
+ raise SecretManagerServiceException("initialize_gcp_client", original_error=str(e))
84
+
85
+ def _get_secret_path(self, vault_path: str) -> str:
86
+ """Get the full secret path in GCP format.
87
+
88
+ Args:
89
+ vault_path: The path/name of the secret.
90
+
91
+ Returns:
92
+ The full path to the secret in GCP format.
93
+ """
94
+ return f"projects/{self.project_id}/secrets/{vault_path}"
95
+
96
+ def _get_secret_version_path(self, vault_path: str, version: str = "latest") -> str:
97
+ """Get the full path to a specific secret version.
98
+
99
+ Args:
100
+ vault_path: The path/name of the secret.
101
+ version: The version of the secret (default is "latest").
102
+
103
+ Returns:
104
+ The full path to the secret version in GCP format.
105
+ """
106
+ return f"{self._get_secret_path(vault_path)}/versions/{version}"
107
+
108
+ def create_secret(self, vault_path: str, secret_data: dict) -> str:
109
+ """Create a new secret.
110
+
111
+ Args:
112
+ vault_path: The path/name of the secret.
113
+ region: The region where the secret will be stored.
114
+ secret_data: The data to store in the secret.
115
+
116
+ Returns:
117
+ A success message.
118
+
119
+ Raises:
120
+ SecretAlreadyExistsException: If the secret already exists.
121
+ SecretManagerAuthException: If there are permission issues.
122
+ SecretManagerServiceException: For other service errors.
123
+ """
124
+ try:
125
+ # Convert dictionary to JSON string before saving
126
+ secret_string = json.dumps(secret_data)
127
+
128
+ # First create the secret
129
+ parent = f"projects/{self.project_id}"
130
+ secret = Secret(
131
+ replication={
132
+ "user_managed": {
133
+ "replicas": [
134
+ {"location": self.region }
135
+ ]
136
+ }
137
+ }
138
+ )
139
+
140
+ self.client.create_secret(
141
+ request={
142
+ "parent": parent,
143
+ "secret_id": vault_path,
144
+ "secret": secret
145
+ }
146
+ )
147
+
148
+ # Then add the secret version with the data
149
+ secret_path = self._get_secret_path(vault_path)
150
+ self.client.add_secret_version(
151
+ request={
152
+ "parent": secret_path,
153
+ "payload": {"data": secret_string.encode("UTF-8")}
154
+ }
155
+ )
156
+
157
+ return "Secret created successfully"
158
+ except AlreadyExists as e:
159
+ raise SecretAlreadyExistsException("secret", vault_path, original_error=str(e))
160
+ except PermissionDenied as e:
161
+ raise SecretManagerAuthException("create_secret", original_error=str(e))
162
+ except Forbidden as e:
163
+ raise SecretManagerAuthException("create_secret", original_error=str(e))
164
+ except FailedPrecondition as e:
165
+ # Handle case where secret might be in recovery/deleted state
166
+ if "pending deletion" in str(e).lower() or "scheduled for deletion" in str(e).lower():
167
+ raise SecretAlreadyExistsException("secret", vault_path, original_error=str(e), is_scheduled_for_deletion=True)
168
+ else:
169
+ raise SecretManagerServiceException("create_secret", original_error=str(e))
170
+ except ResourceExhausted as e:
171
+ raise SecretManagerServiceException("create_secret", original_error=str(e))
172
+ except Exception as e:
173
+ raise SecretManagerServiceException("create_secret", original_error=str(e))
174
+
175
+ def get_secret_by_key(self, vault_path: str) -> dict:
176
+ """Get a secret by its key.
177
+
178
+ Args:
179
+ vault_path: The path/name of the secret.
180
+
181
+ Returns:
182
+ The secret data as a dictionary.
183
+
184
+ Raises:
185
+ SecretNotFoundException: If the secret doesn't exist.
186
+ SecretManagerAuthException: If there are permission issues.
187
+ SecretManagerServiceException: For other service errors.
188
+ """
189
+ try:
190
+ # Get the latest version of the secret
191
+ name = self._get_secret_version_path(vault_path)
192
+ response = self.client.access_secret_version(request={"name": name})
193
+
194
+ # Decode the payload and convert JSON string back to dictionary
195
+ secret_string = response.payload.data.decode("UTF-8")
196
+ secret_data = json.loads(secret_string)
197
+ return secret_data
198
+ except NotFound as e:
199
+ raise SecretNotFoundException("secret", vault_path, original_error=str(e))
200
+ except PermissionDenied as e:
201
+ raise SecretManagerAuthException("get_secret", original_error=str(e))
202
+ except Forbidden as e:
203
+ raise SecretManagerAuthException("get_secret", original_error=str(e))
204
+ except InvalidArgument as e:
205
+ raise SecretManagerServiceException("get_secret", original_error=str(e))
206
+ except json.JSONDecodeError as e:
207
+ raise SecretManagerServiceException("get_secret", original_error=str(e))
208
+ except Exception as e:
209
+ raise SecretManagerServiceException("get_secret", original_error=str(e))
210
+
211
+ def update_secret(self, vault_path: str, update_data: dict) -> str:
212
+ """Update an existing secret.
213
+
214
+ Args:
215
+ vault_path: The path/name of the secret.
216
+ update_data: The data to update in the secret.
217
+
218
+ Returns:
219
+ A success message.
220
+
221
+ Raises:
222
+ SecretNotFoundException: If the secret doesn't exist.
223
+ SecretManagerAuthException: If there are permission issues.
224
+ SecretManagerServiceException: For other service errors.
225
+ """
226
+ try:
227
+ # Get current secret data
228
+ current_data = self.get_secret_by_key(vault_path)
229
+
230
+ # Update with new data
231
+ current_data.update(update_data)
232
+
233
+ # Convert updated dictionary to JSON string
234
+ updated_string = json.dumps(current_data)
235
+
236
+ # Add a new version of the secret with updated data
237
+ secret_path = self._get_secret_path(vault_path)
238
+ self.client.add_secret_version(
239
+ request={
240
+ "parent": secret_path,
241
+ "payload": {"data": updated_string.encode("UTF-8")}
242
+ }
243
+ )
244
+
245
+ return "Secret updated successfully"
246
+ except SecretNotFoundException:
247
+ raise
248
+ except PermissionDenied as e:
249
+ raise SecretManagerAuthException("update_secret", original_error=str(e))
250
+ except Forbidden as e:
251
+ raise SecretManagerAuthException("update_secret", original_error=str(e))
252
+ except NotFound as e:
253
+ raise SecretNotFoundException("secret", vault_path, original_error=str(e))
254
+ except InvalidArgument as e:
255
+ raise SecretManagerServiceException("update_secret", original_error=str(e))
256
+ except json.JSONDecodeError as e:
257
+ raise SecretManagerServiceException("update_secret", original_error=str(e))
258
+ except Exception as e:
259
+ raise SecretManagerServiceException("update_secret", original_error=str(e))
260
+
261
+ def delete_secret(self, vault_path: str) -> str:
262
+ """Delete a secret.
263
+
264
+ Args:
265
+ vault_path: The path/name of the secret.
266
+
267
+ Returns:
268
+ A success message.
269
+
270
+ Raises:
271
+ SecretNotFoundException: If the secret doesn't exist.
272
+ SecretManagerAuthException: If there are permission issues.
273
+ SecretManagerServiceException: For other service errors.
274
+ """
275
+ try:
276
+ # Get the full path to the secret
277
+ name = self._get_secret_path(vault_path)
278
+
279
+ # For git-ssh secrets, destroy without recovery
280
+ if "git-ssh" in vault_path:
281
+ # Get all versions to destroy them permanently
282
+ versions = self.client.list_secret_versions(request={"parent": name})
283
+ for version in versions:
284
+ if version.state == secretmanager.SecretVersion.State.ENABLED:
285
+ version_name = f"{name}/versions/{version.name.split('/')[-1]}"
286
+ self.client.destroy_secret_version(request={"name": version_name})
287
+
288
+ # Delete the secret itself
289
+ self.client.delete_secret(request={"name": name})
290
+ else:
291
+ # For regular secrets, use the default 7-day recovery window
292
+ self.client.delete_secret(request={
293
+ "name": name,
294
+ # In GCP, the recovery window is configured at the service level,
295
+ # not per API call, so we don't specify it here
296
+ })
297
+
298
+ return "Secret deleted successfully"
299
+ except NotFound as e:
300
+ raise SecretNotFoundException("secret", vault_path, original_error=str(e))
301
+ except PermissionDenied as e:
302
+ raise SecretManagerAuthException("delete_secret", original_error=str(e))
303
+ except Forbidden as e:
304
+ raise SecretManagerAuthException("delete_secret", original_error=str(e))
305
+ except InvalidArgument as e:
306
+ raise SecretManagerServiceException("delete_secret", original_error=str(e))
307
+ except Exception as e:
308
+ raise SecretManagerServiceException("delete_secret", original_error=str(e))
309
+
310
+ def test_connection(self, vault_path: str) -> str:
311
+ """Test the connection to the secret manager by attempting to access a secret.
312
+
313
+ Args:
314
+ vault_path: The path/name of the secret to test.
315
+
316
+ Returns:
317
+ The status of the secret.
318
+
319
+ Raises:
320
+ SecretNotFoundException: If the secret doesn't exist.
321
+ SecretManagerAuthException: If there are permission issues.
322
+ SecretManagerServiceException: For other service errors.
323
+ """
324
+ try:
325
+ secret = self.get_secret_by_key(vault_path)
326
+ return secret.get('status', 'Unknown')
327
+ except SecretNotFoundException:
328
+ raise
329
+ except (SecretManagerAuthException, SecretManagerServiceException):
330
+ raise
331
+ except Exception as e:
332
+ raise SecretManagerServiceException("test_connection", original_error=str(e))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataflow-core
3
- Version: 2.1.14
3
+ Version: 2.1.15rc1
4
4
  Summary: Dataflow core package
5
5
  Author: Dataflow
6
6
  Author-email:
@@ -11,6 +11,8 @@ Requires-Dist: pymysql
11
11
  Requires-Dist: requests
12
12
  Requires-Dist: azure-identity
13
13
  Requires-Dist: azure-keyvault-secrets
14
+ Requires-Dist: google-auth
15
+ Requires-Dist: google-cloud-secret-manager
14
16
  Dynamic: author
15
17
  Dynamic: requires-dist
16
18
  Dynamic: summary
@@ -40,18 +40,19 @@ dataflow/scripts/clone_environment.sh,sha256=Qy0GylsA3kUVUL_L1MirxIWujOFhT1tikKq
40
40
  dataflow/scripts/create_environment.sh,sha256=3FHgNplJuEZvyTsLqlCJNX9oyfXgsfqn80VZk2xtvso,828
41
41
  dataflow/scripts/update_environment.sh,sha256=2dtn2xlNi6frpig-sqlGE1_IKRbbkqYOCpf_qyMKKII,992
42
42
  dataflow/secrets_manager/__init__.py,sha256=idGqIDtYl0De2WIK9Obl-N7SDPSYtVM0D-wXfZjCiy4,559
43
- dataflow/secrets_manager/factory.py,sha256=k1sIyXBKtas1upWJpq8Mks2d8kjLAHU7CFvjeuMXXxs,2160
43
+ dataflow/secrets_manager/factory.py,sha256=LblshkGG9q2C3RHYp0QykianUtpOOQz7sBdlerutyWY,2479
44
44
  dataflow/secrets_manager/interface.py,sha256=HhrKpQrprWIbDsVfU_qc59OXmSIuHXv106OXv6-Epqc,506
45
45
  dataflow/secrets_manager/service.py,sha256=SSWgTXJTAwVPqMIc76cB2hR6nghNVOoMpIN9M0i7Su0,7241
46
46
  dataflow/secrets_manager/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
47
  dataflow/secrets_manager/providers/aws_manager.py,sha256=16peXyKeuAjv2RVTMUjrzArPYENK9Zu7jREWVgMfScA,8671
48
48
  dataflow/secrets_manager/providers/azure_manager.py,sha256=sWOz-7ALnLt6vyM3lt14GBpzpmDnlH3hkdqtuApqkgU,9430
49
+ dataflow/secrets_manager/providers/gcp_manager.py,sha256=AJgotHZRQraxtmfJX1Z8u2Gcr7KJLRJTN_qbth3A5Xk,13738
49
50
  dataflow/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
51
  dataflow/utils/exceptions.py,sha256=8GRFoYZ5dPGQckVm2znaHpPi0ZAs69fK-RGKukEsapk,4432
51
52
  dataflow/utils/get_current_user.py,sha256=4nSO3SPVMZhW-MsIgxR3f9ZzrFaIZIuyrM6hvfyE7PQ,1202
52
53
  dataflow/utils/logger.py,sha256=7BFrOq5Oiqn8P4XZbgJzMP5O07d2fpdECbbfsjrUuHw,1213
53
- dataflow_core-2.1.14.dist-info/METADATA,sha256=er2J0BBiZuIgOJJH5yG0mLhVQ81D6qrhHZOad_dYPLE,370
54
- dataflow_core-2.1.14.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
55
- dataflow_core-2.1.14.dist-info/entry_points.txt,sha256=ppj_EIbYrJJwCPg1kfdsZk5q1N-Ejfis1neYrnjhO8o,117
56
- dataflow_core-2.1.14.dist-info/top_level.txt,sha256=SZsUOpSCK9ntUy-3Tusxzf5A2e8ebwD8vouPb1dPt_8,23
57
- dataflow_core-2.1.14.dist-info/RECORD,,
54
+ dataflow_core-2.1.15rc1.dist-info/METADATA,sha256=aRoxdVVfP0yY6qPtPuSH_RGHqxAQ13HCTjLq0UxRnR0,443
55
+ dataflow_core-2.1.15rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
56
+ dataflow_core-2.1.15rc1.dist-info/entry_points.txt,sha256=ppj_EIbYrJJwCPg1kfdsZk5q1N-Ejfis1neYrnjhO8o,117
57
+ dataflow_core-2.1.15rc1.dist-info/top_level.txt,sha256=SZsUOpSCK9ntUy-3Tusxzf5A2e8ebwD8vouPb1dPt_8,23
58
+ dataflow_core-2.1.15rc1.dist-info/RECORD,,