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
7const axios = require('axios');
8const pool = require('../db');
10const DO_API_BASE = 'https://api.digitalocean.com/v2';
13 * DigitalOcean API service.
14 * Multi-tenant aware service for interacting with DigitalOcean App Platform, Droplets, and Databases.
16class DigitalOceanService {
18 * Initialize DigitalOcean service for specific tenant.
19 * @param {number} tenantId - Tenant ID
21 constructor(tenantId) {
22 this.tenantId = tenantId;
28 * Load DigitalOcean configuration for the tenant.
29 * @returns {Promise<object>} Configuration object with API token
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 };
39 // Otherwise, load from database
40 const result = await pool.query(
41 'SELECT * FROM digitalocean_config WHERE tenant_id = $1 AND is_active = true',
45 if (result.rows.length === 0) {
46 throw new Error('DigitalOcean configuration not found for this tenant. Please configure DO API token first.');
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
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
63 async apiRequest(method, endpoint, data = null, params = null) {
65 await this.loadConfig();
71 url: `${DO_API_BASE}${endpoint}`,
73 'Authorization': `Bearer ${this.apiToken}`,
74 'Content-Type': 'application/json'
83 config.params = params;
86 console.log(`[DO] ${method} ${endpoint}`);
87 const response = await axios(config);
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}`);
96 * Test API connection.
97 * @returns {Promise<boolean>} True if connection successful
99 async testConnection() {
101 const account = await this.apiRequest('GET', '/account');
105 email: account.account.email,
106 status: account.account.status,
107 uuid: account.account.uuid
118 // ===========================
119 // App Platform API Methods
120 // ===========================
123 * List all apps in the account
124 * @returns {Promise<Array>} List of app objects
127 const data = await this.apiRequest('GET', '/apps');
128 return data.apps || [];
132 * Get specific app details
133 * @param {string} appId - DigitalOcean App ID
134 * @returns {Promise<object>} App object
136 async getApp(appId) {
137 const data = await this.apiRequest('GET', `/apps/${appId}`);
142 * Get app deployments
143 * @param {string} appId - DigitalOcean App ID
144 * @returns {Promise<Array>} List of deployment objects
146 async getAppDeployments(appId) {
147 const data = await this.apiRequest('GET', `/apps/${appId}/deployments`);
148 return data.deployments || [];
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
157 async createDeployment(appId, forceRebuild = false) {
158 const data = await this.apiRequest('POST', `/apps/${appId}/deployments`, {
159 force_build: forceRebuild
161 return data.deployment;
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
170 async updateApp(appId, appSpec) {
171 const data = await this.apiRequest('PUT', `/apps/${appId}`, {
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
185 async getAppLogs(appId, deploymentId, componentName, type = 'run') {
186 const data = await this.apiRequest('GET', `/apps/${appId}/deployments/${deploymentId}/components/${componentName}/logs`, null, {
190 return data.historic_urls || [];
193 // ===========================
194 // Droplets API Methods
195 // ===========================
198 * List all droplets in the account
199 * @returns {Promise<Array>} List of droplet objects
201 async listDroplets() {
202 const data = await this.apiRequest('GET', '/droplets');
203 return data.droplets || [];
207 * Get specific droplet details
208 * @param {number} dropletId - Droplet ID
209 * @returns {Promise<object>} Droplet object
211 async getDroplet(dropletId) {
212 const data = await this.apiRequest('GET', `/droplets/${dropletId}`);
217 * Create a new droplet
218 * @param {object} config - Droplet configuration
219 * @returns {Promise<object>} Created droplet object
221 async createDroplet(config) {
222 const data = await this.apiRequest('POST', '/droplets', config);
228 * @param {number} dropletId - Droplet ID
229 * @returns {Promise<object>} Success confirmation
231 async deleteDroplet(dropletId) {
232 await this.apiRequest('DELETE', `/droplets/${dropletId}`);
233 return { success: true };
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
242 async dropletAction(dropletId, action) {
243 const data = await this.apiRequest('POST', `/droplets/${dropletId}/actions`, {
249 // ===========================
250 // Databases API Methods
251 // ===========================
254 * List all managed databases
255 * @returns {Promise<Array>} List of database objects
257 async listDatabases() {
258 const data = await this.apiRequest('GET', '/databases');
259 return data.databases || [];
263 * Get specific database details
264 * @param {string} databaseId - Database cluster UUID
265 * @returns {Promise<object>} Database object
267 async getDatabase(databaseId) {
268 const data = await this.apiRequest('GET', `/databases/${databaseId}`);
269 return data.database;
274 * @param {string} databaseId - Database cluster UUID
275 * @returns {Promise<Array>} List of database user objects
277 async getDatabaseUsers(databaseId) {
278 const data = await this.apiRequest('GET', `/databases/${databaseId}/users`);
279 return data.users || [];
283 * Get database connection pools
284 * @param {string} databaseId - Database cluster UUID
285 * @returns {Promise<Array>} List of connection pool objects
287 async getDatabasePools(databaseId) {
288 const data = await this.apiRequest('GET', `/databases/${databaseId}/pools`);
289 return data.pools || [];
292 // ===========================
293 // Volumes API Methods
294 // ===========================
298 * @returns {Promise<Array>} List of volume objects
300 async listVolumes() {
301 const data = await this.apiRequest('GET', '/volumes');
302 return data.volumes || [];
307 * @param {string} volumeId - Volume UUID
308 * @returns {Promise<object>} Volume object
310 async getVolume(volumeId) {
311 const data = await this.apiRequest('GET', `/volumes/${volumeId}`);
315 // ===========================
316 // Projects API Methods
317 // ===========================
321 * @returns {Promise<Array>} List of project objects
323 async listProjects() {
324 const data = await this.apiRequest('GET', '/projects');
325 return data.projects || [];
329 * Get specific project details
330 * @param {string} projectId - Project UUID
331 * @returns {Promise<object>} Project object
333 async getProject(projectId) {
334 const data = await this.apiRequest('GET', `/projects/${projectId}`);
339 * Find project by name
340 * @param {string} projectName - Project name to search for
341 * @returns {Promise<object|undefined>} Project object if found
343 async findProjectByName(projectName) {
344 const projects = await this.listProjects();
345 return projects.find(p => p.name.toLowerCase().includes(projectName.toLowerCase()));
348 // ===========================
349 // Monitoring API Methods
350 // ===========================
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
360 async getDropletMetrics(dropletId, metric = 'cpu', start = null, end = null) {
361 const now = Math.floor(Date.now() / 1000);
364 start: start || (now - 3600), // Default: last hour
368 const data = await this.apiRequest('GET', `/monitoring/metrics/droplet/${metric}`, null, params);
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
377 async getDatabaseMetrics(databaseId) {
379 // Get basic database info first
380 const db = await this.getDatabase(databaseId);
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
387 cpu_count: db.num_nodes || 1,
388 memory_mb: this.parseDatabaseSize(db.size).memory,
389 disk_gb: this.parseDatabaseSize(db.size).disk,
391 host: db.connection?.host || null,
392 port: db.connection?.port || null,
393 protocol: db.connection?.protocol || null
395 nodes: db.num_nodes || 1
398 console.error(`[DO] Error fetching database metrics:`, error.message);
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)
408 parseDatabaseSize(size) {
409 if (!size) return { vcpus: null, memory: null, disk: null };
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
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
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
428 async getAppMetrics(appId) {
430 const app = await this.getApp(appId);
432 // App Platform doesn't expose detailed runtime metrics via API
433 // We return deployment status and tier information instead
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
445 console.error(`[DO] Error fetching app metrics:`, error.message);
450 // ===========================
452 // ===========================
455 * Get all resources summary
456 * @returns {Promise<object>} Summary of all DigitalOcean resources
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(() => [])
472 total_apps: apps.length,
473 total_droplets: droplets.length,
474 total_databases: databases.length,
475 total_volumes: volumes.length
481module.exports = DigitalOceanService;