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:
247
src/connectors/base.ts
Normal file
247
src/connectors/base.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user