@mavogel/cdk-vscode-server 0.0.61 → 0.0.63

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 (99) hide show
  1. package/.jsii +117 -178
  2. package/API.md +96 -199
  3. package/CLAUDE.md +214 -57
  4. package/README.md +39 -1
  5. package/assets/idle-monitor-enabler/idle-monitor-enabler.lambda/index.js +67 -0
  6. package/assets/installer/installer.lambda/index.js +67 -30
  7. package/awslint.json +5 -0
  8. package/examples/git-repo/main.ts +30 -0
  9. package/integ-tests/integ.al2023.ts.snapshot/IntegSetupVSCodeOnAl2023DefaultTestDeployAssert74D8F645.assets.json +2 -2
  10. package/integ-tests/integ.al2023.ts.snapshot/IntegSetupVSCodeOnAl2023DefaultTestDeployAssert74D8F645.template.json +1 -1
  11. package/integ-tests/integ.al2023.ts.snapshot/IntegTestStackAl2023.assets.json +8 -8
  12. package/integ-tests/integ.al2023.ts.snapshot/IntegTestStackAl2023.template.json +257 -94
  13. package/integ-tests/{integ.stop-on-idle.ts.snapshot/asset.33da23274e25bd9f43638c5d83dad26e3931cbe78d462ffd9a9f565e948b4f5f.lambda → integ.al2023.ts.snapshot/asset.2f99f38311da357eaaea1284d67c759759324dec4a1cd11621d9c59eea9e81df.lambda}/index.js +67 -30
  14. package/integ-tests/{integ.ubuntu.ts.snapshot/asset.0ad50fc42afd768c3d0bfdd4701e43284fb077a25f19eea1e8c51a5ca36ebfe4 → integ.al2023.ts.snapshot/asset.efac30c7091c58fed492058fa6403c14f7e58aab8cf4fd595d838b8d5eeec2b9}/index.js +50 -25
  15. package/integ-tests/integ.al2023.ts.snapshot/integ.json +1 -1
  16. package/integ-tests/integ.al2023.ts.snapshot/manifest.json +19 -3
  17. package/integ-tests/integ.al2023.ts.snapshot/tree.json +1 -1
  18. package/integ-tests/integ.custom-domain.ts.snapshot/IntegSetupVSCodeOnCustomDomainDefaultTestDeployAssert6982D514.assets.json +2 -2
  19. package/integ-tests/integ.custom-domain.ts.snapshot/IntegSetupVSCodeOnCustomDomainDefaultTestDeployAssert6982D514.template.json +1 -1
  20. package/integ-tests/integ.custom-domain.ts.snapshot/IntegTestStackCustomDomain.assets.json +8 -8
  21. package/integ-tests/integ.custom-domain.ts.snapshot/IntegTestStackCustomDomain.template.json +270 -94
  22. package/integ-tests/{integ.ubuntu.ts.snapshot/asset.33da23274e25bd9f43638c5d83dad26e3931cbe78d462ffd9a9f565e948b4f5f.lambda → integ.custom-domain.ts.snapshot/asset.2f99f38311da357eaaea1284d67c759759324dec4a1cd11621d9c59eea9e81df.lambda}/index.js +67 -30
  23. package/integ-tests/{integ.al2023.ts.snapshot/asset.0ad50fc42afd768c3d0bfdd4701e43284fb077a25f19eea1e8c51a5ca36ebfe4 → integ.custom-domain.ts.snapshot/asset.efac30c7091c58fed492058fa6403c14f7e58aab8cf4fd595d838b8d5eeec2b9}/index.js +50 -25
  24. package/integ-tests/integ.custom-domain.ts.snapshot/integ.json +1 -1
  25. package/integ-tests/integ.custom-domain.ts.snapshot/manifest.json +19 -3
  26. package/integ-tests/integ.custom-domain.ts.snapshot/tree.json +1 -1
  27. package/integ-tests/integ.stop-on-idle.ts +1 -4
  28. package/integ-tests/integ.stop-on-idle.ts.snapshot/IntegStopOnIdleFunctionalityDefaultTestDeployAssertEECF3FC0.assets.json +2 -2
  29. package/integ-tests/integ.stop-on-idle.ts.snapshot/IntegStopOnIdleFunctionalityDefaultTestDeployAssertEECF3FC0.template.json +4 -4
  30. package/integ-tests/integ.stop-on-idle.ts.snapshot/IntegTestStackStopOnIdle.assets.json +23 -9
  31. package/integ-tests/integ.stop-on-idle.ts.snapshot/IntegTestStackStopOnIdle.template.json +755 -194
  32. package/integ-tests/integ.stop-on-idle.ts.snapshot/asset.22c8a6c357b704e370bef317ae1b52c59f684aa7640422a3d1dfe813d1f77853.lambda/index.js +67 -0
  33. package/integ-tests/{integ.custom-domain.ts.snapshot/asset.33da23274e25bd9f43638c5d83dad26e3931cbe78d462ffd9a9f565e948b4f5f.lambda → integ.stop-on-idle.ts.snapshot/asset.2f99f38311da357eaaea1284d67c759759324dec4a1cd11621d9c59eea9e81df.lambda}/index.js +67 -30
  34. package/integ-tests/integ.stop-on-idle.ts.snapshot/manifest.json +353 -72
  35. package/integ-tests/integ.stop-on-idle.ts.snapshot/tree.json +1 -1
  36. package/integ-tests/integ.ubuntu.ts.snapshot/IntegSetupVSCodeOnUbuntuDefaultTestDeployAssertFF8DF2C5.assets.json +2 -2
  37. package/integ-tests/integ.ubuntu.ts.snapshot/IntegSetupVSCodeOnUbuntuDefaultTestDeployAssertFF8DF2C5.template.json +1 -1
  38. package/integ-tests/integ.ubuntu.ts.snapshot/IntegTestStackUbuntu22.assets.json +8 -8
  39. package/integ-tests/integ.ubuntu.ts.snapshot/IntegTestStackUbuntu22.template.json +270 -94
  40. package/integ-tests/{integ.al2023.ts.snapshot/asset.33da23274e25bd9f43638c5d83dad26e3931cbe78d462ffd9a9f565e948b4f5f.lambda → integ.ubuntu.ts.snapshot/asset.2f99f38311da357eaaea1284d67c759759324dec4a1cd11621d9c59eea9e81df.lambda}/index.js +67 -30
  41. package/integ-tests/{integ.custom-domain.ts.snapshot/asset.0ad50fc42afd768c3d0bfdd4701e43284fb077a25f19eea1e8c51a5ca36ebfe4 → integ.ubuntu.ts.snapshot/asset.efac30c7091c58fed492058fa6403c14f7e58aab8cf4fd595d838b8d5eeec2b9}/index.js +50 -25
  42. package/integ-tests/integ.ubuntu.ts.snapshot/integ.json +1 -1
  43. package/integ-tests/integ.ubuntu.ts.snapshot/manifest.json +19 -3
  44. package/integ-tests/integ.ubuntu.ts.snapshot/tree.json +1 -1
  45. package/integ-tests/integ.ubuntu24.ts +69 -0
  46. package/integ-tests/integ.ubuntu24.ts.snapshot/IntegSetupVSCodeOnUbuntuDefaultTestDeployAssertFF8DF2C5.assets.json +33 -0
  47. package/integ-tests/integ.ubuntu24.ts.snapshot/IntegSetupVSCodeOnUbuntuDefaultTestDeployAssertFF8DF2C5.template.json +337 -0
  48. package/integ-tests/integ.ubuntu24.ts.snapshot/IntegTestStackUbuntu24.assets.json +118 -0
  49. package/integ-tests/integ.ubuntu24.ts.snapshot/IntegTestStackUbuntu24.template.json +2725 -0
  50. package/integ-tests/integ.ubuntu24.ts.snapshot/asset.2819175352ad1ce0dae768e83fc328fb70fb5f10b4a8ff0ccbcb791f02b0716d/index.js +1 -0
  51. package/integ-tests/integ.ubuntu24.ts.snapshot/asset.2f99f38311da357eaaea1284d67c759759324dec4a1cd11621d9c59eea9e81df.lambda/index.js +180 -0
  52. package/integ-tests/integ.ubuntu24.ts.snapshot/asset.530055f7515b3f0a47900f5df37e729ba40ca977b2d07b952bdefa2b8f883f42.bundle/index.js +30676 -0
  53. package/integ-tests/integ.ubuntu24.ts.snapshot/asset.781ab0ab74634cdaf61539ab208ab777829ef07097ac21f95b9e15a3b1eedc1b.lambda/index.js +57 -0
  54. package/integ-tests/integ.ubuntu24.ts.snapshot/asset.7fa1e366ee8a9ded01fc355f704cff92bfd179574e6f9cfee800a3541df1b200/__entrypoint__.js +1 -0
  55. package/integ-tests/integ.ubuntu24.ts.snapshot/asset.7fa1e366ee8a9ded01fc355f704cff92bfd179574e6f9cfee800a3541df1b200/index.js +1 -0
  56. package/integ-tests/integ.ubuntu24.ts.snapshot/asset.9d043014be736e8162bcc7ec5590cc6d2ff24fd0d9c73a5c5d595151c5fdad00/index.js +1 -0
  57. package/integ-tests/integ.ubuntu24.ts.snapshot/asset.bdc104ed9cab1b5b6421713c8155f0b753380595356f710400609664d3635eca/cfn-response.js +1 -0
  58. package/integ-tests/integ.ubuntu24.ts.snapshot/asset.bdc104ed9cab1b5b6421713c8155f0b753380595356f710400609664d3635eca/consts.js +1 -0
  59. package/integ-tests/integ.ubuntu24.ts.snapshot/asset.bdc104ed9cab1b5b6421713c8155f0b753380595356f710400609664d3635eca/framework.js +3 -0
  60. package/integ-tests/integ.ubuntu24.ts.snapshot/asset.bdc104ed9cab1b5b6421713c8155f0b753380595356f710400609664d3635eca/outbound.js +1 -0
  61. package/integ-tests/integ.ubuntu24.ts.snapshot/asset.bdc104ed9cab1b5b6421713c8155f0b753380595356f710400609664d3635eca/util.js +1 -0
  62. package/integ-tests/integ.ubuntu24.ts.snapshot/asset.efac30c7091c58fed492058fa6403c14f7e58aab8cf4fd595d838b8d5eeec2b9/index.js +6017 -0
  63. package/integ-tests/integ.ubuntu24.ts.snapshot/integ.json +23 -0
  64. package/integ-tests/integ.ubuntu24.ts.snapshot/manifest.json +1473 -0
  65. package/integ-tests/integ.ubuntu24.ts.snapshot/tree.json +1 -0
  66. package/lib/idle-monitor/idle-monitor-function.js +2 -2
  67. package/lib/idle-monitor/idle-monitor.js +5 -2
  68. package/lib/idle-monitor-enabler/idle-monitor-enabler-function.d.ts +13 -0
  69. package/lib/idle-monitor-enabler/idle-monitor-enabler-function.js +22 -0
  70. package/lib/idle-monitor-enabler/idle-monitor-enabler.d.ts +25 -0
  71. package/lib/idle-monitor-enabler/idle-monitor-enabler.js +76 -0
  72. package/lib/idle-monitor-enabler/idle-monitor-enabler.lambda.d.ts +9 -0
  73. package/lib/idle-monitor-enabler/idle-monitor-enabler.lambda.js +48 -0
  74. package/lib/index.d.ts +0 -1
  75. package/lib/index.js +1 -2
  76. package/lib/installer/installer-function.js +2 -2
  77. package/lib/installer/installer.d.ts +100 -0
  78. package/lib/installer/installer.js +658 -301
  79. package/lib/installer/installer.lambda.js +64 -30
  80. package/lib/secret-retriever/secret-retriever-function.js +2 -2
  81. package/lib/vscode-server.d.ts +40 -0
  82. package/lib/vscode-server.js +27 -4
  83. package/package.json +12 -12
  84. package/.claude/hooks/file_checker.sh +0 -178
  85. package/.qlty/.gitignore +0 -7
  86. package/.qlty/configs/.yamllint.yaml +0 -21
  87. package/.qlty/qlty.toml +0 -115
  88. package/assets/status-check/status-check.lambda/index.js +0 -123
  89. package/integ-tests/integ.al2023.ts.snapshot/cdk.out +0 -1
  90. package/integ-tests/integ.al2023.ts.snapshot/read.13497.1.lock +0 -1
  91. package/integ-tests/integ.custom-domain.ts.snapshot/read.13497.1.lock +0 -1
  92. package/integ-tests/integ.ubuntu.ts.snapshot/cdk.out +0 -1
  93. package/integ-tests/integ.ubuntu.ts.snapshot/read.13497.1.lock +0 -1
  94. package/lib/status-check/status-check-function.d.ts +0 -13
  95. package/lib/status-check/status-check-function.js +0 -22
  96. package/lib/status-check/status-check.d.ts +0 -36
  97. package/lib/status-check/status-check.js +0 -109
  98. package/lib/status-check/status-check.lambda.d.ts +0 -2
  99. package/lib/status-check/status-check.lambda.js +0 -104
