ec2-instance-running-scheduler 0.1.8 → 0.2.0
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.
- package/.jsii +110 -36
- package/API.md +77 -7
- package/README.md +39 -12
- package/assets/funcs/running-scheduler.lambda/index.js +144 -38
- package/assets/funcs/running-scheduler.lambda/index.js.map +4 -4
- package/lib/constructs/ec2-instance-running-scheduler.d.ts +31 -1
- package/lib/constructs/ec2-instance-running-scheduler.js +10 -3
- package/lib/funcs/running-scheduler-polling-config.d.ts +17 -0
- package/lib/funcs/running-scheduler-polling-config.js +21 -0
- package/lib/funcs/running-scheduler-polling-env.d.ts +8 -0
- package/lib/funcs/running-scheduler-polling-env.js +53 -0
- package/lib/funcs/running-scheduler-predicates.d.ts +56 -0
- package/lib/funcs/running-scheduler-predicates.js +55 -2
- package/lib/funcs/running-scheduler.lambda.d.ts +12 -3
- package/lib/funcs/running-scheduler.lambda.js +53 -11
- package/lib/index.d.ts +3 -1
- package/lib/index.js +4 -2
- package/lib/stacks/ec2-instance-running-schedule-stack.d.ts +6 -4
- package/lib/stacks/ec2-instance-running-schedule-stack.js +6 -6
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -4,22 +4,23 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/ec2-instance-running-scheduler)
|
|
5
5
|
[](https://github.com/gammarers-aws-cdk-constructs/ec2-instance-running-scheduler/actions/workflows/release.yml)
|
|
6
6
|
[](https://github.com/gammarers-aws-cdk-constructs/ec2-instance-running-scheduler/releases)
|
|
7
|
-
|
|
8
7
|
[](https://constructs.dev/packages/ec2-instance-running-scheduler)
|
|
9
8
|
|
|
10
|
-
AWS CDK construct that starts and stops EC2 instances on a cron schedule using **EventBridge Scheduler** and a **Durable Execution Lambda**. The handler discovers instances with the **Resource Groups Tagging API**,
|
|
9
|
+
AWS CDK construct library that starts and stops EC2 instances on a cron schedule using **EventBridge Scheduler** and a **Durable Execution Lambda**. The handler discovers instances with the **Resource Groups Tagging API**, issues start/stop, **polls until each instance reaches a stable target state** (durable `step` / `wait`), processes **multiple instances in parallel** (bounded concurrency), and posts **Slack** summary and per-instance thread messages using a secret from **Secrets Manager**. The Lambda emits **structured application logs** alongside JSON platform logs.
|
|
11
10
|
|
|
12
11
|
## Features
|
|
13
12
|
|
|
14
13
|
- **Tag-based targeting** – Select EC2 instances by tag key and values (e.g. `Schedule` / `YES`) via `tag:GetResources`.
|
|
15
14
|
- **EventBridge Scheduler** – Separate cron rules for start and stop, with per-rule timezone (`aws-cdk-lib` `TimeZone`).
|
|
16
15
|
- **Durable Lambda** – One Lambda with AWS Lambda Durable Execution (`step`, `wait`, `map`, child contexts per instance) for long-running workflows without Step Functions.
|
|
17
|
-
- **Stable-state polling** – After start/stop, the function waits and re-describes instances until `running` (start mode) or `stopped` (stop mode)
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
16
|
+
- **Stable-state polling** – After start/stop, the function waits (20 seconds between attempts) and re-describes instances until `running` (start mode) or `stopped` (stop mode).
|
|
17
|
+
- **Configurable polling limits** – Per-instance **max loop count** and **max elapsed time** via `resourcePolling` (default: 90 loops / 1800 seconds). Failures use explicit `ResourcePollingFailed:*` messages instead of running until the Durable execution timeout (construct default: 2 hours).
|
|
18
|
+
- **Validated environment variables** – The bundled handler parses required and polling env vars at invocation using strict decimal integer rules; polling limits must be **positive integers** (`> 0`).
|
|
19
|
+
- **Slack notifications** – Parent message plus threaded updates per instance; credentials from Secrets Manager JSON (`token`, `channel`). The construct sets **`SLACK_SECRET_NAME`** on the function.
|
|
20
|
+
- **Structured logging** – Durable execution **`ctx.logger`** for traceable JSON application logs (invocation, describe/start/stop/wait loops, polling limit errors, Slack steps, completion).
|
|
20
21
|
- **Scheduling toggle** – Enable or disable both schedules without removing the stack (`enableScheduling`).
|
|
21
22
|
- **Configurable schedules** – Optional cron overrides for start and stop (`minute`, `hour`, `week`, `timezone`); sensible defaults if omitted.
|
|
22
|
-
- **IAM and observability** – EC2 and tagging API permissions, Slack secret read grant, **Parameters and Secrets Lambda Extension
|
|
23
|
+
- **IAM and observability** – EC2 and tagging API permissions, Slack secret read grant, **Parameters and Secrets Lambda Extension**, JSON logging, and a dedicated log group (construct defaults).
|
|
23
24
|
|
|
24
25
|
## Installation
|
|
25
26
|
|
|
@@ -74,10 +75,14 @@ new EC2InstanceRunningScheduler(stack, 'EC2InstanceRunningScheduler', {
|
|
|
74
75
|
week: 'MON-FRI',
|
|
75
76
|
},
|
|
76
77
|
enableScheduling: true,
|
|
78
|
+
resourcePolling: {
|
|
79
|
+
maxLoopCount: 120,
|
|
80
|
+
maxElapsedSeconds: 3600,
|
|
81
|
+
},
|
|
77
82
|
});
|
|
78
83
|
```
|
|
79
84
|
|
|
80
|
-
Use the **stack** `EC2InstanceRunningScheduleStack` when deploying the scheduler as its own stack. It accepts the
|
|
85
|
+
Use the **stack** `EC2InstanceRunningScheduleStack` when deploying the scheduler as its own stack. It accepts the same **targeting, schedules, secrets, and enable flag** as the construct (plus standard `StackProps` such as `env`). For **`resourcePolling`**, use the construct directly or extend the stack in your app.
|
|
81
86
|
|
|
82
87
|
```typescript
|
|
83
88
|
import * as cdk from 'aws-cdk-lib';
|
|
@@ -110,19 +115,32 @@ new EC2InstanceRunningScheduleStack(app, 'EC2InstanceRunningScheduleStack', {
|
|
|
110
115
|
});
|
|
111
116
|
```
|
|
112
117
|
|
|
113
|
-
EventBridge Scheduler invokes the Lambda with `Params.TagKey`, `Params.TagValues`, and `Params.Mode` (`Start` or `Stop`); the construct wires this for you. The function environment includes
|
|
118
|
+
EventBridge Scheduler invokes the Lambda with `Params.TagKey`, `Params.TagValues`, and `Params.Mode` (`Start` or `Stop`); the construct wires this for you. The function environment includes:
|
|
119
|
+
|
|
120
|
+
| Variable | Source | Purpose |
|
|
121
|
+
|----------|--------|---------|
|
|
122
|
+
| `SLACK_SECRET_NAME` | `secrets.slackSecretName` | Secrets Manager secret for Slack (required) |
|
|
123
|
+
| `PROCESS_RESOURCE_MAX_LOOP_COUNT` | `resourcePolling.maxLoopCount` (default `90`) | Max describe/poll iterations per instance |
|
|
124
|
+
| `PROCESS_RESOURCE_MAX_ELAPSED_SECONDS` | `resourcePolling.maxElapsedSeconds` (default `1800`) | Max wall-clock seconds polling one instance |
|
|
125
|
+
|
|
126
|
+
When you set polling limits via `resourcePolling`, the construct writes them as decimal integer strings. If you override these variables manually, each value must be a **strict decimal integer** (e.g. `"120"` is valid; `"0x10"`, `"3.14"`, or `"10abc"` are rejected) and **greater than zero**. Invalid or missing `SLACK_SECRET_NAME` causes the handler to fail at the start of an invocation.
|
|
114
127
|
|
|
115
128
|
## Options
|
|
116
129
|
|
|
117
|
-
|
|
130
|
+
### EC2InstanceRunningScheduler
|
|
118
131
|
|
|
119
132
|
| Option | Type | Required | Description |
|
|
120
133
|
|--------|------|----------|-------------|
|
|
121
134
|
| `targetResource` | `TargetResource` | Yes | Tag key and values used to select EC2 instances. |
|
|
122
|
-
| `secrets` | `Secrets` | Yes |
|
|
135
|
+
| `secrets` | `Secrets` | Yes | Secrets Manager secret for Slack (`slackSecretName`). |
|
|
123
136
|
| `startSchedule` | `Schedule` | No | Cron for starting instances (default: `50 7 ? * MON-FRI *` in `Etc/UTC`). |
|
|
124
137
|
| `stopSchedule` | `Schedule` | No | Cron for stopping instances (default: `5 19 ? * MON-FRI *` in `Etc/UTC`). |
|
|
125
138
|
| `enableScheduling` | `boolean` | No | Whether both scheduler rules are enabled (default: `true`). |
|
|
139
|
+
| `resourcePolling` | `ResourcePollingLimits` | No | Per-instance polling caps (see below). |
|
|
140
|
+
|
|
141
|
+
### EC2InstanceRunningScheduleStack
|
|
142
|
+
|
|
143
|
+
Includes `targetResource`, `secrets`, `startSchedule`, `stopSchedule`, `enableScheduling`, and standard `StackProps`. Does **not** expose `resourcePolling`; use `EC2InstanceRunningScheduler` when you need custom polling limits.
|
|
126
144
|
|
|
127
145
|
### TargetResource
|
|
128
146
|
|
|
@@ -138,13 +156,22 @@ These options apply to **`EC2InstanceRunningScheduler`** and to **`EC2InstanceRu
|
|
|
138
156
|
|
|
139
157
|
### Secrets
|
|
140
158
|
|
|
141
|
-
- `slackSecretName` – Name of the AWS Secrets Manager secret. The Lambda expects JSON with **`token`** (Slack bot token) and **`channel`** (channel ID or name for `chat.postMessage`).
|
|
159
|
+
- `slackSecretName` – Name of the AWS Secrets Manager secret. The Lambda expects JSON with **`token`** (Slack bot token) and **`channel`** (channel ID or name for `chat.postMessage`).
|
|
160
|
+
|
|
161
|
+
### ResourcePollingLimits
|
|
162
|
+
|
|
163
|
+
Written to `PROCESS_RESOURCE_MAX_LOOP_COUNT` and `PROCESS_RESOURCE_MAX_ELAPSED_SECONDS` on the running scheduler Lambda.
|
|
164
|
+
|
|
165
|
+
- `maxLoopCount` – Maximum describe/poll loop iterations per instance (default: **90**). Must be a positive integer when set.
|
|
166
|
+
- `maxElapsedSeconds` – Maximum wall-clock seconds spent polling one instance (default: **1800**, 30 minutes). Must be a positive integer when set.
|
|
167
|
+
|
|
168
|
+
When a limit is exceeded during polling, the handler throws an error with prefix `ResourcePollingFailed:` (`MaxLoopCountExceeded`, `MaxElapsedTimeExceeded`, or `UnexpectedInstanceState` for unknown EC2 states).
|
|
142
169
|
|
|
143
170
|
## Requirements
|
|
144
171
|
|
|
145
172
|
- **Node.js** ≥ 20.0.0 (for developing or synthesizing CDK apps that depend on this package).
|
|
146
173
|
- **aws-cdk-lib** ^2.232.0 and **constructs** ^10.5.1 (peer dependencies).
|
|
147
|
-
- **AWS** – EventBridge Scheduler; Lambda with **Durable Execution** (
|
|
174
|
+
- **AWS** – EventBridge Scheduler; Lambda with **Durable Execution** (Node.js **24.x** runtime in the construct; Durable Execution requires a supported Node.js runtime in your region), a **live alias**, **Parameters and Secrets Lambda Extension**; EC2 (`DescribeInstances`, `StartInstances`, `StopInstances`); Resource Groups Tagging API (`tag:GetResources`); Secrets Manager. The deployed function uses **arm64**, Durable Execution IAM policies, a 2-hour Durable execution timeout (construct default), and a bundled handler that loads secrets via **aws-lambda-secret-fetcher**.
|
|
148
175
|
|
|
149
176
|
## License
|
|
150
177
|
|
|
@@ -20017,7 +20017,22 @@ var require_lib3 = __commonJS({
|
|
|
20017
20017
|
"use strict";
|
|
20018
20018
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
20019
20019
|
exports2.SafeEnvGetter = exports2.SafeEnvType = exports2.SafeEnvGetterValidationError = exports2.SafeEnvGetterError = void 0;
|
|
20020
|
+
var STRICT_INTEGER_PATTERN = /^-?\d+$/;
|
|
20021
|
+
var parseStrictInteger = (raw) => {
|
|
20022
|
+
const trimmed = raw.trim();
|
|
20023
|
+
if (trimmed === "" || !STRICT_INTEGER_PATTERN.test(trimmed)) {
|
|
20024
|
+
return void 0;
|
|
20025
|
+
}
|
|
20026
|
+
const n = Number(trimmed);
|
|
20027
|
+
if (!Number.isFinite(n) || !Number.isInteger(n)) {
|
|
20028
|
+
return void 0;
|
|
20029
|
+
}
|
|
20030
|
+
return n;
|
|
20031
|
+
};
|
|
20020
20032
|
var SafeEnvGetterError = class extends Error {
|
|
20033
|
+
/**
|
|
20034
|
+
* @param message - Error message.
|
|
20035
|
+
*/
|
|
20021
20036
|
constructor(message) {
|
|
20022
20037
|
super(message);
|
|
20023
20038
|
this.name = "SafeEnvGetterError";
|
|
@@ -20027,6 +20042,10 @@ var require_lib3 = __commonJS({
|
|
|
20027
20042
|
var SafeEnvGetterValidationError = class _SafeEnvGetterValidationError extends SafeEnvGetterError {
|
|
20028
20043
|
/**
|
|
20029
20044
|
* Formats validation errors into a human-readable error message.
|
|
20045
|
+
*
|
|
20046
|
+
* @template K - Environment variable key type.
|
|
20047
|
+
* @param errors - Validation error entries to format.
|
|
20048
|
+
* @returns Multi-line summary listing each key and message.
|
|
20030
20049
|
*/
|
|
20031
20050
|
static format(errors) {
|
|
20032
20051
|
const lines = errors.map((e) => `- ${e.key}: ${e.message}${e.raw == null ? "" : ` (raw="${e.raw}")`}`);
|
|
@@ -20035,6 +20054,8 @@ ${lines.join("\n")}`;
|
|
|
20035
20054
|
}
|
|
20036
20055
|
/**
|
|
20037
20056
|
* Creates a new validation error from one or more `SafeEnvError` entries.
|
|
20057
|
+
*
|
|
20058
|
+
* @param errors - One or more structured validation errors.
|
|
20038
20059
|
*/
|
|
20039
20060
|
constructor(errors) {
|
|
20040
20061
|
const msg = _SafeEnvGetterValidationError.format(errors);
|
|
@@ -20046,9 +20067,12 @@ ${lines.join("\n")}`;
|
|
|
20046
20067
|
};
|
|
20047
20068
|
exports2.SafeEnvGetterValidationError = SafeEnvGetterValidationError;
|
|
20048
20069
|
exports2.SafeEnvType = {
|
|
20049
|
-
/** Spec for a string value. */
|
|
20070
|
+
/** Spec for a string value (returned as-is). */
|
|
20050
20071
|
String: { type: "string" },
|
|
20051
|
-
/**
|
|
20072
|
+
/**
|
|
20073
|
+
* Spec for a strict decimal integer.
|
|
20074
|
+
* Rejects whitespace-only, hex, `Infinity`, `NaN`, and non-integer forms.
|
|
20075
|
+
*/
|
|
20052
20076
|
Number: { type: "number" },
|
|
20053
20077
|
/** Spec for a boolean value (1/true/yes/on → true). */
|
|
20054
20078
|
Boolean: { type: "boolean" },
|
|
@@ -20073,8 +20097,8 @@ ${lines.join("\n")}`;
|
|
|
20073
20097
|
}
|
|
20074
20098
|
switch (spec.type) {
|
|
20075
20099
|
case "number": {
|
|
20076
|
-
const n =
|
|
20077
|
-
if (
|
|
20100
|
+
const n = parseStrictInteger(raw);
|
|
20101
|
+
if (n === void 0) {
|
|
20078
20102
|
throw new SafeEnvGetterValidationError([
|
|
20079
20103
|
{ key, message: `Env ${key}: expected number, got "${raw}"`, raw, kind: "invalid_number" }
|
|
20080
20104
|
]);
|
|
@@ -20113,8 +20137,8 @@ ${lines.join("\n")}`;
|
|
|
20113
20137
|
continue;
|
|
20114
20138
|
}
|
|
20115
20139
|
if (spec.type === "number") {
|
|
20116
|
-
const n =
|
|
20117
|
-
if (
|
|
20140
|
+
const n = parseStrictInteger(raw);
|
|
20141
|
+
if (n === void 0) {
|
|
20118
20142
|
errors.push({ key, message: `Env ${key}: expected number, got "${raw}"`, raw, kind: "invalid_number" });
|
|
20119
20143
|
continue;
|
|
20120
20144
|
}
|
|
@@ -20145,7 +20169,9 @@ ${lines.join("\n")}`;
|
|
|
20145
20169
|
return envs;
|
|
20146
20170
|
};
|
|
20147
20171
|
exports2.SafeEnvGetter = {
|
|
20172
|
+
/** Reads and parses a single environment variable. See {@link getEnv}. */
|
|
20148
20173
|
getEnv,
|
|
20174
|
+
/** Reads and parses multiple environment variables. See {@link getEnvs}. */
|
|
20149
20175
|
getEnvs
|
|
20150
20176
|
};
|
|
20151
20177
|
}
|
|
@@ -20167,13 +20193,7 @@ var import_node_console = require("node:console");
|
|
|
20167
20193
|
var import_node_util = __toESM(require("node:util"), 1);
|
|
20168
20194
|
var import_node_url = require("node:url");
|
|
20169
20195
|
var import_node_path = require("node:path");
|
|
20170
|
-
var import_node_url2 = __toESM(require("node:url"), 1);
|
|
20171
|
-
var import_node_path2 = __toESM(require("node:path"), 1);
|
|
20172
|
-
var import_node_module = __toESM(require("node:module"), 1);
|
|
20173
20196
|
var import_meta = {};
|
|
20174
|
-
var __filename = import_node_url2.default.fileURLToPath(import_meta.url);
|
|
20175
|
-
var __dirname = import_node_path2.default.dirname(__filename);
|
|
20176
|
-
var require2 = import_node_module.default.createRequire(import_meta.url);
|
|
20177
20197
|
var DurableExecutionMode;
|
|
20178
20198
|
(function(DurableExecutionMode2) {
|
|
20179
20199
|
DurableExecutionMode2["ExecutionMode"] = "ExecutionMode";
|
|
@@ -23344,34 +23364,25 @@ var createDefaultLogger = (executionContext) => {
|
|
|
23344
23364
|
return new DefaultLogger(executionContext);
|
|
23345
23365
|
};
|
|
23346
23366
|
var runtimeDir = process.env.LAMBDA_RUNTIME_DIR || "/var/runtime";
|
|
23367
|
+
var moduleFilePath;
|
|
23368
|
+
if (typeof __filename !== "undefined") {
|
|
23369
|
+
moduleFilePath = __filename;
|
|
23370
|
+
} else if (typeof import_meta?.url === "string") {
|
|
23371
|
+
moduleFilePath = (0, import_node_url.fileURLToPath)(import_meta.url);
|
|
23372
|
+
}
|
|
23347
23373
|
function isInLambdaRuntime() {
|
|
23348
23374
|
try {
|
|
23349
|
-
if (
|
|
23350
|
-
|
|
23351
|
-
if (typeof __filename !== "undefined") {
|
|
23352
|
-
currentFilePath = __filename;
|
|
23353
|
-
} else {
|
|
23354
|
-
try {
|
|
23355
|
-
const getImportMeta = new Function("return import.meta");
|
|
23356
|
-
const importMeta = getImportMeta();
|
|
23357
|
-
if (importMeta && importMeta.url) {
|
|
23358
|
-
currentFilePath = (0, import_node_url.fileURLToPath)(importMeta.url);
|
|
23359
|
-
}
|
|
23360
|
-
} catch {
|
|
23361
|
-
}
|
|
23362
|
-
}
|
|
23363
|
-
if (currentFilePath) {
|
|
23364
|
-
const libraryDirectory = (0, import_node_path.dirname)(currentFilePath);
|
|
23365
|
-
return libraryDirectory.startsWith(runtimeDir);
|
|
23375
|
+
if (!moduleFilePath) {
|
|
23376
|
+
return false;
|
|
23366
23377
|
}
|
|
23367
|
-
return
|
|
23378
|
+
return (0, import_node_path.dirname)(moduleFilePath).startsWith(runtimeDir);
|
|
23368
23379
|
} catch {
|
|
23369
23380
|
return false;
|
|
23370
23381
|
}
|
|
23371
23382
|
}
|
|
23372
23383
|
var isRuntimeBundled = isInLambdaRuntime();
|
|
23373
23384
|
var SDK_NAME = "aws-durable-execution-sdk-js";
|
|
23374
|
-
var baseVersion = "1.1.
|
|
23385
|
+
var baseVersion = "1.1.7";
|
|
23375
23386
|
var SDK_VERSION = isRuntimeBundled ? `${baseVersion}-bundled` : baseVersion;
|
|
23376
23387
|
var defaultLambdaClient;
|
|
23377
23388
|
var DurableExecutionApiClient = class {
|
|
@@ -23692,10 +23703,73 @@ var import_client_ec2 = require("@aws-sdk/client-ec2");
|
|
|
23692
23703
|
var import_client_resource_groups_tagging_api = require("@aws-sdk/client-resource-groups-tagging-api");
|
|
23693
23704
|
var import_web_api = __toESM(require_dist5());
|
|
23694
23705
|
var import_aws_lambda_secret_fetcher = __toESM(require_lib2());
|
|
23706
|
+
var import_safe_env_getter2 = __toESM(require_lib3());
|
|
23707
|
+
|
|
23708
|
+
// src/funcs/running-scheduler-polling-env.ts
|
|
23695
23709
|
var import_safe_env_getter = __toESM(require_lib3());
|
|
23696
23710
|
|
|
23711
|
+
// src/funcs/running-scheduler-polling-config.ts
|
|
23712
|
+
var PROCESS_RESOURCE_MAX_LOOP_COUNT_ENV = "PROCESS_RESOURCE_MAX_LOOP_COUNT";
|
|
23713
|
+
var PROCESS_RESOURCE_MAX_ELAPSED_SECONDS_ENV = "PROCESS_RESOURCE_MAX_ELAPSED_SECONDS";
|
|
23714
|
+
|
|
23697
23715
|
// src/funcs/running-scheduler-predicates.ts
|
|
23716
|
+
var DEFAULT_RESOURCE_POLLING_LIMITS = {
|
|
23717
|
+
maxLoopCount: 90,
|
|
23718
|
+
maxElapsedSeconds: 1800
|
|
23719
|
+
};
|
|
23698
23720
|
var isDesiredStableState = (mode, currentState) => mode === "Start" && currentState === "running" || mode === "Stop" && currentState === "stopped";
|
|
23721
|
+
var isTransitioningState = (mode, currentState) => mode === "Start" && currentState === "pending" || mode === "Stop" && (currentState === "stopping" || currentState === "shutting-down");
|
|
23722
|
+
var getPollingAbortReason = (loopCount, startedAtMs, nowMs, limits) => {
|
|
23723
|
+
if (loopCount >= limits.maxLoopCount) {
|
|
23724
|
+
return "MaxLoopCountExceeded";
|
|
23725
|
+
}
|
|
23726
|
+
const elapsedSeconds = (nowMs - startedAtMs) / 1e3;
|
|
23727
|
+
if (elapsedSeconds >= limits.maxElapsedSeconds) {
|
|
23728
|
+
return "MaxElapsedTimeExceeded";
|
|
23729
|
+
}
|
|
23730
|
+
return void 0;
|
|
23731
|
+
};
|
|
23732
|
+
var formatResourcePollingFailure = (reason, context) => {
|
|
23733
|
+
const common = `identifier=${context.identifier} mode=${context.mode} currentState=${context.currentState} loopCount=${context.loopCount}`;
|
|
23734
|
+
if (reason === "MaxLoopCountExceeded") {
|
|
23735
|
+
return `ResourcePollingFailed:MaxLoopCountExceeded: ${common} maxLoopCount=${context.limits.maxLoopCount}`;
|
|
23736
|
+
}
|
|
23737
|
+
if (reason === "MaxElapsedTimeExceeded") {
|
|
23738
|
+
return `ResourcePollingFailed:MaxElapsedTimeExceeded: ${common} maxElapsedSeconds=${context.limits.maxElapsedSeconds}`;
|
|
23739
|
+
}
|
|
23740
|
+
return `ResourcePollingFailed:UnexpectedInstanceState: ${common}`;
|
|
23741
|
+
};
|
|
23742
|
+
|
|
23743
|
+
// src/funcs/running-scheduler-polling-env.ts
|
|
23744
|
+
var assertPositiveEnvInt = (key, value) => {
|
|
23745
|
+
if (value <= 0) {
|
|
23746
|
+
const raw = process.env[key];
|
|
23747
|
+
throw new Error(`Invalid ${key}: must be a positive integer (got "${raw ?? ""}")`);
|
|
23748
|
+
}
|
|
23749
|
+
return value;
|
|
23750
|
+
};
|
|
23751
|
+
var parseResourcePollingLimitsFromEnv = () => {
|
|
23752
|
+
const parsed = import_safe_env_getter.SafeEnvGetter.getEnvs({
|
|
23753
|
+
[PROCESS_RESOURCE_MAX_LOOP_COUNT_ENV]: [
|
|
23754
|
+
import_safe_env_getter.SafeEnvType.Number,
|
|
23755
|
+
{ default: DEFAULT_RESOURCE_POLLING_LIMITS.maxLoopCount }
|
|
23756
|
+
],
|
|
23757
|
+
[PROCESS_RESOURCE_MAX_ELAPSED_SECONDS_ENV]: [
|
|
23758
|
+
import_safe_env_getter.SafeEnvType.Number,
|
|
23759
|
+
{ default: DEFAULT_RESOURCE_POLLING_LIMITS.maxElapsedSeconds }
|
|
23760
|
+
]
|
|
23761
|
+
});
|
|
23762
|
+
return {
|
|
23763
|
+
maxLoopCount: assertPositiveEnvInt(
|
|
23764
|
+
PROCESS_RESOURCE_MAX_LOOP_COUNT_ENV,
|
|
23765
|
+
parsed[PROCESS_RESOURCE_MAX_LOOP_COUNT_ENV]
|
|
23766
|
+
),
|
|
23767
|
+
maxElapsedSeconds: assertPositiveEnvInt(
|
|
23768
|
+
PROCESS_RESOURCE_MAX_ELAPSED_SECONDS_ENV,
|
|
23769
|
+
parsed[PROCESS_RESOURCE_MAX_ELAPSED_SECONDS_ENV]
|
|
23770
|
+
)
|
|
23771
|
+
};
|
|
23772
|
+
};
|
|
23699
23773
|
|
|
23700
23774
|
// src/funcs/running-scheduler.lambda.ts
|
|
23701
23775
|
var STATE_LIST = [
|
|
@@ -23707,7 +23781,7 @@ var getStateDisplay = (current) => {
|
|
|
23707
23781
|
const found = STATE_LIST.find((s) => s.state === current);
|
|
23708
23782
|
return found ? { emoji: found.emoji, name: found.name } : void 0;
|
|
23709
23783
|
};
|
|
23710
|
-
var processOneResource = async (ctx, targetResource, params, resourceIndex) => {
|
|
23784
|
+
var processOneResource = async (ctx, targetResource, params, resourceIndex, pollingLimits) => {
|
|
23711
23785
|
const parts = targetResource.split("/");
|
|
23712
23786
|
const identifier = parts[parts.length - 1] ?? "unknown";
|
|
23713
23787
|
const arnParts = targetResource.split(":");
|
|
@@ -23719,11 +23793,36 @@ var processOneResource = async (ctx, targetResource, params, resourceIndex) => {
|
|
|
23719
23793
|
identifier,
|
|
23720
23794
|
region,
|
|
23721
23795
|
account,
|
|
23722
|
-
mode: params.Mode
|
|
23796
|
+
mode: params.Mode,
|
|
23797
|
+
maxLoopCount: pollingLimits.maxLoopCount,
|
|
23798
|
+
maxElapsedSeconds: pollingLimits.maxElapsedSeconds
|
|
23723
23799
|
});
|
|
23800
|
+
const startedAtMs = await ctx.step(`${stepPrefix}-polling-started-at`, async () => Date.now());
|
|
23724
23801
|
let loopCount = 0;
|
|
23725
23802
|
let currentState = "";
|
|
23726
23803
|
do {
|
|
23804
|
+
const abortReason = await ctx.step(
|
|
23805
|
+
`${stepPrefix}-poll-limit-check-${loopCount}`,
|
|
23806
|
+
async () => getPollingAbortReason(loopCount, startedAtMs, Date.now(), pollingLimits)
|
|
23807
|
+
);
|
|
23808
|
+
if (abortReason) {
|
|
23809
|
+
const message = formatResourcePollingFailure(abortReason, {
|
|
23810
|
+
identifier,
|
|
23811
|
+
mode: params.Mode,
|
|
23812
|
+
currentState: currentState || "unknown",
|
|
23813
|
+
loopCount,
|
|
23814
|
+
limits: pollingLimits
|
|
23815
|
+
});
|
|
23816
|
+
ctx.logger.error("processOneResource: polling limit exceeded", {
|
|
23817
|
+
identifier,
|
|
23818
|
+
abortReason,
|
|
23819
|
+
loopCount,
|
|
23820
|
+
currentState,
|
|
23821
|
+
maxLoopCount: pollingLimits.maxLoopCount,
|
|
23822
|
+
maxElapsedSeconds: pollingLimits.maxElapsedSeconds
|
|
23823
|
+
});
|
|
23824
|
+
throw new Error(message);
|
|
23825
|
+
}
|
|
23727
23826
|
currentState = await ctx.step(`${stepPrefix}-describe-${loopCount}`, async () => {
|
|
23728
23827
|
const ec2 = new import_client_ec2.EC2Client({});
|
|
23729
23828
|
const out = await ec2.send(new import_client_ec2.DescribeInstancesCommand({ InstanceIds: [identifier] }));
|
|
@@ -23765,8 +23864,7 @@ var processOneResource = async (ctx, targetResource, params, resourceIndex) => {
|
|
|
23765
23864
|
continue;
|
|
23766
23865
|
}
|
|
23767
23866
|
if (!isDesiredStableState(mode, currentState)) {
|
|
23768
|
-
|
|
23769
|
-
if (transitioning) {
|
|
23867
|
+
if (isTransitioningState(mode, currentState)) {
|
|
23770
23868
|
ctx.logger.info("processOneResource: wait while transitioning", {
|
|
23771
23869
|
identifier,
|
|
23772
23870
|
loopCount,
|
|
@@ -23778,13 +23876,20 @@ var processOneResource = async (ctx, targetResource, params, resourceIndex) => {
|
|
|
23778
23876
|
loopCount += 1;
|
|
23779
23877
|
continue;
|
|
23780
23878
|
}
|
|
23879
|
+
const message = formatResourcePollingFailure("UnexpectedInstanceState", {
|
|
23880
|
+
identifier,
|
|
23881
|
+
mode,
|
|
23882
|
+
currentState,
|
|
23883
|
+
loopCount,
|
|
23884
|
+
limits: pollingLimits
|
|
23885
|
+
});
|
|
23781
23886
|
ctx.logger.error("processOneResource: unexpected state", {
|
|
23782
23887
|
identifier,
|
|
23783
23888
|
mode,
|
|
23784
23889
|
currentState,
|
|
23785
23890
|
loopCount
|
|
23786
23891
|
});
|
|
23787
|
-
throw new Error(
|
|
23892
|
+
throw new Error(message);
|
|
23788
23893
|
}
|
|
23789
23894
|
} while (!isDesiredStableState(params.Mode, currentState));
|
|
23790
23895
|
ctx.logger.info("processOneResource: reached desired stable state", {
|
|
@@ -23811,7 +23916,8 @@ var handler = withDurableExecution(async (event, ctx) => {
|
|
|
23811
23916
|
if (!params?.TagKey || !params?.TagValues || !params?.Mode) {
|
|
23812
23917
|
throw new Error("Invalid event: Params.TagKey, Params.TagValues, Params.Mode are required.");
|
|
23813
23918
|
}
|
|
23814
|
-
const
|
|
23919
|
+
const pollingLimits = parseResourcePollingLimitsFromEnv();
|
|
23920
|
+
const slackSecretName = import_safe_env_getter2.SafeEnvGetter.getEnv("SLACK_SECRET_NAME");
|
|
23815
23921
|
const slackSecretValue = await ctx.step("fetch-slack-secret", async () => {
|
|
23816
23922
|
ctx.logger.info("running-scheduler: fetching Slack secret", { secretName: slackSecretName });
|
|
23817
23923
|
return import_aws_lambda_secret_fetcher.secretFetcher.getSecretValue(slackSecretName);
|
|
@@ -23862,7 +23968,7 @@ var handler = withDurableExecution(async (event, ctx) => {
|
|
|
23862
23968
|
// ),
|
|
23863
23969
|
async (mapCtx, targetResource, index) => {
|
|
23864
23970
|
return mapCtx.runInChildContext(`resource-${index}`, async (childCtx) => {
|
|
23865
|
-
const result = await processOneResource(childCtx, targetResource, params, index);
|
|
23971
|
+
const result = await processOneResource(childCtx, targetResource, params, index, pollingLimits);
|
|
23866
23972
|
childCtx.logger.info("running-scheduler: posting thread Slack message", {
|
|
23867
23973
|
index,
|
|
23868
23974
|
identifier: result.identifier,
|