@mavogel/cdk-vscode-server 0.0.60 → 0.0.62
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/.claude/hooks/file_checker.sh +3 -0
- package/.jsii +454 -31
- package/API.md +514 -0
- package/README.md +57 -0
- package/assets/idle-monitor/idle-monitor.lambda/index.js +110 -0
- package/assets/status-check/status-check.lambda/index.js +123 -0
- package/examples/auto-stop/main.ts +75 -0
- package/integ-tests/functions/idle-test-handler.ts +178 -0
- package/integ-tests/functions/login-handler.ts +62 -33
- package/integ-tests/integ.al2023.ts.snapshot/read.13497.1.lock +1 -0
- package/integ-tests/integ.custom-domain.ts.snapshot/read.13497.1.lock +1 -0
- package/integ-tests/integ.stop-on-idle.ts +175 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/IntegStopOnIdleFunctionalityDefaultTestDeployAssertEECF3FC0.assets.json +33 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/IntegStopOnIdleFunctionalityDefaultTestDeployAssertEECF3FC0.template.json +692 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/IntegTestStackStopOnIdle.assets.json +146 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/IntegTestStackStopOnIdle.template.json +3077 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.2819175352ad1ce0dae768e83fc328fb70fb5f10b4a8ff0ccbcb791f02b0716d/index.js +1 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.33da23274e25bd9f43638c5d83dad26e3931cbe78d462ffd9a9f565e948b4f5f.lambda/index.js +143 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.530055f7515b3f0a47900f5df37e729ba40ca977b2d07b952bdefa2b8f883f42.bundle/index.js +30676 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.781ab0ab74634cdaf61539ab208ab777829ef07097ac21f95b9e15a3b1eedc1b.lambda/index.js +57 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.7fa1e366ee8a9ded01fc355f704cff92bfd179574e6f9cfee800a3541df1b200/__entrypoint__.js +1 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.7fa1e366ee8a9ded01fc355f704cff92bfd179574e6f9cfee800a3541df1b200/index.js +1 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.8dd4be31c5a6cd8750dc55c07c1e2f19596f8a27b032d02c18554ed44eabe065.lambda/index.js +110 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.9d043014be736e8162bcc7ec5590cc6d2ff24fd0d9c73a5c5d595151c5fdad00/index.js +1 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.bdc104ed9cab1b5b6421713c8155f0b753380595356f710400609664d3635eca/cfn-response.js +1 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.bdc104ed9cab1b5b6421713c8155f0b753380595356f710400609664d3635eca/consts.js +1 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.bdc104ed9cab1b5b6421713c8155f0b753380595356f710400609664d3635eca/framework.js +3 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.bdc104ed9cab1b5b6421713c8155f0b753380595356f710400609664d3635eca/outbound.js +1 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.bdc104ed9cab1b5b6421713c8155f0b753380595356f710400609664d3635eca/util.js +1 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.d061a1ca61c6339fcb77bb6fc19194a60c96bb16531eaf1e4e733b50089512ca/index.js +118 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.efac30c7091c58fed492058fa6403c14f7e58aab8cf4fd595d838b8d5eeec2b9/index.js +6017 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/integ.json +20 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/manifest.json +1942 -0
- package/integ-tests/integ.stop-on-idle.ts.snapshot/tree.json +1 -0
- package/integ-tests/integ.ubuntu.ts.snapshot/read.13497.1.lock +1 -0
- package/lib/idle-monitor/idle-monitor-function.d.ts +13 -0
- package/lib/idle-monitor/idle-monitor-function.js +22 -0
- package/lib/idle-monitor/idle-monitor.d.ts +53 -0
- package/lib/idle-monitor/idle-monitor.js +84 -0
- package/lib/idle-monitor/idle-monitor.lambda.d.ts +2 -0
- package/lib/idle-monitor/idle-monitor.lambda.js +97 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +3 -1
- package/lib/status-check/status-check-function.d.ts +13 -0
- package/lib/status-check/status-check-function.js +22 -0
- package/lib/status-check/status-check.d.ts +36 -0
- package/lib/status-check/status-check.js +109 -0
- package/lib/status-check/status-check.lambda.d.ts +2 -0
- package/lib/status-check/status-check.lambda.js +104 -0
- package/lib/vscode-server.d.ts +42 -0
- package/lib/vscode-server.js +51 -7
- package/mavogelcdkvscodeserver/go.mod +1 -1
- package/mavogelcdkvscodeserver/jsii/jsii.go +2 -2
- package/mavogelcdkvscodeserver/version +1 -1
- package/package.json +21 -16
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/idle-monitor/idle-monitor.lambda.ts
|
|
21
|
+
var idle_monitor_lambda_exports = {};
|
|
22
|
+
__export(idle_monitor_lambda_exports, {
|
|
23
|
+
handler: () => handler
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(idle_monitor_lambda_exports);
|
|
26
|
+
var import_client_cloudwatch = require("@aws-sdk/client-cloudwatch");
|
|
27
|
+
var import_client_ec2 = require("@aws-sdk/client-ec2");
|
|
28
|
+
var cloudwatch = new import_client_cloudwatch.CloudWatchClient({});
|
|
29
|
+
var ec2 = new import_client_ec2.EC2Client({});
|
|
30
|
+
var INSTANCE_ID = process.env.INSTANCE_ID;
|
|
31
|
+
var DISTRIBUTION_ID = process.env.DISTRIBUTION_ID;
|
|
32
|
+
var IDLE_TIMEOUT_MINUTES = parseInt(process.env.IDLE_TIMEOUT_MINUTES || "30");
|
|
33
|
+
var handler = async (event) => {
|
|
34
|
+
console.log("IdleMonitor triggered", { event, INSTANCE_ID, DISTRIBUTION_ID, IDLE_TIMEOUT_MINUTES });
|
|
35
|
+
try {
|
|
36
|
+
const endTime = /* @__PURE__ */ new Date();
|
|
37
|
+
const startTime = new Date(endTime.getTime() - IDLE_TIMEOUT_MINUTES * 60 * 1e3);
|
|
38
|
+
const metricsCommand = new import_client_cloudwatch.GetMetricStatisticsCommand({
|
|
39
|
+
Namespace: "AWS/CloudFront",
|
|
40
|
+
MetricName: "Requests",
|
|
41
|
+
Dimensions: [
|
|
42
|
+
{
|
|
43
|
+
Name: "DistributionId",
|
|
44
|
+
Value: DISTRIBUTION_ID
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
StartTime: startTime,
|
|
48
|
+
EndTime: endTime,
|
|
49
|
+
Period: IDLE_TIMEOUT_MINUTES * 60,
|
|
50
|
+
Statistics: ["Sum"]
|
|
51
|
+
});
|
|
52
|
+
const metricsResponse = await cloudwatch.send(metricsCommand);
|
|
53
|
+
const requestCount = metricsResponse.Datapoints?.[0]?.Sum || 0;
|
|
54
|
+
console.log("CloudFront request count:", requestCount);
|
|
55
|
+
const describeCommand = new import_client_ec2.DescribeInstancesCommand({
|
|
56
|
+
InstanceIds: [INSTANCE_ID]
|
|
57
|
+
});
|
|
58
|
+
const describeResponse = await ec2.send(describeCommand);
|
|
59
|
+
const instanceState = describeResponse.Reservations?.[0]?.Instances?.[0]?.State?.Name;
|
|
60
|
+
console.log("Current instance state:", instanceState);
|
|
61
|
+
const transitionalStates = ["pending", "stopping", "shutting-down", "rebooting"];
|
|
62
|
+
if (transitionalStates.includes(instanceState || "")) {
|
|
63
|
+
console.log(`Instance is in transitional state '${instanceState}', skipping idle check`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (instanceState === "running" && process.env.SKIP_STATUS_CHECKS !== "true") {
|
|
67
|
+
const statusCommand = new import_client_ec2.DescribeInstanceStatusCommand({
|
|
68
|
+
InstanceIds: [INSTANCE_ID],
|
|
69
|
+
IncludeAllInstances: false
|
|
70
|
+
// Only return running instances with status info
|
|
71
|
+
});
|
|
72
|
+
const statusResponse = await ec2.send(statusCommand);
|
|
73
|
+
const instanceStatus = statusResponse.InstanceStatuses?.[0];
|
|
74
|
+
if (instanceStatus) {
|
|
75
|
+
const systemStatus = instanceStatus.SystemStatus?.Status;
|
|
76
|
+
const instanceCheckStatus = instanceStatus.InstanceStatus?.Status;
|
|
77
|
+
console.log("Instance status checks:", {
|
|
78
|
+
systemStatus,
|
|
79
|
+
instanceCheckStatus
|
|
80
|
+
});
|
|
81
|
+
if (systemStatus !== "ok" || instanceCheckStatus !== "ok") {
|
|
82
|
+
console.log("Instance status checks are not passing, skipping idle check");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
console.log("Instance status information not available yet, skipping idle check");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
} else if (process.env.SKIP_STATUS_CHECKS === "true" && instanceState === "running") {
|
|
90
|
+
console.log("Status check verification skipped (SKIP_STATUS_CHECKS=true)");
|
|
91
|
+
}
|
|
92
|
+
if (requestCount === 0 && instanceState === "running") {
|
|
93
|
+
console.log("No activity detected, stopping instance");
|
|
94
|
+
const stopCommand = new import_client_ec2.StopInstancesCommand({
|
|
95
|
+
InstanceIds: [INSTANCE_ID]
|
|
96
|
+
});
|
|
97
|
+
await ec2.send(stopCommand);
|
|
98
|
+
console.log("Instance stopped successfully");
|
|
99
|
+
} else if (requestCount > 0) {
|
|
100
|
+
console.log("Activity detected, instance will remain running");
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error("Error in IdleMonitor:", error);
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
108
|
+
0 && (module.exports = {
|
|
109
|
+
handler
|
|
110
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/status-check/status-check.lambda.ts
|
|
21
|
+
var status_check_lambda_exports = {};
|
|
22
|
+
__export(status_check_lambda_exports, {
|
|
23
|
+
handler: () => handler
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(status_check_lambda_exports);
|
|
26
|
+
var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
|
|
27
|
+
var import_client_ec2 = require("@aws-sdk/client-ec2");
|
|
28
|
+
var import_lib_dynamodb = require("@aws-sdk/lib-dynamodb");
|
|
29
|
+
var ec2 = new import_client_ec2.EC2Client({});
|
|
30
|
+
var ddbClient = new import_client_dynamodb.DynamoDBClient({});
|
|
31
|
+
var ddb = import_lib_dynamodb.DynamoDBDocumentClient.from(ddbClient);
|
|
32
|
+
var TABLE_NAME = process.env.TABLE_NAME;
|
|
33
|
+
var handler = async (event) => {
|
|
34
|
+
console.log("StatusCheck/Start triggered", { event });
|
|
35
|
+
const instanceId = event.pathParameters?.instanceId;
|
|
36
|
+
const isStartRequest = event.resource?.includes("/start") && event.httpMethod === "POST";
|
|
37
|
+
if (!instanceId) {
|
|
38
|
+
return {
|
|
39
|
+
statusCode: 400,
|
|
40
|
+
headers: {
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
"Access-Control-Allow-Origin": "*"
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({ error: "Missing instanceId" })
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
if (isStartRequest) {
|
|
49
|
+
console.log("Starting instance:", instanceId);
|
|
50
|
+
const startCommand = new import_client_ec2.StartInstancesCommand({
|
|
51
|
+
InstanceIds: [instanceId]
|
|
52
|
+
});
|
|
53
|
+
await ec2.send(startCommand);
|
|
54
|
+
await ddb.send(new import_lib_dynamodb.UpdateCommand({
|
|
55
|
+
TableName: TABLE_NAME,
|
|
56
|
+
Key: { instanceId },
|
|
57
|
+
UpdateExpression: "SET instanceState = :state, lastActivityTime = :time",
|
|
58
|
+
ExpressionAttributeValues: {
|
|
59
|
+
":state": "starting",
|
|
60
|
+
":time": (/* @__PURE__ */ new Date()).toISOString()
|
|
61
|
+
}
|
|
62
|
+
}));
|
|
63
|
+
return {
|
|
64
|
+
statusCode: 200,
|
|
65
|
+
headers: {
|
|
66
|
+
"Content-Type": "application/json",
|
|
67
|
+
"Access-Control-Allow-Origin": "*"
|
|
68
|
+
},
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
message: "Instance start initiated",
|
|
71
|
+
state: "starting",
|
|
72
|
+
instanceId
|
|
73
|
+
})
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const statusCommand = new import_client_ec2.DescribeInstanceStatusCommand({
|
|
77
|
+
InstanceIds: [instanceId],
|
|
78
|
+
IncludeAllInstances: true
|
|
79
|
+
});
|
|
80
|
+
const statusResponse = await ec2.send(statusCommand);
|
|
81
|
+
const instanceStatus = statusResponse.InstanceStatuses?.[0];
|
|
82
|
+
const state = instanceStatus?.InstanceState?.Name || "unknown";
|
|
83
|
+
console.log("Instance state:", state);
|
|
84
|
+
if (state === "running") {
|
|
85
|
+
await ddb.send(new import_lib_dynamodb.UpdateCommand({
|
|
86
|
+
TableName: TABLE_NAME,
|
|
87
|
+
Key: { instanceId },
|
|
88
|
+
UpdateExpression: "SET instanceState = :state, lastActivityTime = :time",
|
|
89
|
+
ExpressionAttributeValues: {
|
|
90
|
+
":state": "running",
|
|
91
|
+
":time": (/* @__PURE__ */ new Date()).toISOString()
|
|
92
|
+
}
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
statusCode: 200,
|
|
97
|
+
headers: {
|
|
98
|
+
"Content-Type": "application/json",
|
|
99
|
+
"Access-Control-Allow-Origin": "*",
|
|
100
|
+
"Cache-Control": "no-cache, no-store, must-revalidate"
|
|
101
|
+
},
|
|
102
|
+
body: JSON.stringify({
|
|
103
|
+
state,
|
|
104
|
+
ready: state === "running",
|
|
105
|
+
instanceId
|
|
106
|
+
})
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error("Error in status check/start:", error);
|
|
110
|
+
return {
|
|
111
|
+
statusCode: 500,
|
|
112
|
+
headers: {
|
|
113
|
+
"Content-Type": "application/json",
|
|
114
|
+
"Access-Control-Allow-Origin": "*"
|
|
115
|
+
},
|
|
116
|
+
body: JSON.stringify({ error: "Internal server error" })
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
121
|
+
0 && (module.exports = {
|
|
122
|
+
handler
|
|
123
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { App, Stack, StackProps } from 'aws-cdk-lib';
|
|
2
|
+
import * as ec2 from 'aws-cdk-lib/aws-ec2';
|
|
3
|
+
import { Construct } from 'constructs';
|
|
4
|
+
import {
|
|
5
|
+
LinuxArchitectureType,
|
|
6
|
+
LinuxFlavorType,
|
|
7
|
+
VSCodeServer,
|
|
8
|
+
} from '../../src/index';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Example: VS Code Server with Auto-Stop Functionality
|
|
12
|
+
*
|
|
13
|
+
* This example demonstrates how to configure automatic instance stopping
|
|
14
|
+
* to save costs when the VS Code Server is idle.
|
|
15
|
+
*
|
|
16
|
+
* Cost Savings:
|
|
17
|
+
* - Without auto-stop: m7g.xlarge running 24/7 = ~$120/month
|
|
18
|
+
* - With auto-stop (8 hours/day, 5 days/week): ~$30/month
|
|
19
|
+
* - Savings: ~$90/month (75% reduction)
|
|
20
|
+
*
|
|
21
|
+
* How it works:
|
|
22
|
+
* 1. CloudWatch monitors request metrics from CloudFront
|
|
23
|
+
* 2. EventBridge triggers IdleMonitor Lambda every 5 minutes (configurable)
|
|
24
|
+
* 3. If no requests detected for 30 minutes (configurable), instance stops
|
|
25
|
+
* 4. Elastic IP ensures consistent addressing across stop/start cycles
|
|
26
|
+
* 5. Manual restart via AWS Console or CLI when needed
|
|
27
|
+
*
|
|
28
|
+
* Note: This is ideal for development and workshop environments where
|
|
29
|
+
* the server is not actively used 24/7.
|
|
30
|
+
*/
|
|
31
|
+
export class AutoStopExampleStack extends Stack {
|
|
32
|
+
constructor(scope: Construct, id: string, props: StackProps = {}) {
|
|
33
|
+
super(scope, id, props);
|
|
34
|
+
|
|
35
|
+
new VSCodeServer(this, 'vscode-auto-stop', {
|
|
36
|
+
// Instance configuration
|
|
37
|
+
instanceClass: ec2.InstanceClass.M7G,
|
|
38
|
+
instanceSize: ec2.InstanceSize.XLARGE,
|
|
39
|
+
instanceVolumeSize: 40,
|
|
40
|
+
instanceOperatingSystem: LinuxFlavorType.UBUNTU_22,
|
|
41
|
+
instanceCpuArchitecture: LinuxArchitectureType.ARM,
|
|
42
|
+
|
|
43
|
+
// 🔥 Auto-Stop Configuration
|
|
44
|
+
enableAutoStop: true, // Enable automatic instance stop when idle
|
|
45
|
+
|
|
46
|
+
// Stop instance after 30 minutes of no activity (default)
|
|
47
|
+
// Adjust based on your usage patterns:
|
|
48
|
+
// - Development: 15-30 minutes
|
|
49
|
+
// - Workshops: 30-60 minutes
|
|
50
|
+
// - Demo environments: 60-120 minutes
|
|
51
|
+
idleTimeoutMinutes: 30,
|
|
52
|
+
|
|
53
|
+
// Check for idle activity every 5 minutes (default)
|
|
54
|
+
// Lower values = faster detection but more Lambda invocations
|
|
55
|
+
// Higher values = slower detection but fewer Lambda invocations
|
|
56
|
+
idleCheckIntervalMinutes: 5,
|
|
57
|
+
|
|
58
|
+
// Additional configuration
|
|
59
|
+
additionalTags: {
|
|
60
|
+
Environment: 'Development',
|
|
61
|
+
CostCenter: 'Engineering',
|
|
62
|
+
AutoStop: 'Enabled',
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const env = {
|
|
69
|
+
account: process.env.CDK_DEFAULT_ACCOUNT || '123456789012',
|
|
70
|
+
region: process.env.CDK_DEFAULT_REGION || 'eu-west-1',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const app = new App();
|
|
74
|
+
new AutoStopExampleStack(app, 'vscode-auto-stop-example', { env });
|
|
75
|
+
app.synth();
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { EC2Client, DescribeInstancesCommand, StartInstancesCommand } from '@aws-sdk/client-ec2';
|
|
2
|
+
import { EventBridgeClient, DisableRuleCommand } from '@aws-sdk/client-eventbridge';
|
|
3
|
+
|
|
4
|
+
const ec2 = new EC2Client({});
|
|
5
|
+
const eventBridge = new EventBridgeClient({});
|
|
6
|
+
|
|
7
|
+
interface IdleTestEvent {
|
|
8
|
+
testPhase: 'verify-auto-stop' | 'disable-idle-monitor' | 'start-instance';
|
|
9
|
+
domainName?: string;
|
|
10
|
+
instanceId?: string;
|
|
11
|
+
idleTimeoutMinutes?: number;
|
|
12
|
+
idleMonitorRuleName?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Integration test handler for stop-on-idle functionality
|
|
17
|
+
*
|
|
18
|
+
* This Lambda function tests the stop-on-idle workflow:
|
|
19
|
+
* 1. verify-auto-stop: Instance stops after being idle
|
|
20
|
+
* 2. disable-idle-monitor: Disable EventBridge rule to prevent re-stopping
|
|
21
|
+
* 3. start-instance: Start the instance and wait for running state
|
|
22
|
+
*
|
|
23
|
+
* Note: Login verification is handled by a separate login-handler Lambda
|
|
24
|
+
*/
|
|
25
|
+
export const handler = async (event: IdleTestEvent): Promise<string> => {
|
|
26
|
+
console.log('Idle test event:', JSON.stringify(event, null, 2));
|
|
27
|
+
|
|
28
|
+
const { testPhase, instanceId, idleTimeoutMinutes = 5, idleMonitorRuleName } = event;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
if (testPhase === 'verify-auto-stop') {
|
|
32
|
+
if (!instanceId) throw new Error('instanceId is required for verify-auto-stop');
|
|
33
|
+
return await verifyAutoStop(instanceId, idleTimeoutMinutes);
|
|
34
|
+
} else if (testPhase === 'disable-idle-monitor') {
|
|
35
|
+
if (!idleMonitorRuleName) throw new Error('idleMonitorRuleName is required for disable-idle-monitor');
|
|
36
|
+
return await disableIdleMonitor(idleMonitorRuleName);
|
|
37
|
+
} else if (testPhase === 'start-instance') {
|
|
38
|
+
if (!instanceId) throw new Error('instanceId is required for start-instance');
|
|
39
|
+
return await startInstance(instanceId);
|
|
40
|
+
} else {
|
|
41
|
+
throw new Error(`Unknown test phase: ${testPhase}`);
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Test failed:', error);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Verify auto-stop functionality
|
|
51
|
+
*
|
|
52
|
+
* Test flow:
|
|
53
|
+
* 1. Wait for instance to be in 'running' state (if not already)
|
|
54
|
+
* 2. Poll for 'stopped' state as IdleMonitor detects inactivity
|
|
55
|
+
*
|
|
56
|
+
* How IdleMonitor works with skipStatusChecks=true:
|
|
57
|
+
* - Runs every 1 minute (EventBridge schedule)
|
|
58
|
+
* - Checks CloudFront metrics for the LAST N minutes (idleTimeoutMinutes)
|
|
59
|
+
* - After deployment with 0 requests, stops the instance immediately (no status check wait)
|
|
60
|
+
* - Expected stop time: 1-4 minutes (waiting for IdleMonitor to run and detect no activity)
|
|
61
|
+
*
|
|
62
|
+
* CRITICAL: This test has 120s assertion timeout, so all waits must fit within that
|
|
63
|
+
*/
|
|
64
|
+
async function verifyAutoStop(instanceId: string, idleTimeoutMinutes: number): Promise<string> {
|
|
65
|
+
console.log(`Starting auto-stop verification for instance ${instanceId}`);
|
|
66
|
+
console.log(`Idle timeout: ${idleTimeoutMinutes} minutes`);
|
|
67
|
+
|
|
68
|
+
// Step 1: Poll for stopped state
|
|
69
|
+
// With skipStatusChecks=true, IdleMonitor should stop the instance within 90 seconds:
|
|
70
|
+
// - IdleMonitor runs every 1 minute
|
|
71
|
+
// - No status check wait needed
|
|
72
|
+
// - Instance stops immediately when IdleMonitor detects 0 requests
|
|
73
|
+
console.log('Step 2: Polling for stopped state (IdleMonitor should stop it within 100s with skipStatusChecks)...');
|
|
74
|
+
await waitForInstanceState(instanceId, 'stopped', 100); // 100s max (fits in 120s assertion timeout)
|
|
75
|
+
|
|
76
|
+
console.log('✅ Auto-stop verification successful: instance stopped after idle timeout');
|
|
77
|
+
return 'STOPPED';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get current instance state
|
|
82
|
+
*/
|
|
83
|
+
async function getInstanceState(instanceId: string): Promise<string> {
|
|
84
|
+
const command = new DescribeInstancesCommand({
|
|
85
|
+
InstanceIds: [instanceId],
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const response = await ec2.send(command);
|
|
89
|
+
const instance = response.Reservations?.[0]?.Instances?.[0];
|
|
90
|
+
|
|
91
|
+
if (!instance) {
|
|
92
|
+
throw new Error(`Instance ${instanceId} not found`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const state = instance.State?.Name || 'unknown';
|
|
96
|
+
console.log(`Instance ${instanceId} state: ${state}`);
|
|
97
|
+
return state;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Wait for instance to reach a specific state
|
|
102
|
+
* Polls every 30 seconds up to maxWaitSeconds
|
|
103
|
+
*/
|
|
104
|
+
async function waitForInstanceState(
|
|
105
|
+
instanceId: string,
|
|
106
|
+
targetState: string,
|
|
107
|
+
maxWaitSeconds: number,
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
const pollIntervalMs = 30000; // 30 seconds
|
|
110
|
+
const maxAttempts = Math.ceil(maxWaitSeconds / (pollIntervalMs / 1000));
|
|
111
|
+
|
|
112
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
113
|
+
console.log(`Polling attempt ${attempt}/${maxAttempts}...`);
|
|
114
|
+
|
|
115
|
+
const currentState = await getInstanceState(instanceId);
|
|
116
|
+
|
|
117
|
+
if (currentState === targetState) {
|
|
118
|
+
console.log(`✅ Instance reached target state: ${targetState}`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log(`Instance state is ${currentState}, waiting for ${targetState}...`);
|
|
123
|
+
|
|
124
|
+
if (attempt < maxAttempts) {
|
|
125
|
+
await sleep(pollIntervalMs);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Timeout: Instance did not reach state '${targetState}' within ${maxWaitSeconds} seconds`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Sleep for specified milliseconds
|
|
136
|
+
*/
|
|
137
|
+
function sleep(ms: number): Promise<void> {
|
|
138
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Disable the IdleMonitor EventBridge rule
|
|
143
|
+
* This prevents the instance from being stopped again after we start it
|
|
144
|
+
*/
|
|
145
|
+
async function disableIdleMonitor(ruleName: string): Promise<string> {
|
|
146
|
+
console.log(`Disabling IdleMonitor EventBridge rule: ${ruleName}`);
|
|
147
|
+
|
|
148
|
+
const command = new DisableRuleCommand({
|
|
149
|
+
Name: ruleName,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await eventBridge.send(command);
|
|
153
|
+
|
|
154
|
+
console.log('✅ IdleMonitor EventBridge rule disabled successfully');
|
|
155
|
+
return 'DISABLED';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Start the EC2 instance and wait for it to be running
|
|
160
|
+
*/
|
|
161
|
+
async function startInstance(instanceId: string): Promise<string> {
|
|
162
|
+
console.log(`Starting instance: ${instanceId}`);
|
|
163
|
+
|
|
164
|
+
// Start the instance
|
|
165
|
+
const startCommand = new StartInstancesCommand({
|
|
166
|
+
InstanceIds: [instanceId],
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await ec2.send(startCommand);
|
|
170
|
+
console.log('Instance start command sent');
|
|
171
|
+
|
|
172
|
+
// Wait for instance to be running (max 2 minutes)
|
|
173
|
+
console.log('Waiting for instance to be running...');
|
|
174
|
+
await waitForInstanceState(instanceId, 'running', 120);
|
|
175
|
+
|
|
176
|
+
console.log('✅ Instance started successfully and is running');
|
|
177
|
+
return 'RUNNING';
|
|
178
|
+
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { parse } from 'node-html-parser';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Login handler with retry logic for instance startup
|
|
5
|
+
*
|
|
6
|
+
* Retries up to 20 times with 15-second intervals (5 minutes total)
|
|
7
|
+
* to allow time for EC2 instance startup and VS Code Server initialization
|
|
8
|
+
*/
|
|
3
9
|
export const handler = async (event: any) => {
|
|
4
10
|
const domainName = event.domainName || 'test-domain';
|
|
5
11
|
const password = event.password || 'test-password';
|
|
@@ -8,52 +14,75 @@ export const handler = async (event: any) => {
|
|
|
8
14
|
event,
|
|
9
15
|
});
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (!domainName || !password) {
|
|
14
|
-
throw new Error('Domain name and password are required');
|
|
15
|
-
}
|
|
17
|
+
const maxRetries = 20;
|
|
18
|
+
const retryDelayMs = 15000; // 15 seconds
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
21
|
+
log({ message: `Login attempt ${attempt}/${maxRetries}` });
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
26
|
-
}
|
|
23
|
+
try {
|
|
24
|
+
if (!domainName || !password) {
|
|
25
|
+
throw new Error('Domain name and password are required');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const url = domainName.startsWith('http') ? domainName : `https://${domainName}`;
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
// log({ message: 'Got HTML content' })
|
|
30
|
+
const response = await fetch(url);
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
log({ message: `HTTP ${response.status}, retrying...` });
|
|
34
|
+
if (attempt < maxRetries) {
|
|
35
|
+
await sleep(retryDelayMs);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
39
|
+
}
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
const htmlContent = await response.text();
|
|
42
|
+
const root = parse(htmlContent);
|
|
43
|
+
const centerContainer = root.querySelector('.center-container');
|
|
44
|
+
|
|
45
|
+
if (!centerContainer) {
|
|
46
|
+
log({ message: 'Center container not found yet, retrying...' });
|
|
47
|
+
if (attempt < maxRetries) {
|
|
48
|
+
await sleep(retryDelayMs);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
error({
|
|
52
|
+
message: 'Center container NOT found after all retries',
|
|
53
|
+
htmlContent,
|
|
54
|
+
});
|
|
55
|
+
return 'NOK';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
log({ message: `Found center container on attempt ${attempt}. TADA` });
|
|
59
|
+
return 'OK';
|
|
60
|
+
} catch (err) {
|
|
61
|
+
log({
|
|
62
|
+
message: `Attempt ${attempt} failed`,
|
|
63
|
+
error: err instanceof Error ? err.message : String(err),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (attempt < maxRetries) {
|
|
67
|
+
await sleep(retryDelayMs);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
36
70
|
|
|
37
|
-
if (!centerContainer) {
|
|
38
71
|
error({
|
|
39
|
-
message: '
|
|
40
|
-
|
|
72
|
+
message: 'All retry attempts failed',
|
|
73
|
+
err,
|
|
41
74
|
});
|
|
42
75
|
return 'NOK';
|
|
43
76
|
}
|
|
44
|
-
|
|
45
|
-
log({ message: 'Found center container. TADA' });
|
|
46
|
-
return 'OK';
|
|
47
|
-
} catch (err) {
|
|
48
|
-
error({
|
|
49
|
-
message: 'Error fetching or processing document',
|
|
50
|
-
err,
|
|
51
|
-
htmlContent,
|
|
52
|
-
});
|
|
53
|
-
return 'NOK';
|
|
54
77
|
}
|
|
78
|
+
|
|
79
|
+
return 'NOK';
|
|
55
80
|
};
|
|
56
81
|
|
|
82
|
+
function sleep(ms: number): Promise<void> {
|
|
83
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
84
|
+
}
|
|
85
|
+
|
|
57
86
|
function log(msg: any) {
|
|
58
87
|
console.log(JSON.stringify(msg));
|
|
59
88
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
13497
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
13497
|