package/CLAUDE.md CHANGED
@@ -4,18 +4,25 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4
4
 
5
5
  ## Repository Overview
6
6
 
7
- This is a JSII-enabled CDK construct library published to npm, PyPI, and Go that deploys VS Code Server on AWS. It's designed for workshop/development purposes and creates infrastructure for running a cloud-based VS Code instance accessible through CloudFront.
7
+ A JSII-enabled CDK construct library that deploys VS Code Server on AWS. Published to npm and PyPI. Designed for workshop/development environments with intentionally permissive security for ease of use.
8
8
 
9
- **Key Characteristics**:
10
- - **Projen-managed**: All project configuration is in `.projenrc.ts` - run `npx projen` after modifying it
11
- - **Multi-language**: TypeScript source published to Python and Go via JSII
12
- - **Not production-ready**: Intentionally permissive for workshop scenarios
9
+ **Key Characteristic**: This is **NOT production-ready** by design - it's optimized for quick workshop setup and learning environments.
10
+
11
+ ## Projen-Managed Project
12
+
13
+ **Critical**: ALL project configuration is in `.projenrc.ts`. After modifying it, run `npx projen` to regenerate managed files.
14
+
15
+ **Never manually edit**:
16
+ - `package.json`
17
+ - Task definitions
18
+ - GitHub workflows
19
+ - Any file with "~~ Generated by projen" header
13
20
 
