feat: manage OS images in database instead of hardcoded values

- Add os_images table with linode_image_id and vultr_os_id columns
- Support Ubuntu (24.04, 22.04), Debian (11-13), AlmaLinux (8-9),
  Rocky Linux (8-9), and Fedora 42
- AlmaLinux and Rocky Linux added as CentOS migration alternatives
- Default OS changed from ubuntu_22_04 to ubuntu_24_04
- Fix Vultr OS IDs (1743=22.04, 2284=24.04)
- Remove hardcoded OS validation, validate against DB
- Return available OS list in error message for invalid image

Migration: migrations/003_os_images.sql

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-28 10:31:14 +09:00
parent 7d9edc14a3
commit 3c420d2841
5 changed files with 164 additions and 11 deletions

View File

@@ -41,9 +41,26 @@ export class ProvisioningService {
*/
async provisionServer(request: ProvisionRequest): Promise<ProvisionResponse> {
const { telegram_id, pricing_id, label, image, dry_run } = request;
const osImageKey = image || 'ubuntu_22_04';
// Step 1: Validate user by telegram_id
// Step 1: Validate OS image from DB (or get default)
let osImage = image ? await this.repo.getOsImageByKey(image) : await this.repo.getDefaultOsImage();
if (!osImage) {
// If specified image not found, try to get list for error message
const availableImages = await this.repo.getActiveOsImages();
const availableKeys = availableImages.map(img => img.key).join(', ');
return {
success: false,
error: {
code: 'INVALID_OS_IMAGE',
message: image
? `Invalid OS image '${image}'. Available: ${availableKeys}`
: 'No default OS image configured',
},
};
}
const osImageKey = osImage.key;
// Step 2: Validate user by telegram_id
const user = await this.repo.getUserByTelegramId(telegram_id);
if (!user) {
return {
@@ -52,7 +69,7 @@ export class ProvisioningService {
};
}
// Step 2: Get pricing details with provider info
// Step 4: Get pricing details with provider info
const pricing = await this.repo.getPricingWithProvider(pricing_id);
if (!pricing) {
return {
@@ -61,12 +78,12 @@ export class ProvisioningService {
};
}
// Step 3: Calculate price in KRW using real-time exchange rate
// Step 5: Calculate price in KRW using real-time exchange rate
// Round to nearest 500 KRW for cleaner pricing
const exchangeRate = await getExchangeRate(this.env);
const priceKrw = Math.round((pricing.monthly_price * exchangeRate) / 500) * 500;
// Step 4: Check balance
// Step 6: Check balance
const currentBalance = await this.repo.getBalance(user.id);
if (currentBalance < priceKrw) {
return {
@@ -206,8 +223,24 @@ export class ProvisioningService {
return;
}
// Get OS image ID
const osImageId = provider.getOsImageId(image);
// Get OS image details from DB
const osImage = await this.repo.getOsImageByKey(image);
if (!osImage) {
console.error(`[ProvisioningService] OS image '${image}' not found for order ${order_id}`);
await this.repo.rollbackOrder(order_id, user_id, order.price_paid, `OS image '${image}' not found`);
return;
}
// Get provider-specific OS image ID
const osImageId = pricing.source_provider === 'linode'
? osImage.linode_image_id
: String(osImage.vultr_os_id);
if (!osImageId) {
console.error(`[ProvisioningService] OS image '${image}' not available for ${pricing.source_provider}`);
await this.repo.rollbackOrder(order_id, user_id, order.price_paid, `OS image not available for ${pricing.source_provider}`);
return;
}
// Call provider API (use source_region_code for actual provider region)
const createResult = await provider.createServer({