cumulusci-plus 5.0.21__py3-none-any.whl → 5.0.43__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.
Files changed (135) hide show
  1. cumulusci/__about__.py +1 -1
  2. cumulusci/cli/logger.py +2 -2
  3. cumulusci/cli/service.py +20 -0
  4. cumulusci/cli/task.py +19 -3
  5. cumulusci/cli/tests/test_error.py +3 -1
  6. cumulusci/cli/tests/test_flow.py +279 -2
  7. cumulusci/cli/tests/test_org.py +5 -0
  8. cumulusci/cli/tests/test_service.py +15 -12
  9. cumulusci/cli/tests/test_task.py +122 -2
  10. cumulusci/cli/tests/utils.py +1 -4
  11. cumulusci/core/config/__init__.py +1 -0
  12. cumulusci/core/config/base_task_flow_config.py +26 -1
  13. cumulusci/core/config/org_config.py +2 -1
  14. cumulusci/core/config/project_config.py +14 -20
  15. cumulusci/core/config/scratch_org_config.py +12 -0
  16. cumulusci/core/config/tests/test_config.py +1 -0
  17. cumulusci/core/config/tests/test_config_expensive.py +9 -3
  18. cumulusci/core/config/universal_config.py +3 -4
  19. cumulusci/core/dependencies/base.py +5 -1
  20. cumulusci/core/dependencies/dependencies.py +1 -1
  21. cumulusci/core/dependencies/github.py +1 -2
  22. cumulusci/core/dependencies/resolvers.py +1 -1
  23. cumulusci/core/dependencies/tests/test_dependencies.py +1 -1
  24. cumulusci/core/dependencies/tests/test_resolvers.py +1 -1
  25. cumulusci/core/flowrunner.py +90 -6
  26. cumulusci/core/github.py +1 -1
  27. cumulusci/core/sfdx.py +3 -1
  28. cumulusci/core/source_transforms/tests/test_transforms.py +1 -1
  29. cumulusci/core/source_transforms/transforms.py +1 -1
  30. cumulusci/core/tasks.py +13 -2
  31. cumulusci/core/tests/test_flowrunner.py +100 -0
  32. cumulusci/core/tests/test_tasks.py +65 -0
  33. cumulusci/core/utils.py +3 -1
  34. cumulusci/core/versions.py +1 -1
  35. cumulusci/cumulusci.yml +73 -1
  36. cumulusci/oauth/client.py +1 -1
  37. cumulusci/plugins/plugin_base.py +5 -3
  38. cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +1 -1
  39. cumulusci/salesforce_api/rest_deploy.py +1 -1
  40. cumulusci/schema/cumulusci.jsonschema.json +69 -0
  41. cumulusci/tasks/apex/anon.py +1 -1
  42. cumulusci/tasks/apex/testrunner.py +421 -144
  43. cumulusci/tasks/apex/tests/test_apex_tasks.py +917 -1
  44. cumulusci/tasks/bulkdata/extract.py +0 -1
  45. cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py +1 -1
  46. cumulusci/tasks/bulkdata/extract_dataset_utils/synthesize_extract_declarations.py +1 -1
  47. cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_extract_yml.py +1 -1
  48. cumulusci/tasks/bulkdata/generate_and_load_data.py +136 -12
  49. cumulusci/tasks/bulkdata/mapping_parser.py +139 -44
  50. cumulusci/tasks/bulkdata/select_utils.py +1 -1
  51. cumulusci/tasks/bulkdata/snowfakery.py +100 -25
  52. cumulusci/tasks/bulkdata/tests/test_generate_and_load.py +159 -0
  53. cumulusci/tasks/bulkdata/tests/test_load.py +0 -2
  54. cumulusci/tasks/bulkdata/tests/test_mapping_parser.py +763 -1
  55. cumulusci/tasks/bulkdata/tests/test_select_utils.py +46 -0
  56. cumulusci/tasks/bulkdata/tests/test_snowfakery.py +133 -0
  57. cumulusci/tasks/create_package_version.py +190 -16
  58. cumulusci/tasks/datadictionary.py +1 -1
  59. cumulusci/tasks/metadata_etl/__init__.py +2 -0
  60. cumulusci/tasks/metadata_etl/applications.py +256 -0
  61. cumulusci/tasks/metadata_etl/base.py +7 -3
  62. cumulusci/tasks/metadata_etl/layouts.py +1 -1
  63. cumulusci/tasks/metadata_etl/permissions.py +1 -1
  64. cumulusci/tasks/metadata_etl/remote_site_settings.py +2 -2
  65. cumulusci/tasks/metadata_etl/tests/test_applications.py +710 -0
  66. cumulusci/tasks/push/README.md +15 -17
  67. cumulusci/tasks/release_notes/README.md +13 -13
  68. cumulusci/tasks/release_notes/generator.py +13 -8
  69. cumulusci/tasks/robotframework/tests/test_robotframework.py +6 -1
  70. cumulusci/tasks/salesforce/Deploy.py +53 -2
  71. cumulusci/tasks/salesforce/SfPackageCommands.py +363 -0
  72. cumulusci/tasks/salesforce/__init__.py +1 -0
  73. cumulusci/tasks/salesforce/assign_ps_psg.py +448 -0
  74. cumulusci/tasks/salesforce/composite.py +1 -1
  75. cumulusci/tasks/salesforce/custom_settings_wait.py +1 -1
  76. cumulusci/tasks/salesforce/enable_prediction.py +5 -1
  77. cumulusci/tasks/salesforce/getPackageVersion.py +89 -0
  78. cumulusci/tasks/salesforce/insert_record.py +18 -19
  79. cumulusci/tasks/salesforce/sourcetracking.py +1 -1
  80. cumulusci/tasks/salesforce/tests/test_Deploy.py +316 -1
  81. cumulusci/tasks/salesforce/tests/test_SfPackageCommands.py +554 -0
  82. cumulusci/tasks/salesforce/tests/test_assign_ps_psg.py +1055 -0
  83. cumulusci/tasks/salesforce/tests/test_enable_prediction.py +4 -2
  84. cumulusci/tasks/salesforce/tests/test_getPackageVersion.py +651 -0
  85. cumulusci/tasks/salesforce/tests/test_update_dependencies.py +1 -1
  86. cumulusci/tasks/salesforce/tests/test_update_external_auth_identity_provider.py +927 -0
  87. cumulusci/tasks/salesforce/tests/test_update_external_credential.py +1427 -0
  88. cumulusci/tasks/salesforce/tests/test_update_named_credential.py +1042 -0
  89. cumulusci/tasks/salesforce/tests/test_update_record.py +512 -0
  90. cumulusci/tasks/salesforce/update_dependencies.py +2 -2
  91. cumulusci/tasks/salesforce/update_external_auth_identity_provider.py +551 -0
  92. cumulusci/tasks/salesforce/update_external_credential.py +647 -0
  93. cumulusci/tasks/salesforce/update_named_credential.py +441 -0
  94. cumulusci/tasks/salesforce/update_profile.py +17 -13
  95. cumulusci/tasks/salesforce/update_record.py +217 -0
  96. cumulusci/tasks/salesforce/users/permsets.py +62 -5
  97. cumulusci/tasks/salesforce/users/tests/test_permsets.py +237 -11
  98. cumulusci/tasks/sfdmu/__init__.py +0 -0
  99. cumulusci/tasks/sfdmu/sfdmu.py +376 -0
  100. cumulusci/tasks/sfdmu/tests/__init__.py +1 -0
  101. cumulusci/tasks/sfdmu/tests/test_runner.py +212 -0
  102. cumulusci/tasks/sfdmu/tests/test_sfdmu.py +1012 -0
  103. cumulusci/tasks/tests/test_create_package_version.py +716 -1
  104. cumulusci/tasks/tests/test_util.py +42 -0
  105. cumulusci/tasks/util.py +37 -1
  106. cumulusci/tasks/utility/copyContents.py +402 -0
  107. cumulusci/tasks/utility/credentialManager.py +302 -0
  108. cumulusci/tasks/utility/directoryRecreator.py +30 -0
  109. cumulusci/tasks/utility/env_management.py +1 -1
  110. cumulusci/tasks/utility/secretsToEnv.py +135 -0
  111. cumulusci/tasks/utility/tests/test_copyContents.py +1719 -0
  112. cumulusci/tasks/utility/tests/test_credentialManager.py +1150 -0
  113. cumulusci/tasks/utility/tests/test_directoryRecreator.py +439 -0
  114. cumulusci/tasks/utility/tests/test_secretsToEnv.py +1118 -0
  115. cumulusci/tests/test_integration_infrastructure.py +3 -1
  116. cumulusci/tests/test_utils.py +70 -6
  117. cumulusci/utils/__init__.py +54 -9
  118. cumulusci/utils/classutils.py +5 -2
  119. cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +31 -30
  120. cumulusci/utils/options.py +23 -1
  121. cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +1 -1
  122. cumulusci/utils/yaml/cumulusci_yml.py +8 -3
  123. cumulusci/utils/yaml/model_parser.py +2 -2
  124. cumulusci/utils/yaml/tests/test_cumulusci_yml.py +1 -1
  125. cumulusci/utils/yaml/tests/test_model_parser.py +3 -3
  126. cumulusci/vcs/base.py +23 -15
  127. cumulusci/vcs/bootstrap.py +5 -4
  128. cumulusci/vcs/utils/list_modified_files.py +189 -0
  129. cumulusci/vcs/utils/tests/test_list_modified_files.py +588 -0
  130. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/METADATA +11 -10
  131. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/RECORD +135 -104
  132. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/WHEEL +1 -1
  133. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/entry_points.txt +0 -0
  134. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/licenses/AUTHORS.rst +0 -0
  135. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,302 @@