14
21
  ## Development Commands
15
22
 
16
23
  ### Build and Test
17
24
  ```bash
18
- # Build project (compiles TS, bundles Lambdas, generates API docs)
25
+ # Full build (compiles TS, bundles Lambdas, generates API docs)
19
26
  npx projen build
20
27
 
21
28
  # Run unit tests
@@ -24,10 +31,10 @@ npx projen test
24
31
  # Run single test file
25
32
  npx jest test/vscode-server.test.ts
26
33
 
27
- # Run tests in watch mode
34
+ # Watch mode for tests
28
35
  npx projen test:watch
29
36
 
30
- # Integration tests (deploys to eu-west-1, eu-west-2, eu-north-1)
37
+ # Integration tests (deploys to eu-west-1, eu-west-2, eu-north-1, eu-west-3)
31
38
  npm run integ-test
32
39
  ```
33
40
 
@@ -44,82 +51,232 @@ npm run awslint
44
51
  ```bash
45
52
  # Bundle specific Lambda
46
53
  npx projen bundle:installer/installer.lambda
54
+ npx projen bundle:idle-monitor/idle-monitor.lambda
55
+ npx projen bundle:idle-monitor-enabler/idle-monitor-enabler.lambda
47
56
  npx projen bundle:secret-retriever/secret-retriever.lambda
48
57
 
49
- # Watch mode for development
58
+ # Watch mode for Lambda development
50
59
  npx projen bundle:installer/installer.lambda:watch
51
60
  ```
52
61
 
53
62
  ### Publishing
54
63
  ```bash
55
- # Package for all targets (JS, Python, Go)
64
+ # Package for all targets (npm, Python)
56
65
  npx projen package-all
57
66
 
58
67
  # Package specific target
59
68
  npx projen package:js
60
69
  npx projen package:python
61
- npx projen package:go
62
70
  ```
63
71
 
64
- ## Architecture
72
+ ## Architecture Overview
73
+
74
+ ### Main Construct: `VSCodeServer` (src/vscode-server.ts)
65
75
 
66
- ### Main Construct (`VSCodeServer`)
67
- Located in `src/vscode-server.ts`, orchestrates:
76
+ Orchestrates the entire infrastructure:
68
77
  - VPC with single public subnet
69
78
  - EC2 instance (default: m7g.xlarge Graviton3 ARM)
70
- - CloudFront distribution with custom cache policies for VS Code Server
79
+ - CloudFront distribution with custom cache policies
71
80
  - Security groups (CloudFront prefix list restriction only)
