@mavogel/cdk-vscode-server 0.0.59 → 0.0.61

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.
Files changed (55) hide show
  1. package/.claude/hooks/file_checker.sh +3 -0
  2. package/.jsii +453 -30
  3. package/API.md +514 -0
  4. package/README.md +57 -0
  5. package/assets/idle-monitor/idle-monitor.lambda/index.js +110 -0
  6. package/assets/status-check/status-check.lambda/index.js +123 -0
  7. package/examples/auto-stop/main.ts +75 -0
  8. package/integ-tests/functions/idle-test-handler.ts +178 -0
  9. package/integ-tests/functions/login-handler.ts +62 -33
  10. package/integ-tests/integ.al2023.ts.snapshot/read.13497.1.lock +1 -0
  11. package/integ-tests/integ.custom-domain.ts.snapshot/read.13497.1.lock +1 -0
  12. package/integ-tests/integ.stop-on-idle.ts +175 -0
  13. package/integ-tests/integ.stop-on-idle.ts.snapshot/IntegStopOnIdleFunctionalityDefaultTestDeployAssertEECF3FC0.assets.json +33 -0
  14. package/integ-tests/integ.stop-on-idle.ts.snapshot/IntegStopOnIdleFunctionalityDefaultTestDeployAssertEECF3FC0.template.json +692 -0
  15. package/integ-tests/integ.stop-on-idle.ts.snapshot/IntegTestStackStopOnIdle.assets.json +146 -0
  16. package/integ-tests/integ.stop-on-idle.ts.snapshot/IntegTestStackStopOnIdle.template.json +3077 -0
  17. package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.2819175352ad1ce0dae768e83fc328fb70fb5f10b4a8ff0ccbcb791f02b0716d/index.js +1 -0
  18. package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.33da23274e25bd9f43638c5d83dad26e3931cbe78d462ffd9a9f565e948b4f5f.lambda/index.js +143 -0
  19. package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.530055f7515b3f0a47900f5df37e729ba40ca977b2d07b952bdefa2b8f883f42.bundle/index.js +30676 -0
  20. package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.781ab0ab74634cdaf61539ab208ab777829ef07097ac21f95b9e15a3b1eedc1b.lambda/index.js +57 -0
  21. package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.7fa1e366ee8a9ded01fc355f704cff92bfd179574e6f9cfee800a3541df1b200/__entrypoint__.js +1 -0
  22. package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.7fa1e366ee8a9ded01fc355f704cff92bfd179574e6f9cfee800a3541df1b200/index.js +1 -0
  23. package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.8dd4be31c5a6cd8750dc55c07c1e2f19596f8a27b032d02c18554ed44eabe065.lambda/index.js +110 -0
  24. package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.9d043014be736e8162bcc7ec5590cc6d2ff24fd0d9c73a5c5d595151c5fdad00/index.js +1 -0
  25. package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.bdc104ed9cab1b5b6421713c8155f0b753380595356f710400609664d3635eca/cfn-response.js +1 -0
  26. package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.bdc104ed9cab1b5b6421713c8155f0b753380595356f710400609664d3635eca/consts.js +1 -0
  27. package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.bdc104ed9cab1b5b6421713c8155f0b753380595356f710400609664d3635eca/framework.js +3 -0
  28. package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.bdc104ed9cab1b5b6421713c8155f0b753380595356f710400609664d3635eca/outbound.js +1 -0
  29. package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.bdc104ed9cab1b5b6421713c8155f0b753380595356f710400609664d3635eca/util.js +1 -0
  30. package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.d061a1ca61c6339fcb77bb6fc19194a60c96bb16531eaf1e4e733b50089512ca/index.js +118 -0
  31. package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.efac30c7091c58fed492058fa6403c14f7e58aab8cf4fd595d838b8d5eeec2b9/index.js +6017 -0
  32. package/integ-tests/integ.stop-on-idle.ts.snapshot/integ.json +20 -0
  33. package/integ-tests/integ.stop-on-idle.ts.snapshot/manifest.json +1942 -0
  34. package/integ-tests/integ.stop-on-idle.ts.snapshot/tree.json +1 -0
  35. package/integ-tests/integ.ubuntu.ts.snapshot/read.13497.1.lock +1 -0
  36. package/lib/idle-monitor/idle-monitor-function.d.ts +13 -0
  37. package/lib/idle-monitor/idle-monitor-function.js +22 -0
  38. package/lib/idle-monitor/idle-monitor.d.ts +53 -0
  39. package/lib/idle-monitor/idle-monitor.js +84 -0
  40. package/lib/idle-monitor/idle-monitor.lambda.d.ts +2 -0
  41. package/lib/idle-monitor/idle-monitor.lambda.js +97 -0
  42. package/lib/index.d.ts +2 -0
  43. package/lib/index.js +3 -1
  44. package/lib/status-check/status-check-function.d.ts +13 -0
  45. package/lib/status-check/status-check-function.js +22 -0
  46. package/lib/status-check/status-check.d.ts +36 -0
  47. package/lib/status-check/status-check.js +109 -0
  48. package/lib/status-check/status-check.lambda.d.ts +2 -0
  49. package/lib/status-check/status-check.lambda.js +104 -0
  50. package/lib/vscode-server.d.ts +42 -0
  51. package/lib/vscode-server.js +51 -7
  52. package/mavogelcdkvscodeserver/go.mod +2 -2
  53. package/mavogelcdkvscodeserver/jsii/jsii.go +2 -2
  54. package/mavogelcdkvscodeserver/version +1 -1
  55. 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
- let htmlContent: string = '';
12
- try {
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
- const url = domainName.startsWith('http') ? domainName : `https://${domainName}`;
20
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
21
+ log({ message: `Login attempt ${attempt}/${maxRetries}` });
18
22
 
19
- // No logs due to error 'Response object is too long'
20
- // https://github.com/aws/aws-cdk/issues/24490
21
- // log({ message: 'Fetching document', url })
22
- const response = await fetch(url);
23
- // log({ message: 'Got response' })
24
- if (!response.ok) {
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
- htmlContent = await response.text();
29
- // log({ message: 'Got HTML content' })
30
+ const response = await fetch(url);
30
31
 
31
- // log({ message: 'Parsing HTML' })
32
- const root = parse(htmlContent);
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
- // log({ message: 'Querying document' })
35
- const centerContainer = root.querySelector('.center-container');
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: 'Center container NOT found. See retrieved htmlContent',
40
- htmlContent,
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
  }