1
+ import logging
2
+ import os
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Optional, Union
5
+
6
+
7
+ # Abstract Base Class for Credential Providers
8
+ class CredentialProvider(ABC):
9
+ """
10
+ Abstract Base Class that defines the interface for all credential providers.
11
+ """
12
+
13
+ # A class-level dictionary to store a mapping of provider names to provider classes
14
+ _registry = {}
15
+ logger: logging.Logger
16
+
17
+ def __init__(self, **kwargs):
18
+ self.key_prefix = kwargs.get(
19
+ "key_prefix", os.getenv("CUMULUSCI_PREFIX_SECRETS", "").upper()
20
+ )
21
+ self.logger = logging.getLogger(__name__)
22
+
23
+ def __init_subclass__(cls, **kwargs):
24
+ """
25
+ This method is called automatically when a new class inherits from CredentialProvider.
26
+ It's used to register the new class in our registry.
27
+ """
28
+ super().__init_subclass__(**kwargs)
29
+ # We'll use an attribute on each class to define its provider type.
30
+ # This will be the key in our registry.
31
+ if hasattr(cls, "provider_type"):
32
+ CredentialProvider._registry[cls.provider_type] = cls
33
+
34
+ @abstractmethod
35
+ def get_credentials(
36
+ self, key: str, options: Optional[dict[str, Any]] = None
37
+ ) -> Any:
38
+ """
39
+ Retrieves and returns credentials.
40
+ """
41
+ raise NotImplementedError("Subclasses must implement this method")
42
+
43
+ @abstractmethod
44
+ def get_all_credentials(
45
+ self, key: str, options: Optional[dict[str, Any]] = None
46
+ ) -> Any:
47
+ """
48
+ Retrieves and returns all credentials in a group, for example all secrets in AWS secret.
49
+ """
50
+ raise NotImplementedError("Subclasses must implement this method")
51
+
52
+ def get_key(self, key: str) -> str:
53
+ return f"{self.key_prefix}{key}"
54
+
55
+
56
+ # Concrete Credential Provider for Local Development
57
+ class DevEnvironmentVariableProvider(CredentialProvider):
58
+ """
59
+ Retrieves secrets from environment variables.
60
+ This is suitable for local development.
61
+ """
62
+
63
+ provider_type = "local"
64
+
65
+ def get_credentials(
66
+ self, key: str, options: Optional[dict[str, Any]] = None
67
+ ) -> Any:
68
+ value = options.get("value", None)
69
+ if isinstance(value, dict):
70
+ value = next(iter(value.values()))
71
+
72
+ self.logger.info(f"Credentials for {key} from local environment is {value}.")
73
+ return value
74
+
75
+ def get_all_credentials(
76
+ self, key: str, options: Optional[dict[str, Any]] = None
77
+ ) -> Any:
78
+ """Local provider doesn't support retrieving all credentials."""
79
+ raise NotImplementedError("Local provider doesn't support get_all_credentials")
80
+
81
+
82
+ # Concrete Credential Provider for Local Development
83
+ class EnvironmentVariableProvider(CredentialProvider):
84
+ """
85
+ Retrieves secrets from environment variables.
86
+ This is suitable for local development.
87
+ """
88
+
89
+ provider_type = "environment"
90
+
91
+ def get_credentials(
92
+ self, key: str, options: Optional[dict[str, Any]] = None
93
+ ) -> Any:
94
+ value = options.get("value", None)
95
+ if isinstance(value, dict):
96
+ value = next(iter(value.values()))
97
+
98
+ ret_value = os.getenv(self.get_key(key))
99
+ if ret_value is None and value is not None:
100
+ ret_value = os.getenv(self.get_key(value))
101
+ if ret_value is None:
102
+ self.logger.info(f"Credentials for {key} from environment is {value}.")
103
+ return ret_value or value
104
+
105
+ def get_all_credentials(
106
+ self, key: str, options: Optional[dict[str, Any]] = None
107
+ ) -> Any:
108
+ """Environment provider doesn't support retrieving all credentials."""
109
+ raise NotImplementedError(
110
+ "Environment provider doesn't support get_all_credentials"
111
+ )
112
+
113
+
114
+ # Concrete Credential Provider for Azure Pipelines
115
+ class AwsSecretsManagerProvider(CredentialProvider):
116
+ """
117
+ Retrieves secrets from AWS Secrets Manager.
118
+ This is designed for use in a CI/CD environment like Azure Pipelines,
119
+ where a Service Connection provides a role to assume.
120
+ """
121
+
122
+ provider_type = "aws_secrets"
123
+ secrets_cache: dict[str, dict[str, Any]]
124
+ aws_region: str
125
+
126
+ def __init__(self, **kwargs):
127
+ super().__init__(**kwargs)
128
+ self.secrets_cache = kwargs.get("secrets_cache", {})
129
+
130
+ self.aws_region = kwargs.get("aws_region", os.getenv("AWS_REGION", None))
131
+ if self.aws_region is None:
132
+ raise ValueError(
133
+ "AWS_REGION environment variable or aws_region option is required for AWS Secrets Manager."
134
+ )
135
+
136
+ def get_credentials(
137
+ self, key: str, options: Optional[dict[str, Any]] = None
138
+ ) -> Any:
139
+ """
140
+ Connects to AWS Secrets Manager to retrieve a secret.
141
+ The boto3 client automatically uses the credentials provided by the
142
+ Azure DevOps AWS Service Connection (e.g., through OIDC or static keys).
143
+ """
144
+ return self.aws_creds(key, options).get(key, None)
145
+
146
+ def get_all_credentials(
147
+ self, key: str, options: Optional[dict[str, Any]] = None
148
+ ) -> Any:
149
+ return self.aws_creds(key, options)
150
+
151
+ def aws_creds(
152
+ self, key: str, options: Optional[dict[str, Any]] = None
153
+ ) -> dict[str, Any]:
154
+ secret_name = options.get("secret_name", None)
155
+ if secret_name is None:
156
+ raise ValueError("Secret name is required for AWS Secrets Manager.")
157
+
158
+ try:
159
+ import json
160
+
161
+ import boto3
162
+ from botocore.exceptions import ClientError
163
+ except ImportError:
164
+ raise RuntimeError(
165
+ "boto3 is not installed. Please install it using 'pip install boto3' or 'pipx inject cumulusci-plus-azure-devops boto3'."
166
+ )
167
+
168
+ try:
169
+ if secret_name in self.secrets_cache:
170
+ return self.secrets_cache[secret_name]
171
+
172
+ # Boto3 automatically handles credential lookup. In an Azure Pipeline,
173
+ # it will find the temporary credentials provided by the AWS Service Connection.
174
+ # Create a Secrets Manager client
175
+ session = boto3.session.Session()
176
+ client = session.client(
177
+ service_name="secretsmanager", region_name=self.aws_region
178
+ )
179
+
180
+ get_secret_value_response = client.get_secret_value(SecretId=secret_name)
181
+
182
+ if "SecretString" in get_secret_value_response:
183
+ secret = json.loads(get_secret_value_response["SecretString"])
184
+ elif "SecretBinary" in get_secret_value_response:
185
+ file_path = self.create_binary_file(
186
+ secret_name, get_secret_value_response["SecretBinary"]
187
+ )
188
+ secret_key = key if key and key != "*" else os.path.basename(file_path)
189
+ secret = {secret_key: file_path}
190
+ else:
191
+ raise ValueError(f"Secret {secret_name} is not a valid secret.")
192
+
193
+ # Assuming the secret is a JSON string with the credentials
194
+ # We need to check the binary type of the secret at later development
195
+ self.secrets_cache[secret_name] = secret
196
+
197
+ return self.secrets_cache[secret_name]
198
+ except ClientError as e:
199
+ # For a list of exceptions thrown, see
200
+ # https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html
201
+ raise e
202
+ except ValueError:
203
+ # Re-raise ValueError as-is (e.g., invalid secret format)
204
+ raise
205
+ except Exception as e:
206
+ raise RuntimeError(f"Failed to retrieve secret '{key}': {e}")
207
+
208
+ def create_binary_file(self, secret_name: str, content: Union[str, bytes]) -> str:
209
+ """
210
+ Write the binary content to a file.
211
+ secret_name is relative path to the root of the project.
212
+ create the directory if it doesn't exist.
213
+ return the absolute path to the file with windows or linux path separator.
214
+ """
215
+ absolute_path = os.path.abspath(os.path.join(".cci", secret_name))
216
+
217
+ try:
218
+ os.makedirs(os.path.dirname(absolute_path), exist_ok=True)
219
+ with open(absolute_path, "wb") as f:
220
+ f.write(content)
221
+ return absolute_path
222
+ except Exception as e:
223
+ raise RuntimeError(f"Failed to create binary file {absolute_path}: {e}")
224
+
225
+
226
+ # Concrete Credential Provider for Azure Variable Groups
227
+ class AzureVariableGroupProvider(CredentialProvider):
228
+ """
229
+ Retrieves secrets from Azure Pipeline Variable Groups.
230
+ When a variable group is linked, its variables are exposed as
231
+ environment variables in the pipeline job.
232
+ """
233
+
234
+ provider_type = "ado_variables"
235
+
236
+ def get_credentials(
237
+ self, key: str, options: Optional[dict[str, Any]] = None
238
+ ) -> Any:
239
+ """
240
+ Looks for AWS credentials in environment variables exposed by
241
+ an Azure variable group.
242
+ """
243
+ value = options.get("value", None)
244
+ if isinstance(value, dict):
245
+ value = next(iter(value.values()))
246
+
247
+ # Azure pipelines convert variable names to uppercase and replace dots with underscores.
248
+ ret_value = os.getenv(self.get_key(key).upper().replace(".", "_"))
249
+
250
+ if ret_value is None and value is not None:
251
+ ret_value = os.getenv(self.get_key(value).upper().replace(".", "_"))
252
+
253
+ return ret_value
254
+
255
+ def get_all_credentials(
256
+ self, key: str, options: Optional[dict[str, Any]] = None
257
+ ) -> Any:
258
+ """Azure variable group provider doesn't support retrieving all credentials."""
259
+ raise NotImplementedError(
260
+ "Azure variable group provider doesn't support get_all_credentials"
261
+ )
262
+
263
+
264
+ # The CredentialManager to select the right provider
265
+ class CredentialManager:
266
+ """
267
+ Factory class to determine and return the correct CredentialProvider based on the environment.
268
+ """
269
+
270
+ env_secrets_type = "CUMULUSCI_SECRETS_TYPE"
271
+
272
+ @staticmethod
273
+ def load_secrets_type_from_environment() -> str:
274
+ """Load any secrets specified by environment variables"""
275
+ provider_type = os.getenv(CredentialManager.env_secrets_type)
276
+
277
+ # If no provider type is found, use the dev provider
278
+ return (provider_type or "local").lower()
279
+
280
+ @staticmethod
281
+ def get_provider(
282
+ provider_type: Optional[str] = None, **kwargs
283
+ ) -> CredentialProvider:
284
+ """
285
+ Looks up the provider class in the registry and returns an instance.
286
+ """
287
+ if not provider_type:
288
+ # If no config is provided, load the secrets from the environment
289
+ provider_type = CredentialManager.load_secrets_type_from_environment()
290
+
291
+ # Check if the requested provider type exists in our registry
292
+ if provider_type not in CredentialProvider._registry:
293
+ raise ValueError(f"Unknown provider type specified: '{provider_type}'")
294
+
295
+ # Get the class from the registry
296
+ ProviderClass = CredentialProvider._registry[provider_type]
297
+ provider = ProviderClass(**kwargs)
298
+ provider.logger.info(
299
+ f'Using "{provider.provider_type}" provider for credentials.'
300
+ )
301
+
302
+ return provider
@@ -0,0 +1,30 @@
1
+ import os
2
+ import shutil
3
+ from pathlib import Path
4
+
5
+ from cumulusci.core.exceptions import TaskOptionsError
6
+ from cumulusci.core.tasks import BaseTask
7
+ from cumulusci.utils.options import CCIOptions, Field
8
+
9
+
10
+ class DirectoryRecreator(BaseTask):
11
+ class Options(CCIOptions):
12
+ path: Path = Field(..., description="Path to the directory to recreate")
13
+
14
+ parsed_options: Options
15
+
16
+ def _init_options(self, kwargs):
17
+ super()._init_options(kwargs)
18
+
19
+ if os.path.isfile(self.parsed_options.path):
20
+ raise TaskOptionsError(f"Path {self.parsed_options.path} is a file")
21
+
22
+ def _run_task(self):
23
+ created = "created"
24
+ if os.path.exists(self.parsed_options.path):
25
+ shutil.rmtree(self.parsed_options.path)
26
+ created = "removed and created"
27
+
28
+ os.makedirs(self.parsed_options.path, exist_ok=True)
29
+
30
+ self.logger.info(f"Directory {self.parsed_options.path} {created}.")
@@ -5,7 +5,7 @@ from datetime import date
5
5
  from pathlib import Path