72
- - IAM role with CDK permissions for workshop use
73
- - Optional Route53 + ACM certificate integration via `domainName` prop
81
+ - IAM role with broad CDK permissions (workshop-friendly)
82
+ - Optional: Route53 + ACM certificate integration
83
+ - Optional: Auto-stop feature with idle monitoring
84
+
85
+ ### Custom Resource Pattern
74
86
 
75
- ### Custom Resources Pattern
76
87
  Uses Lambda-backed custom resources via CDK Provider construct:
77
- - **Installer** (`src/installer/`): OS-specific VS Code Server installation via SSM documents
78
- - **SecretRetriever** (`src/secret-retriever/`): Extracts generated password from Secrets Manager
79
- - **PrefixListRetriever** (`src/prefixlist-retriever/`): Fetches AWS-managed CloudFront prefix lists
80
88
 
81
- ### AMI Selection
82
- `src/mappings.ts` contains SSM parameter paths for:
89
+ 1. **Installer** (`src/installer/`)
90
+ - Runs SSM documents to install VS Code Server
91
+ - OS-specific installation for Ubuntu 22/24 and Amazon Linux 2023
92
+ - Returns SUCCESS when installation completes
93
+ - Custom resource name: `SSMInstallerCustomResource`
94
+
95
+ 2. **SecretRetriever** (`src/secret-retriever/`)
96
+ - Extracts generated password from Secrets Manager
97
+ - Returns password as CloudFormation output
98
+
99
+ 3. **PrefixListRetriever** (`src/prefixlist-retriever/`)
100
+ - Fetches AWS-managed CloudFront prefix lists
101
+ - Used to restrict security group ingress
102
+
103
+ 4. **IdleMonitorEnabler** (`src/idle-monitor-enabler/`)
104
+ - Enables EventBridge rule ONLY after installation completes
105
+ - Prevents race condition where IdleMonitor stops instance mid-installation
106
+ - Critical dependency chain: `Installer → IdleMonitorEnabler → IdleMonitor active`
107
+
108
+ ### Auto-Stop Feature Architecture
109
+
110
+ **Problem Solved**: Race condition where IdleMonitor could stop instance during installation.
111
+
112
+ **Solution Flow**:
113
+ 1. EventBridge rule created in **DISABLED** state (`src/idle-monitor/idle-monitor.ts:118`)
114
+ 2. Installer runs and completes VS Code Server setup
115
+ 3. IdleMonitorEnabler custom resource **enables** the rule after installation succeeds
116
+ 4. IdleMonitor now safely monitors CloudFront metrics for idle detection
117
+
118
+ **Key Files**:
119
+ - `src/idle-monitor/idle-monitor.ts` - EventBridge rule + Lambda for monitoring
120
+ - `src/idle-monitor/idle-monitor.lambda.ts` - Checks CloudWatch metrics, stops instance if idle
121
+ - `src/idle-monitor-enabler/idle-monitor-enabler.ts` - Custom resource construct
122
+ - `src/idle-monitor-enabler/idle-monitor-enabler.lambda.ts` - Enables EventBridge rule via AWS SDK
123
+
124
+ **Dependency Wiring** (`src/vscode-server.ts:984`):
125
+ ```typescript
126
+ const installerCustomResource = this.node.findChild('SSMInstallerCustomResource');
127
+ enabler.node.addDependency(installerCustomResource);
128
+ ```
129
+
130
+ ### AMI Selection (`src/mappings.ts`)
131
+
132
+ Contains SSM parameter paths for:
83
133
  - Ubuntu 22/24 (ARM + x86_64)
84
134
  - Amazon Linux 2023 (ARM + x86_64)
85
135
 
86
136
  Function `getAmiSSMParameterForLinuxArchitectureAndFlavor()` returns region-specific SSM parameter for latest AMI.
87
137
 
88
- ### Key Props
89
- `VSCodeServerProps` in `src/vscode-server.ts:27-155`:
90
- - **Instance**: `instanceClass`, `instanceSize`, `instanceVolumeSize`
91
- - **OS**: `instanceOperatingSystem` (LinuxFlavorType enum), `instanceCpuArchitecture` (LinuxArchitectureType enum)
92
- - **VS Code**: `vscodeUser`, `vscodePassword`, `homeFolder`, `devServerPort`, `devServerBasePath`
93
- - **Domain**: `domainName`, `hostedZoneId`, `certificateArn`, `autoCreateCertificate`
94
- - **Extensions**: `additionalInstanceRolePolicies`, `additionalTags`
138
+ ### Key Props (`src/vscode-server.ts:27-200`)
95
139
 
96
- ## Important Patterns
140
+ **Instance Configuration**:
141
+ - `instanceClass`, `instanceSize`, `instanceVolumeSize`
142
+ - `instanceOperatingSystem` (LinuxFlavorType enum)
143
+ - `instanceCpuArchitecture` (LinuxArchitectureType enum)
97
144
 
98
- ### Projen Workflow
99
- 1. Edit `.projenrc.ts` for project changes (dependencies, build config, etc.)
100
- 2. Run `npx projen` to regenerate managed files
101
- 3. Never manually edit `package.json`, task definitions, or GitHub workflows
145
+ **VS Code Configuration**:
146
+ - `vscodeUser`, `vscodePassword`, `homeFolder`
147
+ - `devServerPort`, `devServerBasePath`
102
148
 
103
- ### JSII Constraints
149
+ **Domain/Certificate**:
150
+ - `domainName`, `hostedZoneId`, `certificateArn`, `autoCreateCertificate`
151
+
152
+ **Auto-Stop**:
153
+ - `enableAutoStop` - Enable automatic instance stop when idle
154
+ - `idleTimeoutMinutes` - Minutes of inactivity before stopping (default: 30)
155
+ - `idleCheckIntervalMinutes` - How often to check (default: 5)
156
+ - `skipStatusChecks` - Skip EC2 status checks before stopping (for testing only)
157
+
158
+ **Extensions**:
159
+ - `additionalInstanceRolePolicies`, `additionalTags`
160
+
161
+ ## JSII Constraints
162
+
163
+ **Critical for JSII compatibility**:
104
164
  - All public APIs must be JSII-compatible (no TS-specific types)
