feat: add server ordering system with session-based flow
- Add server recommendation integration (SERVER_RECOMMEND worker) - Implement KV-based session management for multi-step ordering - Add Linode/Vultr API clients for server provisioning - Add server-tool for Function Calling support refactor: major code reorganization (Phase 1-3) - Remove 443 lines of deprecated callback handlers - Extract handlers to separate files (message-handler, callback-handler) - Extract cloud-spec-service, server-recommend-service - Centralize constants (OS_IMAGES, REGION_FLAGS, NUM_EMOJIS) - webhook.ts reduced from 1,951 to 30 lines Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
240
src/services/vultr-api.ts
Normal file
240
src/services/vultr-api.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* Vultr API Client
|
||||
*
|
||||
* REST API 클라이언트 for Vultr cloud provider
|
||||
* - Instance management (create, get)
|
||||
* - Region listing
|
||||
* - Automatic retry with exponential backoff
|
||||
*/
|
||||
|
||||
import type { Env, VultrInstance, VultrCreateRequest } from '../types';
|
||||
import { createLogger } from '../utils/logger';
|
||||
import { retryWithBackoff } from '../utils/retry';
|
||||
|
||||
const logger = createLogger('vultr-api');
|
||||
|
||||
/**
|
||||
* Vultr API Base URLs
|
||||
*/
|
||||
const DEFAULT_API_BASE = 'https://api.vultr.com/v2';
|
||||
|
||||
/**
|
||||
* Vultr Region
|
||||
*/
|
||||
export interface VultrRegion {
|
||||
id: string;
|
||||
city: string;
|
||||
country: string;
|
||||
continent: string;
|
||||
options: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Vultr API Error
|
||||
*/
|
||||
export class VultrAPIError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly statusCode: number,
|
||||
public readonly response?: any
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'VultrAPIError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Vultr instance
|
||||
*
|
||||
* @param params - Instance creation parameters
|
||||
* @param env - Environment variables (API key, base URL)
|
||||
* @returns Created instance information
|
||||
* @throws VultrAPIError if API call fails
|
||||
*/
|
||||
export async function createInstance(
|
||||
params: VultrCreateRequest,
|
||||
env: Env
|
||||
): Promise<VultrInstance> {
|
||||
const apiKey = env.VULTR_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('VULTR_API_KEY is not configured');
|
||||
}
|
||||
|
||||
const apiBase = env.VULTR_API_BASE || DEFAULT_API_BASE;
|
||||
const url = `${apiBase}/instances`;
|
||||
|
||||
logger.info('Creating Vultr instance', {
|
||||
plan: params.plan,
|
||||
region: params.region,
|
||||
label: params.label,
|
||||
});
|
||||
|
||||
return retryWithBackoff(
|
||||
async () => {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.json().catch(() => ({}));
|
||||
logger.error('Vultr API create instance failed', undefined, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorBody,
|
||||
});
|
||||
|
||||
throw new VultrAPIError(
|
||||
`Vultr API error: ${response.status} ${response.statusText}`,
|
||||
response.status,
|
||||
errorBody
|
||||
);
|
||||
}
|
||||
|
||||
const responseData = await response.json() as { instance: VultrInstance };
|
||||
const instance = responseData.instance;
|
||||
|
||||
logger.info('Vultr instance created successfully', {
|
||||
id: instance.id,
|
||||
label: instance.label,
|
||||
main_ip: instance.main_ip,
|
||||
});
|
||||
|
||||
return instance;
|
||||
},
|
||||
{
|
||||
maxRetries: 3,
|
||||
initialDelayMs: 1000,
|
||||
serviceName: 'vultr-api',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Vultr instance by ID
|
||||
*
|
||||
* @param instanceId - Vultr instance ID
|
||||
* @param env - Environment variables (API key, base URL)
|
||||
* @returns Instance information
|
||||
* @throws VultrAPIError if API call fails
|
||||
*/
|
||||
export async function getInstance(
|
||||
instanceId: string,
|
||||
env: Env
|
||||
): Promise<VultrInstance> {
|
||||
const apiKey = env.VULTR_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('VULTR_API_KEY is not configured');
|
||||
}
|
||||
|
||||
const apiBase = env.VULTR_API_BASE || DEFAULT_API_BASE;
|
||||
const url = `${apiBase}/instances/${instanceId}`;
|
||||
|
||||
logger.info('Getting Vultr instance', { instanceId });
|
||||
|
||||
return retryWithBackoff(
|
||||
async () => {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.json().catch(() => ({}));
|
||||
logger.error('Vultr API get instance failed', undefined, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
instanceId,
|
||||
error: errorBody,
|
||||
});
|
||||
|
||||
throw new VultrAPIError(
|
||||
`Vultr API error: ${response.status} ${response.statusText}`,
|
||||
response.status,
|
||||
errorBody
|
||||
);
|
||||
}
|
||||
|
||||
const responseData = await response.json() as { instance: VultrInstance };
|
||||
const instance = responseData.instance;
|
||||
|
||||
logger.info('Vultr instance retrieved successfully', {
|
||||
id: instance.id,
|
||||
label: instance.label,
|
||||
status: instance.status,
|
||||
});
|
||||
|
||||
return instance;
|
||||
},
|
||||
{
|
||||
maxRetries: 3,
|
||||
initialDelayMs: 1000,
|
||||
serviceName: 'vultr-api',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of available Vultr regions
|
||||
*
|
||||
* @param env - Environment variables (API key, base URL)
|
||||
* @returns Array of available regions
|
||||
* @throws VultrAPIError if API call fails
|
||||
*/
|
||||
export async function getRegions(env: Env): Promise<VultrRegion[]> {
|
||||
const apiKey = env.VULTR_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('VULTR_API_KEY is not configured');
|
||||
}
|
||||
|
||||
const apiBase = env.VULTR_API_BASE || DEFAULT_API_BASE;
|
||||
const url = `${apiBase}/regions`;
|
||||
|
||||
logger.info('Getting Vultr regions');
|
||||
|
||||
return retryWithBackoff(
|
||||
async () => {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.json().catch(() => ({}));
|
||||
logger.error('Vultr API get regions failed', undefined, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorBody,
|
||||
});
|
||||
|
||||
throw new VultrAPIError(
|
||||
`Vultr API error: ${response.status} ${response.statusText}`,
|
||||
response.status,
|
||||
errorBody
|
||||
);
|
||||
}
|
||||
|
||||
const responseData = await response.json() as { regions: VultrRegion[] };
|
||||
const regions = responseData.regions;
|
||||
|
||||
logger.info('Vultr regions retrieved successfully', {
|
||||
count: regions.length,
|
||||
});
|
||||
|
||||
return regions;
|
||||
},
|
||||
{
|
||||
maxRetries: 3,
|
||||
initialDelayMs: 1000,
|
||||
serviceName: 'vultr-api',
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user