@edgible-team/cli 1.0.1
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/LICENSE +136 -0
- package/README.md +450 -0
- package/dist/client/api-client.js +1057 -0
- package/dist/client/index.js +21 -0
- package/dist/commands/agent.js +1280 -0
- package/dist/commands/ai.js +608 -0
- package/dist/commands/application.js +885 -0
- package/dist/commands/auth.js +570 -0
- package/dist/commands/base/BaseCommand.js +93 -0
- package/dist/commands/base/CommandHandler.js +7 -0
- package/dist/commands/base/command-wrapper.js +58 -0
- package/dist/commands/base/middleware.js +77 -0
- package/dist/commands/config.js +116 -0
- package/dist/commands/connectivity.js +59 -0
- package/dist/commands/debug.js +98 -0
- package/dist/commands/discover.js +144 -0
- package/dist/commands/examples/migrated-command-example.js +180 -0
- package/dist/commands/gateway.js +494 -0
- package/dist/commands/managedGateway.js +787 -0
- package/dist/commands/utils/config-validator.js +76 -0
- package/dist/commands/utils/gateway-prompt.js +79 -0
- package/dist/commands/utils/input-parser.js +120 -0
- package/dist/commands/utils/output-formatter.js +109 -0
- package/dist/config/app-config.js +99 -0
- package/dist/detection/SystemCapabilityDetector.js +1244 -0
- package/dist/detection/ToolDetector.js +305 -0
- package/dist/detection/WorkloadDetector.js +314 -0
- package/dist/di/bindings.js +99 -0
- package/dist/di/container.js +88 -0
- package/dist/di/types.js +32 -0
- package/dist/index.js +52 -0
- package/dist/interfaces/IDaemonManager.js +3 -0
- package/dist/repositories/config-repository.js +62 -0
- package/dist/repositories/gateway-repository.js +35 -0
- package/dist/scripts/postinstall.js +101 -0
- package/dist/services/AgentStatusManager.js +299 -0
- package/dist/services/ConnectivityTester.js +271 -0
- package/dist/services/DependencyInstaller.js +475 -0
- package/dist/services/LocalAgentManager.js +2216 -0
- package/dist/services/application/ApplicationService.js +299 -0
- package/dist/services/auth/AuthService.js +214 -0
- package/dist/services/aws.js +644 -0
- package/dist/services/daemon/DaemonManagerFactory.js +65 -0
- package/dist/services/daemon/DockerDaemonManager.js +395 -0
- package/dist/services/daemon/LaunchdDaemonManager.js +257 -0
- package/dist/services/daemon/PodmanDaemonManager.js +369 -0
- package/dist/services/daemon/SystemdDaemonManager.js +221 -0
- package/dist/services/daemon/WindowsServiceDaemonManager.js +210 -0
- package/dist/services/daemon/index.js +16 -0
- package/dist/services/edgible.js +3060 -0
- package/dist/services/gateway/GatewayService.js +334 -0
- package/dist/state/config.js +146 -0
- package/dist/types/AgentConfig.js +5 -0
- package/dist/types/AgentStatus.js +5 -0
- package/dist/types/ApiClient.js +5 -0
- package/dist/types/ApiRequests.js +5 -0
- package/dist/types/ApiResponses.js +5 -0
- package/dist/types/Application.js +5 -0
- package/dist/types/CaddyJson.js +5 -0
- package/dist/types/UnifiedAgentStatus.js +56 -0
- package/dist/types/WireGuard.js +5 -0
- package/dist/types/Workload.js +5 -0
- package/dist/types/agent.js +5 -0
- package/dist/types/command-options.js +5 -0
- package/dist/types/connectivity.js +5 -0
- package/dist/types/errors.js +250 -0
- package/dist/types/gateway-types.js +5 -0
- package/dist/types/index.js +48 -0
- package/dist/types/models/ApplicationData.js +5 -0
- package/dist/types/models/CertificateData.js +5 -0
- package/dist/types/models/DeviceData.js +5 -0
- package/dist/types/models/DevicePoolData.js +5 -0
- package/dist/types/models/OrganizationData.js +5 -0
- package/dist/types/models/OrganizationInviteData.js +5 -0
- package/dist/types/models/ProviderConfiguration.js +5 -0
- package/dist/types/models/ResourceData.js +5 -0
- package/dist/types/models/ServiceResourceData.js +5 -0
- package/dist/types/models/UserData.js +5 -0
- package/dist/types/route.js +5 -0
- package/dist/types/validation/schemas.js +218 -0
- package/dist/types/validation.js +5 -0
- package/dist/utils/FileIntegrityManager.js +256 -0
- package/dist/utils/PathMigration.js +219 -0
- package/dist/utils/PathResolver.js +235 -0
- package/dist/utils/PlatformDetector.js +277 -0
- package/dist/utils/console-logger.js +130 -0
- package/dist/utils/docker-compose-parser.js +179 -0
- package/dist/utils/errors.js +130 -0
- package/dist/utils/health-checker.js +155 -0
- package/dist/utils/json-logger.js +72 -0
- package/dist/utils/log-formatter.js +293 -0
- package/dist/utils/logger.js +59 -0
- package/dist/utils/network-utils.js +217 -0
- package/dist/utils/output.js +182 -0
- package/dist/utils/passwordValidation.js +91 -0
- package/dist/utils/progress.js +167 -0
- package/dist/utils/sudo-checker.js +22 -0
- package/dist/utils/urls.js +32 -0
- package/dist/utils/validation.js +31 -0
- package/dist/validation/schemas.js +175 -0
- package/dist/validation/validator.js +67 -0
- package/package.json +83 -0
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.AWSService = void 0;
|
|
40
|
+
const client_ec2_1 = require("@aws-sdk/client-ec2");
|
|
41
|
+
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
42
|
+
const client_ssm_1 = require("@aws-sdk/client-ssm");
|
|
43
|
+
const credential_provider_node_1 = require("@aws-sdk/credential-provider-node");
|
|
44
|
+
const ssh2_1 = require("ssh2");
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const os = __importStar(require("os"));
|
|
48
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
49
|
+
class AWSService {
|
|
50
|
+
constructor(profile, region = 'us-east-1') {
|
|
51
|
+
this.profile = profile || 'default';
|
|
52
|
+
this.region = region;
|
|
53
|
+
const credentials = (0, credential_provider_node_1.defaultProvider)({
|
|
54
|
+
profile: this.profile
|
|
55
|
+
});
|
|
56
|
+
this.ec2Client = new client_ec2_1.EC2Client({
|
|
57
|
+
region: this.region,
|
|
58
|
+
credentials
|
|
59
|
+
});
|
|
60
|
+
this.s3Client = new client_s3_1.S3Client({
|
|
61
|
+
region: this.region,
|
|
62
|
+
credentials
|
|
63
|
+
});
|
|
64
|
+
this.ssmClient = new client_ssm_1.SSMClient({
|
|
65
|
+
region: this.region,
|
|
66
|
+
credentials
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Check if AWS CLI is available and get available profiles
|
|
71
|
+
*/
|
|
72
|
+
async checkAWSCLI() {
|
|
73
|
+
try {
|
|
74
|
+
const { execSync } = require('child_process');
|
|
75
|
+
const output = execSync('aws configure list-profiles', { encoding: 'utf8', timeout: 5000 });
|
|
76
|
+
const profiles = output.trim().split('\n').filter((p) => p.trim());
|
|
77
|
+
return { available: true, profiles };
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
return { available: false, profiles: [] };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Validate AWS credentials for a profile
|
|
85
|
+
*/
|
|
86
|
+
async validateCredentials(profile) {
|
|
87
|
+
try {
|
|
88
|
+
const testClient = new client_ec2_1.EC2Client({
|
|
89
|
+
region: this.region,
|
|
90
|
+
credentials: (0, credential_provider_node_1.defaultProvider)({
|
|
91
|
+
profile: profile || this.profile
|
|
92
|
+
})
|
|
93
|
+
});
|
|
94
|
+
await testClient.send(new client_ec2_1.DescribeInstancesCommand({ MaxResults: 5 }));
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.error(chalk_1.default.red('AWS credentials validation failed:'), error);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Create a key pair for EC2 instance
|
|
104
|
+
*/
|
|
105
|
+
async createKeyPair(keyName) {
|
|
106
|
+
try {
|
|
107
|
+
const command = new client_ec2_1.CreateKeyPairCommand({
|
|
108
|
+
KeyName: keyName,
|
|
109
|
+
TagSpecifications: [{
|
|
110
|
+
ResourceType: 'key-pair',
|
|
111
|
+
Tags: [{
|
|
112
|
+
Key: 'Purpose',
|
|
113
|
+
Value: 'Edgible-Gateway'
|
|
114
|
+
}]
|
|
115
|
+
}]
|
|
116
|
+
});
|
|
117
|
+
const response = await this.ec2Client.send(command);
|
|
118
|
+
if (!response.KeyMaterial) {
|
|
119
|
+
throw new Error('Key material not returned from AWS');
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
keyName: response.KeyName,
|
|
123
|
+
keyFingerprint: response.KeyFingerprint,
|
|
124
|
+
privateKey: response.KeyMaterial,
|
|
125
|
+
publicKey: '' // Will be generated from private key if needed
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.error(chalk_1.default.red('Error creating key pair:'), error);
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Check if key pair exists
|
|
135
|
+
*/
|
|
136
|
+
async keyPairExists(keyName) {
|
|
137
|
+
try {
|
|
138
|
+
const command = new client_ec2_1.DescribeKeyPairsCommand({
|
|
139
|
+
KeyNames: [keyName]
|
|
140
|
+
});
|
|
141
|
+
await this.ec2Client.send(command);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Create EC2 instance for gateway
|
|
150
|
+
*/
|
|
151
|
+
async createEC2Instance(config) {
|
|
152
|
+
try {
|
|
153
|
+
const userData = config.userData || this.generateUserData();
|
|
154
|
+
// Get or create security group with SSH access
|
|
155
|
+
const securityGroupId = await this.getOrCreateSSHSecurityGroup();
|
|
156
|
+
const command = new client_ec2_1.RunInstancesCommand({
|
|
157
|
+
ImageId: 'ami-0c462b53550d4fca8', // Amazon Linux 2 AMI for ap-southeast-2
|
|
158
|
+
MinCount: 1,
|
|
159
|
+
MaxCount: 1,
|
|
160
|
+
InstanceType: (config.instanceType || 't3.micro'),
|
|
161
|
+
KeyName: config.keyPairName,
|
|
162
|
+
SecurityGroupIds: config.securityGroupIds || [securityGroupId],
|
|
163
|
+
SubnetId: config.subnetId,
|
|
164
|
+
UserData: Buffer.from(userData).toString('base64'),
|
|
165
|
+
TagSpecifications: [{
|
|
166
|
+
ResourceType: 'instance',
|
|
167
|
+
Tags: [{
|
|
168
|
+
Key: 'Name',
|
|
169
|
+
Value: config.name
|
|
170
|
+
}, {
|
|
171
|
+
Key: 'Purpose',
|
|
172
|
+
Value: 'Edgible-Gateway'
|
|
173
|
+
}]
|
|
174
|
+
}]
|
|
175
|
+
});
|
|
176
|
+
const response = await this.ec2Client.send(command);
|
|
177
|
+
const instance = response.Instances?.[0];
|
|
178
|
+
if (!instance?.InstanceId) {
|
|
179
|
+
throw new Error('Failed to create EC2 instance');
|
|
180
|
+
}
|
|
181
|
+
// Wait for instance to be running
|
|
182
|
+
await this.waitForInstanceRunning(instance.InstanceId);
|
|
183
|
+
// Get instance details
|
|
184
|
+
const instanceDetails = await this.getInstanceDetails(instance.InstanceId);
|
|
185
|
+
return {
|
|
186
|
+
instanceId: instance.InstanceId,
|
|
187
|
+
publicIp: instanceDetails.publicIp,
|
|
188
|
+
privateIp: instanceDetails.privateIp,
|
|
189
|
+
state: instanceDetails.state,
|
|
190
|
+
keyPairName: config.keyPairName,
|
|
191
|
+
region: this.region,
|
|
192
|
+
launchTime: instance.LaunchTime || new Date()
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
console.error(chalk_1.default.red('Error creating EC2 instance:'), error);
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Get instance details
|
|
202
|
+
*/
|
|
203
|
+
async getInstanceDetails(instanceId) {
|
|
204
|
+
const command = new client_ec2_1.DescribeInstancesCommand({
|
|
205
|
+
InstanceIds: [instanceId]
|
|
206
|
+
});
|
|
207
|
+
const response = await this.ec2Client.send(command);
|
|
208
|
+
const instance = response.Reservations?.[0]?.Instances?.[0];
|
|
209
|
+
if (!instance) {
|
|
210
|
+
throw new Error('Instance not found');
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
publicIp: instance.PublicIpAddress || '',
|
|
214
|
+
privateIp: instance.PrivateIpAddress || '',
|
|
215
|
+
state: instance.State?.Name || 'unknown'
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Wait for instance to be running
|
|
220
|
+
*/
|
|
221
|
+
async waitForInstanceRunning(instanceId, maxWaitTime = 300000) {
|
|
222
|
+
const startTime = Date.now();
|
|
223
|
+
while (Date.now() - startTime < maxWaitTime) {
|
|
224
|
+
try {
|
|
225
|
+
const details = await this.getInstanceDetails(instanceId);
|
|
226
|
+
if (details.state === 'running') {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10 seconds
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
throw new Error('Instance did not start within expected time');
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Terminate EC2 instance
|
|
239
|
+
*/
|
|
240
|
+
async terminateInstance(instanceId) {
|
|
241
|
+
try {
|
|
242
|
+
const command = new client_ec2_1.TerminateInstancesCommand({
|
|
243
|
+
InstanceIds: [instanceId]
|
|
244
|
+
});
|
|
245
|
+
await this.ec2Client.send(command);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
console.error(chalk_1.default.red('Error terminating instance:'), error);
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Check if instance is ready for SSH connections
|
|
254
|
+
*/
|
|
255
|
+
async waitForSSHReady(instanceId, maxWaitTime = 300) {
|
|
256
|
+
console.log(chalk_1.default.gray('Waiting for instance to be ready for SSH...'));
|
|
257
|
+
const startTime = Date.now();
|
|
258
|
+
while (Date.now() - startTime < maxWaitTime * 1000) {
|
|
259
|
+
try {
|
|
260
|
+
const instanceDetails = await this.getInstanceDetails(instanceId);
|
|
261
|
+
if (instanceDetails.state === 'running' && instanceDetails.publicIp) {
|
|
262
|
+
console.log(chalk_1.default.green(`✓ Instance ready: ${instanceDetails.publicIp}`));
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
// Continue waiting
|
|
268
|
+
}
|
|
269
|
+
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Create or get security group with SSH access
|
|
275
|
+
*/
|
|
276
|
+
async getOrCreateSSHSecurityGroup() {
|
|
277
|
+
const groupName = 'edgible-gateway-ssh';
|
|
278
|
+
try {
|
|
279
|
+
// Check if security group already exists
|
|
280
|
+
const describeCommand = new client_ec2_1.DescribeSecurityGroupsCommand({
|
|
281
|
+
GroupNames: [groupName]
|
|
282
|
+
});
|
|
283
|
+
const describeResponse = await this.ec2Client.send(describeCommand);
|
|
284
|
+
if (describeResponse.SecurityGroups && describeResponse.SecurityGroups.length > 0) {
|
|
285
|
+
const groupId = describeResponse.SecurityGroups[0].GroupId;
|
|
286
|
+
console.log(chalk_1.default.gray(`Using existing security group: ${groupId}`));
|
|
287
|
+
return groupId;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
// Security group doesn't exist, create it
|
|
292
|
+
}
|
|
293
|
+
// Create security group
|
|
294
|
+
console.log(chalk_1.default.gray('Creating security group with SSH access...'));
|
|
295
|
+
const createCommand = new client_ec2_1.CreateSecurityGroupCommand({
|
|
296
|
+
GroupName: groupName,
|
|
297
|
+
Description: 'Security group for Edgible Gateway with SSH access'
|
|
298
|
+
});
|
|
299
|
+
const createResponse = await this.ec2Client.send(createCommand);
|
|
300
|
+
const groupId = createResponse.GroupId;
|
|
301
|
+
if (!groupId) {
|
|
302
|
+
throw new Error('Failed to create security group');
|
|
303
|
+
}
|
|
304
|
+
// Add SSH rule
|
|
305
|
+
const authorizeCommand = new client_ec2_1.AuthorizeSecurityGroupIngressCommand({
|
|
306
|
+
GroupId: groupId,
|
|
307
|
+
IpPermissions: [{
|
|
308
|
+
IpProtocol: 'tcp',
|
|
309
|
+
FromPort: 22,
|
|
310
|
+
ToPort: 22,
|
|
311
|
+
IpRanges: [{ CidrIp: '0.0.0.0/0' }]
|
|
312
|
+
}]
|
|
313
|
+
});
|
|
314
|
+
await this.ec2Client.send(authorizeCommand);
|
|
315
|
+
console.log(chalk_1.default.green(`✓ Security group created: ${groupId}`));
|
|
316
|
+
return groupId;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Delete key pair
|
|
320
|
+
*/
|
|
321
|
+
async deleteKeyPair(keyName) {
|
|
322
|
+
try {
|
|
323
|
+
const command = new client_ec2_1.DeleteKeyPairCommand({
|
|
324
|
+
KeyName: keyName
|
|
325
|
+
});
|
|
326
|
+
await this.ec2Client.send(command);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
console.error(chalk_1.default.red('Error deleting key pair:'), error);
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Execute SSH command on instance
|
|
335
|
+
*/
|
|
336
|
+
async executeSSHCommand(connection, command, timeout = 600000) {
|
|
337
|
+
return new Promise((resolve, reject) => {
|
|
338
|
+
const conn = new ssh2_1.Client();
|
|
339
|
+
let commandExecuted = false;
|
|
340
|
+
// Set overall timeout for command execution (default 10 minutes)
|
|
341
|
+
const timeoutId = setTimeout(() => {
|
|
342
|
+
if (!commandExecuted) {
|
|
343
|
+
conn.end();
|
|
344
|
+
reject(new Error(`Command execution timeout after ${timeout / 1000} seconds`));
|
|
345
|
+
}
|
|
346
|
+
}, timeout);
|
|
347
|
+
conn.on('ready', () => {
|
|
348
|
+
conn.exec(command, (err, stream) => {
|
|
349
|
+
if (err) {
|
|
350
|
+
clearTimeout(timeoutId);
|
|
351
|
+
conn.end();
|
|
352
|
+
reject(err);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
let stdout = '';
|
|
356
|
+
let stderr = '';
|
|
357
|
+
stream.on('close', (code) => {
|
|
358
|
+
commandExecuted = true;
|
|
359
|
+
clearTimeout(timeoutId);
|
|
360
|
+
conn.end();
|
|
361
|
+
resolve({ stdout, stderr, exitCode: code });
|
|
362
|
+
});
|
|
363
|
+
stream.on('data', (data) => {
|
|
364
|
+
stdout += data.toString();
|
|
365
|
+
});
|
|
366
|
+
stream.stderr.on('data', (data) => {
|
|
367
|
+
stderr += data.toString();
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
conn.on('error', (err) => {
|
|
372
|
+
clearTimeout(timeoutId);
|
|
373
|
+
reject(err);
|
|
374
|
+
});
|
|
375
|
+
conn.connect({
|
|
376
|
+
host: connection.host,
|
|
377
|
+
port: connection.port,
|
|
378
|
+
username: connection.username,
|
|
379
|
+
privateKey: connection.privateKey,
|
|
380
|
+
readyTimeout: 30000, // 30 seconds
|
|
381
|
+
keepaliveInterval: 10000, // 10 seconds
|
|
382
|
+
keepaliveCountMax: 3
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Upload file via SSH
|
|
388
|
+
*/
|
|
389
|
+
async uploadFile(connection, localPath, remotePath) {
|
|
390
|
+
return new Promise((resolve, reject) => {
|
|
391
|
+
// Verify local file exists before attempting upload
|
|
392
|
+
if (!require('fs').existsSync(localPath)) {
|
|
393
|
+
reject(new Error(`Local file does not exist: ${localPath}`));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const fs = require('fs');
|
|
397
|
+
const stats = fs.statSync(localPath);
|
|
398
|
+
const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(2);
|
|
399
|
+
const conn = new ssh2_1.Client();
|
|
400
|
+
let sftpConnected = false;
|
|
401
|
+
let uploadStarted = false;
|
|
402
|
+
conn.on('ready', () => {
|
|
403
|
+
conn.sftp((err, sftp) => {
|
|
404
|
+
if (err) {
|
|
405
|
+
conn.end();
|
|
406
|
+
reject(new Error(`SFTP connection failed: ${err.message || String(err)}`));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
sftpConnected = true;
|
|
410
|
+
// Check if remote directory exists and is writable
|
|
411
|
+
const remoteDir = require('path').dirname(remotePath);
|
|
412
|
+
sftp.stat(remoteDir, (statErr) => {
|
|
413
|
+
if (statErr) {
|
|
414
|
+
conn.end();
|
|
415
|
+
reject(new Error(`Remote directory does not exist or is not accessible: ${remoteDir} (Error: ${statErr.message || String(statErr)}, Code: ${statErr.code || 'unknown'})`));
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
uploadStarted = true;
|
|
419
|
+
// Attempt the upload with detailed error handling
|
|
420
|
+
sftp.fastPut(localPath, remotePath, (uploadErr) => {
|
|
421
|
+
conn.end();
|
|
422
|
+
if (uploadErr) {
|
|
423
|
+
const errorMessage = `SFTP upload failed: ${uploadErr.message || String(uploadErr)}`;
|
|
424
|
+
const errorCode = uploadErr.code ? ` (Code: ${uploadErr.code})` : '';
|
|
425
|
+
const contextInfo = `\n Local: ${localPath} (${fileSizeMB} MB)\n Remote: ${remotePath}\n User: ${connection.username}\n Host: ${connection.host}`;
|
|
426
|
+
reject(new Error(`${errorMessage}${errorCode}${contextInfo}`));
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
resolve();
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
conn.on('error', (err) => {
|
|
436
|
+
reject(new Error(`SSH connection error: ${err.message || String(err)}`));
|
|
437
|
+
});
|
|
438
|
+
// Set a timeout for the entire operation
|
|
439
|
+
const timeout = setTimeout(() => {
|
|
440
|
+
if (!sftpConnected) {
|
|
441
|
+
conn.end();
|
|
442
|
+
reject(new Error(`SFTP connection timeout after 30 seconds. Upload may be too large (${fileSizeMB} MB)`));
|
|
443
|
+
}
|
|
444
|
+
else if (!uploadStarted) {
|
|
445
|
+
conn.end();
|
|
446
|
+
reject(new Error(`SFTP upload start timeout. Remote directory may be inaccessible: ${require('path').dirname(remotePath)}`));
|
|
447
|
+
}
|
|
448
|
+
}, 60000); // 60 second timeout
|
|
449
|
+
conn.on('close', () => {
|
|
450
|
+
clearTimeout(timeout);
|
|
451
|
+
});
|
|
452
|
+
conn.connect({
|
|
453
|
+
host: connection.host,
|
|
454
|
+
port: connection.port,
|
|
455
|
+
username: connection.username,
|
|
456
|
+
privateKey: connection.privateKey,
|
|
457
|
+
readyTimeout: 30000, // 30 seconds
|
|
458
|
+
keepaliveInterval: 10000, // 10 seconds
|
|
459
|
+
keepaliveCountMax: 3
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Generate user data script for EC2 instance
|
|
465
|
+
*/
|
|
466
|
+
generateUserData() {
|
|
467
|
+
return `#!/bin/bash
|
|
468
|
+
# Update system
|
|
469
|
+
yum update -y
|
|
470
|
+
|
|
471
|
+
# Install Node.js
|
|
472
|
+
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
|
|
473
|
+
export NVM_DIR="$HOME/.nvm"
|
|
474
|
+
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
|
475
|
+
nvm install 18
|
|
476
|
+
nvm use 18
|
|
477
|
+
|
|
478
|
+
# Create system-wide symlink for Node.js (so root can access it)
|
|
479
|
+
NODE_VERSION=$(nvm list | grep -oE 'v18\.[0-9]+\.[0-9]+' | head -1)
|
|
480
|
+
if [ -n "$NODE_VERSION" ]; then
|
|
481
|
+
sudo ln -sf "/home/ec2-user/.nvm/versions/node/$NODE_VERSION/bin/node" /usr/local/bin/node
|
|
482
|
+
sudo ln -sf "/home/ec2-user/.nvm/versions/node/$NODE_VERSION/bin/npm" /usr/local/bin/npm
|
|
483
|
+
fi
|
|
484
|
+
|
|
485
|
+
# Install PM2 for process management (install globally for root as well)
|
|
486
|
+
npm install -g pm2
|
|
487
|
+
sudo npm install -g pm2 || true
|
|
488
|
+
|
|
489
|
+
# Create directory for agent (owned by root)
|
|
490
|
+
sudo mkdir -p /opt/edgible-agent
|
|
491
|
+
cd /opt/edgible-agent
|
|
492
|
+
sudo chown root:root /opt/edgible-agent
|
|
493
|
+
|
|
494
|
+
# Note: Agent code will be uploaded by CLI
|
|
495
|
+
echo "Agent directory created. Waiting for agent code upload..."
|
|
496
|
+
|
|
497
|
+
# Set up systemd service for agent (running as root)
|
|
498
|
+
cat > /tmp/edgible-agent.service << 'EOF'
|
|
499
|
+
[Unit]
|
|
500
|
+
Description=Edgible Agent
|
|
501
|
+
After=network.target
|
|
502
|
+
|
|
503
|
+
[Service]
|
|
504
|
+
Type=simple
|
|
505
|
+
User=root
|
|
506
|
+
WorkingDirectory=/opt/edgible-agent
|
|
507
|
+
ExecStart=/usr/local/bin/node /opt/edgible-agent/index.js start -c /opt/edgible-agent/agent.config.json
|
|
508
|
+
Restart=always
|
|
509
|
+
RestartSec=10
|
|
510
|
+
Environment=NODE_ENV=production
|
|
511
|
+
Environment=EDGIBLE_CONFIG_PATH=/opt/edgible-agent/.edgible/agent
|
|
512
|
+
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
513
|
+
|
|
514
|
+
[Install]
|
|
515
|
+
WantedBy=multi-user.target
|
|
516
|
+
EOF
|
|
517
|
+
sudo mv /tmp/edgible-agent.service /etc/systemd/system/edgible-agent.service
|
|
518
|
+
|
|
519
|
+
# Enable service (but don't start until agent is uploaded)
|
|
520
|
+
systemctl enable edgible-agent.service
|
|
521
|
+
|
|
522
|
+
echo "Edgible Agent setup complete"
|
|
523
|
+
`;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Save SSH key to file
|
|
527
|
+
*/
|
|
528
|
+
saveSSHKey(keyPair, keyName) {
|
|
529
|
+
const sshDir = path.join(os.homedir(), '.ssh');
|
|
530
|
+
const keyPath = path.join(sshDir, `${keyName}.pem`);
|
|
531
|
+
// Ensure .ssh directory exists
|
|
532
|
+
if (!fs.existsSync(sshDir)) {
|
|
533
|
+
fs.mkdirSync(sshDir, { mode: 0o700 });
|
|
534
|
+
}
|
|
535
|
+
// Save private key
|
|
536
|
+
fs.writeFileSync(keyPath, keyPair.privateKey, { mode: 0o600 });
|
|
537
|
+
return keyPath;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Load SSH key from file
|
|
541
|
+
*/
|
|
542
|
+
loadSSHKey(keyPath) {
|
|
543
|
+
if (!fs.existsSync(keyPath)) {
|
|
544
|
+
throw new Error(`SSH key file not found: ${keyPath}`);
|
|
545
|
+
}
|
|
546
|
+
return fs.readFileSync(keyPath, 'utf8');
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Download agent from S3 bucket
|
|
550
|
+
* @param bucketName S3 bucket name
|
|
551
|
+
* @param keyPath S3 object key (path within bucket)
|
|
552
|
+
* @param destinationPath Local file path to save the downloaded file
|
|
553
|
+
* @param useCloudFront Whether to use CloudFront URL instead of direct S3
|
|
554
|
+
* @param cloudFrontUrl CloudFront distribution URL (e.g., https://distribution.edgible.com)
|
|
555
|
+
*/
|
|
556
|
+
async downloadAgentFromS3(bucketName, keyPath, destinationPath, useCloudFront = false, cloudFrontUrl) {
|
|
557
|
+
try {
|
|
558
|
+
if (useCloudFront && cloudFrontUrl) {
|
|
559
|
+
// Use CloudFront URL for download (public access)
|
|
560
|
+
const url = `${cloudFrontUrl}/${keyPath}`;
|
|
561
|
+
console.log(chalk_1.default.gray(`Downloading from CloudFront: ${url}`));
|
|
562
|
+
const response = await fetch(url);
|
|
563
|
+
if (!response.ok) {
|
|
564
|
+
// Provide specific error messages for common issues
|
|
565
|
+
let errorMessage = `Failed to download from CloudFront: ${response.status} ${response.statusText}`;
|
|
566
|
+
if (response.status === 404) {
|
|
567
|
+
errorMessage = `Agent version not found at ${url}\n\n` +
|
|
568
|
+
`The agent may not have been deployed to S3 yet.\n` +
|
|
569
|
+
`To deploy the agent:\n` +
|
|
570
|
+
` 1. Navigate to agent-v2 directory\n` +
|
|
571
|
+
` 2. Run: npm run build\n` +
|
|
572
|
+
` 3. Run: ./scripts/deploy-to-s3.sh --env production\n\n` +
|
|
573
|
+
`Expected S3 path: s3://${bucketName}/${keyPath}\n` +
|
|
574
|
+
`Current environment: ${keyPath.split('/')[0] || 'unknown'}`;
|
|
575
|
+
}
|
|
576
|
+
else if (response.status === 403) {
|
|
577
|
+
errorMessage = `Access denied to ${url}\n\n` +
|
|
578
|
+
`This usually means one of:\n` +
|
|
579
|
+
` 1. Agent not deployed: The file doesn't exist at s3://${bucketName}/${keyPath}\n` +
|
|
580
|
+
` → Deploy agent: cd agent-v2 && ./scripts/deploy-to-s3.sh --env production\n\n` +
|
|
581
|
+
` 2. CloudFront misconfiguration: OAC or bucket policy issue\n` +
|
|
582
|
+
` → Check backend Pulumi outputs for distribution status\n\n` +
|
|
583
|
+
` 3. Wrong bucket: Using bucket "${bucketName}" but file may be in different bucket\n` +
|
|
584
|
+
` → Check Pulumi output: AgentDistributionBucket\n\n` +
|
|
585
|
+
`To verify file exists:\n` +
|
|
586
|
+
` aws s3 ls s3://${bucketName}/${keyPath} --profile edgible`;
|
|
587
|
+
}
|
|
588
|
+
throw new Error(errorMessage);
|
|
589
|
+
}
|
|
590
|
+
const buffer = await response.arrayBuffer();
|
|
591
|
+
fs.writeFileSync(destinationPath, Buffer.from(buffer));
|
|
592
|
+
console.log(chalk_1.default.green(`✓ Downloaded to ${destinationPath}`));
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
// Use S3 SDK for direct download
|
|
596
|
+
console.log(chalk_1.default.gray(`Downloading from S3: s3://${bucketName}/${keyPath}`));
|
|
597
|
+
const command = new client_s3_1.GetObjectCommand({
|
|
598
|
+
Bucket: bucketName,
|
|
599
|
+
Key: keyPath
|
|
600
|
+
});
|
|
601
|
+
const response = await this.s3Client.send(command);
|
|
602
|
+
if (!response.Body) {
|
|
603
|
+
throw new Error('No data returned from S3');
|
|
604
|
+
}
|
|
605
|
+
// Ensure destination directory exists
|
|
606
|
+
const destDir = path.dirname(destinationPath);
|
|
607
|
+
if (!fs.existsSync(destDir)) {
|
|
608
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
609
|
+
}
|
|
610
|
+
// Convert stream to buffer and write to file
|
|
611
|
+
const chunks = [];
|
|
612
|
+
for await (const chunk of response.Body) {
|
|
613
|
+
chunks.push(chunk);
|
|
614
|
+
}
|
|
615
|
+
const buffer = Buffer.concat(chunks);
|
|
616
|
+
fs.writeFileSync(destinationPath, buffer);
|
|
617
|
+
console.log(chalk_1.default.green(`✓ Downloaded to ${destinationPath}`));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
console.error(chalk_1.default.red('Error downloading from S3:'), error);
|
|
622
|
+
throw error;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Get S3 download URL (for use in SSH commands on remote server)
|
|
627
|
+
* @param bucketName S3 bucket name
|
|
628
|
+
* @param keyPath S3 object key
|
|
629
|
+
* @param useCloudFront Whether to use CloudFront URL
|
|
630
|
+
* @param cloudFrontUrl CloudFront distribution URL
|
|
631
|
+
* @returns URL string for downloading
|
|
632
|
+
*/
|
|
633
|
+
getS3DownloadUrl(bucketName, keyPath, useCloudFront = false, cloudFrontUrl) {
|
|
634
|
+
if (useCloudFront && cloudFrontUrl) {
|
|
635
|
+
return `${cloudFrontUrl}/${keyPath}`;
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
// Return S3 public URL (requires bucket to be public or presigned URL)
|
|
639
|
+
return `https://${bucketName}.s3.${this.region}.amazonaws.com/${keyPath}`;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
exports.AWSService = AWSService;
|
|
644
|
+
//# sourceMappingURL=aws.js.map
|