cumulusci-plus 5.0.19__py3-none-any.whl → 5.0.35__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 (123) 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 +17 -0
  5. cumulusci/cli/tests/test_error.py +3 -1
  6. cumulusci/cli/tests/test_flow.py +279 -2
  7. cumulusci/cli/tests/test_service.py +15 -12
  8. cumulusci/cli/tests/test_task.py +88 -2
  9. cumulusci/cli/tests/utils.py +1 -4
  10. cumulusci/core/config/base_task_flow_config.py +26 -1
  11. cumulusci/core/config/project_config.py +2 -20
  12. cumulusci/core/config/tests/test_config_expensive.py +9 -3
  13. cumulusci/core/config/universal_config.py +3 -4
  14. cumulusci/core/dependencies/base.py +1 -1
  15. cumulusci/core/dependencies/dependencies.py +1 -1
  16. cumulusci/core/dependencies/github.py +1 -2
  17. cumulusci/core/dependencies/resolvers.py +1 -1
  18. cumulusci/core/dependencies/tests/test_dependencies.py +1 -1
  19. cumulusci/core/dependencies/tests/test_resolvers.py +1 -1
  20. cumulusci/core/flowrunner.py +90 -6
  21. cumulusci/core/github.py +1 -1
  22. cumulusci/core/sfdx.py +3 -1
  23. cumulusci/core/source_transforms/tests/test_transforms.py +1 -1
  24. cumulusci/core/source_transforms/transforms.py +1 -1
  25. cumulusci/core/tasks.py +13 -2
  26. cumulusci/core/tests/test_flowrunner.py +100 -0
  27. cumulusci/core/tests/test_tasks.py +65 -0
  28. cumulusci/core/utils.py +3 -1
  29. cumulusci/core/versions.py +1 -1
  30. cumulusci/cumulusci.yml +55 -0
  31. cumulusci/oauth/client.py +1 -1
  32. cumulusci/plugins/plugin_base.py +5 -3
  33. cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +1 -1
  34. cumulusci/salesforce_api/rest_deploy.py +1 -1
  35. cumulusci/schema/cumulusci.jsonschema.json +64 -0
  36. cumulusci/tasks/apex/anon.py +1 -1
  37. cumulusci/tasks/apex/testrunner.py +416 -142
  38. cumulusci/tasks/apex/tests/test_apex_tasks.py +917 -1
  39. cumulusci/tasks/bulkdata/extract.py +0 -1
  40. cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py +1 -1
  41. cumulusci/tasks/bulkdata/extract_dataset_utils/synthesize_extract_declarations.py +1 -1
  42. cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_extract_yml.py +1 -1
  43. cumulusci/tasks/bulkdata/generate_and_load_data.py +136 -12
  44. cumulusci/tasks/bulkdata/mapping_parser.py +139 -44
  45. cumulusci/tasks/bulkdata/select_utils.py +1 -1
  46. cumulusci/tasks/bulkdata/snowfakery.py +100 -25
  47. cumulusci/tasks/bulkdata/tests/test_generate_and_load.py +159 -0
  48. cumulusci/tasks/bulkdata/tests/test_load.py +0 -2
  49. cumulusci/tasks/bulkdata/tests/test_mapping_parser.py +763 -1
  50. cumulusci/tasks/bulkdata/tests/test_select_utils.py +26 -0
  51. cumulusci/tasks/bulkdata/tests/test_snowfakery.py +133 -0
  52. cumulusci/tasks/create_package_version.py +190 -16
  53. cumulusci/tasks/datadictionary.py +1 -1
  54. cumulusci/tasks/metadata_etl/base.py +7 -3
  55. cumulusci/tasks/metadata_etl/layouts.py +1 -1
  56. cumulusci/tasks/metadata_etl/permissions.py +1 -1
  57. cumulusci/tasks/metadata_etl/remote_site_settings.py +2 -2
  58. cumulusci/tasks/push/README.md +15 -17
  59. cumulusci/tasks/release_notes/README.md +13 -13
  60. cumulusci/tasks/release_notes/generator.py +13 -8
  61. cumulusci/tasks/robotframework/tests/test_robotframework.py +6 -1
  62. cumulusci/tasks/salesforce/Deploy.py +53 -2
  63. cumulusci/tasks/salesforce/SfPackageCommands.py +363 -0
  64. cumulusci/tasks/salesforce/__init__.py +1 -0
  65. cumulusci/tasks/salesforce/assign_ps_psg.py +448 -0
  66. cumulusci/tasks/salesforce/composite.py +1 -1
  67. cumulusci/tasks/salesforce/custom_settings_wait.py +1 -1
  68. cumulusci/tasks/salesforce/enable_prediction.py +5 -1
  69. cumulusci/tasks/salesforce/getPackageVersion.py +89 -0
  70. cumulusci/tasks/salesforce/profiles.py +13 -9
  71. cumulusci/tasks/salesforce/sourcetracking.py +1 -1
  72. cumulusci/tasks/salesforce/tests/test_Deploy.py +316 -1
  73. cumulusci/tasks/salesforce/tests/test_SfPackageCommands.py +554 -0
  74. cumulusci/tasks/salesforce/tests/test_assign_ps_psg.py +1055 -0
  75. cumulusci/tasks/salesforce/tests/test_getPackageVersion.py +651 -0
  76. cumulusci/tasks/salesforce/tests/test_profiles.py +43 -3
  77. cumulusci/tasks/salesforce/tests/test_update_dependencies.py +1 -1
  78. cumulusci/tasks/salesforce/tests/test_update_external_credential.py +912 -0
  79. cumulusci/tasks/salesforce/tests/test_update_named_credential.py +1042 -0
  80. cumulusci/tasks/salesforce/update_dependencies.py +2 -2
  81. cumulusci/tasks/salesforce/update_external_credential.py +562 -0
  82. cumulusci/tasks/salesforce/update_named_credential.py +441 -0
  83. cumulusci/tasks/salesforce/update_profile.py +17 -13
  84. cumulusci/tasks/salesforce/users/permsets.py +62 -5
  85. cumulusci/tasks/salesforce/users/tests/test_permsets.py +237 -11
  86. cumulusci/tasks/sfdmu/__init__.py +0 -0
  87. cumulusci/tasks/sfdmu/sfdmu.py +363 -0
  88. cumulusci/tasks/sfdmu/tests/__init__.py +1 -0
  89. cumulusci/tasks/sfdmu/tests/test_runner.py +212 -0
  90. cumulusci/tasks/sfdmu/tests/test_sfdmu.py +1012 -0
  91. cumulusci/tasks/tests/test_create_package_version.py +716 -1
  92. cumulusci/tasks/tests/test_util.py +42 -0
  93. cumulusci/tasks/util.py +37 -1
  94. cumulusci/tasks/utility/copyContents.py +402 -0
  95. cumulusci/tasks/utility/credentialManager.py +256 -0
  96. cumulusci/tasks/utility/directoryRecreator.py +30 -0
  97. cumulusci/tasks/utility/env_management.py +1 -1
  98. cumulusci/tasks/utility/secretsToEnv.py +135 -0
  99. cumulusci/tasks/utility/tests/test_copyContents.py +1719 -0
  100. cumulusci/tasks/utility/tests/test_credentialManager.py +564 -0
  101. cumulusci/tasks/utility/tests/test_directoryRecreator.py +439 -0
  102. cumulusci/tasks/utility/tests/test_secretsToEnv.py +1091 -0
  103. cumulusci/tests/test_integration_infrastructure.py +3 -1
  104. cumulusci/tests/test_utils.py +70 -6
  105. cumulusci/utils/__init__.py +54 -9
  106. cumulusci/utils/classutils.py +5 -2
  107. cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +31 -30
  108. cumulusci/utils/options.py +23 -1
  109. cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +1 -1
  110. cumulusci/utils/yaml/cumulusci_yml.py +7 -3
  111. cumulusci/utils/yaml/model_parser.py +2 -2
  112. cumulusci/utils/yaml/tests/test_cumulusci_yml.py +1 -1
  113. cumulusci/utils/yaml/tests/test_model_parser.py +3 -3
  114. cumulusci/vcs/base.py +23 -15
  115. cumulusci/vcs/bootstrap.py +5 -4
  116. cumulusci/vcs/utils/list_modified_files.py +189 -0
  117. cumulusci/vcs/utils/tests/test_list_modified_files.py +588 -0
  118. {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/METADATA +12 -10
  119. {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/RECORD +123 -98
  120. {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/WHEEL +0 -0
  121. {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/entry_points.txt +0 -0
  122. {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/licenses/AUTHORS.rst +0 -0
  123. {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/licenses/LICENSE +0 -0
@@ -56,7 +56,7 @@ class UpdateDependencies(BaseSalesforceTask):
56
56
  "description": "The name of a sequence of resolution_strategy (from project__dependency_resolutions) to apply to dynamic dependencies."
57
57
  },
58
58
  "packages_only": {
59
- "description": "Install only packaged dependencies. Ignore all unmanaged metadata. Defaults to False."
59
+ "description": "Install only packaged dependencies. Ignore all unpackaged metadata. Defaults to False."
60
60
  },
61
61
  "interactive": {
62
62
  "description": "If True, stop after identifying all dependencies and output the package Ids that will be installed. Defaults to False."
@@ -65,7 +65,7 @@ class UpdateDependencies(BaseSalesforceTask):
65
65
  "description": "If `interactive` is set to True, display package Ids using a format string ({} will be replaced with the package Id)."
66
66
  },
67
67
  "force_pre_post_install": {
68
- "description": "Forces the pre-install and post-install steps to be run. Defaults to False."
68
+ "description": "Forces the dependency_flow_pre flows and dependency_flow_post flows to run even if the dependency version is already installed. Defaults to False."
69
69
  },
70
70
  **{k: v for k, v in PACKAGE_INSTALL_TASK_OPTIONS.items() if k != "password"},
71
71
  }
@@ -0,0 +1,562 @@
1
+ import os
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ from pydantic.v1 import root_validator
5
+
6
+ from cumulusci.core.exceptions import SalesforceDXException
7
+ from cumulusci.salesforce_api.utils import get_simple_salesforce_connection
8
+ from cumulusci.tasks.salesforce import BaseSalesforceApiTask
9
+ from cumulusci.utils.options import CCIOptions, Field
10
+
11
+
12
+ class ExtParameter(CCIOptions):
13
+ """Http Header options"""
14
+
15
+ name: str = Field(
16
+ None,
17
+ description="Parameter name. [default to None]",
18
+ )
19
+ value: str = Field(
20
+ None,
21
+ description="Parameter value. [default to None]",
22
+ )
23
+ group: str = Field(
24
+ None,
25
+ description="Parameter group. [default to None]",
26
+ )
27
+ sequence_number: int = Field(
28
+ None,
29
+ description="Sequence number. [default to None]",
30
+ )
31
+
32
+
33
+ class HttpHeader(ExtParameter):
34
+ """Http Header options"""
35
+
36
+ sequence_number: int = Field(
37
+ None,
38
+ description="Sequence number. [default to None]",
39
+ )
40
+ secret: bool = Field(
41
+ False,
42
+ description="Is the value a secret. [default to False]",
43
+ )
44
+
45
+
46
+ class ExternalCredential(HttpHeader):
47
+ client_secret: str = Field(
48
+ None,
49
+ description="Client secret. [default to None]",
50
+ )
51
+ client_id: str = Field(
52
+ None,
53
+ description="Client id. [default to None]",
54
+ )
55
+ auth_protocol: str = Field(
56
+ "OAuth",
57
+ description="Authentication protocol. [default to OAuth]",
58
+ )
59
+
60
+
61
+ class ExternalCredentialParameter(CCIOptions):
62
+ """External Credential Parameter options"""
63
+
64
+ auth_header: HttpHeader = Field(
65
+ None,
66
+ description="Auth header value. [default to None]",
67
+ )
68
+ auth_provider: str = Field(
69
+ None,
70
+ description="Auth provider name (only subscriber editable in 2GP). [default to None]",
71
+ )
72
+ auth_provider_url: str = Field(
73
+ None,
74
+ description="Auth provider URL. [default to None]",
75
+ )
76
+ auth_provider_url_query_parameter: ExtParameter = Field(
77
+ None,
78
+ description="Auth provider URL query parameter, The allowed AuthProviderUrlQueryParameter values are AwsExternalId and AwsDuration, used with AWS STS. [default to None]",
79
+ )
80
+ auth_parameter: ExtParameter = Field(
81
+ None,
82
+ description="Auth parameter. Allows the user to add additional authentication settings. parameterName defines the parameter to set. [default to None]",
83
+ )
84
+ # aws_sts_principal: str = Field(
85
+ # None,
86
+ # description="AWS STS Principal (only for external credentials that use AWS Signature v4 authentication with STS). [default to None]",
87
+ # )
88
+ jwt_body_claim: ExtParameter = Field(
89
+ None,
90
+ description="Specifies a JWT (JSON Web Token) body claim. [default to None]",
91
+ )
92
+ jwt_header_claim: ExtParameter = Field(
93
+ None,
94
+ description="Specifies a JWT header claim. [default to None]",
95
+ )
96
+ named_principal: ExternalCredential = Field(
97
+ None,
98
+ description="Named principal. [default to None]",
99
+ )
100
+ per_user_principal: str = Field(
101
+ None,
102
+ description="Per user principal. [default to None]",
103
+ )
104
+ signing_certificate: str = Field(
105
+ None,
106
+ description="Signing certificate (only subscriber editable in 2GP). [default to None]",
107
+ )
108
+ secret: bool = Field(
109
+ False,
110
+ description="Is the value a secret. [default to False]",
111
+ )
112
+
113
+ @root_validator
114
+ def check_parameters(cls, values):
115
+ """Check if at least one parameter is provided"""
116
+ param_fields = [
117
+ "auth_header",
118
+ "auth_provider",
119
+ "auth_provider_url",
120
+ "auth_provider_url_query_parameter",
121
+ "auth_parameter",
122
+ # "aws_sts_principal",
123
+ "jwt_body_claim",
124
+ "jwt_header_claim",
125
+ "named_principal",
126
+ "per_user_principal",
127
+ "signing_certificate",
128
+ ]
129
+
130
+ provided_params = [
131
+ field for field in param_fields if values.get(field) is not None
132
+ ]
133
+
134
+ if len(provided_params) == 0:
135
+ raise ValueError("At least and only one parameter must be provided.")
136
+
137
+ if len(provided_params) > 1:
138
+ raise ValueError("At least and only one parameter must be provided.")
139
+
140
+ return values
141
+
142
+ def get_external_credential_parameter(self):
143
+ ext_cred_param = {"parameterGroup": "DefaultGroup"}
144
+
145
+ """Get the external credential parameter based on which field is set"""
146
+ if self.auth_header is not None:
147
+ ext_cred_param["parameterType"] = "AuthHeader"
148
+ ext_cred_param["parameterValue"] = self.auth_header.value
149
+ ext_cred_param["parameterName"] = self.auth_header.name
150
+
151
+ if self.auth_header.sequence_number is not None:
152
+ ext_cred_param["sequenceNumber"] = self.auth_header.sequence_number
153
+ if self.auth_header.secret is not None:
154
+ ext_cred_param["secret"] = self.auth_header.secret
155
+ if self.auth_header.group is not None:
156
+ ext_cred_param["parameterGroup"] = self.auth_header.group
157
+
158
+ if self.auth_provider is not None:
159
+ ext_cred_param["parameterType"] = "AuthProvider"
160
+ ext_cred_param["parameterValue"] = self.auth_provider
161
+ ext_cred_param["parameterName"] = "AuthProvider"
162
+ ext_cred_param["authProvider"] = ext_cred_param["parameterValue"]
163
+
164
+ if self.auth_provider_url is not None:
165
+ ext_cred_param["parameterType"] = "AuthProviderUrl"
166
+ ext_cred_param["parameterValue"] = self.auth_provider_url
167
+
168
+ if self.auth_provider_url_query_parameter is not None:
169
+ ext_cred_param["parameterType"] = "AuthProviderUrlQueryParameter"
170
+ ext_cred_param[
171
+ "parameterValue"
172
+ ] = self.auth_provider_url_query_parameter.value
173
+ ext_cred_param[
174
+ "parameterName"
175
+ ] = self.auth_provider_url_query_parameter.name
176
+
177
+ if self.auth_parameter is not None:
178
+ ext_cred_param["parameterType"] = "AuthParameter"
179
+ ext_cred_param["parameterValue"] = self.auth_parameter.value
180
+ ext_cred_param["parameterName"] = self.auth_parameter.name
181
+ if self.auth_parameter.group is not None:
182
+ ext_cred_param["parameterGroup"] = self.auth_parameter.group
183
+
184
+ # if self.aws_sts_principal is not None:
185
+ # ext_cred_param["parameterType"] = "AwsStsPrincipal"
186
+ # ext_cred_param["parameterName"] = 'AwsStsPrincipal'
187
+ # ext_cred_param["parameterValue"] = self.aws_sts_principal
188
+
189
+ if self.jwt_body_claim is not None:
190
+ ext_cred_param["parameterType"] = "JwtBodyClaim"
191
+ ext_cred_param["parameterName"] = self.jwt_body_claim.name
192
+ ext_cred_param["parameterValue"] = self.jwt_body_claim.value
193
+
194
+ if self.jwt_header_claim is not None:
195
+ ext_cred_param["parameterType"] = "JwtHeaderClaim"
196
+ ext_cred_param["parameterName"] = self.jwt_header_claim.name
197
+ ext_cred_param["parameterValue"] = self.jwt_header_claim.value
198
+
199
+ if self.named_principal is not None:
200
+ ext_cred_param["parameterType"] = "NamedPrincipal"
201
+ ext_cred_param["parameterName"] = self.named_principal.name
202
+ ext_cred_param["parameterValue"] = None
203
+ if self.named_principal.sequence_number is not None:
204
+ ext_cred_param["sequenceNumber"] = self.named_principal.sequence_number
205
+ ext_cred_param["parameterGroup"] = (
206
+ self.named_principal.group
207
+ if self.named_principal.group is not None
208
+ else self.named_principal.name
209
+ )
210
+
211
+ if self.per_user_principal is not None:
212
+ ext_cred_param["parameterType"] = "PerUserPrincipal"
213
+ ext_cred_param["parameterName"] = "PerUserPrincipal"
214
+ ext_cred_param["parameterGroup"] = "PerUser"
215
+ ext_cred_param["parameterValue"] = self.per_user_principal
216
+
217
+ if self.signing_certificate is not None:
218
+ ext_cred_param["parameterType"] = "SigningCertificate"
219
+ ext_cred_param["parameterName"] = "SigningCertificate"
220
+ ext_cred_param["certificate"] = self.signing_certificate
221
+ ext_cred_param["parameterValue"] = self.signing_certificate
222
+
223
+ return ext_cred_param
224
+
225
+ def get_principal_credential(self, ext_cred_full_name: str):
226
+ if self.named_principal is None:
227
+ return None
228
+
229
+ return {
230
+ "principalType": "NamedPrincipal",
231
+ "principalName": self.named_principal.name,
232
+ "externalCredential": ext_cred_full_name,
233
+ }
234
+
235
+ def get_credential_parameter(self):
236
+ if self.named_principal is None or (
237
+ self.named_principal.client_secret is None
238
+ and self.named_principal.client_id is None
239
+ ):
240
+ return None
241
+
242
+ return {
243
+ "clientId": {"encrypted": False, "value": self.named_principal.client_id},
244
+ "clientSecret": {
245
+ "encrypted": True,
246
+ "value": self.named_principal.client_secret,
247
+ },
248
+ }
249
+
250
+ def get_credential(self, ext_cred_full_name: str):
251
+ return {
252
+ "authenticationProtocol": self.named_principal.auth_protocol,
253
+ "credentials": self.get_credential_parameter(),
254
+ "externalCredential": ext_cred_full_name,
255
+ "principalName": self.named_principal.name,
256
+ "principalType": "NamedPrincipal",
257
+ }
258
+
259
+
260
+ class TransformExternalCredentialParameter(ExternalCredentialParameter):
261
+ """Transform External Credential Parameter with environment variable support"""
262
+
263
+ def get_external_credential_parameter(self):
264
+ ret = super().get_external_credential_parameter()
265
+ if ret.get("parameterValue", None) is not None:
266
+ ret["parameterValue"] = os.getenv(ret.get("parameterValue"))
267
+ return ret
268
+
269
+ def get_credential_parameter(self):
270
+
271
+ value = super().get_credential_parameter()
272
+
273
+ if value is None:
274
+ return None
275
+
276
+ value["clientSecret"]["value"] = os.getenv(value["clientSecret"]["value"])
277
+ value["clientId"]["value"] = os.getenv(
278
+ value["clientId"]["value"], value["clientId"]["value"]
279
+ )
280
+
281
+ return value
282
+
283
+
284
+ ExternalCredentialParameter.update_forward_refs()
285
+ TransformExternalCredentialParameter.update_forward_refs()
286
+
287
+
288
+ class UpdateExternalCredential(BaseSalesforceApiTask):
289
+ """Custom task to update external credential parameters.
290
+ This task is based on the manageability rules of external credentials in 2GP.
291
+ https://developer.salesforce.com/docs/atlas.en-us.pkg2_dev.meta/pkg2_dev/packaging_packageable_components.htm#mdc_external_credential
292
+ """
293
+
294
+ class Options(CCIOptions):
295
+ name: str = Field(..., description="Name of the external credential to update.")
296
+ namespace: str = Field(
297
+ "",
298
+ description="Namespace of the external credential to update. [default to empty string]",
299
+ )
300
+ # External credential parameters
301
+ parameters: List[ExternalCredentialParameter] = Field(
302
+ [],
303
+ description="Parameters to update. [default to empty list]",
304
+ )
305
+ # Transform parameters (from environment variables)
306
+ transform_parameters: List[TransformExternalCredentialParameter] = Field(
307
+ [],
308
+ description="Parameters to transform from environment variables. [default to empty list]",
309
+ )
310
+
311
+ parsed_options: Options
312
+
313
+ def _init_task(self):
314
+ self.tooling = get_simple_salesforce_connection(
315
+ self.project_config,
316
+ self.org_config,
317
+ api_version=self.project_config.project__package__api_version,
318
+ base_url="tooling",
319
+ )
320
+ self.connect = get_simple_salesforce_connection(
321
+ self.project_config,
322
+ self.org_config,
323
+ api_version=self.project_config.project__package__api_version,
324
+ )
325
+
326
+ def _run_task(self):
327
+ # Step 1: Get the external credential id from the external credential name
328
+ external_credential_id = self._get_external_credential_id()
329
+
330
+ if not external_credential_id:
331
+ msg = f"External credential '{self.parsed_options.name}' not found"
332
+ raise SalesforceDXException(msg)
333
+
334
+ # Step 2: Get the external credential object
335
+ external_credential = self._get_external_credential_object(
336
+ external_credential_id
337
+ )
338
+
339
+ if not external_credential:
340
+ msg = f"Failed to retrieve external credential object for '{self.parsed_options.name}'"
341
+ raise SalesforceDXException(msg)
342
+
343
+ # Step 3: Update the external credential parameters
344
+ self._update_external_credential_parameters(external_credential)
345
+
346
+ updated_external_credential = self._update_external_credential_object(
347
+ external_credential_id, external_credential
348
+ )
349
+
350
+ if not updated_external_credential:
351
+ msg = f"Failed to update external credential object for '{self.parsed_options.name}'"
352
+ raise SalesforceDXException(msg)
353
+
354
+ self._update_credential()
355
+
356
+ self.logger.info(
357
+ f"Successfully updated external credential '{self.parsed_options.name}'"
358
+ )
359
+
360
+ def _get_external_credential_id(self) -> Optional[str]:
361
+ """Get the external credential ID from the external credential name"""
362
+ query = f"SELECT Id FROM ExternalCredential WHERE DeveloperName='{self.parsed_options.name}'"
363
+
364
+ if self.parsed_options.namespace:
365
+ query += f" AND NamespacePrefix='{self.parsed_options.namespace}'"
366
+
367
+ query += " LIMIT 1"
368
+
369
+ try:
370
+ res = self.tooling.query(query)
371
+ if res["size"] == 0:
372
+ return None
373
+ return res["records"][0]["Id"]
374
+ except Exception as e:
375
+ self.logger.error(f"Error querying external credential: {str(e)}")
376
+ return None
377
+
378
+ def _get_external_credential_object(
379
+ self, external_credential_id: str
380
+ ) -> Optional[Dict[str, Any]]:
381
+ """Get the external credential object using Tooling API"""
382
+ try:
383
+ # Use Tooling API to get the external credential metadata
384
+ result = self.tooling._call_salesforce(
385
+ method="GET",
386
+ url=f"{self.tooling.base_url}sobjects/ExternalCredential/{external_credential_id}",
387
+ )
388
+
389
+ if result.status_code != 200:
390
+ self.logger.error(
391
+ f"Error retrieving external credential object: {result.json()}"
392
+ )
393
+ return None
394
+
395
+ return result.json().get("Metadata", None)
396
+
397
+ except Exception as e:
398
+ self.logger.error(f"Error retrieving external credential object: {str(e)}")
399
+ return None
400
+
401
+ def _update_external_credential_object(
402
+ self, external_credential_id: str, external_credential: Dict[str, Any]
403
+ ) -> Optional[bool]:
404
+ """Update the external credential object"""
405
+ try:
406
+ result_update = self.tooling._call_salesforce(
407
+ method="PATCH",
408
+ url=f"{self.tooling.base_url}sobjects/ExternalCredential/{external_credential_id}",
409
+ json={"Metadata": external_credential},
410
+ )
411
+ if not result_update.ok:
412
+ self.logger.error(
413
+ f"Error updating external credential object: {result_update.json()}"
414
+ )
415
+ return None
416
+
417
+ return result_update.ok
418
+ except Exception as e:
419
+ raise SalesforceDXException(
420
+ f"Failed to update external credential object: {str(e)}"
421
+ )
422
+
423
+ def _update_external_credential_parameters(
424
+ self, external_credential: Dict[str, Any]
425
+ ):
426
+ """Update the external credential parameters"""
427
+ try:
428
+ # Get template parameter for new parameters
429
+ template_param = self._get_external_credential_template_parameter()
430
+
431
+ # Update regular parameters
432
+ self._update_parameters(
433
+ external_credential, self.parsed_options.parameters, template_param
434
+ )
435
+
436
+ # Update transform parameters (from environment variables)
437
+ self._update_parameters(
438
+ external_credential,
439
+ self.parsed_options.transform_parameters,
440
+ template_param,
441
+ )
442
+
443
+ except Exception as e:
444
+ raise SalesforceDXException(f"Failed to update parameters: {str(e)}")
445
+
446
+ def _update_parameters(
447
+ self,
448
+ external_credential: Dict[str, Any],
449
+ external_credential_parameters: List[ExternalCredentialParameter],
450
+ template_param: Dict[str, Any],
451
+ ):
452
+ """Update the parameters"""
453
+ for param_input in external_credential_parameters:
454
+ param_to_update = param_input.get_external_credential_parameter()
455
+ secret = param_to_update.pop("secret", False)
456
+
457
+ # Create a copy for matching (without parameterValue)
458
+ param_to_match = {
459
+ k: v for k, v in param_to_update.items() if k != "parameterValue"
460
+ }
461
+
462
+ # Find existing parameter
463
+ cred_param = next(
464
+ (
465
+ param
466
+ for param in external_credential.get(
467
+ "externalCredentialParameters", []
468
+ )
469
+ if param_to_match.items() <= param.items()
470
+ ),
471
+ None,
472
+ )
473
+
474
+ if cred_param:
475
+ # Update existing parameter
476
+ cred_param.update(param_to_update)
477
+ self.logger.info(
478
+ f"Updated parameter {cred_param['parameterType']}"
479
+ + (
480
+ f"-{cred_param.get('parameterName', 'N/A')}"
481
+ if cred_param.get("parameterName")
482
+ else ""
483
+ )
484
+ + f" with new value {param_to_update['parameterValue'] if not secret else '********'}"
485
+ )
486
+ else:
487
+ # Add new parameter
488
+ copy_template_param = template_param.copy()
489
+ copy_template_param.update(param_to_update)
490
+ if "externalCredentialParameters" not in external_credential:
491
+ external_credential["externalCredentialParameters"] = []
492
+ external_credential["externalCredentialParameters"].append(
493
+ copy_template_param
494
+ )
495
+ self.logger.info(
496
+ f"Added parameter {copy_template_param['parameterType']}"
497
+ + (
498
+ f"-{copy_template_param.get('parameterName', 'N/A')}"
499
+ if copy_template_param.get("parameterName")
500
+ else ""
501
+ )
502
+ + f" with new value {param_to_update['parameterValue'] if not secret else '********'}"
503
+ )
504
+
505
+ def _get_external_credential_template_parameter(self) -> Dict[str, Any]:
506
+ """Get the external credential template parameter"""
507
+ return {
508
+ "authProvider": None,
509
+ "certificate": None,
510
+ "description": None,
511
+ "externalAuthIdentityProvider": None,
512
+ "parameterGroup": None,
513
+ "parameterName": None,
514
+ "parameterType": None,
515
+ "parameterValue": None,
516
+ "sequenceNumber": None,
517
+ }
518
+
519
+ def _update_credential(self):
520
+ """Update the credential"""
521
+ for param in (
522
+ self.parsed_options.parameters + self.parsed_options.transform_parameters
523
+ ):
524
+ if param.named_principal is None or (
525
+ param.named_principal.client_secret is None
526
+ and param.named_principal.client_id is None
527
+ ):
528
+ continue
529
+
530
+ namespace = (
531
+ f"{self.parsed_options.namespace}__"
532
+ if self.parsed_options.namespace
533
+ else ""
534
+ )
535
+ credential_param = param.get_principal_credential(
536
+ f"{namespace}{self.parsed_options.name}"
537
+ )
538
+
539
+ self.logger.info(f"Managing credential for {param.named_principal.name}...")
540
+
541
+ credential_response = self.connect._call_salesforce(
542
+ method="GET",
543
+ url=f"{self.connect.base_url}named-credentials/credential",
544
+ params=credential_param,
545
+ )
546
+
547
+ credential = credential_response.json()
548
+ credential.pop("authenticationStatus")
549
+ http_verb = "PUT" if credential["credentials"] else "POST"
550
+ credential["credentials"] = param.get_credential_parameter()
551
+
552
+ response = self.connect._call_salesforce(
553
+ method=http_verb,
554
+ url=f"{self.connect.base_url}named-credentials/credential",
555
+ json=credential,
556
+ )
557
+
558
+ if not response.ok:
559
+ msg = f"Failed to update credential {param.named_principal.name}: {response.json()}"
560
+ raise SalesforceDXException(msg)
561
+
562
+ self.logger.info(f"Updated credential {param.named_principal.name}")