ec2-instance-running-scheduler 0.2.7 → 0.3.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 +357 -47
- package/API.md +402 -27
- package/README.md +55 -17
- package/assets/funcs/running-scheduler.lambda/index.js +333 -212
- package/assets/funcs/running-scheduler.lambda/index.js.map +4 -4
- package/lib/constructs/ec2-instance-running-scheduler.d.ts +31 -14
- package/lib/constructs/ec2-instance-running-scheduler.js +26 -13
- package/lib/constructs/running-scheduler-failure-detection.d.ts +87 -0
- package/lib/constructs/running-scheduler-failure-detection.js +185 -0
- package/lib/funcs/running-scheduler-predicates.d.ts +24 -24
- package/lib/funcs/running-scheduler-predicates.js +18 -18
- package/lib/funcs/running-scheduler-wait-config.d.ts +17 -0
- package/lib/funcs/running-scheduler-wait-config.js +21 -0
- package/lib/funcs/running-scheduler-wait-env.d.ts +9 -0
- package/lib/funcs/running-scheduler-wait-env.js +54 -0
- package/lib/funcs/running-scheduler.lambda.d.ts +11 -4
- package/lib/funcs/running-scheduler.lambda.js +88 -50
- package/lib/index.d.ts +2 -1
- package/lib/index.js +3 -2
- package/lib/stacks/ec2-instance-running-schedule-stack.d.ts +9 -3
- package/lib/stacks/ec2-instance-running-schedule-stack.js +8 -3
- package/package.json +5 -6
- package/lib/funcs/running-scheduler-polling-config.d.ts +0 -17
- package/lib/funcs/running-scheduler-polling-config.js +0 -21
- package/lib/funcs/running-scheduler-polling-env.d.ts +0 -8
- package/lib/funcs/running-scheduler-polling-env.js +0 -53
|
@@ -20305,21 +20305,92 @@ var require_lib = __commonJS({
|
|
|
20305
20305
|
"node_modules/fetch-retrier/lib/index.js"(exports2) {
|
|
20306
20306
|
"use strict";
|
|
20307
20307
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
20308
|
-
exports2.fetchRetrier = void 0;
|
|
20308
|
+
exports2.fetchRetrier = exports2.FetchRetrierUnreachableError = exports2.FetchRetrierNetworkError = exports2.FetchRetrierHttpError = exports2.FetchRetrierAlreadyAbortedError = exports2.FetchRetrierAbortError = void 0;
|
|
20309
|
+
var FetchRetrierAbortError = class _FetchRetrierAbortError extends Error {
|
|
20310
|
+
/**
|
|
20311
|
+
* @param message - Human-readable reason (default: `'Aborted'`)
|
|
20312
|
+
*/
|
|
20313
|
+
constructor(message = "Aborted") {
|
|
20314
|
+
super(message);
|
|
20315
|
+
this.name = "FetchRetrierAbortError";
|
|
20316
|
+
Object.setPrototypeOf(this, _FetchRetrierAbortError.prototype);
|
|
20317
|
+
}
|
|
20318
|
+
};
|
|
20319
|
+
exports2.FetchRetrierAbortError = FetchRetrierAbortError;
|
|
20320
|
+
var FetchRetrierAlreadyAbortedError = class _FetchRetrierAlreadyAbortedError extends FetchRetrierAbortError {
|
|
20321
|
+
/**
|
|
20322
|
+
* @param message - Human-readable reason (default: `'Signal was already aborted'`)
|
|
20323
|
+
*/
|
|
20324
|
+
constructor(message = "Signal was already aborted") {
|
|
20325
|
+
super(message);
|
|
20326
|
+
this.name = "FetchRetrierAlreadyAbortedError";
|
|
20327
|
+
Object.setPrototypeOf(this, _FetchRetrierAlreadyAbortedError.prototype);
|
|
20328
|
+
}
|
|
20329
|
+
};
|
|
20330
|
+
exports2.FetchRetrierAlreadyAbortedError = FetchRetrierAlreadyAbortedError;
|
|
20331
|
+
var FetchRetrierHttpError = class _FetchRetrierHttpError extends Error {
|
|
20332
|
+
/**
|
|
20333
|
+
* @param message - Error description
|
|
20334
|
+
* @param status - HTTP status code from the last non-OK response
|
|
20335
|
+
*/
|
|
20336
|
+
constructor(message, status) {
|
|
20337
|
+
super(message);
|
|
20338
|
+
this.status = status;
|
|
20339
|
+
this.name = "FetchRetrierHttpError";
|
|
20340
|
+
Object.setPrototypeOf(this, _FetchRetrierHttpError.prototype);
|
|
20341
|
+
}
|
|
20342
|
+
};
|
|
20343
|
+
exports2.FetchRetrierHttpError = FetchRetrierHttpError;
|
|
20344
|
+
var FetchRetrierNetworkError = class _FetchRetrierNetworkError extends Error {
|
|
20345
|
+
/**
|
|
20346
|
+
* @param message - Human-readable reason (default: `'Network error'`)
|
|
20347
|
+
* @param cause - Original error from the underlying `fetch`, when available
|
|
20348
|
+
*/
|
|
20349
|
+
constructor(message = "Network error", cause) {
|
|
20350
|
+
super(message);
|
|
20351
|
+
this.cause = cause;
|
|
20352
|
+
this.name = "FetchRetrierNetworkError";
|
|
20353
|
+
Object.setPrototypeOf(this, _FetchRetrierNetworkError.prototype);
|
|
20354
|
+
}
|
|
20355
|
+
};
|
|
20356
|
+
exports2.FetchRetrierNetworkError = FetchRetrierNetworkError;
|
|
20357
|
+
var FetchRetrierUnreachableError = class _FetchRetrierUnreachableError extends Error {
|
|
20358
|
+
/**
|
|
20359
|
+
* @param message - Human-readable reason (default: `'Unreachable'`)
|
|
20360
|
+
*/
|
|
20361
|
+
constructor(message = "Unreachable") {
|
|
20362
|
+
super(message);
|
|
20363
|
+
this.name = "FetchRetrierUnreachableError";
|
|
20364
|
+
Object.setPrototypeOf(this, _FetchRetrierUnreachableError.prototype);
|
|
20365
|
+
}
|
|
20366
|
+
};
|
|
20367
|
+
exports2.FetchRetrierUnreachableError = FetchRetrierUnreachableError;
|
|
20309
20368
|
var defaultShouldRetry = (res) => {
|
|
20310
20369
|
return [429, 500, 502, 503, 504].includes(res.status);
|
|
20311
20370
|
};
|
|
20312
20371
|
var fetchRetrier = async (url, options) => {
|
|
20313
|
-
const { headers, retries, timeoutMs, baseBackoffMs, shouldRetry = defaultShouldRetry } = options;
|
|
20372
|
+
const { headers, init, retries, timeoutMs, baseBackoffMs, signal: externalSignal, shouldRetry = defaultShouldRetry } = options;
|
|
20314
20373
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
20374
|
+
if (externalSignal?.aborted) {
|
|
20375
|
+
throw new FetchRetrierAlreadyAbortedError();
|
|
20376
|
+
}
|
|
20315
20377
|
const controller = new AbortController();
|
|
20316
20378
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
20379
|
+
const onExternalAbort = () => {
|
|
20380
|
+
clearTimeout(timer);
|
|
20381
|
+
controller.abort();
|
|
20382
|
+
};
|
|
20383
|
+
if (externalSignal) {
|
|
20384
|
+
externalSignal.addEventListener("abort", onExternalAbort);
|
|
20385
|
+
}
|
|
20317
20386
|
try {
|
|
20318
20387
|
const res = await fetch(url, {
|
|
20319
|
-
|
|
20388
|
+
...init,
|
|
20389
|
+
...headers !== void 0 ? { headers } : {},
|
|
20320
20390
|
signal: controller.signal
|
|
20321
20391
|
});
|
|
20322
20392
|
clearTimeout(timer);
|
|
20393
|
+
externalSignal?.removeEventListener("abort", onExternalAbort);
|
|
20323
20394
|
if (res.ok) {
|
|
20324
20395
|
return res;
|
|
20325
20396
|
}
|
|
@@ -20327,30 +20398,31 @@ var require_lib = __commonJS({
|
|
|
20327
20398
|
const isContinue = shouldRetry(res, text);
|
|
20328
20399
|
if (isContinue) {
|
|
20329
20400
|
if (attempt === retries) {
|
|
20330
|
-
throw new
|
|
20401
|
+
throw new FetchRetrierHttpError(`HTTP ${res.status}`, res.status);
|
|
20331
20402
|
}
|
|
20332
20403
|
await wait(fullJitter(baseBackoffMs, attempt));
|
|
20333
20404
|
} else {
|
|
20334
|
-
throw new
|
|
20405
|
+
throw new FetchRetrierHttpError(`Non-retriable HTTP error: ${res.status}`, res.status);
|
|
20335
20406
|
}
|
|
20336
20407
|
} catch (err) {
|
|
20337
20408
|
clearTimeout(timer);
|
|
20409
|
+
externalSignal?.removeEventListener("abort", onExternalAbort);
|
|
20338
20410
|
if (err instanceof Error && err.name === "AbortError") {
|
|
20339
20411
|
if (attempt === retries)
|
|
20340
|
-
throw err;
|
|
20412
|
+
throw err instanceof FetchRetrierAbortError ? err : new FetchRetrierAbortError();
|
|
20341
20413
|
await wait(fullJitter(baseBackoffMs, attempt));
|
|
20342
20414
|
continue;
|
|
20343
20415
|
}
|
|
20344
20416
|
if (err instanceof TypeError) {
|
|
20345
20417
|
if (attempt === retries)
|
|
20346
|
-
throw err;
|
|
20418
|
+
throw new FetchRetrierNetworkError("Network error", err);
|
|
20347
20419
|
await wait(fullJitter(baseBackoffMs, attempt));
|
|
20348
20420
|
continue;
|
|
20349
20421
|
}
|
|
20350
20422
|
throw err;
|
|
20351
20423
|
}
|
|
20352
20424
|
}
|
|
20353
|
-
throw new
|
|
20425
|
+
throw new FetchRetrierUnreachableError();
|
|
20354
20426
|
};
|
|
20355
20427
|
exports2.fetchRetrier = fetchRetrier;
|
|
20356
20428
|
var wait = (ms) => {
|
|
@@ -20363,87 +20435,32 @@ var require_lib = __commonJS({
|
|
|
20363
20435
|
}
|
|
20364
20436
|
});
|
|
20365
20437
|
|
|
20366
|
-
// node_modules/
|
|
20438
|
+
// node_modules/strict-env-resolver/lib/index.js
|
|
20367
20439
|
var require_lib2 = __commonJS({
|
|
20368
|
-
"node_modules/
|
|
20440
|
+
"node_modules/strict-env-resolver/lib/index.js"(exports2) {
|
|
20369
20441
|
"use strict";
|
|
20370
20442
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
20371
|
-
exports2.
|
|
20372
|
-
var
|
|
20373
|
-
var
|
|
20374
|
-
const
|
|
20375
|
-
|
|
20376
|
-
const requestOptions = {
|
|
20377
|
-
headers: {
|
|
20378
|
-
"X-Aws-Parameters-Secrets-Token": process.env.AWS_SESSION_TOKEN ?? ""
|
|
20379
|
-
},
|
|
20380
|
-
retries,
|
|
20381
|
-
timeoutMs,
|
|
20382
|
-
baseBackoffMs,
|
|
20383
|
-
shouldRetry: (res, body) => {
|
|
20384
|
-
if ([429, 500, 502, 503, 504].includes(res.status))
|
|
20385
|
-
return true;
|
|
20386
|
-
if (res.status === 400 && /not\s+ready.*traffic/i.test(body))
|
|
20387
|
-
return true;
|
|
20388
|
-
return false;
|
|
20389
|
-
}
|
|
20390
|
-
};
|
|
20391
|
-
const response = await (0, fetch_retrier_1.fetchRetrier)(url, requestOptions);
|
|
20392
|
-
const raw = await response.json();
|
|
20393
|
-
if (!isSecretResponse(raw)) {
|
|
20394
|
-
throw new Error("Invalid secret response format");
|
|
20395
|
-
}
|
|
20396
|
-
const data = raw;
|
|
20397
|
-
const secretString = data.SecretString;
|
|
20398
|
-
if (looksLikeJson(secretString)) {
|
|
20399
|
-
return JSON.parse(secretString);
|
|
20400
|
-
}
|
|
20401
|
-
return secretString;
|
|
20402
|
-
};
|
|
20403
|
-
var isSecretResponse = (value) => {
|
|
20404
|
-
if (typeof value !== "object" || value === null)
|
|
20405
|
-
return false;
|
|
20406
|
-
const v = value;
|
|
20407
|
-
return typeof v.SecretString === "string" && typeof v.Name === "string" && typeof v.ARN === "string" && (v.VersionId === void 0 || typeof v.VersionId === "string");
|
|
20408
|
-
};
|
|
20409
|
-
var looksLikeJson = (str) => {
|
|
20410
|
-
return typeof str === "string" && str.trim().startsWith("{");
|
|
20411
|
-
};
|
|
20412
|
-
exports2.secretFetcher = {
|
|
20413
|
-
getSecretValue
|
|
20414
|
-
};
|
|
20415
|
-
}
|
|
20416
|
-
});
|
|
20417
|
-
|
|
20418
|
-
// node_modules/safe-env-getter/lib/index.js
|
|
20419
|
-
var require_lib3 = __commonJS({
|
|
20420
|
-
"node_modules/safe-env-getter/lib/index.js"(exports2) {
|
|
20421
|
-
"use strict";
|
|
20422
|
-
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
20423
|
-
exports2.SafeEnvGetter = exports2.SafeEnvType = exports2.SafeEnvGetterValidationError = exports2.SafeEnvGetterError = void 0;
|
|
20424
|
-
var STRICT_INTEGER_PATTERN = /^-?\d+$/;
|
|
20425
|
-
var parseStrictInteger = (raw) => {
|
|
20426
|
-
const trimmed = raw.trim();
|
|
20427
|
-
if (trimmed === "" || !STRICT_INTEGER_PATTERN.test(trimmed)) {
|
|
20428
|
-
return void 0;
|
|
20429
|
-
}
|
|
20430
|
-
const n = Number(trimmed);
|
|
20431
|
-
if (!Number.isFinite(n) || !Number.isInteger(n)) {
|
|
20443
|
+
exports2.StrictEnvResolver = exports2.StrictEnvType = exports2.StrictEnvValidationError = exports2.StrictEnvError = void 0;
|
|
20444
|
+
var TRUE_BOOLEAN_PATTERN = /^(1|true|yes|on)$/i;
|
|
20445
|
+
var parseNumber = (raw) => {
|
|
20446
|
+
const n = Number(raw);
|
|
20447
|
+
if (!Number.isFinite(n)) {
|
|
20432
20448
|
return void 0;
|
|
20433
20449
|
}
|
|
20434
20450
|
return n;
|
|
20435
20451
|
};
|
|
20436
|
-
var
|
|
20452
|
+
var parseBooleanEnvValue = (normalizedRaw) => TRUE_BOOLEAN_PATTERN.test(normalizedRaw);
|
|
20453
|
+
var StrictEnvError = class extends Error {
|
|
20437
20454
|
/**
|
|
20438
20455
|
* @param message - Error message.
|
|
20439
20456
|
*/
|
|
20440
20457
|
constructor(message) {
|
|
20441
20458
|
super(message);
|
|
20442
|
-
this.name = "
|
|
20459
|
+
this.name = "StrictEnvError";
|
|
20443
20460
|
}
|
|
20444
20461
|
};
|
|
20445
|
-
exports2.
|
|
20446
|
-
var
|
|
20462
|
+
exports2.StrictEnvError = StrictEnvError;
|
|
20463
|
+
var StrictEnvValidationError = class _StrictEnvValidationError extends StrictEnvError {
|
|
20447
20464
|
/**
|
|
20448
20465
|
* Formats validation errors into a human-readable error message.
|
|
20449
20466
|
*
|
|
@@ -20457,126 +20474,206 @@ var require_lib3 = __commonJS({
|
|
|
20457
20474
|
${lines.join("\n")}`;
|
|
20458
20475
|
}
|
|
20459
20476
|
/**
|
|
20460
|
-
* Creates a new validation error from one or more `
|
|
20477
|
+
* Creates a new validation error from one or more `StrictEnvValidationEntry` entries.
|
|
20461
20478
|
*
|
|
20462
20479
|
* @param errors - One or more structured validation errors.
|
|
20463
20480
|
*/
|
|
20464
20481
|
constructor(errors) {
|
|
20465
|
-
const msg =
|
|
20482
|
+
const msg = _StrictEnvValidationError.format(errors);
|
|
20466
20483
|
super(msg);
|
|
20467
|
-
this.name = "
|
|
20484
|
+
this.name = "StrictEnvValidationError";
|
|
20468
20485
|
this.errors = errors;
|
|
20469
20486
|
this.keys = errors.map((e) => e.key);
|
|
20470
20487
|
}
|
|
20471
20488
|
};
|
|
20472
|
-
exports2.
|
|
20473
|
-
exports2.
|
|
20474
|
-
/** Spec for a string value (returned as-is). */
|
|
20489
|
+
exports2.StrictEnvValidationError = StrictEnvValidationError;
|
|
20490
|
+
exports2.StrictEnvType = {
|
|
20491
|
+
/** Spec for a string value (returned as-is unless `trim: true` is set in options). */
|
|
20475
20492
|
String: { type: "string" },
|
|
20476
20493
|
/**
|
|
20477
|
-
* Spec for a
|
|
20478
|
-
* Rejects whitespace-only, hex, `Infinity`, `NaN`, and non-integer forms.
|
|
20494
|
+
* Spec for a finite numeric value (`Number()` parsing; rejects `NaN` and `Infinity`).
|
|
20479
20495
|
*/
|
|
20480
20496
|
Number: { type: "number" },
|
|
20481
|
-
/** Spec for a boolean value (1
|
|
20497
|
+
/** Spec for a boolean value (`1`/`true`/`yes`/`on` → `true`; other non-empty values → `false`). */
|
|
20482
20498
|
Boolean: { type: "boolean" },
|
|
20483
20499
|
/**
|
|
20484
20500
|
* Returns a spec that restricts the value to one of the given choices.
|
|
20501
|
+
*
|
|
20485
20502
|
* @param choices - Allowed string literals.
|
|
20486
|
-
* @returns Enum spec for use with `
|
|
20503
|
+
* @returns Enum spec for use with `resolve` or `resolveAll`.
|
|
20487
20504
|
*/
|
|
20488
20505
|
Enum: (choices) => ({ type: "enum", choices })
|
|
20489
20506
|
};
|
|
20490
|
-
|
|
20491
|
-
|
|
20492
|
-
|
|
20507
|
+
var defaultTrimForSpec = (spec) => spec.type !== "string";
|
|
20508
|
+
var normalizeEnvRaw = (raw, trim) => {
|
|
20509
|
+
if (raw == null) {
|
|
20510
|
+
return void 0;
|
|
20511
|
+
}
|
|
20512
|
+
if (!trim) {
|
|
20513
|
+
return raw;
|
|
20514
|
+
}
|
|
20515
|
+
return raw.trim();
|
|
20516
|
+
};
|
|
20517
|
+
var resolveSchemaEntry = (entry) => ({
|
|
20518
|
+
spec: Array.isArray(entry) ? entry[0] : entry,
|
|
20519
|
+
options: Array.isArray(entry) ? entry[1] : void 0
|
|
20520
|
+
});
|
|
20521
|
+
var parseEnvValue = (key, spec, raw, defaultValue, options) => {
|
|
20522
|
+
const trim = options?.trim ?? defaultTrimForSpec(spec);
|
|
20523
|
+
const normalizedRaw = normalizeEnvRaw(raw, trim);
|
|
20493
20524
|
const hasDefault = defaultValue !== void 0;
|
|
20494
|
-
if (
|
|
20495
|
-
if (
|
|
20496
|
-
|
|
20497
|
-
{ key, message: `Missing required environment variable: ${key}`, raw, kind: "missing" }
|
|
20498
|
-
]);
|
|
20525
|
+
if (normalizedRaw == null || normalizedRaw === "") {
|
|
20526
|
+
if (hasDefault) {
|
|
20527
|
+
return { ok: true, value: defaultValue };
|
|
20499
20528
|
}
|
|
20500
|
-
return
|
|
20529
|
+
return {
|
|
20530
|
+
ok: false,
|
|
20531
|
+
error: { key, message: `Missing required environment variable: ${key}`, raw, kind: "missing" }
|
|
20532
|
+
};
|
|
20501
20533
|
}
|
|
20502
20534
|
switch (spec.type) {
|
|
20503
20535
|
case "number": {
|
|
20504
|
-
const n =
|
|
20536
|
+
const n = parseNumber(normalizedRaw);
|
|
20505
20537
|
if (n === void 0) {
|
|
20506
|
-
|
|
20507
|
-
|
|
20508
|
-
|
|
20538
|
+
return {
|
|
20539
|
+
ok: false,
|
|
20540
|
+
error: { key, message: `Env ${key}: expected number, got "${raw}"`, raw, kind: "invalid_number" }
|
|
20541
|
+
};
|
|
20509
20542
|
}
|
|
20510
|
-
return n;
|
|
20543
|
+
return { ok: true, value: n };
|
|
20511
20544
|
}
|
|
20512
20545
|
case "boolean":
|
|
20513
|
-
return
|
|
20546
|
+
return { ok: true, value: parseBooleanEnvValue(normalizedRaw) };
|
|
20514
20547
|
case "enum":
|
|
20515
|
-
if (!spec.choices.includes(
|
|
20516
|
-
|
|
20517
|
-
|
|
20518
|
-
|
|
20548
|
+
if (!spec.choices.includes(normalizedRaw)) {
|
|
20549
|
+
return {
|
|
20550
|
+
ok: false,
|
|
20551
|
+
error: {
|
|
20552
|
+
key,
|
|
20553
|
+
message: `Env ${key}: must be one of [${spec.choices.join(", ")}]`,
|
|
20554
|
+
raw,
|
|
20555
|
+
kind: "invalid_enum"
|
|
20556
|
+
}
|
|
20557
|
+
};
|
|
20519
20558
|
}
|
|
20520
|
-
return
|
|
20559
|
+
return { ok: true, value: normalizedRaw };
|
|
20521
20560
|
default:
|
|
20522
|
-
return
|
|
20561
|
+
return { ok: true, value: normalizedRaw };
|
|
20523
20562
|
}
|
|
20524
|
-
}
|
|
20525
|
-
var
|
|
20563
|
+
};
|
|
20564
|
+
var resolve = (key, spec = exports2.StrictEnvType.String, options) => {
|
|
20565
|
+
const result = parseEnvValue(key, spec, process.env[key], options?.default, options);
|
|
20566
|
+
if (!result.ok) {
|
|
20567
|
+
throw new StrictEnvValidationError([result.error]);
|
|
20568
|
+
}
|
|
20569
|
+
return result.value;
|
|
20570
|
+
};
|
|
20571
|
+
var resolveAll = (schema) => {
|
|
20526
20572
|
const envs = {};
|
|
20527
20573
|
const errors = [];
|
|
20528
20574
|
for (const key of Object.keys(schema)) {
|
|
20529
|
-
const
|
|
20530
|
-
const
|
|
20531
|
-
|
|
20532
|
-
|
|
20533
|
-
const defaultValue = options?.default;
|
|
20534
|
-
const hasDefault = defaultValue !== void 0;
|
|
20535
|
-
if (raw == null || raw === "") {
|
|
20536
|
-
if (!hasDefault) {
|
|
20537
|
-
errors.push({ key, message: `Missing required environment variable: ${key}`, raw, kind: "missing" });
|
|
20538
|
-
continue;
|
|
20539
|
-
}
|
|
20540
|
-
envs[key] = defaultValue;
|
|
20541
|
-
continue;
|
|
20542
|
-
}
|
|
20543
|
-
if (spec.type === "number") {
|
|
20544
|
-
const n = parseStrictInteger(raw);
|
|
20545
|
-
if (n === void 0) {
|
|
20546
|
-
errors.push({ key, message: `Env ${key}: expected number, got "${raw}"`, raw, kind: "invalid_number" });
|
|
20547
|
-
continue;
|
|
20548
|
-
}
|
|
20549
|
-
envs[key] = n;
|
|
20550
|
-
continue;
|
|
20551
|
-
}
|
|
20552
|
-
if (spec.type === "boolean") {
|
|
20553
|
-
envs[key] = /^(1|true|yes|on)$/i.test(raw) ? true : false;
|
|
20554
|
-
continue;
|
|
20555
|
-
}
|
|
20556
|
-
if (spec.type === "enum") {
|
|
20557
|
-
if (!spec.choices.includes(raw)) {
|
|
20558
|
-
errors.push({
|
|
20559
|
-
key,
|
|
20560
|
-
message: `Env ${key}: must be one of [${spec.choices.join(", ")}]`,
|
|
20561
|
-
raw,
|
|
20562
|
-
kind: "invalid_enum"
|
|
20563
|
-
});
|
|
20564
|
-
continue;
|
|
20565
|
-
}
|
|
20566
|
-
envs[key] = raw;
|
|
20575
|
+
const { spec, options } = resolveSchemaEntry(schema[key]);
|
|
20576
|
+
const result = parseEnvValue(key, spec, process.env[key], options?.default, options);
|
|
20577
|
+
if (!result.ok) {
|
|
20578
|
+
errors.push(result.error);
|
|
20567
20579
|
continue;
|
|
20568
20580
|
}
|
|
20569
|
-
envs[key] =
|
|
20581
|
+
envs[key] = result.value;
|
|
20570
20582
|
}
|
|
20571
20583
|
if (errors.length > 0)
|
|
20572
|
-
throw new
|
|
20584
|
+
throw new StrictEnvValidationError(errors);
|
|
20573
20585
|
return envs;
|
|
20574
20586
|
};
|
|
20575
|
-
exports2.
|
|
20576
|
-
/** Reads and parses a single environment variable. See {@link
|
|
20577
|
-
|
|
20578
|
-
/** Reads and parses multiple environment variables. See {@link
|
|
20579
|
-
|
|
20587
|
+
exports2.StrictEnvResolver = {
|
|
20588
|
+
/** Reads and parses a single environment variable. See {@link resolve}. */
|
|
20589
|
+
resolve,
|
|
20590
|
+
/** Reads and parses multiple environment variables in one pass. See {@link resolveAll}. */
|
|
20591
|
+
resolveAll
|
|
20592
|
+
};
|
|
20593
|
+
}
|
|
20594
|
+
});
|
|
20595
|
+
|
|
20596
|
+
// node_modules/aws-lambda-secret-fetcher/lib/index.js
|
|
20597
|
+
var require_lib3 = __commonJS({
|
|
20598
|
+
"node_modules/aws-lambda-secret-fetcher/lib/index.js"(exports2) {
|
|
20599
|
+
"use strict";
|
|
20600
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
20601
|
+
exports2.secretFetcher = void 0;
|
|
20602
|
+
var fetch_retrier_1 = require_lib();
|
|
20603
|
+
var strict_env_resolver_1 = require_lib2();
|
|
20604
|
+
var getSecretValue = async (name, options = {}) => {
|
|
20605
|
+
const { extensionHttpPort, timeoutMs = 2e3, retries = 3, baseBackoffMs = 300 } = options;
|
|
20606
|
+
const port = resolveExtensionHttpPort(extensionHttpPort);
|
|
20607
|
+
const sessionToken = resolveAwsSessionToken();
|
|
20608
|
+
const url = `http://localhost:${port}/secretsmanager/get?secretId=${encodeURIComponent(name)}`;
|
|
20609
|
+
const requestOptions = {
|
|
20610
|
+
headers: {
|
|
20611
|
+
"X-Aws-Parameters-Secrets-Token": sessionToken
|
|
20612
|
+
},
|
|
20613
|
+
retries,
|
|
20614
|
+
timeoutMs,
|
|
20615
|
+
baseBackoffMs,
|
|
20616
|
+
shouldRetry: (res, body) => {
|
|
20617
|
+
if ([429, 500, 502, 503, 504].includes(res.status))
|
|
20618
|
+
return true;
|
|
20619
|
+
if (res.status === 400 && /not\s+ready.*traffic/i.test(body))
|
|
20620
|
+
return true;
|
|
20621
|
+
return false;
|
|
20622
|
+
}
|
|
20623
|
+
};
|
|
20624
|
+
const response = await (0, fetch_retrier_1.fetchRetrier)(url, requestOptions);
|
|
20625
|
+
const raw = await response.json();
|
|
20626
|
+
if (!isSecretResponse(raw)) {
|
|
20627
|
+
throw new Error("Invalid secret response format");
|
|
20628
|
+
}
|
|
20629
|
+
const data = raw;
|
|
20630
|
+
const secretString = data.SecretString;
|
|
20631
|
+
if (looksLikeJson(secretString)) {
|
|
20632
|
+
return JSON.parse(secretString);
|
|
20633
|
+
}
|
|
20634
|
+
return secretString;
|
|
20635
|
+
};
|
|
20636
|
+
var AWS_SESSION_TOKEN_GUIDANCE = "AWS_SESSION_TOKEN is not set. This library only works inside an AWS Lambda execution environment where the runtime provides AWS_SESSION_TOKEN for the Parameters and Secrets Extension. Attach the extension layer and run your code as a Lambda function handler.";
|
|
20637
|
+
var MIN_TCP_PORT = 1;
|
|
20638
|
+
var MAX_TCP_PORT = 65535;
|
|
20639
|
+
var resolveAwsSessionToken = () => {
|
|
20640
|
+
try {
|
|
20641
|
+
return strict_env_resolver_1.StrictEnvResolver.resolve("AWS_SESSION_TOKEN", strict_env_resolver_1.StrictEnvType.String, { trim: true });
|
|
20642
|
+
} catch (e) {
|
|
20643
|
+
if (e instanceof strict_env_resolver_1.StrictEnvValidationError && e.errors.length === 1 && e.errors[0]?.key === "AWS_SESSION_TOKEN" && e.errors[0]?.kind === "missing") {
|
|
20644
|
+
throw new Error(AWS_SESSION_TOKEN_GUIDANCE);
|
|
20645
|
+
}
|
|
20646
|
+
throw e;
|
|
20647
|
+
}
|
|
20648
|
+
};
|
|
20649
|
+
var resolveExtensionHttpPort = (overridePort) => {
|
|
20650
|
+
const port = overridePort === void 0 ? strict_env_resolver_1.StrictEnvResolver.resolve("PARAMETERS_SECRETS_EXTENSION_HTTP_PORT", strict_env_resolver_1.StrictEnvType.Number, { default: 2773 }) : parseExtensionHttpPortOverride(overridePort);
|
|
20651
|
+
return assertValidTcpPort(port);
|
|
20652
|
+
};
|
|
20653
|
+
var parseExtensionHttpPortOverride = (overridePort) => {
|
|
20654
|
+
const candidate = String(overridePort).trim();
|
|
20655
|
+
if (!/^\d+$/.test(candidate)) {
|
|
20656
|
+
throw new Error("Invalid extension HTTP port: must be a number");
|
|
20657
|
+
}
|
|
20658
|
+
return Number.parseInt(candidate, 10);
|
|
20659
|
+
};
|
|
20660
|
+
var assertValidTcpPort = (port) => {
|
|
20661
|
+
if (port < MIN_TCP_PORT || port > MAX_TCP_PORT) {
|
|
20662
|
+
throw new Error("Invalid extension HTTP port: must be between 1 and 65535");
|
|
20663
|
+
}
|
|
20664
|
+
return String(port);
|
|
20665
|
+
};
|
|
20666
|
+
var isSecretResponse = (value) => {
|
|
20667
|
+
if (typeof value !== "object" || value === null)
|
|
20668
|
+
return false;
|
|
20669
|
+
const v = value;
|
|
20670
|
+
return typeof v.SecretString === "string" && typeof v.Name === "string" && typeof v.ARN === "string" && (v.VersionId === void 0 || typeof v.VersionId === "string");
|
|
20671
|
+
};
|
|
20672
|
+
var looksLikeJson = (str) => {
|
|
20673
|
+
return typeof str === "string" && str.trim().startsWith("{");
|
|
20674
|
+
};
|
|
20675
|
+
exports2.secretFetcher = {
|
|
20676
|
+
getSecretValue
|
|
20580
20677
|
};
|
|
20581
20678
|
}
|
|
20582
20679
|
});
|
|
@@ -24106,24 +24203,17 @@ var DEFAULT_CONFIG = {
|
|
|
24106
24203
|
var import_client_ec2 = require("@aws-sdk/client-ec2");
|
|
24107
24204
|
var import_client_resource_groups_tagging_api = require("@aws-sdk/client-resource-groups-tagging-api");
|
|
24108
24205
|
var import_web_api = __toESM(require_dist5());
|
|
24109
|
-
var import_aws_lambda_secret_fetcher = __toESM(
|
|
24110
|
-
var
|
|
24111
|
-
|
|
24112
|
-
// src/funcs/running-scheduler-polling-env.ts
|
|
24113
|
-
var import_safe_env_getter = __toESM(require_lib3());
|
|
24114
|
-
|
|
24115
|
-
// src/funcs/running-scheduler-polling-config.ts
|
|
24116
|
-
var PROCESS_RESOURCE_MAX_LOOP_COUNT_ENV = "PROCESS_RESOURCE_MAX_LOOP_COUNT";
|
|
24117
|
-
var PROCESS_RESOURCE_MAX_ELAPSED_SECONDS_ENV = "PROCESS_RESOURCE_MAX_ELAPSED_SECONDS";
|
|
24206
|
+
var import_aws_lambda_secret_fetcher = __toESM(require_lib3());
|
|
24207
|
+
var import_strict_env_resolver2 = __toESM(require_lib2());
|
|
24118
24208
|
|
|
24119
24209
|
// src/funcs/running-scheduler-predicates.ts
|
|
24120
|
-
var
|
|
24210
|
+
var DEFAULT_RESOURCE_WAIT_LIMITS = {
|
|
24121
24211
|
maxLoopCount: 90,
|
|
24122
24212
|
maxElapsedSeconds: 1800
|
|
24123
24213
|
};
|
|
24124
24214
|
var isDesiredStableState = (mode, currentState) => mode === "Start" && currentState === "running" || mode === "Stop" && currentState === "stopped";
|
|
24125
24215
|
var isTransitioningState = (mode, currentState) => mode === "Start" && currentState === "pending" || mode === "Stop" && (currentState === "stopping" || currentState === "shutting-down");
|
|
24126
|
-
var
|
|
24216
|
+
var getWaitAbortReason = (loopCount, startedAtMs, nowMs, limits) => {
|
|
24127
24217
|
if (loopCount >= limits.maxLoopCount) {
|
|
24128
24218
|
return "MaxLoopCountExceeded";
|
|
24129
24219
|
}
|
|
@@ -24133,18 +24223,25 @@ var getPollingAbortReason = (loopCount, startedAtMs, nowMs, limits) => {
|
|
|
24133
24223
|
}
|
|
24134
24224
|
return void 0;
|
|
24135
24225
|
};
|
|
24136
|
-
var
|
|
24226
|
+
var formatResourceWaitFailure = (reason, context) => {
|
|
24137
24227
|
const common = `identifier=${context.identifier} mode=${context.mode} currentState=${context.currentState} loopCount=${context.loopCount}`;
|
|
24138
24228
|
if (reason === "MaxLoopCountExceeded") {
|
|
24139
|
-
return `
|
|
24229
|
+
return `ResourceWaitFailed:MaxLoopCountExceeded: ${common} maxLoopCount=${context.limits.maxLoopCount}`;
|
|
24140
24230
|
}
|
|
24141
24231
|
if (reason === "MaxElapsedTimeExceeded") {
|
|
24142
|
-
return `
|
|
24232
|
+
return `ResourceWaitFailed:MaxElapsedTimeExceeded: ${common} maxElapsedSeconds=${context.limits.maxElapsedSeconds}`;
|
|
24143
24233
|
}
|
|
24144
|
-
return `
|
|
24234
|
+
return `ResourceWaitFailed:UnexpectedInstanceState: ${common}`;
|
|
24145
24235
|
};
|
|
24146
24236
|
|
|
24147
|
-
// src/funcs/running-scheduler-
|
|
24237
|
+
// src/funcs/running-scheduler-wait-env.ts
|
|
24238
|
+
var import_strict_env_resolver = __toESM(require_lib2());
|
|
24239
|
+
|
|
24240
|
+
// src/funcs/running-scheduler-wait-config.ts
|
|
24241
|
+
var PROCESS_RESOURCE_MAX_LOOP_COUNT_ENV = "PROCESS_RESOURCE_MAX_LOOP_COUNT";
|
|
24242
|
+
var PROCESS_RESOURCE_MAX_ELAPSED_SECONDS_ENV = "PROCESS_RESOURCE_MAX_ELAPSED_SECONDS";
|
|
24243
|
+
|
|
24244
|
+
// src/funcs/running-scheduler-wait-env.ts
|
|
24148
24245
|
var assertPositiveEnvInt = (key, value) => {
|
|
24149
24246
|
if (value <= 0) {
|
|
24150
24247
|
const raw = process.env[key];
|
|
@@ -24152,15 +24249,15 @@ var assertPositiveEnvInt = (key, value) => {
|
|
|
24152
24249
|
}
|
|
24153
24250
|
return value;
|
|
24154
24251
|
};
|
|
24155
|
-
var
|
|
24156
|
-
const parsed =
|
|
24252
|
+
var parseResourceWaitLimitsFromEnv = () => {
|
|
24253
|
+
const parsed = import_strict_env_resolver.StrictEnvResolver.resolveAll({
|
|
24157
24254
|
[PROCESS_RESOURCE_MAX_LOOP_COUNT_ENV]: [
|
|
24158
|
-
|
|
24159
|
-
{ default:
|
|
24255
|
+
import_strict_env_resolver.StrictEnvType.Number,
|
|
24256
|
+
{ default: DEFAULT_RESOURCE_WAIT_LIMITS.maxLoopCount }
|
|
24160
24257
|
],
|
|
24161
24258
|
[PROCESS_RESOURCE_MAX_ELAPSED_SECONDS_ENV]: [
|
|
24162
|
-
|
|
24163
|
-
{ default:
|
|
24259
|
+
import_strict_env_resolver.StrictEnvType.Number,
|
|
24260
|
+
{ default: DEFAULT_RESOURCE_WAIT_LIMITS.maxElapsedSeconds }
|
|
24164
24261
|
]
|
|
24165
24262
|
});
|
|
24166
24263
|
return {
|
|
@@ -24181,11 +24278,16 @@ var STATE_LIST = [
|
|
|
24181
24278
|
{ name: "STOPPED", emoji: "\u{1F634}", state: "stopped" }
|
|
24182
24279
|
];
|
|
24183
24280
|
var STATUS_CHANGE_WAIT_SECONDS = 20;
|
|
24281
|
+
var SLACK_SECRET_FETCH_OPTIONS = {
|
|
24282
|
+
timeoutMs: 3e3,
|
|
24283
|
+
retries: 5,
|
|
24284
|
+
baseBackoffMs: 300
|
|
24285
|
+
};
|
|
24184
24286
|
var getStateDisplay = (current) => {
|
|
24185
24287
|
const found = STATE_LIST.find((s) => s.state === current);
|
|
24186
24288
|
return found ? { emoji: found.emoji, name: found.name } : void 0;
|
|
24187
24289
|
};
|
|
24188
|
-
var processOneResource = async (ctx, targetResource, params, resourceIndex,
|
|
24290
|
+
var processOneResource = async (ctx, targetResource, params, resourceIndex, waitLimits) => {
|
|
24189
24291
|
const parts = targetResource.split("/");
|
|
24190
24292
|
const identifier = parts[parts.length - 1] ?? "unknown";
|
|
24191
24293
|
const arnParts = targetResource.split(":");
|
|
@@ -24198,32 +24300,32 @@ var processOneResource = async (ctx, targetResource, params, resourceIndex, poll
|
|
|
24198
24300
|
region,
|
|
24199
24301
|
account,
|
|
24200
24302
|
mode: params.Mode,
|
|
24201
|
-
maxLoopCount:
|
|
24202
|
-
maxElapsedSeconds:
|
|
24303
|
+
maxLoopCount: waitLimits.maxLoopCount,
|
|
24304
|
+
maxElapsedSeconds: waitLimits.maxElapsedSeconds
|
|
24203
24305
|
});
|
|
24204
|
-
const startedAtMs = await ctx.step(`${stepPrefix}-
|
|
24306
|
+
const startedAtMs = await ctx.step(`${stepPrefix}-wait-started-at`, async () => Date.now());
|
|
24205
24307
|
let loopCount = 0;
|
|
24206
24308
|
let currentState = "";
|
|
24207
24309
|
do {
|
|
24208
24310
|
const abortReason = await ctx.step(
|
|
24209
|
-
`${stepPrefix}-
|
|
24210
|
-
async () =>
|
|
24311
|
+
`${stepPrefix}-wait-limit-check-${loopCount}`,
|
|
24312
|
+
async () => getWaitAbortReason(loopCount, startedAtMs, Date.now(), waitLimits)
|
|
24211
24313
|
);
|
|
24212
24314
|
if (abortReason) {
|
|
24213
|
-
const message =
|
|
24315
|
+
const message = formatResourceWaitFailure(abortReason, {
|
|
24214
24316
|
identifier,
|
|
24215
24317
|
mode: params.Mode,
|
|
24216
24318
|
currentState: currentState || "unknown",
|
|
24217
24319
|
loopCount,
|
|
24218
|
-
limits:
|
|
24320
|
+
limits: waitLimits
|
|
24219
24321
|
});
|
|
24220
|
-
ctx.logger.error("processOneResource:
|
|
24322
|
+
ctx.logger.error("processOneResource: wait limit exceeded", {
|
|
24221
24323
|
identifier,
|
|
24222
24324
|
abortReason,
|
|
24223
24325
|
loopCount,
|
|
24224
24326
|
currentState,
|
|
24225
|
-
maxLoopCount:
|
|
24226
|
-
maxElapsedSeconds:
|
|
24327
|
+
maxLoopCount: waitLimits.maxLoopCount,
|
|
24328
|
+
maxElapsedSeconds: waitLimits.maxElapsedSeconds
|
|
24227
24329
|
});
|
|
24228
24330
|
throw new Error(message);
|
|
24229
24331
|
}
|
|
@@ -24280,12 +24382,12 @@ var processOneResource = async (ctx, targetResource, params, resourceIndex, poll
|
|
|
24280
24382
|
loopCount += 1;
|
|
24281
24383
|
continue;
|
|
24282
24384
|
}
|
|
24283
|
-
const message =
|
|
24385
|
+
const message = formatResourceWaitFailure("UnexpectedInstanceState", {
|
|
24284
24386
|
identifier,
|
|
24285
24387
|
mode,
|
|
24286
24388
|
currentState,
|
|
24287
24389
|
loopCount,
|
|
24288
|
-
limits:
|
|
24390
|
+
limits: waitLimits
|
|
24289
24391
|
});
|
|
24290
24392
|
ctx.logger.error("processOneResource: unexpected state", {
|
|
24291
24393
|
identifier,
|
|
@@ -24320,11 +24422,11 @@ var handler = withDurableExecution(async (event, ctx) => {
|
|
|
24320
24422
|
if (!params?.TagKey || !params?.TagValues || !params?.Mode) {
|
|
24321
24423
|
throw new Error("Invalid event: Params.TagKey, Params.TagValues, Params.Mode are required.");
|
|
24322
24424
|
}
|
|
24323
|
-
const
|
|
24324
|
-
const slackSecretName =
|
|
24425
|
+
const waitLimits = parseResourceWaitLimitsFromEnv();
|
|
24426
|
+
const slackSecretName = import_strict_env_resolver2.StrictEnvResolver.resolve("SLACK_SECRET_NAME", import_strict_env_resolver2.StrictEnvType.String);
|
|
24325
24427
|
const slackSecretValue = await ctx.step("fetch-slack-secret", async () => {
|
|
24326
24428
|
ctx.logger.info("running-scheduler: fetching Slack secret", { secretName: slackSecretName });
|
|
24327
|
-
return import_aws_lambda_secret_fetcher.secretFetcher.getSecretValue(slackSecretName);
|
|
24429
|
+
return import_aws_lambda_secret_fetcher.secretFetcher.getSecretValue(slackSecretName, SLACK_SECRET_FETCH_OPTIONS);
|
|
24328
24430
|
});
|
|
24329
24431
|
ctx.logger.info("running-scheduler: Slack secret loaded");
|
|
24330
24432
|
if (!slackSecretValue?.token || !slackSecretValue?.channel) {
|
|
@@ -24352,10 +24454,19 @@ var handler = withDurableExecution(async (event, ctx) => {
|
|
|
24352
24454
|
instanceCount: targetResources.length
|
|
24353
24455
|
});
|
|
24354
24456
|
const slackParentMessageResult = await ctx.step("post-slack-messages", async () => {
|
|
24355
|
-
|
|
24356
|
-
|
|
24357
|
-
|
|
24358
|
-
|
|
24457
|
+
try {
|
|
24458
|
+
return await client.chat.postMessage({
|
|
24459
|
+
channel,
|
|
24460
|
+
text: `${params.Mode === "Start" ? "\u{1F606} Starts" : "\u{1F971} Stops"} the scheduled EC2 Instance.`
|
|
24461
|
+
});
|
|
24462
|
+
} catch (error) {
|
|
24463
|
+
ctx.logger.error("running-scheduler: Slack post failed", {
|
|
24464
|
+
step: "post-slack-messages",
|
|
24465
|
+
channel,
|
|
24466
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
24467
|
+
});
|
|
24468
|
+
throw error;
|
|
24469
|
+
}
|
|
24359
24470
|
});
|
|
24360
24471
|
ctx.logger.info("running-scheduler: parent Slack message posted", {
|
|
24361
24472
|
threadTs: slackParentMessageResult?.ts ?? null
|
|
@@ -24372,7 +24483,7 @@ var handler = withDurableExecution(async (event, ctx) => {
|
|
|
24372
24483
|
// ),
|
|
24373
24484
|
async (mapCtx, targetResource, index) => {
|
|
24374
24485
|
return mapCtx.runInChildContext(`resource-${index}`, async (childCtx) => {
|
|
24375
|
-
const result = await processOneResource(childCtx, targetResource, params, index,
|
|
24486
|
+
const result = await processOneResource(childCtx, targetResource, params, index, waitLimits);
|
|
24376
24487
|
childCtx.logger.info("running-scheduler: posting thread Slack message", {
|
|
24377
24488
|
index,
|
|
24378
24489
|
identifier: result.identifier,
|
|
@@ -24380,22 +24491,32 @@ var handler = withDurableExecution(async (event, ctx) => {
|
|
|
24380
24491
|
});
|
|
24381
24492
|
await childCtx.step("post-slack-child-messages", async () => {
|
|
24382
24493
|
const display = getStateDisplay(result.status);
|
|
24383
|
-
|
|
24384
|
-
|
|
24385
|
-
|
|
24386
|
-
|
|
24387
|
-
|
|
24388
|
-
|
|
24389
|
-
|
|
24390
|
-
|
|
24391
|
-
|
|
24392
|
-
|
|
24393
|
-
|
|
24394
|
-
|
|
24395
|
-
|
|
24396
|
-
|
|
24397
|
-
|
|
24398
|
-
|
|
24494
|
+
try {
|
|
24495
|
+
return await client.chat.postMessage({
|
|
24496
|
+
channel,
|
|
24497
|
+
thread_ts: slackParentMessageResult?.ts,
|
|
24498
|
+
attachments: [
|
|
24499
|
+
{
|
|
24500
|
+
color: "#36a64f",
|
|
24501
|
+
pretext: `${display?.emoji} The status of the EC2 Instance ${result.identifier} changed to ${display?.name} due to the schedule.`,
|
|
24502
|
+
fields: [
|
|
24503
|
+
{ title: "Account", value: result.account, short: true },
|
|
24504
|
+
{ title: "Region", value: result.region, short: true },
|
|
24505
|
+
{ title: "Identifier", value: result.identifier, short: true },
|
|
24506
|
+
{ title: "Status", value: display?.name ?? "Unknown", short: true }
|
|
24507
|
+
]
|
|
24508
|
+
}
|
|
24509
|
+
]
|
|
24510
|
+
});
|
|
24511
|
+
} catch (error) {
|
|
24512
|
+
childCtx.logger.error("running-scheduler: Slack post failed", {
|
|
24513
|
+
step: "post-slack-child-messages",
|
|
24514
|
+
channel,
|
|
24515
|
+
identifier: result.identifier,
|
|
24516
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
24517
|
+
});
|
|
24518
|
+
throw error;
|
|
24519
|
+
}
|
|
24399
24520
|
});
|
|
24400
24521
|
return result;
|
|
24401
24522
|
});
|