feat: add Queue-based async server provisioning

- Add Cloudflare Queue for async server provisioning workflow
- Implement VPS provider abstraction (Linode, Vultr)
- Add provisioning API endpoints with API key authentication
- Fix race condition in balance deduction (atomic query)
- Remove root_password from Queue for security (fetch from DB)
- Add IP assignment wait logic after server creation
- Add rollback/refund on all failure cases

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-27 17:19:19 +09:00
parent 8c543eeaa5
commit 9b51b8d427
12 changed files with 1796 additions and 5 deletions

View File

@@ -0,0 +1,189 @@
/**
* Vultr VPS Provider Implementation
* API Docs: https://www.vultr.com/api/
*/
import type { VPSProviderConfig, CreateServerRequest, CreateServerResponse } from '../types';
import { VPSProviderBase, OS_IMAGE_MAP } from './vps-provider';
interface VultrInstance {
id: string;
os: string;
ram: number;
disk: number;
main_ip: string;
vcpu_count: number;
region: string;
plan: string;
date_created: string;
status: string;
power_status: string;
server_status: string;
v6_main_ip: string;
v6_network: string;
hostname: string;
label: string;
tag: string;
}
interface VultrCreateResponse {
instance: VultrInstance;
}
interface VultrError {
error: string;
status: number;
}
export class VultrProvider extends VPSProviderBase {
constructor(apiKey: string, timeout: number = 30000) {
super({
apiKey,
baseUrl: 'https://api.vultr.com/v2',
timeout,
});
}
async createServer(request: CreateServerRequest): Promise<CreateServerResponse> {
const url = `${this.config.baseUrl}/instances`;
const body = {
plan: request.plan,
region: request.region,
os_id: parseInt(request.osImage, 10),
label: request.label || `server-${Date.now()}`,
hostname: request.label || `server-${Date.now()}`,
tag: request.tags?.[0] || '',
sshkey_id: request.sshKeys || [],
enable_ipv6: true,
backups: 'disabled',
ddos_protection: false,
};
try {
const response = await this.fetchWithRetry(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) {
const error = (await response.json()) as VultrError;
return {
success: false,
error: {
code: `VULTR_${response.status}`,
message: error.error || 'Unknown error',
},
};
}
const data = (await response.json()) as VultrCreateResponse;
const instance = data.instance;
return {
success: true,
instanceId: instance.id,
ipv4: instance.main_ip !== '0.0.0.0' ? instance.main_ip : undefined,
ipv6: instance.v6_main_ip || undefined,
status: instance.status,
};
} catch (error) {
return {
success: false,
error: {
code: 'VULTR_NETWORK_ERROR',
message: error instanceof Error ? error.message : 'Network error',
},
};
}
}
async deleteServer(instanceId: string): Promise<{ success: boolean; error?: string }> {
const url = `${this.config.baseUrl}/instances/${instanceId}`;
try {
const response = await this.fetchWithRetry(url, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${this.config.apiKey}`,
},
});
// Vultr returns 204 No Content on success
if (response.status === 204 || response.ok) {
return { success: true };
}
const error = (await response.json()) as VultrError;
return {
success: false,
error: error.error || 'Failed to delete instance',
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Network error',
};
}
}
async getServerStatus(
instanceId: string
): Promise<{ status: string; ipv4?: string; ipv6?: string }> {
const url = `${this.config.baseUrl}/instances/${instanceId}`;
try {
const response = await this.fetchWithRetry(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${this.config.apiKey}`,
},
});
if (!response.ok) {
return { status: 'unknown' };
}
const data = (await response.json()) as { instance: VultrInstance };
return {
status: data.instance.status,
ipv4: data.instance.main_ip !== '0.0.0.0' ? data.instance.main_ip : undefined,
ipv6: data.instance.v6_main_ip || undefined,
};
} catch {
return { status: 'unknown' };
}
}
getOsImageId(osImage: string): string {
return OS_IMAGE_MAP.vultr[osImage as keyof typeof OS_IMAGE_MAP.vultr] || '2284'; // Default Ubuntu 22.04
}
/**
* Wait for server to be ready (IP assigned)
* Vultr servers may take a minute to get IP addresses
*/
async waitForReady(
instanceId: string,
maxWaitMs: number = 120000
): Promise<{ ready: boolean; ipv4?: string; ipv6?: string }> {
const startTime = Date.now();
const pollInterval = 5000;
while (Date.now() - startTime < maxWaitMs) {
const status = await this.getServerStatus(instanceId);
if (status.status === 'active' && status.ipv4) {
return { ready: true, ipv4: status.ipv4, ipv6: status.ipv6 };
}
await this.sleep(pollInterval);
}
return { ready: false };
}
}