• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

localstack / localstack / 18580604313

17 Oct 2025 12:09AM UTC coverage: 86.896% (+0.01%) from 86.886%
18580604313

push

github

web-flow
APIGW: expand coverage for API Keys and Usage Plans (#13201)

Co-authored-by: Benjamin Simon <benjh.simon@gmail.com>

19 of 19 new or added lines in 1 file covered. (100.0%)

118 existing lines in 7 files now uncovered.

68346 of 78653 relevant lines covered (86.9%)

0.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.79
/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py
1
import copy
1✔
2
import logging
1✔
3
import os
1✔
4
import re
1✔
5
import uuid
1✔
6
from collections.abc import Callable
1✔
7
from dataclasses import dataclass
1✔
8
from datetime import UTC, datetime
1✔
9
from typing import Final, Protocol, TypeVar
1✔
10

11
from localstack import config
1✔
12
from localstack.aws.api.cloudformation import (
1✔
13
    ChangeAction,
14
    Output,
15
    ResourceStatus,
16
    StackStatus,
17
)
18
from localstack.constants import INTERNAL_AWS_SECRET_ACCESS_KEY
1✔
19
from localstack.services.cloudformation.analytics import track_resource_operation
1✔
20
from localstack.services.cloudformation.deployment_utils import log_not_available_message
1✔
21
from localstack.services.cloudformation.engine.v2.change_set_model import (
1✔
22
    NodeDependsOn,
23
    NodeOutput,
24
    NodeResource,
25
    is_nothing,
26
)
27
from localstack.services.cloudformation.engine.v2.change_set_model_preproc import (
1✔
28
    _AWS_URL_SUFFIX,
29
    MOCKED_REFERENCE,
30
    ChangeSetModelPreproc,
31
    PreprocEntityDelta,
32
    PreprocOutput,
33
    PreprocProperties,
34
    PreprocResource,
35
)
36
from localstack.services.cloudformation.resource_provider import (
1✔
37
    Credentials,
38
    OperationStatus,
39
    ProgressEvent,
40
    ResourceProviderExecutor,
41
    ResourceProviderPayload,
42
)
43
from localstack.services.cloudformation.v2.entities import ChangeSet, ResolvedResource
1✔
44

45
LOG = logging.getLogger(__name__)
1✔
46

47
EventOperationFromAction = {"Add": "CREATE", "Modify": "UPDATE", "Remove": "DELETE"}
1✔
48

49
REGEX_OUTPUT_APIGATEWAY = re.compile(
1✔
50
    rf"^(https?://.+\.execute-api\.)(?:[^-]+-){{2,3}}\d\.(amazonaws\.com|{_AWS_URL_SUFFIX})/?(.*)$"
51
)
52

53
_T = TypeVar("_T")
1✔
54

55

56
@dataclass
1✔
57
class ChangeSetModelExecutorResult:
1✔
58
    resources: dict[str, ResolvedResource]
1✔
59
    outputs: list[Output]
1✔
60
    failure_message: str | None = None
1✔
61

62

63
class DeferredAction(Protocol):
1✔
64
    def __call__(self) -> None: ...
1✔
65

66

67
@dataclass
1✔
68
class Deferred:
1✔
69
    name: str
1✔
70
    action: DeferredAction
1✔
71

72

73
class TriggerRollback(Exception):
1✔
74
    """
75
    Sentinel exception to signal that the deployment should be stopped for a reason
76
    """
77

78
    def __init__(self, logical_resource_id: str, reason: str | None):
1✔
79
        self.logical_resource_id = logical_resource_id
1✔
80
        self.reason = reason
1✔
81

82

83
class ChangeSetModelExecutor(ChangeSetModelPreproc):
1✔
84
    # TODO: add typing for resolved resources and parameters.
85
    resources: Final[dict[str, ResolvedResource]]
1✔
86
    outputs: Final[list[Output]]
1✔
87
    _deferred_actions: list[Deferred]
1✔
88

89
    def __init__(self, change_set: ChangeSet):
1✔
90
        super().__init__(change_set=change_set)
1✔
91
        self.resources = {}
1✔
92
        self.outputs = []
1✔
93
        self._deferred_actions = []
1✔
94
        self.resource_provider_executor = ResourceProviderExecutor(
1✔
95
            stack_name=change_set.stack.stack_name,
96
            stack_id=change_set.stack.stack_id,
97
        )
98

99
    def execute(self) -> ChangeSetModelExecutorResult:
1✔
100
        # constructive process
101
        failure_message = None
1✔
102
        try:
1✔
103
            self.process()
1✔
104
        except TriggerRollback as e:
1✔
105
            failure_message = e.reason
1✔
106
        except Exception as e:
×
107
            failure_message = str(e)
×
108

109
        is_deletion = self._change_set.stack.status == StackStatus.DELETE_IN_PROGRESS
1✔
110
        if self._deferred_actions:
1✔
111
            if not is_deletion:
1✔
112
                # TODO: correct status
113
                # TODO: differentiate between update and create
114
                self._change_set.stack.set_stack_status(
1✔
115
                    StackStatus.ROLLBACK_IN_PROGRESS
116
                    if failure_message
117
                    else StackStatus.UPDATE_COMPLETE_CLEANUP_IN_PROGRESS
118
                )
119

120
            # perform all deferred actions such as deletions. These must happen in reverse from their
121
            # defined order so that resource dependencies are honoured
122
            # TODO: errors will stop all rollbacks; get parity on this behaviour
123
            for deferred in self._deferred_actions[::-1]:
1✔
124
                LOG.debug("executing deferred action: '%s'", deferred.name)
1✔
125
                deferred.action()
1✔
126

127
        if failure_message and not is_deletion:
1✔
128
            # TODO: differentiate between update and create
129
            self._change_set.stack.set_stack_status(StackStatus.ROLLBACK_COMPLETE)
1✔
130

131
        return ChangeSetModelExecutorResult(
1✔
132
            resources=self.resources, outputs=self.outputs, failure_message=failure_message
133
        )
134

135
    def _defer_action(self, name: str, action: DeferredAction):
1✔
136
        self._deferred_actions.append(Deferred(name=name, action=action))
1✔
137

138
    def _get_physical_id(self, logical_resource_id, strict: bool = True) -> str | None:
1✔
139
        physical_resource_id = None
1✔
140
        try:
1✔
141
            physical_resource_id = self._after_resource_physical_id(logical_resource_id)
1✔
142
        except RuntimeError:
×
143
            # The physical id is missing or is set to None, which is invalid.
144
            pass
×
145
        if physical_resource_id is None:
1✔
146
            # The physical resource id is None after an update that didn't rewrite the resource, the previous
147
            # resource id is therefore the current physical id of this resource.
148

149
            try:
1✔
150
                physical_resource_id = self._before_resource_physical_id(logical_resource_id)
1✔
151
            except RuntimeError as e:
×
152
                if strict:
×
153
                    raise e
×
154
        return physical_resource_id
1✔
155

156
    def _process_event(
1✔
157
        self,
158
        *,
159
        action: ChangeAction,
160
        logical_resource_id,
161
        event_status: OperationStatus,
162
        resource_type: str,
163
        special_action: str = None,
164
        reason: str = None,
165
    ):
166
        status_from_action = special_action or EventOperationFromAction[action.value]
1✔
167
        if event_status == OperationStatus.SUCCESS:
1✔
168
            status = f"{status_from_action}_COMPLETE"
1✔
169
        else:
170
            status = f"{status_from_action}_{event_status.name}"
1✔
171

172
        physical_resource_id = self._get_physical_id(logical_resource_id, False)
1✔
173
        self._change_set.stack.set_resource_status(
1✔
174
            logical_resource_id=logical_resource_id,
175
            physical_resource_id=physical_resource_id,
176
            resource_type=resource_type,
177
            status=ResourceStatus(status),
178
            resource_status_reason=reason,
179
        )
180

181
        if event_status == OperationStatus.FAILED:
1✔
182
            self._change_set.stack.set_stack_status(StackStatus(status))
1✔
183

184
    def _after_deployed_property_value_of(
1✔
185
        self, resource_logical_id: str, property_name: str
186
    ) -> str:
187
        after_resolved_resources = self.resources
1✔
188
        return self._deployed_property_value_of(
1✔
189
            resource_logical_id=resource_logical_id,
190
            property_name=property_name,
191
            resolved_resources=after_resolved_resources,
192
        )
193

194
    def _after_resource_physical_id(self, resource_logical_id: str) -> str:
1✔
195
        after_resolved_resources = self.resources
1✔
196
        return self._resource_physical_resource_id_from(
1✔
197
            logical_resource_id=resource_logical_id, resolved_resources=after_resolved_resources
198
        )
199

200
    def visit_node_depends_on(self, node_depends_on: NodeDependsOn) -> PreprocEntityDelta:
1✔
201
        array_identifiers_delta = super().visit_node_depends_on(node_depends_on=node_depends_on)
1✔
202

203
        # Visit depends_on resources before returning.
204
        depends_on_resource_logical_ids: set[str] = set()
1✔
205
        if array_identifiers_delta.before:
1✔
206
            depends_on_resource_logical_ids.update(array_identifiers_delta.before)
1✔
207
        if array_identifiers_delta.after:
1✔
208
            depends_on_resource_logical_ids.update(array_identifiers_delta.after)
1✔
209
        for depends_on_resource_logical_id in depends_on_resource_logical_ids:
1✔
210
            node_resource = self._get_node_resource_for(
1✔
211
                resource_name=depends_on_resource_logical_id,
212
                node_template=self._change_set.update_model.node_template,
213
            )
214
            self.visit(node_resource)
1✔
215

216
        return array_identifiers_delta
1✔
217

218
    def visit_node_resource(
1✔
219
        self, node_resource: NodeResource
220
    ) -> PreprocEntityDelta[PreprocResource, PreprocResource]:
221
        """
222
        Overrides the default preprocessing for NodeResource objects by annotating the
223
        `after` delta with the physical resource ID, if side effects resulted in an update.
224
        """
225
        try:
1✔
226
            delta = super().visit_node_resource(node_resource=node_resource)
1✔
227
        except Exception as e:
×
228
            LOG.debug(
×
229
                "preprocessing resource '%s' failed: %s",
230
                node_resource.name,
231
                e,
232
                exc_info=LOG.isEnabledFor(logging.DEBUG) and config.CFN_VERBOSE_ERRORS,
233
            )
234
            self._process_event(
×
235
                action=node_resource.change_type.to_change_action(),
236
                logical_resource_id=node_resource.name,
237
                event_status=OperationStatus.FAILED,
238
                resource_type=node_resource.type_.value,
239
                reason=str(e),
240
            )
241
            raise e
×
242

243
        before = delta.before
1✔
244
        after = delta.after
1✔
245

246
        if before != after:
1✔
247
            # There are changes for this resource.
248
            self._execute_resource_change(name=node_resource.name, before=before, after=after)
1✔
249
        else:
250
            # There are no updates for this resource; iff the resource was previously
251
            # deployed, then the resolved details are copied in the current state for
252
            # references or other downstream operations.
253
            if not is_nothing(before):
1✔
254
                before_logical_id = delta.before.logical_id
1✔
255
                before_resource = self._before_resolved_resources.get(before_logical_id, {})
1✔
256
                self.resources[before_logical_id] = before_resource
1✔
257

258
        # Update the latest version of this resource for downstream references.
259
        if not is_nothing(after):
1✔
260
            after_logical_id = after.logical_id
1✔
261
            resource = self.resources[after_logical_id]
1✔
262
            resource_failed_to_deploy = resource["ResourceStatus"] in {
1✔
263
                ResourceStatus.CREATE_FAILED,
264
                ResourceStatus.UPDATE_FAILED,
265
            }
266
            if not resource_failed_to_deploy:
1✔
267
                after_physical_id: str = self._after_resource_physical_id(
1✔
268
                    resource_logical_id=after_logical_id
269
                )
270
                after.physical_resource_id = after_physical_id
1✔
271
            after.status = resource["ResourceStatus"]
1✔
272

273
            # terminate the deployment process
274
            if resource_failed_to_deploy:
1✔
275
                raise TriggerRollback(
1✔
276
                    logical_resource_id=after_logical_id,
277
                    reason=resource.get("ResourceStatusReason"),
278
                )
279
        return delta
1✔
280

281
    def visit_node_output(
1✔
282
        self, node_output: NodeOutput
283
    ) -> PreprocEntityDelta[PreprocOutput, PreprocOutput]:
284
        delta = super().visit_node_output(node_output=node_output)
1✔
285
        after = delta.after
1✔
286
        if is_nothing(after) or (isinstance(after, PreprocOutput) and after.condition is False):
1✔
287
            return delta
1✔
288

289
        output = Output(
1✔
290
            OutputKey=delta.after.name,
291
            OutputValue=delta.after.value,
292
            # TODO
293
            # Description=delta.after.description
294
        )
295
        if after.export:
1✔
296
            output["ExportName"] = after.export["Name"]
1✔
297
        self.outputs.append(output)
1✔
298
        return delta
1✔
299

300
    def _execute_resource_change(
1✔
301
        self, name: str, before: PreprocResource | None, after: PreprocResource | None
302
    ) -> None:
303
        # Changes are to be made about this resource.
304
        # TODO: this logic is a POC and should be revised.
305
        if not is_nothing(before) and not is_nothing(after):
1✔
306
            # Case: change on same type.
307
            if before.resource_type == after.resource_type:
1✔
308
                # Register a Modified if changed.
309
                # XXX hacky, stick the previous resources' properties into the payload
310
                before_properties = self._merge_before_properties(name, before)
1✔
311

312
                self._process_event(
1✔
313
                    action=ChangeAction.Modify,
314
                    logical_resource_id=name,
315
                    event_status=OperationStatus.IN_PROGRESS,
316
                    resource_type=before.resource_type,
317
                )
318
                if after.requires_replacement:
1✔
319
                    event = self._execute_resource_action(
1✔
320
                        action=ChangeAction.Add,
321
                        logical_resource_id=name,
322
                        resource_type=before.resource_type,
323
                        before_properties=None,
324
                        after_properties=after.properties,
325
                    )
326
                    self._process_event(
1✔
327
                        action=ChangeAction.Modify,
328
                        logical_resource_id=name,
329
                        event_status=event.status,
330
                        resource_type=before.resource_type,
331
                        reason=event.message,
332
                    )
333

334
                    def cleanup():
1✔
335
                        self._process_event(
1✔
336
                            action=ChangeAction.Remove,
337
                            logical_resource_id=name,
338
                            event_status=OperationStatus.IN_PROGRESS,
339
                            resource_type=before.resource_type,
340
                        )
341
                        event = self._execute_resource_action(
1✔
342
                            action=ChangeAction.Remove,
343
                            logical_resource_id=name,
344
                            resource_type=before.resource_type,
345
                            before_properties=before_properties,
346
                            after_properties=None,
347
                            part_of_replacement=True,
348
                        )
349
                        self._process_event(
1✔
350
                            action=ChangeAction.Remove,
351
                            logical_resource_id=name,
352
                            event_status=event.status,
353
                            resource_type=before.resource_type,
354
                            reason=event.message,
355
                        )
356

357
                    self._defer_action(f"cleanup-from-replacement-{name}", cleanup)
1✔
358
                else:
359
                    event = self._execute_resource_action(
1✔
360
                        action=ChangeAction.Modify,
361
                        logical_resource_id=name,
362
                        resource_type=before.resource_type,
363
                        before_properties=before_properties,
364
                        after_properties=after.properties,
365
                    )
366
                    self._process_event(
1✔
367
                        action=ChangeAction.Modify,
368
                        logical_resource_id=name,
369
                        event_status=event.status,
370
                        resource_type=before.resource_type,
371
                        reason=event.message,
372
                    )
373
            # Case: type migration.
374
            # TODO: Add test to assert that on type change the resources are replaced.
375
            else:
376
                # XXX hacky, stick the previous resources' properties into the payload
377
                before_properties = self._merge_before_properties(name, before)
×
378
                # Register a Removed for the previous type.
379

380
                def perform_deletion():
×
381
                    event = self._execute_resource_action(
×
382
                        action=ChangeAction.Remove,
383
                        logical_resource_id=name,
384
                        resource_type=before.resource_type,
385
                        before_properties=before_properties,
386
                        after_properties=None,
387
                    )
388
                    self._process_event(
×
389
                        action=ChangeAction.Modify,
390
                        logical_resource_id=name,
391
                        event_status=event.status,
392
                        resource_type=before.resource_type,
393
                        reason=event.message,
394
                    )
395

396
                self._defer_action(f"type-migration-{name}", perform_deletion)
×
397

398
                event = self._execute_resource_action(
×
399
                    action=ChangeAction.Add,
400
                    logical_resource_id=name,
401
                    resource_type=after.resource_type,
402
                    before_properties=None,
403
                    after_properties=after.properties,
404
                )
405
                self._process_event(
×
406
                    action=ChangeAction.Modify,
407
                    logical_resource_id=name,
408
                    event_status=event.status,
409
                    resource_type=before.resource_type,
410
                    reason=event.message,
411
                )
412
        elif not is_nothing(before):
1✔
413
            # Case: removal
414
            # XXX hacky, stick the previous resources' properties into the payload
415
            # XXX hacky, stick the previous resources' properties into the payload
416
            before_properties = self._merge_before_properties(name, before)
1✔
417

418
            def perform_deletion():
1✔
419
                self._process_event(
1✔
420
                    action=ChangeAction.Remove,
421
                    logical_resource_id=name,
422
                    resource_type=before.resource_type,
423
                    event_status=OperationStatus.IN_PROGRESS,
424
                )
425
                event = self._execute_resource_action(
1✔
426
                    action=ChangeAction.Remove,
427
                    logical_resource_id=name,
428
                    resource_type=before.resource_type,
429
                    before_properties=before_properties,
430
                    after_properties=None,
431
                )
432
                self._process_event(
1✔
433
                    action=ChangeAction.Remove,
434
                    logical_resource_id=name,
435
                    event_status=event.status,
436
                    resource_type=before.resource_type,
437
                    reason=event.message,
438
                )
439

440
            self._defer_action(f"remove-{name}", perform_deletion)
1✔
441
        elif not is_nothing(after):
1✔
442
            # Case: addition
443
            self._process_event(
1✔
444
                action=ChangeAction.Add,
445
                logical_resource_id=name,
446
                event_status=OperationStatus.IN_PROGRESS,
447
                resource_type=after.resource_type,
448
            )
449
            event = self._execute_resource_action(
1✔
450
                action=ChangeAction.Add,
451
                logical_resource_id=name,
452
                resource_type=after.resource_type,
453
                before_properties=None,
454
                after_properties=after.properties,
455
            )
456
            self._process_event(
1✔
457
                action=ChangeAction.Add,
458
                logical_resource_id=name,
459
                event_status=event.status,
460
                resource_type=after.resource_type,
461
                reason=event.message,
462
            )
463

464
    def _merge_before_properties(
1✔
465
        self, name: str, preproc_resource: PreprocResource
466
    ) -> PreprocProperties:
467
        if previous_resource_properties := self._change_set.stack.resolved_resources.get(
1✔
468
            name, {}
469
        ).get("Properties"):
470
            return PreprocProperties(properties=previous_resource_properties)
1✔
471

472
        # XXX fall back to returning the input value
473
        return copy.deepcopy(preproc_resource.properties)
1✔
474

475
    def _execute_resource_action(
1✔
476
        self,
477
        action: ChangeAction,
478
        logical_resource_id: str,
479
        resource_type: str,
480
        before_properties: PreprocProperties | None,
481
        after_properties: PreprocProperties | None,
482
        part_of_replacement: bool = False,
483
    ) -> ProgressEvent:
484
        LOG.debug("Executing resource action: %s for resource '%s'", action, logical_resource_id)
1✔
485
        payload = self.create_resource_provider_payload(
1✔
486
            action=action,
487
            logical_resource_id=logical_resource_id,
488
            resource_type=resource_type,
489
            before_properties=before_properties,
490
            after_properties=after_properties,
491
        )
492
        resource_provider = self.resource_provider_executor.try_load_resource_provider(
1✔
493
            resource_type
494
        )
495
        track_resource_operation(action, resource_type, missing=resource_provider is not None)
1✔
496

497
        extra_resource_properties = {}
1✔
498
        if resource_provider is not None:
1✔
499
            try:
1✔
500
                event = self.resource_provider_executor.deploy_loop(
1✔
501
                    resource_provider, extra_resource_properties, payload
502
                )
503
            except Exception as e:
1✔
504
                reason = str(e)
1✔
505
                LOG.warning(
1✔
506
                    "Resource provider operation failed: '%s'",
507
                    reason,
508
                    exc_info=LOG.isEnabledFor(logging.DEBUG) and config.CFN_VERBOSE_ERRORS,
509
                )
510
                event = ProgressEvent(
1✔
511
                    OperationStatus.FAILED,
512
                    resource_model={},
513
                    message=f"Resource provider operation failed: {reason}",
514
                )
515
        elif config.CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES:
1✔
516
            log_not_available_message(
1✔
517
                resource_type,
518
                f'No resource provider found for "{resource_type}"',
519
            )
520
            if "CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES" not in os.environ:
1✔
521
                LOG.warning(
1✔
522
                    "Deployment of resource type %s succeeded, but will fail in upcoming LocalStack releases unless CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES is explicitly enabled.",
523
                    resource_type,
524
                )
525
            event = ProgressEvent(
1✔
526
                OperationStatus.SUCCESS,
527
                resource_model={},
528
                message=f"Resource type {resource_type} is not supported but was deployed as a fallback",
529
            )
530
        else:
531
            log_not_available_message(
×
532
                resource_type,
533
                f'No resource provider found for "{resource_type}"',
534
            )
535
            event = ProgressEvent(
×
536
                OperationStatus.FAILED,
537
                resource_model={},
538
                message=f"Resource type {resource_type} not supported",
539
            )
540

541
        if part_of_replacement and action == ChangeAction.Remove:
1✔
542
            # Early return as we don't want to update internal state of the executor if this is a
543
            # cleanup of an old resource. The new resource has already been created and the state
544
            # updated
545
            return event
1✔
546

547
        status_from_action = EventOperationFromAction[action.value]
1✔
548
        resolved_resource = ResolvedResource(
1✔
549
            Properties=event.resource_model,
550
            LogicalResourceId=logical_resource_id,
551
            Type=resource_type,
552
            LastUpdatedTimestamp=datetime.now(UTC),
553
        )
554
        match event.status:
1✔
555
            case OperationStatus.SUCCESS:
1✔
556
                # merge the resources state with the external state
557
                # TODO: this is likely a duplicate of updating from extra_resource_properties
558

559
                # TODO: add typing
560
                # TODO: avoid the use of string literals for sampling from the object, use typed classes instead
561
                # TODO: avoid sampling from resources and use tmp var reference
562
                # TODO: add utils functions to abstract this logic away (resource.update(..))
563
                # TODO: avoid the use of setdefault (debuggability/readability)
564
                # TODO: review the use of merge
565

566
                # Don't update the resolved resources if we have deleted that resource
567
                if action != ChangeAction.Remove:
1✔
568
                    physical_resource_id = (
1✔
569
                        extra_resource_properties["PhysicalResourceId"]
570
                        if resource_provider
571
                        else MOCKED_REFERENCE
572
                    )
573
                    resolved_resource["PhysicalResourceId"] = physical_resource_id
1✔
574
                    resolved_resource["ResourceStatus"] = ResourceStatus(
1✔
575
                        f"{status_from_action}_COMPLETE"
576
                    )
577
                    # TODO: do we actually need this line?
578
                    resolved_resource.update(extra_resource_properties)
1✔
579
            case OperationStatus.FAILED:
1✔
580
                reason = event.message
1✔
581
                LOG.warning(
1✔
582
                    "Resource provider operation failed: '%s'",
583
                    reason,
584
                )
585
                resolved_resource["ResourceStatus"] = ResourceStatus(f"{status_from_action}_FAILED")
1✔
586
                resolved_resource["ResourceStatusReason"] = reason
1✔
UNCOV
587
            case other:
×
588
                raise NotImplementedError(f"Event status '{other}' not handled")
589

590
        self.resources[logical_resource_id] = resolved_resource
1✔
591
        return event
1✔
592

593
    def create_resource_provider_payload(
1✔
594
        self,
595
        action: ChangeAction,
596
        logical_resource_id: str,
597
        resource_type: str,
598
        before_properties: PreprocProperties | None,
599
        after_properties: PreprocProperties | None,
600
    ) -> ResourceProviderPayload | None:
601
        # FIXME: use proper credentials
602
        creds: Credentials = {
1✔
603
            "accessKeyId": self._change_set.stack.account_id,
604
            "secretAccessKey": INTERNAL_AWS_SECRET_ACCESS_KEY,
605
            "sessionToken": "",
606
        }
607
        before_properties_value = before_properties.properties if before_properties else None
1✔
608
        after_properties_value = after_properties.properties if after_properties else None
1✔
609

610
        match action:
1✔
611
            case ChangeAction.Add:
1✔
612
                resource_properties = after_properties_value or {}
1✔
613
                previous_resource_properties = None
1✔
614
            case ChangeAction.Modify | ChangeAction.Dynamic:
1✔
615
                resource_properties = after_properties_value or {}
1✔
616
                previous_resource_properties = before_properties_value or {}
1✔
617
            case ChangeAction.Remove:
1✔
618
                resource_properties = before_properties_value or {}
1✔
619
                # previous_resource_properties = None
620
                # HACK: our providers use a mix of `desired_state` and `previous_state` so ensure the payload is present for both
621
                previous_resource_properties = resource_properties
1✔
UNCOV
622
            case _:
×
623
                raise NotImplementedError(f"Action '{action}' not handled")
624

625
        resource_provider_payload: ResourceProviderPayload = {
1✔
626
            "awsAccountId": self._change_set.stack.account_id,
627
            "callbackContext": {},
628
            "stackId": self._change_set.stack.stack_name,
629
            "resourceType": resource_type,
630
            "resourceTypeVersion": "000000",
631
            # TODO: not actually a UUID
632
            "bearerToken": str(uuid.uuid4()),
633
            "region": self._change_set.stack.region_name,
634
            "action": str(action),
635
            "requestData": {
636
                "logicalResourceId": logical_resource_id,
637
                "resourceProperties": resource_properties,
638
                "previousResourceProperties": previous_resource_properties,
639
                "callerCredentials": creds,
640
                "providerCredentials": creds,
641
                "systemTags": {},
642
                "previousSystemTags": {},
643
                "stackTags": {},
644
                "previousStackTags": {},
645
            },
646
        }
647
        return resource_provider_payload
1✔
648

649
    def _maybe_perform_on_delta(
1✔
650
        self, delta: PreprocEntityDelta, f: Callable[[_T], _T]
651
    ) -> PreprocEntityDelta:
652
        # we only care about the after state
653
        if isinstance(delta.after, str):
1✔
654
            delta.after = f(delta.after)
1✔
655
        return delta
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc