@gugananuvem/aws-local-simulator 1.0.34 → 1.0.39
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/README.md +662 -658
- package/package.json +11 -4
- package/src/config/default-config.js +71 -0
- package/src/config/env-loader.js +4 -2
- package/src/middleware/authorization.js +271 -0
- package/src/middleware/cloudtrail-logger.js +333 -0
- package/src/middleware/error-handler.js +138 -0
- package/src/middleware/index.js +79 -0
- package/src/middleware/policy-enforcement.js +407 -0
- package/src/server.js +74 -4
- package/src/services/acm/index.js +66 -0
- package/src/services/acm/models/certificate.js +202 -0
- package/src/services/acm/server.js +224 -0
- package/src/services/acm/simulator.js +293 -0
- package/src/services/alb/index.js +104 -0
- package/src/services/alb/models/loadBalancer.js +88 -0
- package/src/services/alb/models/targetGroup.js +128 -0
- package/src/services/alb/server.js +160 -0
- package/src/services/alb/simulator.js +262 -0
- package/src/services/apigateway/index.js +22 -0
- package/src/services/apigateway/server.js +247 -35
- package/src/services/apigateway/simulator.js +780 -26
- package/src/services/asg/index.js +100 -0
- package/src/services/asg/models/autoScalingGroup.js +110 -0
- package/src/services/asg/models/launchTemplate.js +41 -0
- package/src/services/asg/models/scalingPolicy.js +51 -0
- package/src/services/asg/server.js +236 -0
- package/src/services/asg/simulator.js +410 -0
- package/src/services/aurora/cluster-manager.js +731 -0
- package/src/services/aurora/data-api.js +514 -0
- package/src/services/aurora/db-engine.js +270 -0
- package/src/services/aurora/index.js +159 -0
- package/src/services/aurora/rds-api.js +189 -0
- package/src/services/aurora/server.js +213 -0
- package/src/services/aurora/sql-translator.js +334 -0
- package/src/services/cloudformation/index.js +2 -2
- package/src/services/cloudformation/server.js +1 -0
- package/src/services/cloudfront/cache-engine.js +225 -0
- package/src/services/cloudfront/geo-restrictions.js +377 -0
- package/src/services/cloudfront/index.js +220 -0
- package/src/services/cloudfront/models/behavior.js +158 -0
- package/src/services/cloudfront/models/distribution.js +284 -0
- package/src/services/cloudfront/models/index.js +99 -0
- package/src/services/cloudfront/models/invalidation.js +109 -0
- package/src/services/cloudfront/models/origin.js +155 -0
- package/src/services/cloudfront/origin-router.js +552 -0
- package/src/services/cloudfront/origin-validator.js +398 -0
- package/src/services/cloudfront/server.js +512 -0
- package/src/services/cloudfront/signed-url-generator.js +377 -0
- package/src/services/cloudfront/simulator.js +425 -0
- package/src/services/cloudfront/store.js +326 -0
- package/src/services/cloudshell/env-builder.js +27 -0
- package/src/services/cloudshell/index.js +48 -0
- package/src/services/cloudshell/routes.js +60 -0
- package/src/services/cloudshell/session-manager.js +226 -0
- package/src/services/cloudshell/static/.gitkeep +0 -0
- package/src/services/cloudshell/static/index.html +406 -0
- package/src/services/cloudshell/websocket-handler.js +107 -0
- package/src/services/cloudtrail/index.js +2 -3
- package/src/services/cloudtrail/server.js +68 -0
- package/src/services/cognito/server.js +4 -1
- package/src/services/dynamodb/server.js +4 -1
- package/src/services/dynamodb/simulator.js +2 -1
- package/src/services/ec2/index.js +101 -0
- package/src/services/ec2/instanceServer.js +311 -0
- package/src/services/ec2/models/ami.js +39 -0
- package/src/services/ec2/models/instance.js +98 -0
- package/src/services/ec2/models/networkInterface.js +74 -0
- package/src/services/ec2/models/securityGroup.js +71 -0
- package/src/services/ec2/server.js +350 -0
- package/src/services/ec2/simulator.js +436 -0
- package/src/services/eventbridge/server.js +36 -0
- package/src/services/eventbridge/simulator.js +230 -45
- package/src/services/iam/index.js +65 -0
- package/src/services/iam/middleware.js +99 -0
- package/src/services/iam/server.js +331 -0
- package/src/services/iam/simulator.js +1432 -0
- package/src/services/index.js +28 -0
- package/src/services/kinesis-analytics/index.js +105 -0
- package/src/services/kinesis-analytics/models/application.js +325 -0
- package/src/services/kinesis-analytics/models/index.js +12 -0
- package/src/services/kinesis-analytics/server.js +250 -0
- package/src/services/kinesis-analytics/simulator.js +407 -0
- package/src/services/kinesis-analytics/store.js +176 -0
- package/src/services/kinesis-data-streams/index.js +106 -0
- package/src/services/kinesis-data-streams/models/index.js +12 -0
- package/src/services/kinesis-data-streams/models/record.js +70 -0
- package/src/services/kinesis-data-streams/models/stream.js +280 -0
- package/src/services/kinesis-data-streams/server.js +255 -0
- package/src/services/kinesis-data-streams/simulator.js +514 -0
- package/src/services/kinesis-data-streams/store.js +163 -0
- package/src/services/kinesis-firehose/index.js +106 -0
- package/src/services/kinesis-firehose/models/delivery-stream.js +265 -0
- package/src/services/kinesis-firehose/models/index.js +10 -0
- package/src/services/kinesis-firehose/server.js +215 -0
- package/src/services/kinesis-firehose/simulator.js +388 -0
- package/src/services/kinesis-firehose/store.js +163 -0
- package/src/services/kms/server.js +4 -1
- package/src/services/lambda/handler-loader.js +50 -5
- package/src/services/lambda/index.js +26 -0
- package/src/services/lambda/server.js +18 -12
- package/src/services/lambda/simulator.js +73 -17
- package/src/services/opensearch/index.js +105 -0
- package/src/services/opensearch/models/document.js +115 -0
- package/src/services/opensearch/models/index.js +73 -0
- package/src/services/opensearch/server.js +310 -0
- package/src/services/opensearch/simulator.js +605 -0
- package/src/services/opensearch/store.js +162 -0
- package/src/services/organizations/control-tower-simulator.js +321 -0
- package/src/services/organizations/control-tower-store.js +182 -0
- package/src/services/organizations/iam-integration.js +413 -0
- package/src/services/organizations/index.js +172 -0
- package/src/services/organizations/models/index.js +13 -0
- package/src/services/organizations/models/invitation.js +48 -0
- package/src/services/organizations/models/organization.js +566 -0
- package/src/services/organizations/models/role.js +244 -0
- package/src/services/organizations/models/scp.js +131 -0
- package/src/services/organizations/server.js +736 -0
- package/src/services/organizations/simulator.js +593 -0
- package/src/services/organizations/store.js +235 -0
- package/src/services/organizations/vpc-integration.js +453 -0
- package/src/services/route53/index.js +67 -0
- package/src/services/route53/models/healthCheck.js +101 -0
- package/src/services/route53/models/hostedZone.js +37 -0
- package/src/services/route53/models/resourceRecord.js +106 -0
- package/src/services/route53/server.js +841 -0
- package/src/services/route53/simulator.js +350 -0
- package/src/services/s3/index.js +62 -0
- package/src/services/s3/presigned-url-generator.js +337 -0
- package/src/services/s3/server.js +231 -0
- package/src/services/s3/simulator.js +551 -4
- package/src/services/sns/index.js +31 -0
- package/src/services/sns/server.js +9 -0
- package/src/services/sqs/index.js +38 -0
- package/src/services/sqs/server.js +57 -4
- package/src/services/sqs/simulator.js +423 -33
- package/src/services/sts/simulator.js +209 -17
- package/src/services/vpc/README.md +459 -0
- package/src/services/vpc/ec2ResponseFormatter.js +431 -0
- package/src/services/vpc/index.js +90 -0
- package/src/services/vpc/models/internetGateway.js +71 -0
- package/src/services/vpc/models/routeTable.js +160 -0
- package/src/services/vpc/models/subnet.js +48 -0
- package/src/services/vpc/models/vpc.js +37 -0
- package/src/services/vpc/models/vpcEndpoint.js +100 -0
- package/src/services/vpc/models/vpcGateway.js +134 -0
- package/src/services/vpc/server.js +836 -0
- package/src/services/vpc/simulator.js +613 -0
- package/src/utils/policy-cache.js +215 -0
- package/aws-config +0 -154
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CloudFront JSON Persistence Store
|
|
6
|
+
* Manages persistence of distributions and related data to JSON files
|
|
7
|
+
*/
|
|
8
|
+
class CloudFrontStore {
|
|
9
|
+
constructor(storePath = './data/cloudfront') {
|
|
10
|
+
this.storePath = storePath;
|
|
11
|
+
this.distributionsFile = path.join(storePath, 'distributions.json');
|
|
12
|
+
this.invalidationsFile = path.join(storePath, 'invalidations.json');
|
|
13
|
+
this.keyPairsFile = path.join(storePath, 'key-pairs.json');
|
|
14
|
+
this.init();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize store - create directories and files if needed
|
|
19
|
+
*/
|
|
20
|
+
init() {
|
|
21
|
+
if (!fs.existsSync(this.storePath)) {
|
|
22
|
+
fs.mkdirSync(this.storePath, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Initialize distribution file
|
|
26
|
+
if (!fs.existsSync(this.distributionsFile)) {
|
|
27
|
+
this._writeFile(this.distributionsFile, { distributions: [] });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Initialize invalidations file
|
|
31
|
+
if (!fs.existsSync(this.invalidationsFile)) {
|
|
32
|
+
this._writeFile(this.invalidationsFile, { invalidations: [] });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Initialize key pairs file
|
|
36
|
+
if (!fs.existsSync(this.keyPairsFile)) {
|
|
37
|
+
this._writeFile(this.keyPairsFile, { keyPairs: [] });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Save a distribution
|
|
43
|
+
* @param {object} distribution - Distribution object
|
|
44
|
+
*/
|
|
45
|
+
saveDistribution(distribution) {
|
|
46
|
+
const data = this._readFile(this.distributionsFile);
|
|
47
|
+
|
|
48
|
+
const index = data.distributions.findIndex(d => d.id === distribution.id);
|
|
49
|
+
if (index !== -1) {
|
|
50
|
+
data.distributions[index] = distribution;
|
|
51
|
+
} else {
|
|
52
|
+
data.distributions.push(distribution);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this._writeFile(this.distributionsFile, data);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get a distribution
|
|
60
|
+
* @param {string} distributionId - Distribution ID
|
|
61
|
+
* @returns {object|null} Distribution or null
|
|
62
|
+
*/
|
|
63
|
+
getDistribution(distributionId) {
|
|
64
|
+
const data = this._readFile(this.distributionsFile);
|
|
65
|
+
return data.distributions.find(d => d.id === distributionId) || null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get all distributions
|
|
70
|
+
* @returns {object[]} Array of distributions
|
|
71
|
+
*/
|
|
72
|
+
getAllDistributions() {
|
|
73
|
+
const data = this._readFile(this.distributionsFile);
|
|
74
|
+
return data.distributions || [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Delete a distribution
|
|
79
|
+
* @param {string} distributionId - Distribution ID
|
|
80
|
+
* @returns {boolean} True if deleted
|
|
81
|
+
*/
|
|
82
|
+
deleteDistribution(distributionId) {
|
|
83
|
+
const data = this._readFile(this.distributionsFile);
|
|
84
|
+
|
|
85
|
+
const index = data.distributions.findIndex(d => d.id === distributionId);
|
|
86
|
+
if (index === -1) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
data.distributions.splice(index, 1);
|
|
91
|
+
this._writeFile(this.distributionsFile, data);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Save invalidation
|
|
97
|
+
* @param {object} invalidation - Invalidation object
|
|
98
|
+
*/
|
|
99
|
+
saveInvalidation(invalidation) {
|
|
100
|
+
const data = this._readFile(this.invalidationsFile);
|
|
101
|
+
|
|
102
|
+
const index = data.invalidations.findIndex(i => i.id === invalidation.id);
|
|
103
|
+
if (index !== -1) {
|
|
104
|
+
data.invalidations[index] = invalidation;
|
|
105
|
+
} else {
|
|
106
|
+
data.invalidations.push(invalidation);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this._writeFile(this.invalidationsFile, data);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get invalidation
|
|
114
|
+
* @param {string} invalidationId - Invalidation ID
|
|
115
|
+
* @returns {object|null} Invalidation or null
|
|
116
|
+
*/
|
|
117
|
+
getInvalidation(invalidationId) {
|
|
118
|
+
const data = this._readFile(this.invalidationsFile);
|
|
119
|
+
return data.invalidations.find(i => i.id === invalidationId) || null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get invalidations by distribution
|
|
124
|
+
* @param {string} distributionId - Distribution ID
|
|
125
|
+
* @returns {object[]} Array of invalidations
|
|
126
|
+
*/
|
|
127
|
+
getInvalidationsByDistribution(distributionId) {
|
|
128
|
+
const data = this._readFile(this.invalidationsFile);
|
|
129
|
+
return data.invalidations.filter(i => i.distributionId === distributionId) || [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Delete invalidation
|
|
134
|
+
* @param {string} invalidationId - Invalidation ID
|
|
135
|
+
* @returns {boolean} True if deleted
|
|
136
|
+
*/
|
|
137
|
+
deleteInvalidation(invalidationId) {
|
|
138
|
+
const data = this._readFile(this.invalidationsFile);
|
|
139
|
+
|
|
140
|
+
const index = data.invalidations.findIndex(i => i.id === invalidationId);
|
|
141
|
+
if (index === -1) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
data.invalidations.splice(index, 1);
|
|
146
|
+
this._writeFile(this.invalidationsFile, data);
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Save key pair
|
|
152
|
+
* @param {object} keyPair - Key pair object
|
|
153
|
+
*/
|
|
154
|
+
saveKeyPair(keyPair) {
|
|
155
|
+
const data = this._readFile(this.keyPairsFile);
|
|
156
|
+
|
|
157
|
+
const index = data.keyPairs.findIndex(k => k.id === keyPair.id);
|
|
158
|
+
if (index !== -1) {
|
|
159
|
+
data.keyPairs[index] = keyPair;
|
|
160
|
+
} else {
|
|
161
|
+
data.keyPairs.push(keyPair);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this._writeFile(this.keyPairsFile, data);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get key pair
|
|
169
|
+
* @param {string} keyPairId - Key pair ID
|
|
170
|
+
* @returns {object|null} Key pair or null
|
|
171
|
+
*/
|
|
172
|
+
getKeyPair(keyPairId) {
|
|
173
|
+
const data = this._readFile(this.keyPairsFile);
|
|
174
|
+
return data.keyPairs.find(k => k.id === keyPairId) || null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get all key pairs
|
|
179
|
+
* @returns {object[]} Array of key pairs
|
|
180
|
+
*/
|
|
181
|
+
getAllKeyPairs() {
|
|
182
|
+
const data = this._readFile(this.keyPairsFile);
|
|
183
|
+
return data.keyPairs || [];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Delete key pair
|
|
188
|
+
* @param {string} keyPairId - Key pair ID
|
|
189
|
+
* @returns {boolean} True if deleted
|
|
190
|
+
*/
|
|
191
|
+
deleteKeyPair(keyPairId) {
|
|
192
|
+
const data = this._readFile(this.keyPairsFile);
|
|
193
|
+
|
|
194
|
+
const index = data.keyPairs.findIndex(k => k.id === keyPairId);
|
|
195
|
+
if (index === -1) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
data.keyPairs.splice(index, 1);
|
|
200
|
+
this._writeFile(this.keyPairsFile, data);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Export all data
|
|
206
|
+
* @returns {object} All data in the store
|
|
207
|
+
*/
|
|
208
|
+
export() {
|
|
209
|
+
return {
|
|
210
|
+
distributions: this.getAllDistributions(),
|
|
211
|
+
invalidations: this._readFile(this.invalidationsFile).invalidations || [],
|
|
212
|
+
keyPairs: this.getAllKeyPairs(),
|
|
213
|
+
exportedAt: new Date().toISOString()
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Import data
|
|
219
|
+
* @param {object} data - Data to import
|
|
220
|
+
*/
|
|
221
|
+
import(data) {
|
|
222
|
+
if (data.distributions && Array.isArray(data.distributions)) {
|
|
223
|
+
const distributionData = { distributions: data.distributions };
|
|
224
|
+
this._writeFile(this.distributionsFile, distributionData);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (data.invalidations && Array.isArray(data.invalidations)) {
|
|
228
|
+
const invalidationData = { invalidations: data.invalidations };
|
|
229
|
+
this._writeFile(this.invalidationsFile, invalidationData);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (data.keyPairs && Array.isArray(data.keyPairs)) {
|
|
233
|
+
const keyPairData = { keyPairs: data.keyPairs };
|
|
234
|
+
this._writeFile(this.keyPairsFile, keyPairData);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Clear all data
|
|
240
|
+
*/
|
|
241
|
+
clear() {
|
|
242
|
+
this._writeFile(this.distributionsFile, { distributions: [] });
|
|
243
|
+
this._writeFile(this.invalidationsFile, { invalidations: [] });
|
|
244
|
+
this._writeFile(this.keyPairsFile, { keyPairs: [] });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get storage stats
|
|
249
|
+
* @returns {object} Storage statistics
|
|
250
|
+
*/
|
|
251
|
+
getStats() {
|
|
252
|
+
const distributionData = this._readFile(this.distributionsFile);
|
|
253
|
+
const invalidationData = this._readFile(this.invalidationsFile);
|
|
254
|
+
const keyPairData = this._readFile(this.keyPairsFile);
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
distributions: distributionData.distributions?.length || 0,
|
|
258
|
+
invalidations: invalidationData.invalidations?.length || 0,
|
|
259
|
+
keyPairs: keyPairData.keyPairs?.length || 0,
|
|
260
|
+
storePath: this.storePath,
|
|
261
|
+
lastModified: new Date().toISOString()
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Create a backup
|
|
267
|
+
* @param {string} [backupPath] - Optional custom backup path
|
|
268
|
+
* @returns {string} Path to backup file
|
|
269
|
+
*/
|
|
270
|
+
createBackup(backupPath) {
|
|
271
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
272
|
+
const backup = backupPath || path.join(this.storePath, `backup-${timestamp}.json`);
|
|
273
|
+
|
|
274
|
+
const data = this.export();
|
|
275
|
+
this._writeFile(backup, data);
|
|
276
|
+
|
|
277
|
+
return backup;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Restore from backup
|
|
282
|
+
* @param {string} backupPath - Path to backup file
|
|
283
|
+
*/
|
|
284
|
+
restoreFromBackup(backupPath) {
|
|
285
|
+
if (!fs.existsSync(backupPath)) {
|
|
286
|
+
throw new Error(`Backup file not found: ${backupPath}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const data = this._readFile(backupPath);
|
|
290
|
+
this.import(data);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Private Methods
|
|
295
|
+
*/
|
|
296
|
+
|
|
297
|
+
_readFile(filePath) {
|
|
298
|
+
try {
|
|
299
|
+
if (!fs.existsSync(filePath)) {
|
|
300
|
+
return {};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
304
|
+
return JSON.parse(content);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.error(`Error reading file ${filePath}:`, error);
|
|
307
|
+
return {};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
_writeFile(filePath, data) {
|
|
312
|
+
try {
|
|
313
|
+
const directory = path.dirname(filePath);
|
|
314
|
+
if (!fs.existsSync(directory)) {
|
|
315
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
319
|
+
} catch (error) {
|
|
320
|
+
console.error(`Error writing file ${filePath}:`, error);
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = CloudFrontStore;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Builds the environment variables for a CloudShell PTY session.
|
|
5
|
+
* Includes AWS credentials and endpoint URLs for all active services.
|
|
6
|
+
*
|
|
7
|
+
* @param {Function} getActiveServices - returns array of { name, port }
|
|
8
|
+
* @returns {Object} environment variables object
|
|
9
|
+
*/
|
|
10
|
+
function buildShellEnvironment(getActiveServices) {
|
|
11
|
+
const env = {
|
|
12
|
+
...process.env,
|
|
13
|
+
AWS_ACCESS_KEY_ID: 'local-access-key',
|
|
14
|
+
AWS_SECRET_ACCESS_KEY: 'local-secret-key',
|
|
15
|
+
AWS_DEFAULT_REGION: 'us-east-1',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const services = getActiveServices();
|
|
19
|
+
for (const { name, port } of services) {
|
|
20
|
+
const envKey = `AWS_ENDPOINT_${name.toUpperCase().replace(/-/g, '_')}`;
|
|
21
|
+
env[envKey] = `http://localhost:${port}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return env;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { buildShellEnvironment };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const express = require('express');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Initializes CloudShell: registers routes, sets up WebSocket server,
|
|
8
|
+
* and hooks into the server lifecycle.
|
|
9
|
+
*
|
|
10
|
+
* If node-pty is not available (e.g., native compilation failed),
|
|
11
|
+
* logs a warning and returns a no-op shutdown handle.
|
|
12
|
+
*
|
|
13
|
+
* @param {import('express').Application} app - Management API Express app
|
|
14
|
+
* @param {import('http').Server} httpServer - the underlying HTTP server
|
|
15
|
+
* @param {Function} getActiveServices - function to query active services
|
|
16
|
+
* @returns {{ shutdown: Function }} cleanup handle
|
|
17
|
+
*/
|
|
18
|
+
function initializeCloudShell(app, httpServer, getActiveServices) {
|
|
19
|
+
try {
|
|
20
|
+
// Attempt to load node-pty to verify it's available
|
|
21
|
+
require('node-pty');
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.warn('CloudShell: node-pty not available, feature disabled');
|
|
24
|
+
return { shutdown: () => {} };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { SessionManager } = require('./session-manager');
|
|
28
|
+
const { createWebSocketServer } = require('./websocket-handler');
|
|
29
|
+
const { registerRoutes } = require('./routes');
|
|
30
|
+
|
|
31
|
+
// Instantiate session manager
|
|
32
|
+
const sessionManager = new SessionManager();
|
|
33
|
+
|
|
34
|
+
// Register REST API routes
|
|
35
|
+
registerRoutes(app, sessionManager, getActiveServices);
|
|
36
|
+
|
|
37
|
+
// Set up WebSocket server
|
|
38
|
+
createWebSocketServer(httpServer, sessionManager);
|
|
39
|
+
|
|
40
|
+
// Serve static files (frontend) at /cloudshell
|
|
41
|
+
app.use('/cloudshell', express.static(path.join(__dirname, 'static')));
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
shutdown: () => sessionManager.destroyAll(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { initializeCloudShell };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { buildShellEnvironment } = require('./env-builder');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Registers CloudShell REST API routes on the Express app.
|
|
7
|
+
*
|
|
8
|
+
* @param {import('express').Application} app - the Management API Express app
|
|
9
|
+
* @param {import('./session-manager').SessionManager} sessionManager - session manager instance
|
|
10
|
+
* @param {Function} getActiveServices - function returning active services and ports
|
|
11
|
+
*/
|
|
12
|
+
function registerRoutes(app, sessionManager, getActiveServices) {
|
|
13
|
+
/**
|
|
14
|
+
* GET /__admin/cloudshell/sessions
|
|
15
|
+
* Returns all active sessions with IDs and creation timestamps.
|
|
16
|
+
*/
|
|
17
|
+
app.get('/__admin/cloudshell/sessions', (req, res) => {
|
|
18
|
+
console.log('CloudShell: GET /sessions received');
|
|
19
|
+
const sessions = sessionManager.listSessions();
|
|
20
|
+
res.json({ sessions });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* POST /__admin/cloudshell/sessions
|
|
25
|
+
* Creates a new CloudShell session with pre-configured environment.
|
|
26
|
+
* Returns 201 with { id, createdAt } on success.
|
|
27
|
+
* Returns 500 with { error } if PTY spawn fails.
|
|
28
|
+
*/
|
|
29
|
+
app.post('/__admin/cloudshell/sessions', (req, res) => {
|
|
30
|
+
console.log('CloudShell: POST /sessions received');
|
|
31
|
+
try {
|
|
32
|
+
const env = buildShellEnvironment(getActiveServices);
|
|
33
|
+
console.log('CloudShell: Environment built, creating session...');
|
|
34
|
+
const session = sessionManager.createSession(env);
|
|
35
|
+
console.log('CloudShell: Session created:', session.id);
|
|
36
|
+
res.status(201).json({ id: session.id, createdAt: session.createdAt });
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error('CloudShell: Failed to create session:', err.message);
|
|
39
|
+
res.status(500).json({ error: 'Failed to create session: ' + err.message });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* DELETE /__admin/cloudshell/sessions/:id
|
|
45
|
+
* Terminates a specific session.
|
|
46
|
+
* Returns 200 on success, 404 if session not found.
|
|
47
|
+
*/
|
|
48
|
+
app.delete('/__admin/cloudshell/sessions/:id', (req, res) => {
|
|
49
|
+
const { id } = req.params;
|
|
50
|
+
const destroyed = sessionManager.destroySession(id);
|
|
51
|
+
|
|
52
|
+
if (destroyed) {
|
|
53
|
+
res.status(200).json({});
|
|
54
|
+
} else {
|
|
55
|
+
res.status(404).json({ error: 'Session not found' });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { registerRoutes };
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const pty = require('node-pty');
|
|
4
|
+
const { v4: uuidv4 } = require('uuid');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns the default shell for the current platform.
|
|
8
|
+
* @returns {string} shell executable path or name
|
|
9
|
+
*/
|
|
10
|
+
function getDefaultShell() {
|
|
11
|
+
if (process.platform === 'win32') {
|
|
12
|
+
return 'powershell.exe';
|
|
13
|
+
}
|
|
14
|
+
return process.env.SHELL || '/bin/bash';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} Session
|
|
19
|
+
* @property {string} id - UUID v4 session identifier
|
|
20
|
+
* @property {import('node-pty').IPty} pty - node-pty process instance
|
|
21
|
+
* @property {import('ws').WebSocket|null} ws - currently attached WebSocket (null if disconnected)
|
|
22
|
+
* @property {string} createdAt - ISO timestamp of session creation
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Manages the lifecycle of CloudShell terminal sessions.
|
|
27
|
+
* Stores sessions in an in-memory Map<string, Session>.
|
|
28
|
+
*/
|
|
29
|
+
class SessionManager {
|
|
30
|
+
constructor() {
|
|
31
|
+
/** @type {Map<string, Session>} */
|
|
32
|
+
this.sessions = new Map();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Creates a new PTY session with pre-configured environment.
|
|
37
|
+
* @param {Object} env - environment variables to inject into the PTY process
|
|
38
|
+
* @returns {Session} the newly created session
|
|
39
|
+
*/
|
|
40
|
+
createSession(env) {
|
|
41
|
+
const shell = getDefaultShell();
|
|
42
|
+
const id = uuidv4();
|
|
43
|
+
|
|
44
|
+
let ptyProcess;
|
|
45
|
+
try {
|
|
46
|
+
ptyProcess = pty.spawn(shell, [], {
|
|
47
|
+
name: 'xterm-256color',
|
|
48
|
+
cols: 80,
|
|
49
|
+
rows: 24,
|
|
50
|
+
cwd: process.cwd(),
|
|
51
|
+
env: env,
|
|
52
|
+
});
|
|
53
|
+
} catch (err) {
|
|
54
|
+
throw new Error(`Failed to spawn PTY (${shell}): ${err.message}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const session = {
|
|
58
|
+
id,
|
|
59
|
+
pty: ptyProcess,
|
|
60
|
+
ws: null,
|
|
61
|
+
createdAt: new Date().toISOString(),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Handle unexpected PTY exit to clean up session
|
|
65
|
+
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
66
|
+
// Only clean up if session still exists (wasn't explicitly destroyed)
|
|
67
|
+
if (this.sessions.has(id)) {
|
|
68
|
+
const s = this.sessions.get(id);
|
|
69
|
+
if (s && s.ws) {
|
|
70
|
+
try {
|
|
71
|
+
s.ws.close(1011, 'PTY process exited');
|
|
72
|
+
} catch (e) { /* ignore */ }
|
|
73
|
+
}
|
|
74
|
+
this.sessions.delete(id);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
this.sessions.set(id, session);
|
|
79
|
+
return session;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Returns a session by ID, or undefined if not found.
|
|
84
|
+
* @param {string} id - session ID
|
|
85
|
+
* @returns {Session|undefined}
|
|
86
|
+
*/
|
|
87
|
+
getSession(id) {
|
|
88
|
+
return this.sessions.get(id);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Lists all active sessions.
|
|
93
|
+
* @returns {Array<{id: string, createdAt: string}>}
|
|
94
|
+
*/
|
|
95
|
+
listSessions() {
|
|
96
|
+
const result = [];
|
|
97
|
+
for (const session of this.sessions.values()) {
|
|
98
|
+
result.push({ id: session.id, createdAt: session.createdAt });
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Terminates a session: kills PTY, closes WebSocket, removes from map.
|
|
105
|
+
* @param {string} id - session ID to destroy
|
|
106
|
+
* @returns {boolean} true if session existed and was terminated
|
|
107
|
+
*/
|
|
108
|
+
destroySession(id) {
|
|
109
|
+
const session = this.sessions.get(id);
|
|
110
|
+
if (!session) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
session.pty.kill();
|
|
116
|
+
} catch (err) {
|
|
117
|
+
// PTY may already be dead; ignore
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (session.ws) {
|
|
121
|
+
try {
|
|
122
|
+
session.ws.close(1011, 'Session terminated');
|
|
123
|
+
} catch (err) {
|
|
124
|
+
// WebSocket may already be closed; ignore
|
|
125
|
+
}
|
|
126
|
+
session.ws = null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.sessions.delete(id);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Terminates all sessions. Called on server shutdown.
|
|
135
|
+
*/
|
|
136
|
+
destroyAll() {
|
|
137
|
+
for (const id of this.sessions.keys()) {
|
|
138
|
+
this.destroySession(id);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Attaches a WebSocket to an existing session and pipes PTY output to WS.
|
|
144
|
+
* @param {string} id - session ID
|
|
145
|
+
* @param {import('ws').WebSocket} ws - WebSocket connection to attach
|
|
146
|
+
* @returns {boolean} true if session exists and was attached
|
|
147
|
+
*/
|
|
148
|
+
attachWebSocket(id, ws) {
|
|
149
|
+
const session = this.sessions.get(id);
|
|
150
|
+
if (!session) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Detach any previously attached WebSocket
|
|
155
|
+
if (session.ws) {
|
|
156
|
+
try {
|
|
157
|
+
session.ws.close(1000, 'Replaced by new connection');
|
|
158
|
+
} catch (err) {
|
|
159
|
+
// ignore
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
session.ws = ws;
|
|
164
|
+
|
|
165
|
+
// Pipe PTY output to WebSocket
|
|
166
|
+
const dataListener = session.pty.onData((data) => {
|
|
167
|
+
if (session.ws && session.ws.readyState === 1) { // WebSocket.OPEN === 1
|
|
168
|
+
session.ws.send(data);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Clean up listener when WebSocket closes
|
|
173
|
+
ws.on('close', () => {
|
|
174
|
+
dataListener.dispose();
|
|
175
|
+
if (session.ws === ws) {
|
|
176
|
+
session.ws = null;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Detaches the WebSocket from a session (keeps PTY alive).
|
|
185
|
+
* @param {string} id - session ID
|
|
186
|
+
*/
|
|
187
|
+
detachWebSocket(id) {
|
|
188
|
+
const session = this.sessions.get(id);
|
|
189
|
+
if (!session || !session.ws) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
session.ws.close(1000, 'Detached');
|
|
195
|
+
} catch (err) {
|
|
196
|
+
// ignore
|
|
197
|
+
}
|
|
198
|
+
session.ws = null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Resizes the PTY for a given session. Ignores invalid (≤0) values.
|
|
203
|
+
* @param {string} id - session ID
|
|
204
|
+
* @param {number} cols - new column count
|
|
205
|
+
* @param {number} rows - new row count
|
|
206
|
+
*/
|
|
207
|
+
resizeSession(id, cols, rows) {
|
|
208
|
+
const session = this.sessions.get(id);
|
|
209
|
+
if (!session) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Ignore invalid dimensions
|
|
214
|
+
if (!cols || !rows || cols <= 0 || rows <= 0) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
session.pty.resize(cols, rows);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
// PTY may have exited; ignore resize errors
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = { SessionManager, getDefaultShell };
|
|
File without changes
|