105
- - Bundled dependencies (like `node-html-parser`) in Lambda code must be listed in `.projenrc.ts` `bundledDeps`
106
- - Lambda functions use esbuild bundling configured via Projen
107
-
108
- ### CDK-nag Integration
109
- - `src/suppress-nags.ts` contains suppression patterns
110
- - Suppressions are applied via `NagSuppressions.addResourceSuppressions()` throughout construct code
111
- - Necessary because workshop design intentionally violates production best practices (e.g., broad IAM permissions)
112
-
113
- ### Integration Tests
114
- - Located in `integ-tests/`
115
- - Use `@aws-cdk/integ-tests-alpha` framework
116
- - Deploy actual stacks to 3 regions in parallel
117
- - Include assertion tests (e.g., login test via Lambda in `integ-tests/functions/`)
118
- - Run with `npm run integ-test` (requires AWS credentials)
119
-
120
- ## Domain/Certificate Feature (feat/route53-domain branch)
121
- When `domainName` prop is provided:
122
- - Creates Route53 A record pointing to CloudFront distribution
123
- - Supports `autoCreateCertificate` flag to create ACM cert in us-east-1 with DNS validation
124
- - Alternatively accepts existing `certificateArn`
125
- - Auto-discovers hosted zone if `hostedZoneId` not provided
165
+ - Bundled dependencies (like `node-html-parser`) must be in `.projenrc.ts` `bundledDeps`
166
+ - Lambda functions use esbuild bundling configured via Projen (auto-discovered in `src/**/*.lambda.ts`)
167
+
168
+ ## CDK-nag Integration
169
+
170
+ Suppressions defined in `src/suppress-nags.ts` and applied throughout construct code.
171
+
172
+ **Why suppressions are needed**: Workshop design intentionally violates production best practices:
173
+ - Broad IAM permissions for ease of use
174
+ - Permissive security groups
175
+ - Public subnet deployment
176
+ - No VPC endpoints
177
+
178
+ Apply suppressions via `NagSuppressions.addResourceSuppressions()`.
179
+
180
+ ## Integration Tests
181
+
182
+ Located in `integ-tests/`, using `@aws-cdk/integ-tests-alpha` framework.
183
+
184
+ **Test Files**:
185
+ - `integ.ubuntu.ts` - Basic Ubuntu 22 deployment + login test
186
+ - `integ.al2023.ts` - Amazon Linux 2023 deployment
187
+ - `integ.custom-domain.ts` - Custom domain + ACM certificate
188
+ - `integ.stop-on-idle.ts` - **4-phase auto-stop workflow test**
189
+
190
+ **Stop-on-Idle Test Phases** (`integ-tests/integ.stop-on-idle.ts`):
191
+ 1. `verify-auto-stop` - Wait for instance to stop after idle timeout
192
+ 2. `disable-idle-monitor` - Disable EventBridge rule to prevent re-stopping
193
+ 3. `start-instance` - Start instance and wait for running state
194
+ 4. `verify-login` - Check VS Code Server accessibility via CloudFront
195
+
196
+ **Run Integration Tests**:
197
+ ```bash
198
+ npm run integ-test # Deploys to 4 regions in parallel
199
+ ```
200
+
201
+ **Important**: Integration tests deploy actual stacks and incur AWS costs.
202
+
203
+ ## Lambda Bundling
204
+
205
+ Projen automatically discovers Lambda functions matching `src/**/*.lambda.ts` pattern.
206
+
207
+ **Auto-generated tasks** for each Lambda:
208
+ - `bundle:<path>/name.lambda` - Bundle Lambda code
209
+ - `bundle:<path>/name.lambda:watch` - Watch mode for development
210
+
211
+ **Bundled output**: `assets/<path>/name.lambda/index.js`
212
+
213
+ ## Common Workflows
214
+
215
+ ### Adding a New Lambda Function
216
+
217
+ 1. Create `src/my-feature/my-feature.lambda.ts`
218
+ 2. Create `src/my-feature/my-feature-function.ts` (CDK construct wrapper)
219
+ 3. Run `npx projen` - auto-discovers and creates bundle task
220
+ 4. Import in construct: `import { MyFeatureFunction } from './my-feature/my-feature-function'`
221
+
222
+ ### Modifying VSCodeServer Props
223
+
224
+ 1. Edit `src/vscode-server.ts` - update `VSCodeServerProps` interface
225
+ 2. Run `npx projen build` - regenerates JSII artifacts + API docs
226
+ 3. Update README.md with usage examples
227
+
228
+ ### Testing Changes
229
+
230
+ 1. Unit tests: `npx jest test/vscode-server.test.ts`
231
+ 2. Build validation: `npx projen build` (includes awslint checks)
232
+ 3. Integration test: `npm run integ-test` (full deployment test)
233
+
234
+ ## Race Condition Prevention (Auto-Stop)
235
+
236
+ **Issue**: IdleMonitor EventBridge rule triggers immediately on stack creation, potentially stopping instance during installation (observed: instance stopped 72 seconds after installer started).
237
+
238
+ **Fix**: Three-stage dependency chain ensures safe operation:
239
+
240
+ ```
241
+ EC2 Instance
242
+
243
+ Installer Custom Resource (waits for VS Code Server installation)
244
+
245
+ IdleMonitorEnabler Custom Resource (enables EventBridge rule)
246
+
247
+ IdleMonitor Active (now safe to monitor)
248
+ ```
249
+
250
+ **Implementation Details**:
251
+ - EventBridge rule created with `enabled: false` (`src/idle-monitor/idle-monitor.ts:118`)
252
+ - IdleMonitorEnabler depends on `SSMInstallerCustomResource` via `node.addDependency()`
253
+ - CloudFormation enforces ordering automatically
254
+
255
+ This pattern can be applied to any future features requiring post-installation activation.
256
+
257
+ ## Multi-Language Publishing
258
+
259
+ Project publishes to:
260
+ - **npm**: `@mavogel/cdk-vscode-server`
261
+ - **PyPI**: `cdk-vscode-server`
262
+
263
+ JSII compiles TypeScript to:
264
+ - JavaScript (npm)
265
+ - Python (PyPI)
266
+
267
+ **Publishing** (requires credentials):
268
+ ```bash
269
+ npx projen release
270
+ ```
271
+
272
+ ## Workshop-Specific Considerations
273
+
274
+ **Remember**: This construct is intentionally designed for workshops, NOT production:
275
+
276
+ - IAM roles have broad permissions (all CDK operations)
277
+ - Security groups allow CloudFront only (but permissive within that constraint)
278
+ - No private subnets or VPC endpoints (cost optimization for workshops)
279
+ - Single public subnet (simplicity over high availability)
280
+ - Password-based authentication (ease of use over MFA/SSO)
281
+
282
+ When reviewing changes, prioritize **ease of use** and **quick setup** over security hardening.
package/README.md CHANGED
@@ -21,7 +21,9 @@ we implement new features. Therefore make sure you use an exact version in your
21
21
  - [Features](#features)
22
22
  - [Usage](#usage)
23
23
  - [Standard](#Standard)
24
+ - [Pre-populate with Git Repository](#pre-populate-with-git-repository)
24
25
  - [Custom Domain Configuration](#custom-domain-configuration)
26
+ - [Auto-Stop Configuration](#auto-stop-configuration)
25
27
  - [Solution Design](#solution-design)
26
28
  - [Inspiration](#inspiration)
27
29
 
@@ -31,7 +33,7 @@ we implement new features. Therefore make sure you use an exact version in your
31
33
  - 📏 **Best Practice Setup**: Set up with [projen](https://projen.io/) and a [single configuration file](./.projenrc.ts) to keep your changes centralized.
32
34
  - 🤹‍♂️ **Pre-installed packages**: Besides the [vscode](https://code.visualstudio.com/) server, other tools and software packages such as `git`, `docker`, `awscli` `nodejs` and `python` are pre-installed on the EC2 instance.
33
35
  - 🌐 **Custom Domain Support**: Use your own domain name with automatic ACM certificate creation and Route53 DNS configuration, or bring your existing certificate.
34
- - 💰 **Auto-Stop**: Automatically stop EC2 instances after inactivity with Elastic IP retention - save up to 75% on costs for development environments
36
+ - 💰 **Auto-Stop**: Automatically stop EC2 instances after inactivity with Elastic IP retention - save up to 75% on costs for development environments.
35
37
  - 🏗️ **Extensibility**: Pass in properties to the construct, which start with `additional*`. They allow you to extend the configuration to your needs. There are more to come...
36
38
 
37
39
  ## Usage
@@ -115,6 +117,41 @@ dev.vscodepassword64FBCA12 = foobarbaz
115
117
 
116
118
  See the [examples](./examples) folder for more inspiration.
117
119
 
120
+ ### Pre-populate with Git Repository
121
+
122
+ Clone a git repository into the VS Code Server's home folder during instance setup - perfect for workshops or development environments with starter code:
123
+
124
+ ```ts
125
+ new VSCodeServer(this, 'vscode', {
126
+ // Clone a git repository into the home folder
127
+ repoUrl: 'https://github.com/aws-samples/my-workshop-repo.git',
128
+
129
+ // Optional: customize the home folder path (default: /Workshop)
130
+ homeFolder: '/MyWorkshop',
131
+
132
+ // Optional: specify VS Code user (default: vscode-user)
133
+ vscodeUser: 'workshop-user',
134
+ });
135
+ ```
136
+
137
+ **What happens:**
138
+ 1. During instance setup, the specified git repository is cloned into the user's home folder
139
+ 2. VS Code Server opens with the repository already loaded and ready to use
140
+ 3. Participants can start coding immediately without manual git clone steps
141
+
142
+ **Use cases:**
143
+ - Workshop environments with pre-configured starter code
144
+ - Development environments with boilerplate projects
145
+ - Training sessions with example applications
146
+ - Code review sessions with pre-loaded repositories
147
+
148
+ **Repository requirements:**
149
+ - Must be publicly accessible (no authentication required)
150
+ - HTTPS URLs only (SSH git URLs are not supported)
151
+ - Repository will be cloned using `git clone` during instance initialization
152
+
153
+ For complete examples, see [examples/](./examples).
154
+
118
155
  ### Custom Domain Configuration
119
156
 
120
157
  You can configure your VS Code Server with a custom domain name instead of using the default CloudFront domain. The construct supports three different configuration options:
@@ -200,6 +237,7 @@ new VSCodeServer(this, 'vscode', {
200
237
  - Elastic IP for consistent public addressing
201
238
  - EventBridge rule triggering idle monitoring at configured intervals
202
239
  - IdleMonitor Lambda function checking CloudWatch metrics for request activity
240
+ - IdleMonitorEnabler custom resource ensuring monitoring only starts after installation completes
203
241
  - CloudWatch metrics from CloudFront distribution
204
242
 
205
243
  **Integration Testing:**
@@ -0,0 +1,67 @@
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-enabler/idle-monitor-enabler.lambda.ts
21
+ var idle_monitor_enabler_lambda_exports = {};
22
+ __export(idle_monitor_enabler_lambda_exports, {
23
+ handler: () => handler
24
+ });
25
+ module.exports = __toCommonJS(idle_monitor_enabler_lambda_exports);
26
+ var import_client_eventbridge = require("@aws-sdk/client-eventbridge");
27
+ var eventBridge = new import_client_eventbridge.EventBridge();
28
+ var handler = async (event) => {
29
+ console.log("Event: %j", { ...event, ResponseURL: "..." });
30
+ const ruleName = event.ResourceProperties.RuleName;
31
+ if (!ruleName) {
32
+ throw new Error("RuleName is required in ResourceProperties");
33
+ }
34
+ switch (event.RequestType) {
35
+ case "Create":
36
+ case "Update":
37
+ console.log(`Enabling EventBridge rule: ${ruleName}`);
38
+ await eventBridge.send(
39
+ new import_client_eventbridge.EnableRuleCommand({
40
+ Name: ruleName
41
+ })
42
+ );
43
+ console.log(`Successfully enabled rule: ${ruleName}`);
44
+ return {
45
+ PhysicalResourceId: `idle-monitor-enabler-${ruleName}`
46
+ };
47
+ case "Delete":
48
+ console.log(`Disabling EventBridge rule on deletion: ${ruleName}`);
49
+ try {
50
+ await eventBridge.send(
51
+ new import_client_eventbridge.DisableRuleCommand({
52
+ Name: ruleName
53
+ })
54
+ );
55
+ console.log(`Successfully disabled rule: ${ruleName}`);
56
+ } catch (error) {
57
+ console.log(`Error disabling rule (ignoring): ${error.message}`);
58
+ }
59
+ return {};
60
+ default:
61
+ throw new Error(`Unsupported RequestType: ${event.RequestType}`);
62
+ }
63
+ };
64
+ // Annotate the CommonJS export names for ESM import in node:
65
+ 0 && (module.exports = {
66
+ handler
67
+ });
@@ -59,47 +59,89 @@ var handler = async (event, context) => {
59
59
  parameters[key] = [value];
60
60
  }
61
61
  console.log("mapped parameters: %j", parameters);
62
- let attemptNo = 0;
63
- let timeRemaining = context.getRemainingTimeInMillis();
64
62
  console.log(
65
63
  `Running SSM Document '${documentName}' on EC2 instance '${instanceId}'. Logging to '${cloudWatchLogGroupName}' with parameters: '${JSON.stringify(parameters)}'`
66
64
  );
65
+ let commandId;
66
+ let attemptNo = 0;
67
67
  while (true) {
68
68
  attemptNo += 1;
69
+ const timeRemaining = context.getRemainingTimeInMillis();
69
70
  console.log(
70
- `Attempt: ${attemptNo}. Time Remaining: ${timeRemaining / 1e3}s`
71
+ `Send attempt: ${attemptNo}. Time Remaining: ${timeRemaining / 1e3}s`
71
72
  );
72
73
  try {
73
- const response = await ssm.sendCommand({
74
- DocumentName: documentName,
75
- InstanceIds: [instanceId],
76
- CloudWatchOutputConfig: {
77
- CloudWatchLogGroupName: cloudWatchLogGroupName,
78
- CloudWatchOutputEnabled: true
79
- },
80
- Parameters: parameters
81
- });
82
- console.log(`response: ${JSON.stringify(response)}`);
74
+ const response = await ssm.send(
75
+ new import_client_ssm.SendCommandCommand({
76
+ DocumentName: documentName,
77
+ InstanceIds: [instanceId],
78
+ CloudWatchOutputConfig: {
79
+ CloudWatchLogGroupName: cloudWatchLogGroupName,
80
+ CloudWatchOutputEnabled: true
81
+ },
82
+ Parameters: parameters
83
+ })
84
+ );
85
+ console.log(`sendCommand response: ${JSON.stringify(response)}`);
83
86
  const command = response.Command;
84
- const commandId = command.CommandId;
85
- const responseData = { CommandId: commandId };
86
- switch (command.Status) {
87
+ commandId = command.CommandId;
88
+ console.log(`Command sent successfully. CommandId: ${commandId}`);
89
+ break;
90
+ } catch (error) {
91
+ console.log("Error sending command:", error);
92
+ const isUnauthorized = error.name === "UnauthorizedException" || error.name === "AccessDeniedException" || error.message && error.message.includes("not authorized");
93
+ const isThrottled = error.name === "ThrottlingException" || error.name === "TooManyRequestsException";
94
+ const isRetryable = isUnauthorized || isThrottled;
95
+ const remainingTime = context.getRemainingTimeInMillis();
96
+ if (isRetryable && remainingTime > SLEEP_MS) {
97
+ console.log(
98
+ `Retryable error encountered (${error.name}). Attempt ${attemptNo}. Sleeping: ${SLEEP_MS / 1e3}s before retry`
99
+ );
100
+ await new Promise((resolve) => setTimeout(resolve, SLEEP_MS));
101
+ } else {
102
+ console.log("Non-retryable error or timeout. Failing...");
103
+ throw error;
104
+ }
105
+ }
106
+ }
107
+ let pollAttemptNo = 0;
108
+ const responseData = { CommandId: commandId };
109
+ while (true) {
110
+ pollAttemptNo += 1;
111
+ const timeRemaining = context.getRemainingTimeInMillis();
112
+ console.log(
113
+ `Poll attempt: ${pollAttemptNo}. Time Remaining: ${timeRemaining / 1e3}s`
114
+ );
115
+ try {
116
+ const invocationResponse = await ssm.send(
117
+ new import_client_ssm.GetCommandInvocationCommand({
118
+ CommandId: commandId,
119
+ InstanceId: instanceId
120
+ })
121
+ );
122
+ console.log(
123
+ `getCommandInvocation response: ${JSON.stringify(invocationResponse)}`
124
+ );
125
+ const status = invocationResponse.Status;
126
+ switch (status) {
87
127
  case "Pending":
88
128
  case "InProgress":
89
- timeRemaining = context.getRemainingTimeInMillis();
129
+ case "Delayed":
90
130
  if (timeRemaining > SLEEP_MS) {
91
131
  console.log(
92
- `Instance ${instanceId} not ready: 'InProgress'. Sleeping: ${SLEEP_MS / 1e3}s`
132
+ `Command ${commandId} status: '${status}'. Sleeping: ${SLEEP_MS / 1e3}s`
93
133
  );
94
134
  await new Promise((resolve) => setTimeout(resolve, SLEEP_MS));
95
135
  break;
96
136
  } else {
97
137
  throw new Error(
98
- `SSM Document ${documentName} on EC2 instance ${instanceId} timed out while lambda in progress`
138
+ `SSM Document ${documentName} on EC2 instance ${instanceId} timed out while lambda waiting (status: ${status})`
99
139
  );
100
140
  }
101
141
  case "Success":
102
- console.log(`Instance ${instanceId} successfully bootstrapped`);
142
+ console.log(
143
+ `Instance ${instanceId} successfully bootstrapped. Command ${commandId} completed.`
144
+ );
103
145
  return { Data: responseData };
104
146
  case "TimedOut":
105
147
  throw new Error(
@@ -115,23 +157,18 @@ var handler = async (event, context) => {
115
157
  );
116
158
  default:
117
159
  throw new Error(
118
- `SSM Document ${documentName} on EC2 instance ${instanceId} status ${command.Status}`
160
+ `SSM Document ${documentName} on EC2 instance ${instanceId} unknown status: ${status}`
119
161
  );
120
162
  }
121
- return { Data: responseData };
122
163
  } catch (error) {
123
- console.log("Error occurred:", error);
124
- const isUnauthorized = error.name === "UnauthorizedException" || error.name === "AccessDeniedException" || error.message && error.message.includes("not authorized");
125
- const isThrottled = error.name === "ThrottlingException" || error.name === "TooManyRequestsException";
126
- const isRetryable = isUnauthorized || isThrottled;
127
- timeRemaining = context.getRemainingTimeInMillis();
128
- if (isRetryable && timeRemaining > SLEEP_MS) {
164
+ const remainingTime = context.getRemainingTimeInMillis();
165
+ if (error.name === "InvocationDoesNotExist" && remainingTime > SLEEP_MS) {
129
166
  console.log(
130
- `Retryable error encountered (${error.name}). Attempt ${attemptNo}. Sleeping: ${SLEEP_MS / 1e3}s before retry`
167
+ `Invocation not yet available. Sleeping: ${SLEEP_MS / 1e3}s`
131
168
  );
132
169
  await new Promise((resolve) => setTimeout(resolve, SLEEP_MS));
133
170
  } else {
134
- console.log("Non-retryable error or timeout. Failing...");
171
+ console.log("Error checking command status:", error);
135
172
  throw error;
136
173
  }
137
174
  }
package/awslint.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "exclude": [
3
+ "props-no-arn-refs:@mavogel/cdk-vscode-server.VSCodeServerProps.certificateArn"
4
+ ]
5
+ }
@@ -0,0 +1,30 @@
1
+ import { App, Stack, StackProps } from 'aws-cdk-lib';
2
+ import { Construct } from 'constructs';
3
+ import { VSCodeServer } from '../../src/index'
4
+
5
+ export class MyStack extends Stack {
6
+ constructor(scope: Construct, id: string, props: StackProps = {}) {
7
+ super(scope, id, props);
8
+
9
+ new VSCodeServer(this, 'vscode', {
10
+ // Clone a git repository into the home folder during instance setup
11
+ // Perfect for workshops, training sessions, or development environments
12
+ repoUrl: 'https://github.com/aws-samples/aws-cdk-examples.git',
13
+
14
+ // Optional: customize the home folder path (default: /Workshop)
15
+ homeFolder: '/CdkWorkshop',
16
+
17
+ // Optional: specify VS Code user (default: vscode-user)
18
+ vscodeUser: 'workshop-user',
19
+ });
20
+ }
21
+ }
22
+
23
+ const env = {
24
+ account: '123456789912',
25
+ region: 'eu-central-1',
26
+ };
27
+
28
+ const app = new App();
29
+ new MyStack(app, 'vscode-server-git-repo', { env });
30
+ app.synth();
@@ -14,7 +14,7 @@
14
14
  }
15
15
  }
16
16
  },
17
- "e5a8816554ca9fe1e7bf41d7d9fcc2f77bb6af7b02cb83746f5f815104b12306": {
17
+ "73ca3102a170a72edfc485eaa1468c73bbccd6bf566fd94f904e35c82f0fb905": {
18
18
  "displayName": "IntegSetupVSCodeOnAl2023DefaultTestDeployAssert74D8F645 Template",
19
19
  "source": {
20
20
  "path": "IntegSetupVSCodeOnAl2023DefaultTestDeployAssert74D8F645.template.json",
@@ -23,7 +23,7 @@
23
23
  "destinations": {
24
24
  "current_account-current_region": {
25
25
  "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
26
- "objectKey": "e5a8816554ca9fe1e7bf41d7d9fcc2f77bb6af7b02cb83746f5f815104b12306.json",
26
+ "objectKey": "73ca3102a170a72edfc485eaa1468c73bbccd6bf566fd94f904e35c82f0fb905.json",
27
27
  "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
28
28
  }
29
29
  }
@@ -45,7 +45,7 @@
45
45
  }
46
46
  },
47
47
  "flattenResponse": "false",
48
- "salt": "1759320082329"
48
+ "salt": "1762251879959"
49
49
  },
50
50
  "UpdateReplacePolicy": "Delete",
51
51
  "DeletionPolicy": "Delete"