Coverage for lambda / helm-installer / teardown_provider.py: 100.00%
107 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-10-28 16:19 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-10-28 16:19 +0000
1"""Delete-only custom-resource provider for ordered Helm teardown.
3Create and update events are deliberate no-ops. On delete, the provider starts
4an ordered Step Functions execution that invokes the Helm worker once per chart
5and then waits for that execution to finish. A failed uninstall therefore fails
6the CloudFormation delete instead of allowing EKS access entries or the cluster
7to disappear underneath a still-live release.
9Environment variables:
10 TEARDOWN_STATE_MACHINE_ARN: Ordered uninstall state machine.
11 INSTALL_STATE_MACHINE_ARN: Fire-and-forget convergence state machine to
12 stop and drain before teardown starts.
13"""
15from __future__ import annotations
17import hashlib
18import json
19import logging
20import os
21from typing import Any
23import boto3
24from botocore.exceptions import ClientError
26# <pyflowchart-code-diagram> BEGIN - auto-inserted, do not edit
27# Generated at (UTC): 2026-09-27T04:10:54Z
28# Generated from Git commit: 590c275a9cd8dc0e0ba9f8ea74acefb63754df46
29# Flowchart(s) generated from this file:
30# * ``on_event`` -> ``diagrams/code_diagrams/lambda/helm-installer/teardown_provider.on_event.html``
31# (PNG: ``diagrams/code_diagrams/lambda/helm-installer/teardown_provider.on_event.png``)
32# * ``is_complete`` -> ``diagrams/code_diagrams/lambda/helm-installer/teardown_provider.is_complete.html``
33# (PNG: ``diagrams/code_diagrams/lambda/helm-installer/teardown_provider.is_complete.png``)
34# Regenerate with ``SOURCE_DATE_EPOCH=<unix-seconds> GCO_DIAGRAM_SOURCE_COMMIT=<40-char-sha> python diagrams/generate.py --code-only``.
35# <pyflowchart-code-diagram> END
38logger = logging.getLogger()
39logger.setLevel(logging.INFO)
41_IN_FLIGHT_LAMBDA_DRAIN_SECONDS = 16 * 60
44def _sfn() -> Any:
45 return boto3.client("stepfunctions")
48def _ssm(region: str) -> Any:
49 return boto3.client("ssm", region_name=region)
52def _chart_overrides(props: dict[str, Any]) -> dict[str, Any]:
53 """Return the ``Charts`` overrides with the JSON types the stack gave them.
55 CloudFormation delivers custom-resource properties with every number and
56 boolean as a string, so the regional stack sends ``Charts`` as one JSON
57 string and it is decoded here, the same way the Helm orchestrator decodes
58 it. An object (a stack synthesized before that change) is taken as it is.
59 """
60 charts = props.get("Charts", {})
61 if isinstance(charts, str):
62 try:
63 charts = json.loads(charts)
64 except json.JSONDecodeError as exc:
65 raise ValueError(f"Charts is not valid JSON: {exc}") from exc
66 if not isinstance(charts, dict):
67 raise ValueError("Charts must be a JSON object of per-chart overrides")
68 return charts
71def _execution_name(event: dict[str, Any]) -> str:
72 """Return a retry-stable, Step-Functions-safe execution name."""
73 identity = "|".join(
74 str(event.get(key, "")) for key in ("StackId", "RequestId", "LogicalResourceId")
75 )
76 return f"helm-delete-{hashlib.sha256(identity.encode()).hexdigest()[:32]}"
79def _execution_arn(state_machine_arn: str, execution_name: str) -> str:
80 """Build the execution ARN corresponding to a state-machine ARN."""
81 prefix, separator, state_machine_name = state_machine_arn.partition(":stateMachine:")
82 if not separator or not state_machine_name:
83 raise ValueError(f"Invalid Step Functions state machine ARN: {state_machine_arn}")
84 return f"{prefix}:execution:{state_machine_name}:{execution_name}"
87def _stop_running_install_executions(sfn_client: Any, state_machine_arn: str) -> int:
88 """Stop every visible convergence execution and return the count stopped.
90 ``ListExecutions`` is eventually consistent. The delete provider invokes
91 this once before the teardown state machine starts its unconditional drain;
92 the state machine then invokes ``drain_install_executions`` after each full
93 drain interval and loops whenever a late execution is discovered.
94 """
95 execution_arns: list[str] = []
96 request: dict[str, Any] = {
97 "stateMachineArn": state_machine_arn,
98 "statusFilter": "RUNNING",
99 "maxResults": 100,
100 }
101 while True:
102 response = sfn_client.list_executions(**request)
103 execution_arns.extend(
104 execution["executionArn"] for execution in response.get("executions", [])
105 )
106 next_token = response.get("nextToken")
107 if not next_token:
108 break
109 request["nextToken"] = next_token
111 for execution_arn in execution_arns:
112 try:
113 sfn_client.stop_execution(executionArn=execution_arn)
114 except ClientError as exc:
115 # An execution can finish between ListExecutions and StopExecution.
116 # Re-read only a ValidationException and suppress it only when the
117 # execution is now terminal; every other failure blocks teardown.
118 code = exc.response.get("Error", {}).get("Code")
119 if code in {"ExecutionDoesNotExist", "ExecutionNotRunning"}:
120 continue
121 if code == "ValidationException":
122 try:
123 status = sfn_client.describe_execution(executionArn=execution_arn).get("status")
124 except ClientError as describe_exc:
125 describe_code = describe_exc.response.get("Error", {}).get("Code")
126 if describe_code == "ExecutionDoesNotExist":
127 continue
128 raise
129 if status and status != "RUNNING":
130 continue
131 raise
133 if execution_arns:
134 logger.info("Stopped %d install execution(s)", len(execution_arns))
135 return len(execution_arns)
138def drain_install_executions(
139 _event: dict[str, Any],
140 _context: Any = None,
141) -> dict[str, int]:
142 """State-machine task that stops late work and requests another full drain."""
143 stopped = _stop_running_install_executions(
144 _sfn(),
145 os.environ["INSTALL_STATE_MACHINE_ARN"],
146 )
147 return {"StoppedExecutions": stopped}
150def on_event(event: dict[str, Any], _context: Any = None) -> dict[str, Any]:
151 """Start ordered teardown on Delete; no-op on Create and Update."""
152 request_type = event["RequestType"]
153 physical_id = event.get("PhysicalResourceId") or "helm-teardown"
155 if request_type != "Delete":
156 return {"PhysicalResourceId": physical_id}
158 props = event["ResourceProperties"]
159 # Decoded before anything is written, so a malformed payload fails the
160 # Delete without leaving a fence or stopped executions behind.
161 chart_overrides = _chart_overrides(props)
162 state_machine_arn = os.environ["TEARDOWN_STATE_MACHINE_ARN"]
163 install_state_machine_arn = os.environ["INSTALL_STATE_MACHINE_ARN"]
164 execution_name = _execution_name(event)
165 sfn_client = _sfn()
166 fence_name = f"/{props['ProjectName']}/addons/{props['Region']}/_teardown"
167 _ssm(str(props["Region"])).put_parameter(
168 Name=fence_name,
169 Value=execution_name,
170 Type="String",
171 Overwrite=True,
172 )
173 _stop_running_install_executions(sfn_client, install_state_machine_arn)
174 execution_input = {
175 "ClusterName": props["ClusterName"],
176 "Region": props["Region"],
177 "RegistryRegion": props["RegistryRegion"],
178 "ProjectName": props["ProjectName"],
179 "EnabledCharts": props.get("EnabledCharts", []),
180 "Charts": chart_overrides,
181 "KedaOperatorRoleArn": props.get("KedaOperatorRoleArn"),
182 # ListExecutions is eventually consistent and StopExecution cannot
183 # cancel an in-flight Lambda. Always drain the full invocation bound;
184 # the workflow checker loops if late-visible work is stopped.
185 "WaitForInFlightSeconds": _IN_FLIGHT_LAMBDA_DRAIN_SECONDS,
186 }
187 endpoint_group_arn = props.get("EndpointGroupArn")
188 if endpoint_group_arn:
189 execution_input["EndpointGroupArn"] = endpoint_group_arn
191 try:
192 sfn_client.start_execution(
193 stateMachineArn=state_machine_arn,
194 name=execution_name,
195 input=json.dumps(execution_input),
196 )
197 except ClientError as exc:
198 # Provider retries can replay the same Delete request. Reusing the
199 # deterministic name is idempotent when that execution already exists.
200 if exc.response.get("Error", {}).get("Code") != "ExecutionAlreadyExists":
201 raise
203 logger.info("Started ordered Helm teardown execution %s", execution_name)
204 return {"PhysicalResourceId": physical_id}
207def is_complete(event: dict[str, Any], _context: Any = None) -> dict[str, bool]:
208 """Surface teardown terminal status; the workflow owns the drain loop."""
209 if event["RequestType"] != "Delete":
210 return {"IsComplete": True}
212 state_machine_arn = os.environ["TEARDOWN_STATE_MACHINE_ARN"]
213 execution_name = _execution_name(event)
214 execution_arn = _execution_arn(state_machine_arn, execution_name)
215 sfn_client = _sfn()
217 response = sfn_client.describe_execution(executionArn=execution_arn)
218 status = response["status"]
220 if status == "RUNNING":
221 return {"IsComplete": False}
222 if status == "SUCCEEDED":
223 return {"IsComplete": True}
225 detail = response.get("cause") or response.get("error") or "no failure detail"
226 raise RuntimeError(f"Helm teardown execution {status}: {detail}")