|
4 | 4 | import re
|
5 | 5 | import traceback
|
6 | 6 | import uuid
|
7 |
| -from abc import ABC, abstractmethod |
8 | 7 | from typing import Optional
|
9 | 8 |
|
10 | 9 | from botocore.exceptions import ClientError
|
@@ -831,7 +830,7 @@ def evaluate_resource_condition(conditions: dict[str, bool], resource: dict) ->
|
831 | 830 | # -----------------------
|
832 | 831 |
|
833 | 832 |
|
834 |
| -class TemplateDeployerBase(ABC): |
| 833 | +class TemplateDeployer: |
835 | 834 | def __init__(self, account_id: str, region_name: str, stack):
|
836 | 835 | self.stack = stack
|
837 | 836 | self.account_id = account_id
|
@@ -1338,23 +1337,6 @@ def create_resource_provider_payload(
|
1338 | 1337 | }
|
1339 | 1338 | return resource_provider_payload
|
1340 | 1339 |
|
1341 |
| - @abstractmethod |
1342 |
| - def delete_stack(self): |
1343 |
| - pass |
1344 |
| - |
1345 |
| - @abstractmethod |
1346 |
| - def do_apply_changes_in_loop(self, changes: list[ChangeConfig], stack: Stack) -> list: |
1347 |
| - pass |
1348 |
| - |
1349 |
| - @classmethod |
1350 |
| - def factory(cls, *args, **kwargs): |
1351 |
| - if config.CFN_LEGACY_TEMPLATE_DEPLOYER: |
1352 |
| - return TemplateDeployerLegacy(*args, **kwargs) |
1353 |
| - else: |
1354 |
| - return TemplateDeployerV2(*args, **kwargs) |
1355 |
| - |
1356 |
| - |
1357 |
| -class TemplateDeployerV2(TemplateDeployerBase): |
1358 | 1340 | def delete_stack(self):
|
1359 | 1341 | if not self.stack:
|
1360 | 1342 | return
|
@@ -1531,226 +1513,6 @@ def do_apply_changes_in_loop(self, changes: list[ChangeConfig], stack: Stack) ->
|
1531 | 1513 | return changes_done
|
1532 | 1514 |
|
1533 | 1515 |
|
1534 |
| -class TemplateDeployerLegacy(TemplateDeployerBase): |
1535 |
| - def delete_stack(self): |
1536 |
| - if not self.stack: |
1537 |
| - return |
1538 |
| - self.stack.set_stack_status("DELETE_IN_PROGRESS") |
1539 |
| - stack_resources = list(self.stack.resources.values()) |
1540 |
| - resources = {r["LogicalResourceId"]: clone_safe(r) for r in stack_resources} |
1541 |
| - |
1542 |
| - # TODO: what is this doing? |
1543 |
| - for key, resource in resources.items(): |
1544 |
| - resource["Properties"] = resource.get( |
1545 |
| - "Properties", clone_safe(resource) |
1546 |
| - ) # TODO: why is there a fallback? |
1547 |
| - resource["ResourceType"] = get_resource_type(resource) |
1548 |
| - |
1549 |
| - def _safe_lookup_is_deleted(r_id): |
1550 |
| - """handles the case where self.stack.resource_status(..) fails for whatever reason""" |
1551 |
| - try: |
1552 |
| - return self.stack.resource_status(r_id).get("ResourceStatus") == "DELETE_COMPLETE" |
1553 |
| - except Exception: |
1554 |
| - if config.CFN_VERBOSE_ERRORS: |
1555 |
| - LOG.exception(f"failed to lookup if resource {r_id} is deleted") |
1556 |
| - return True # just an assumption |
1557 |
| - |
1558 |
| - # a bit of a workaround until we have a proper dependency graph |
1559 |
| - max_cycle = 10 # 10 cycles should be a safe choice for now |
1560 |
| - for iteration_cycle in range(1, max_cycle + 1): |
1561 |
| - resources = { |
1562 |
| - r_id: r for r_id, r in resources.items() if not _safe_lookup_is_deleted(r_id) |
1563 |
| - } |
1564 |
| - if len(resources) == 0: |
1565 |
| - break |
1566 |
| - for i, (resource_id, resource) in enumerate(resources.items()): |
1567 |
| - try: |
1568 |
| - # TODO: cache condition value in resource details on deployment and use cached value here |
1569 |
| - if evaluate_resource_condition( |
1570 |
| - self.stack.resolved_conditions, |
1571 |
| - resource, |
1572 |
| - ): |
1573 |
| - executor = self.create_resource_provider_executor() |
1574 |
| - resource_provider_payload = self.create_resource_provider_payload( |
1575 |
| - "Remove", logical_resource_id=resource_id |
1576 |
| - ) |
1577 |
| - # TODO: check actual return value |
1578 |
| - LOG.debug( |
1579 |
| - 'Handling "Remove" for resource "%s" (%s/%s) type "%s" in loop iteration %s', |
1580 |
| - resource_id, |
1581 |
| - i + 1, |
1582 |
| - len(resources), |
1583 |
| - resource["ResourceType"], |
1584 |
| - iteration_cycle, |
1585 |
| - ) |
1586 |
| - resource_provider = executor.try_load_resource_provider( |
1587 |
| - get_resource_type(resource) |
1588 |
| - ) |
1589 |
| - if resource_provider is not None: |
1590 |
| - event = executor.deploy_loop( |
1591 |
| - resource_provider, resource, resource_provider_payload |
1592 |
| - ) |
1593 |
| - else: |
1594 |
| - event = ProgressEvent(OperationStatus.SUCCESS, resource_model={}) |
1595 |
| - match event.status: |
1596 |
| - case OperationStatus.SUCCESS: |
1597 |
| - self.stack.set_resource_status(resource_id, "DELETE_COMPLETE") |
1598 |
| - case OperationStatus.PENDING: |
1599 |
| - # the resource is still being deleted, specifically the provider has |
1600 |
| -
10000
# signalled that the deployment loop should skip this resource this |
1601 |
| - # time and come back to it later, likely due to unmet child |
1602 |
| - # resources still existing because we don't delete things in the |
1603 |
| - # correct order yet. |
1604 |
| - continue |
1605 |
| - case OperationStatus.FAILED: |
1606 |
| - if iteration_cycle == max_cycle: |
1607 |
| - LOG.exception( |
1608 |
| - "Last cycle failed to delete resource with id %s. Reason: %s", |
1609 |
| - resource_id, |
1610 |
| - event.message or "unknown", |
1611 |
| - ) |
1612 |
| - else: |
1613 |
| - # the resource failed to delete this time, but we have more |
1614 |
| - # iterations left to complete the process |
1615 |
| - continue |
1616 |
| - case OperationStatus.IN_PROGRESS: |
1617 |
| - # the resource provider executor should not return this state, so |
1618 |
| - # this state is a programming error |
1619 |
| - raise Exception( |
1620 |
| - "Programming error: ResourceProviderExecutor cannot return IN_PROGRESS" |
1621 |
| - ) |
1622 |
| - case other_status: |
1623 |
| - raise Exception(f"Use of unsupported status found: {other_status}") |
1624 |
| - |
1625 |
| - except Exception as e: |
1626 |
| - if iteration_cycle == max_cycle: |
1627 |
| - LOG.exception( |
1628 |
| - "Last cycle failed to delete resource with id %s. Final exception: %s", |
1629 |
| - resource_id, |
1630 |
| - e, |
1631 |
| - ) |
1632 |
| - else: |
1633 |
| - log_method = LOG.warning |
1634 |
| - if config.CFN_VERBOSE_ERRORS: |
1635 |
| - log_method = LOG.exception |
1636 |
| - log_method( |
1637 |
| - "Failed delete of resource with id %s in iteration cycle %d. Retrying in next cycle.", |
1638 |
| - resource_id, |
1639 |
| - iteration_cycle, |
1640 |
| - ) |
1641 |
| - |
1642 |
| - # update status |
1643 |
| - self.stack.set_stack_status("DELETE_COMPLETE") |
1644 |
| - self.stack.set_time_attribute("DeletionTime") |
1645 |
| - |
1646 |
| - def do_apply_changes_in_loop(self, changes: list[ChangeConfig], stack: Stack) -> list: |
1647 |
| - # apply changes in a retry loop, to resolve resource dependencies and converge to the target state |
1648 |
| - changes_done = [] |
1649 |
| - max_iters = 30 |
1650 |
| - new_resources = stack.resources |
1651 |
| - |
1652 |
| - # start deployment loop |
1653 |
| - for i in range(max_iters): |
1654 |
| - j = 0 |
1655 |
| - updated = False |
1656 |
| - while j < len(changes): |
1657 |
| - change = changes[j] |
1658 |
| - res_change = change["ResourceChange"] |
1659 |
| - action = res_change["Action"] |
1660 |
| - is_add_or_modify = action in ["Add", "Modify"] |
1661 |
| - resource_id = res_change["LogicalResourceId"] |
1662 |
| - |
1663 |
| - # TODO: do resolve_refs_recursively once here |
1664 |
| - try: |
1665 |
| - if is_add_or_modify: |
1666 |
| - resource = new_resources[resource_id] |
1667 |
| - should_deploy = self.prepare_should_deploy_change( |
1668 |
| - resource_id, change, stack, new_resources |
1669 |
| - ) |
1670 |
| - LOG.debug( |
1671 |
| - 'Handling "%s" for resource "%s" (%s/%s) type "%s" in loop iteration %s (should_deploy=%s)', |
1672 |
| - action, |
1673 |
| - resource_id, |
1674 |
| - j + 1, |
1675 |
| - len(changes), |
1676 |
| - res_change["ResourceType"], |
1677 |
| - i + 1, |
1678 |
| - should_deploy, |
1679 |
| - ) |
1680 |
| - if not should_deploy: |
1681 |
| - del changes[j] |
1682 |
| - stack_action = get_action_name_for_resource_change(action) |
1683 |
| - stack.set_resource_status(resource_id, f"{stack_action}_COMPLETE") |
1684 |
| - continue |
1685 |
| - if not self.all_resource_dependencies_satisfied(resource): |
1686 |
| - j += 1 |
1687 |
| - continue |
1688 |
| - elif action == "Remove": |
1689 |
| - should_remove = self.prepare_should_deploy_change( |
1690 |
| - resource_id, change, stack, new_resources |
1691 |
| - ) |
1692 |
| - if not should_remove: |
1693 |
| - del changes[j] |
1694 |
| - continue |
1695 |
| - LOG.debug( |
1696 |
| - 'Handling "%s" for resource "%s" (%s/%s) type "%s" in loop iteration %s', |
1697 |
| - action, |
1698 |
| - resource_id, |
1699 |
| - j + 1, |
1700 |
| - len(changes), |
1701 |
| - res_change["ResourceType"], |
1702 |
| - i + 1, |
1703 |
| - ) |
1704 |
| - self.apply_change(change, stack=stack) |
1705 |
| - changes_done.append(change) |
1706 |
| - del changes[j] |
1707 |
| - updated = True |
1708 |
| - except DependencyNotYetSatisfied as e: |
1709 |
| - log_method = LOG.debug |
1710 |
| - if config.CFN_VERBOSE_ERRORS: |
1711 |
| - log_method = LOG.exception |
1712 |
| - log_method( |
1713 |
| - 'Dependencies for "%s" not yet satisfied, retrying in next loop: %s', |
1714 |
| - resource_id, |
1715 |
| - e, |
1716 |
| - ) |
1717 |
| - j += 1 |
1718 |
| - except Exception as e: |
1719 |
| - status_action = { |
1720 |
| - "Add": "CREATE", |
1721 |
| - "Modify": "UPDATE", |
1722 |
| - "Dynamic": "UPDATE", |
1723 |
| - "Remove": "DELETE", |
1724 |
| - }[action] |
1725 |
| - stack.add_stack_event( |
1726 |
| - resource_id=resource_id, |
1727 |
| - physical_res_id=new_resources[resource_id].get("PhysicalResourceId"), |
1728 |
| - status=f"{status_action}_FAILED", |
1729 |
| - status_reason=str(e), |
1730 |
| - ) |
1731 |
| - if config.CFN_VERBOSE_ERRORS: |
1732 |
| - LOG.exception( |
1733 |
| - f"Failed to deploy resource {resource_id}, stack deploy failed" |
1734 |
| - ) |
1735 |
| - raise |
1736 |
| - if not changes: |
1737 |
| - break |
1738 |
| - if not updated: |
1739 |
| - raise Exception( |
1740 |
| - f"Resource deployment loop completed, pending resource changes: {changes}" |
1741 |
| - ) |
1742 |
| - |
1743 |
| - # clean up references to deleted resources in stack |
1744 |
| - deletes = [c for c in changes_done if c["ResourceChange"]["Action"] == "Remove"] |
1745 |
| - for delete in deletes: |
1746 |
| - stack.template["Resources"].pop(delete["ResourceChange"]["LogicalResourceId"], None) |
1747 |
| - |
1748 |
| - # resolve outputs |
1749 |
| - stack.resolved_outputs = resolve_outputs(self.account_id, self.region_name, stack) |
1750 |
| - |
1751 |
| - return changes_done |
1752 |
| - |
1753 |
| - |
1754 | 1516 | # FIXME: resolve_refs_recursively should not be needed, the resources themselves should have those values available already
|
1755 | 1517 | def resolve_outputs(account_id: str, region_name: str, stack) -> list[dict]:
|
1756 | 1518 | result = []
|
|
0 commit comments