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

localstack / localstack / 20476669022

23 Dec 2025 06:23PM UTC coverage: 86.921% (+0.009%) from 86.912%
20476669022

push

github

web-flow
Fix KMS model annotations (#13563)

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

266 existing lines in 7 files now uncovered.

70055 of 80596 relevant lines covered (86.92%)

0.87 hits per line

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

91.67
/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.engine.v2.unsupported_resource import (
1✔
37
    should_ignore_unsupported_resource_type,
38
)
39
from localstack.services.cloudformation.resource_provider import (
1✔
40
    Credentials,
41
    OperationStatus,
42
    ProgressEvent,
43
    ResourceProviderExecutor,
44
    ResourceProviderPayload,
45
)
46
from localstack.services.cloudformation.v2.entities import ChangeSet, ResolvedResource
1✔
47

48
LOG = logging.getLogger(__name__)
1✔
49

50
EventOperationFromAction = {"Add": "CREATE", "Modify": "UPDATE", "Remove": "DELETE"}
1✔
51

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

56
_T = TypeVar("_T")
1✔
57

58

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

65

66
class DeferredAction(Protocol):
1✔
67
    def __call__(self) -> None: ...
1✔
68

69

70
@dataclass
1✔
71
class Deferred:
1✔
72
    name: str
1✔
73
    action: DeferredAction
1✔
74

75

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

81
    def __init__(self, logical_resource_id: str, reason: str | None):
1✔
82
        self.logical_resource_id = logical_resource_id
1✔
83
        self.reason = reason
1✔
84

85

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

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

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

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

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

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

134
        return ChangeSetModelExecutorResult(
1✔
135
            resources=self.resources, outputs=self.outputs, failure_message=failure_message
136
        )
137

138
    def _defer_action(self, name: str, action: DeferredAction):
1✔
139
        self._deferred_actions.append(Deferred(name=name, action=action))
1✔
140

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

152
            try:
1✔
153
                physical_resource_id = self._before_resource_physical_id(logical_resource_id)
1✔
UNCOV
154
            except RuntimeError as e:
×
UNCOV
155
                if strict:
×
UNCOV
156
                    raise e
×
157
        return physical_resource_id
1✔
158

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

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

184
        if event_status == OperationStatus.FAILED:
1✔
185
            self._change_set.stack.set_stack_status(StackStatus(status))
1✔
186

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

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

203
    def visit_node_depends_on(self, node_depends_on: NodeDependsOn) -> PreprocEntityDelta:
1✔
204
        array_identifiers_delta = super().visit_node_depends_on(node_depends_on=node_depends_on)
1✔
205

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

219
        return array_identifiers_delta
1✔
220

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

246
        before = delta.before
1✔
247
        after = delta.after
1✔
248

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

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

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

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

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

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

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

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

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

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

UNCOV
399
                self._defer_action(f"type-migration-{name}", perform_deletion)
×
400

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

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

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

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

475
        # XXX fall back to returning the input value
476
        return copy.deepcopy(preproc_resource.properties)
1✔
477

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

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

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

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

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

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

595
        self.resources[logical_resource_id] = resolved_resource
1✔
596
        return event
1✔
597

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

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

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

654
    def _maybe_perform_on_delta(
1✔
655
        self, delta: PreprocEntityDelta, f: Callable[[_T], _T]
656
    ) -> PreprocEntityDelta:
657
        # we only care about the after state
658
        if isinstance(delta.after, str):
1✔
659
            delta.after = f(delta.after)
1✔
660
        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