Initial commit: Cloud Instances API

Multi-cloud VM instance database with Cloudflare Workers
- Linode, Vultr, AWS connector integration
- D1 database with regions, instances, pricing
- Query API with filtering, caching, pagination
- Cron-based auto-sync (daily + 6-hourly)
- Health monitoring endpoint

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-21 20:17:07 +09:00
commit 95043049b4
32 changed files with 10151 additions and 0 deletions

247
src/connectors/base.ts Normal file
View File

@@ -0,0 +1,247 @@
import type { VaultClient } from './vault';
import type { VaultCredentials, RegionInput, InstanceTypeInput } from '../types';
/**
* Raw region data from provider API (before normalization)
* Structure varies by provider
*/
export interface RawRegion {
[key: string]: unknown;
}
/**
* Raw instance type data from provider API (before normalization)
* Structure varies by provider
*/
export interface RawInstanceType {
[key: string]: unknown;
}
/**
* Custom error class for connector operations
*
* @example
* throw new ConnectorError('linode', 'fetchRegions', 500, 'API rate limit exceeded');
*/
export class ConnectorError extends Error {
constructor(
public provider: string,
public operation: string,
public statusCode: number | undefined,
message: string
) {
super(message);
this.name = 'ConnectorError';
}
}
/**
* RateLimiter - Token Bucket algorithm implementation
*
* Controls API request rate to prevent hitting provider rate limits.
* Tokens are consumed for each request and refilled at a fixed rate.
*
* @example
* const limiter = new RateLimiter(10, 2); // 10 tokens, refill 2 per second
* await limiter.waitForToken(); // Wait until token is available
* // Make API call
*/
export class RateLimiter {
private tokens: number;
private lastRefillTime: number;
/**
* Create a new rate limiter
*
* @param maxTokens - Maximum number of tokens in the bucket
* @param refillRate - Number of tokens to refill per second
*/
constructor(
private readonly maxTokens: number,
private readonly refillRate: number
) {
this.tokens = maxTokens;
this.lastRefillTime = Date.now();
console.log('[RateLimiter] Initialized', { maxTokens, refillRate });
}
/**
* Wait until a token is available, then consume it
* Automatically refills tokens based on elapsed time
*
* @returns Promise that resolves when a token is available
*/
async waitForToken(): Promise<void> {
this.refillTokens();
// If no tokens available, wait until next refill
while (this.tokens < 1) {
const timeUntilNextToken = (1 / this.refillRate) * 1000; // ms per token
await this.sleep(timeUntilNextToken);
this.refillTokens();
}
// Consume one token
this.tokens -= 1;
console.log('[RateLimiter] Token consumed', { remaining: this.tokens });
}
/**
* Get the current number of available tokens
*
* @returns Number of available tokens (may include fractional tokens)
*/
getAvailableTokens(): number {
this.refillTokens();
return this.tokens;
}
/**
* Refill tokens based on elapsed time
* Tokens are added proportionally to the time elapsed since last refill
*/
private refillTokens(): void {
const now = Date.now();
const elapsedSeconds = (now - this.lastRefillTime) / 1000;
const tokensToAdd = elapsedSeconds * this.refillRate;
// Add tokens, capped at maxTokens
this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd);
this.lastRefillTime = now;
}
/**
* Sleep for specified milliseconds
*
* @param ms - Milliseconds to sleep
*/
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
/**
* CloudConnector - Abstract base class for cloud provider connectors
*
* Implements common authentication, rate limiting, and data fetching patterns.
* Each provider (Linode, Vultr, etc.) extends this class and implements
* provider-specific API calls and data normalization.
*
* @abstract
*
* @example
* class LinodeConnector extends CloudConnector {
* provider = 'linode';
*
* async fetchRegions() {
* await this.rateLimiter.waitForToken();
* // Fetch regions from Linode API
* }
*
* normalizeRegion(raw: RawRegion): RegionInput {
* // Transform Linode region data to standard format
* }
* }
*/
export abstract class CloudConnector {
/**
* Provider identifier (e.g., 'linode', 'vultr', 'aws')
* Must be implemented by subclass
*/
abstract provider: string;
/**
* Cached credentials from Vault
* Populated after calling authenticate()
*/
protected credentials: VaultCredentials | null = null;
/**
* Rate limiter for API requests
* Configured with provider-specific limits
*/
protected rateLimiter: RateLimiter;
/**
* Create a new cloud connector
*
* @param vault - VaultClient instance for credential management
*/
constructor(protected vault: VaultClient) {
// Default rate limiter: 10 requests, refill 2 per second
this.rateLimiter = new RateLimiter(10, 2);
}
/**
* Authenticate with provider using Vault credentials
* Fetches and caches credentials for API calls
*
* @throws ConnectorError if authentication fails
*/
async authenticate(): Promise<void> {
try {
console.log('[CloudConnector] Authenticating', { provider: this.provider });
this.credentials = await this.vault.getCredentials(this.provider);
if (!this.credentials || !this.credentials.api_token) {
throw new ConnectorError(
this.provider,
'authenticate',
undefined,
'Invalid credentials received from Vault'
);
}
console.log('[CloudConnector] Authentication successful', { provider: this.provider });
} catch (error) {
console.error('[CloudConnector] Authentication failed', { provider: this.provider, error });
throw new ConnectorError(
this.provider,
'authenticate',
undefined,
`Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
/**
* Fetch raw region data from provider API
* Must be implemented by subclass
*
* @returns Array of raw region objects from provider API
* @throws ConnectorError on API failure
*/
abstract fetchRegions(): Promise<RawRegion[]>;
/**
* Fetch raw instance type data from provider API
* Must be implemented by subclass
*
* @returns Array of raw instance type objects from provider API
* @throws ConnectorError on API failure
*/
abstract fetchInstanceTypes(): Promise<RawInstanceType[]>;
/**
* Normalize raw region data to standard format
* Transforms provider-specific region structure to RegionInput
* Must be implemented by subclass
*
* @param raw - Raw region data from provider API
* @returns Normalized region data ready for database insertion
*/
abstract normalizeRegion(raw: RawRegion): RegionInput;
/**
* Normalize raw instance type data to standard format
* Transforms provider-specific instance structure to InstanceTypeInput
* Must be implemented by subclass
*
* @param raw - Raw instance type data from provider API
* @returns Normalized instance type data ready for database insertion
*/
abstract normalizeInstance(raw: RawInstanceType): InstanceTypeInput;
}