EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
digitalOceanService.js
Go to the documentation of this file.
1/**
2 * DigitalOcean API Service
3 * Handles API interactions with DigitalOcean App Platform, Droplets, and Databases
4 * Multi-tenant aware - each instance is bound to a specific tenant
5 */
6
7const axios = require('axios');
8const pool = require('../db');
9
10const DO_API_BASE = 'https://api.digitalocean.com/v2';
11
12/**
13 * DigitalOcean API service.
14 * Multi-tenant aware service for interacting with DigitalOcean App Platform, Droplets, and Databases.
15 */
16class DigitalOceanService {
17 /**
18 * Initialize DigitalOcean service for specific tenant.
19 * @param {number} tenantId - Tenant ID
20 */
21 constructor(tenantId) {
22 this.tenantId = tenantId;
23 this.config = null;
24 this.apiToken = null;
25 }
26
27 /**
28 * Load DigitalOcean configuration for the tenant.
29 * @returns {Promise<object>} Configuration object with API token
30 */
31 async loadConfig() {
32 // First, try to use environment variable (for root tenant or global config)
33 if (process.env.DO_API_TOKEN) {
34 console.log('[DO] Using DO_API_TOKEN from environment');
35 this.apiToken = process.env.DO_API_TOKEN;
36 return { api_token_encrypted: this.apiToken };
37 }
38
39 // Otherwise, load from database
40 const result = await pool.query(
41 'SELECT * FROM digitalocean_config WHERE tenant_id = $1 AND is_active = true',
42 [this.tenantId]
43 );
44
45 if (result.rows.length === 0) {
46 throw new Error('DigitalOcean configuration not found for this tenant. Please configure DO API token first.');
47 }
48
49 this.config = result.rows[0];
50 // TODO: Decrypt api_token_encrypted before using
51 this.apiToken = this.config.api_token_encrypted; // Implement decryption later
52 return this.config;
53 }
54
55 /**
56 * Make authenticated API request to DigitalOcean
57 * @param {string} method - HTTP method (GET, POST, PATCH, DELETE)
58 * @param {string} endpoint - API endpoint (e.g., '/apps')
59 * @param {object} [data] - Request body data (for POST/PATCH)
60 * @param {object} [params] - Query parameters
61 * @returns {Promise<object>} API response data
62 */
63 async apiRequest(method, endpoint, data = null, params = null) {
64 if (!this.apiToken) {
65 await this.loadConfig();
66 }
67
68 try {
69 const config = {
70 method,
71 url: `${DO_API_BASE}${endpoint}`,
72 headers: {
73 'Authorization': `Bearer ${this.apiToken}`,
74 'Content-Type': 'application/json'
75 }
76 };
77
78 if (data) {
79 config.data = data;
80 }
81
82 if (params) {
83 config.params = params;
84 }
85
86 console.log(`[DO] ${method} ${endpoint}`);
87 const response = await axios(config);
88 return response.data;
89 } catch (error) {
90 console.error(`[DO] API request failed:`, error.response?.data || error.message);
91 throw new Error(`DigitalOcean API error: ${error.response?.data?.message || error.message}`);
92 }
93 }
94
95 /**
96 * Test API connection.
97 * @returns {Promise<boolean>} True if connection successful
98 */
99 async testConnection() {
100 try {
101 const account = await this.apiRequest('GET', '/account');
102 return {
103 success: true,
104 account: {
105 email: account.account.email,
106 status: account.account.status,
107 uuid: account.account.uuid
108 }
109 };
110 } catch (error) {
111 return {
112 success: false,
113 error: error.message
114 };
115 }
116 }
117
118 // ===========================
119 // App Platform API Methods
120 // ===========================
121
122 /**
123 * List all apps in the account
124 * @returns {Promise<Array>} List of app objects
125 */
126 async listApps() {
127 const data = await this.apiRequest('GET', '/apps');
128 return data.apps || [];
129 }
130
131 /**
132 * Get specific app details
133 * @param {string} appId - DigitalOcean App ID
134 * @returns {Promise<object>} App object
135 */
136 async getApp(appId) {
137 const data = await this.apiRequest('GET', `/apps/${appId}`);
138 return data.app;
139 }
140
141 /**
142 * Get app deployments
143 * @param {string} appId - DigitalOcean App ID
144 * @returns {Promise<Array>} List of deployment objects
145 */
146 async getAppDeployments(appId) {
147 const data = await this.apiRequest('GET', `/apps/${appId}/deployments`);
148 return data.deployments || [];
149 }
150
151 /**
152 * Trigger a new deployment
153 * @param {string} appId - DigitalOcean App ID
154 * @param {boolean} [forceRebuild] - Force rebuild from source
155 * @returns {Promise<object>} Deployment object
156 */
157 async createDeployment(appId, forceRebuild = false) {
158 const data = await this.apiRequest('POST', `/apps/${appId}/deployments`, {
159 force_build: forceRebuild
160 });
161 return data.deployment;
162 }
163
164 /**
165 * Update an app's configuration
166 * @param {string} appId - DigitalOcean App ID
167 * @param {object} appSpec - Updated app specification
168 * @returns {Promise<object>} Updated app object
169 */
170 async updateApp(appId, appSpec) {
171 const data = await this.apiRequest('PUT', `/apps/${appId}`, {
172 spec: appSpec
173 });
174 return data.app;
175 }
176
177 /**
178 * Get app logs
179 * @param {string} appId - DigitalOcean App ID
180 * @param {string} deploymentId - Deployment ID
181 * @param {string} componentName - Component name
182 * @param {string} [type] - Log type ('build', 'deploy', 'run')
183 * @returns {Promise<object>} Log data
184 */
185 async getAppLogs(appId, deploymentId, componentName, type = 'run') {
186 const data = await this.apiRequest('GET', `/apps/${appId}/deployments/${deploymentId}/components/${componentName}/logs`, null, {
187 type,
188 follow: false
189 });
190 return data.historic_urls || [];
191 }
192
193 // ===========================
194 // Droplets API Methods
195 // ===========================
196
197 /**
198 * List all droplets in the account
199 * @returns {Promise<Array>} List of droplet objects
200 */
201 async listDroplets() {
202 const data = await this.apiRequest('GET', '/droplets');
203 return data.droplets || [];
204 }
205
206 /**
207 * Get specific droplet details
208 * @param {number} dropletId - Droplet ID
209 * @returns {Promise<object>} Droplet object
210 */
211 async getDroplet(dropletId) {
212 const data = await this.apiRequest('GET', `/droplets/${dropletId}`);
213 return data.droplet;
214 }
215
216 /**
217 * Create a new droplet
218 * @param {object} config - Droplet configuration
219 * @returns {Promise<object>} Created droplet object
220 */
221 async createDroplet(config) {
222 const data = await this.apiRequest('POST', '/droplets', config);
223 return data.droplet;
224 }
225
226 /**
227 * Delete a droplet
228 * @param {number} dropletId - Droplet ID
229 * @returns {Promise<object>} Success confirmation
230 */
231 async deleteDroplet(dropletId) {
232 await this.apiRequest('DELETE', `/droplets/${dropletId}`);
233 return { success: true };
234 }
235
236 /**
237 * Power on/off droplet
238 * @param {number} dropletId - Droplet ID
239 * @param {string} action - 'power_on', 'power_off', 'reboot', 'shutdown'
240 * @returns {Promise<object>} Action object
241 */
242 async dropletAction(dropletId, action) {
243 const data = await this.apiRequest('POST', `/droplets/${dropletId}/actions`, {
244 type: action
245 });
246 return data.action;
247 }
248
249 // ===========================
250 // Databases API Methods
251 // ===========================
252
253 /**
254 * List all managed databases
255 * @returns {Promise<Array>} List of database objects
256 */
257 async listDatabases() {
258 const data = await this.apiRequest('GET', '/databases');
259 return data.databases || [];
260 }
261
262 /**
263 * Get specific database details
264 * @param {string} databaseId - Database cluster UUID
265 * @returns {Promise<object>} Database object
266 */
267 async getDatabase(databaseId) {
268 const data = await this.apiRequest('GET', `/databases/${databaseId}`);
269 return data.database;
270 }
271
272 /**
273 * Get database users
274 * @param {string} databaseId - Database cluster UUID
275 * @returns {Promise<Array>} List of database user objects
276 */
277 async getDatabaseUsers(databaseId) {
278 const data = await this.apiRequest('GET', `/databases/${databaseId}/users`);
279 return data.users || [];
280 }
281
282 /**
283 * Get database connection pools
284 * @param {string} databaseId - Database cluster UUID
285 * @returns {Promise<Array>} List of connection pool objects
286 */
287 async getDatabasePools(databaseId) {
288 const data = await this.apiRequest('GET', `/databases/${databaseId}/pools`);
289 return data.pools || [];
290 }
291
292 // ===========================
293 // Volumes API Methods
294 // ===========================
295
296 /**
297 * List all volumes
298 * @returns {Promise<Array>} List of volume objects
299 */
300 async listVolumes() {
301 const data = await this.apiRequest('GET', '/volumes');
302 return data.volumes || [];
303 }
304
305 /**
306 * Get volume details
307 * @param {string} volumeId - Volume UUID
308 * @returns {Promise<object>} Volume object
309 */
310 async getVolume(volumeId) {
311 const data = await this.apiRequest('GET', `/volumes/${volumeId}`);
312 return data.volume;
313 }
314
315 // ===========================
316 // Projects API Methods
317 // ===========================
318
319 /**
320 * List all projects
321 * @returns {Promise<Array>} List of project objects
322 */
323 async listProjects() {
324 const data = await this.apiRequest('GET', '/projects');
325 return data.projects || [];
326 }
327
328 /**
329 * Get specific project details
330 * @param {string} projectId - Project UUID
331 * @returns {Promise<object>} Project object
332 */
333 async getProject(projectId) {
334 const data = await this.apiRequest('GET', `/projects/${projectId}`);
335 return data.project;
336 }
337
338 /**
339 * Find project by name
340 * @param {string} projectName - Project name to search for
341 * @returns {Promise<object|undefined>} Project object if found
342 */
343 async findProjectByName(projectName) {
344 const projects = await this.listProjects();
345 return projects.find(p => p.name.toLowerCase().includes(projectName.toLowerCase()));
346 }
347
348 // ===========================
349 // Monitoring API Methods
350 // ===========================
351
352 /**
353 * Get droplet metrics (CPU, memory, disk, bandwidth)
354 * @param {number} dropletId - Droplet ID
355 * @param {string} [metric] - Metric type: 'cpu', 'memory_free', 'memory_available', 'disk_read', 'disk_write', 'bandwidth_inbound', 'bandwidth_outbound'
356 * @param {number} [start] - Start time (UTC unix timestamp)
357 * @param {number} [end] - End time (UTC unix timestamp)
358 * @returns {Promise<object>} Metrics data
359 */
360 async getDropletMetrics(dropletId, metric = 'cpu', start = null, end = null) {
361 const now = Math.floor(Date.now() / 1000);
362 const params = {
363 host_id: dropletId,
364 start: start || (now - 3600), // Default: last hour
365 end: end || now
366 };
367
368 const data = await this.apiRequest('GET', `/monitoring/metrics/droplet/${metric}`, null, params);
369 return data.data;
370 }
371
372 /**
373 * Get database metrics (CPU, memory, disk, connections)
374 * @param {string} databaseId - Database cluster UUID
375 * @returns {Promise<object|null>} Database metrics or null on error
376 */
377 async getDatabaseMetrics(databaseId) {
378 try {
379 // Get basic database info first
380 const db = await this.getDatabase(databaseId);
381
382 // Get metrics - note: DB metrics may not be available for all clusters
383 // DOC doesn't have direct metrics API for databases like droplets
384 // We'll return the database status and connection info instead
385 return {
386 status: db.status,
387 cpu_count: db.num_nodes || 1,
388 memory_mb: this.parseDatabaseSize(db.size).memory,
389 disk_gb: this.parseDatabaseSize(db.size).disk,
390 connection: {
391 host: db.connection?.host || null,
392 port: db.connection?.port || null,
393 protocol: db.connection?.protocol || null
394 },
395 nodes: db.num_nodes || 1
396 };
397 } catch (error) {
398 console.error(`[DO] Error fetching database metrics:`, error.message);
399 return null;
400 }
401 }
402
403 /**
404 * Parse database size string to extract memory and disk
405 * @param {string} size - Size string like 'db-s-1vcpu-1gb' or 'db-s-2vcpu-4gb-60gb'
406 * @returns {object} Parsed size with vcpus, memory (MB), and disk (GB)
407 */
408 parseDatabaseSize(size) {
409 if (!size) return { vcpus: null, memory: null, disk: null };
410
411 // Parse formats like: db-s-1vcpu-1gb, db-s-2vcpu-4gb, db-s-2vcpu-4gb-60gb
412 const vcpuMatch = size.match(/(\d+)vcpu/);
413 const memMatch = size.match(/(\d+)gb(?!-)/i); // GB not followed by hyphen
414 const diskMatch = size.match(/-(\d+)gb$/i); // GB at end with hyphen before
415
416 return {
417 vcpus: vcpuMatch ? parseInt(vcpuMatch[1]) : null,
418 memory: memMatch ? parseInt(memMatch[1]) * 1024 : null, // Convert to MB
419 disk: diskMatch ? parseInt(diskMatch[1]) : null
420 };
421 }
422
423 /**
424 * Get app metrics (CPU, memory from most recent deployment)
425 * @param {string} appId - App UUID
426 * @returns {Promise<object|null>} App metrics or null on error
427 */
428 async getAppMetrics(appId) {
429 try {
430 const app = await this.getApp(appId);
431
432 // App Platform doesn't expose detailed runtime metrics via API
433 // We return deployment status and tier information instead
434 return {
435 status: app.phase || 'unknown',
436 tier: app.tier_slug || 'basic',
437 instance_count: app.spec?.services?.reduce((sum, s) => sum + (s.instance_count || 1), 0) || 1,
438 instance_size: app.spec?.services?.[0]?.instance_size_slug || 'basic-xxs',
439 region: app.region?.slug || null,
440 live_url: app.live_url || null,
441 created_at: app.created_at,
442 updated_at: app.updated_at
443 };
444 } catch (error) {
445 console.error(`[DO] Error fetching app metrics:`, error.message);
446 return null;
447 }
448 }
449
450 // ===========================
451 // Utility Methods
452 // ===========================
453
454 /**
455 * Get all resources summary
456 * @returns {Promise<object>} Summary of all DigitalOcean resources
457 */
458 async getAllResources() {
459 const [apps, droplets, databases, volumes] = await Promise.all([
460 this.listApps().catch(() => []),
461 this.listDroplets().catch(() => []),
462 this.listDatabases().catch(() => []),
463 this.listVolumes().catch(() => [])
464 ]);
465
466 return {
467 apps,
468 droplets,
469 databases,
470 volumes,
471 summary: {
472 total_apps: apps.length,
473 total_droplets: droplets.length,
474 total_databases: databases.length,
475 total_volumes: volumes.length
476 }
477 };
478 }
479}
480
481module.exports = DigitalOceanService;