6
6
  from typing import Any, List, Optional
7
7
 
8
- from pydantic import BaseModel, validator
8
+ from pydantic.v1 import BaseModel, validator
9
9
 
10
10
  from cumulusci.core.config import BaseProjectConfig, OrgConfig, TaskConfig
11
11
  from cumulusci.core.tasks import BaseTask
@@ -0,0 +1,135 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Union
4
+
5
+ from dotenv import dotenv_values
6
+
7
+ from cumulusci.core.tasks import BaseTask
8
+ from cumulusci.utils.options import (
9
+ CCIOptions,
10
+ Field,
11
+ ListOfStringsOption,
12
+ MappingOption,
13
+ )
14
+
15
+ from .credentialManager import CredentialManager
16
+
17
+
18
+ class GenericOptions(CCIOptions):
19
+ env_path: Path = Field("./.env", description="Path to the .env file")
20
+ secrets_provider: str = Field(
21
+ None,
22
+ description='Secrets provider type i.e local, ado_variables, aws_secrets. Default value is None, which will use the secrets type from the environment variable CUMULUSCI_SECRETS_TYPE if it is set, otherwise it will use the "local" provider.',
23
+ )
24
+ provider_options: MappingOption = Field({}, description="Provider options")
25
+
26
+
27
+ class SecretsToEnv(BaseTask):
28
+ class Options(GenericOptions):
29
+ secrets: Union[ListOfStringsOption, MappingOption] = Field(
30
+ ...,
31
+ description="List of secret keys to retrieve be it with a list of keys or a mapping of key to secret name.",
32
+ )
33
+
34
+ parsed_options: Options
35
+
36
+ def _init_options(self, kwargs):
37
+ super()._init_options(kwargs)
38
+ self.provider = CredentialManager.get_provider(
39
+ self.parsed_options.secrets_provider
40
+ or CredentialManager.load_secrets_type_from_environment(),
41
+ **self.parsed_options.provider_options,
42
+ )
43
+ self.env_values = dotenv_values(self.parsed_options.env_path)
44
+
45
+ def _init_secrets(self):
46
+ if (
47
+ isinstance(self.parsed_options.secrets, list)
48
+ and self.parsed_options.secrets
49
+ ):
50
+ try:
51
+ self.secrets = MappingOption.from_str(
52
+ ",".join(self.parsed_options.secrets)
53
+ )
54
+ except Exception:
55
+ self.secrets = {
56
+ secret: secret for secret in self.parsed_options.secrets
57
+ }
58
+ elif isinstance(self.parsed_options.secrets, dict):
59
+ self.secrets = self.parsed_options.secrets
60
+ else:
61
+ self.secrets = {}
62
+
63
+ def _run_task(self):
64
+
65
+ self._init_secrets()
66
+
67
+ output_file = self.parsed_options.env_path
68
+
69
+ for secret_key, secret_name in self.secrets.items():
70
+ if secret_key == "*":
71
+ self.env_values.update(
72
+ self._get_all_credentials(secret_key, secret_name=secret_name)
73
+ )
74
+ else:
75
+ safe_value, _ = self._get_credential(
76
+ secret_key, secret_key, secret_name=secret_name
77
+ )
78
+ self.env_values[secret_key] = safe_value
79
+
80
+ # Ensure output directory exists
81
+ os.makedirs(os.path.dirname(output_file) or ".", exist_ok=True)
82
+
83
+ # Write env file with UTF-8 encoding to support Unicode characters
84
+ with open(output_file, "w", encoding="utf-8") as env_file:
85
+ for key, value in self.env_values.items():
86
+ env_file.write(f'{key}="{value}"\n')
87
+
88
+ def _get_credential(
89
+ self,
90
+ credential_key,
91
+ value,
92
+ env_key=None,
93
+ display_value="*****",
94
+ secret_name=None,
95
+ ):
96
+ if env_key is None:
97
+ env_key = credential_key
98
+
99
+ cred_secret_value = self.provider.get_credentials(
100
+ credential_key, {"value": value, "secret_name": secret_name}
101
+ )
102
+
103
+ if cred_secret_value is None:
104
+ raise ValueError(
105
+ f"Failed to retrieve secret {credential_key} from {self.provider.provider_type}"
106
+ )
107
+
108
+ self.logger.info(
109
+ f"Set secrets env var from {self.provider.provider_type}: {env_key}={display_value}"
110
+ )
111
+ safe_value = cred_secret_value.replace('"', '\\"').replace("\n", "\\n")
112
+ return safe_value, cred_secret_value
113
+
114
+ def _get_all_credentials(
115
+ self, credential_key, display_value="*****", secret_name=None
116
+ ):
117
+
118
+ cred_secret_values = self.provider.get_all_credentials(
119
+ credential_key, {"secret_name": secret_name}
120
+ )
121
+
122
+ if cred_secret_values is None:
123
+ raise ValueError(
124
+ f"Failed to retrieve secret {credential_key}({secret_name}) from {self.provider.provider_type}"
125
+ )
126
+
127
+ dict_values = {}
128
+ for key, value in cred_secret_values.items():
129
+ self.logger.info(
130
+ f"Set secrets env var from {self.provider.provider_type}: {key}={display_value}"
131
+ )
132
+ safe_value = value.replace('"', '\\"').replace("\n", "\\n")
133
+ dict_values[key] = safe_value
134
+
135
+ return dict_values