Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,85 @@ program
}
});

// Enable service command
program
.command('enable-service <name>')
.description('enable auto-start service for a PostgreSQL instance')
.option('--user', 'use user systemd service instead of system service')
.action(async (name, options) => {
const spinner = ora(`Enabling service for instance '${name}'...`).start();

try {
await instanceManager.enableService(name, options.user);
spinner.succeed(`Service enabled for instance '${name}'`);

console.log();
console.log(chalk.gray('The instance will now automatically start after system restart.'));
console.log(chalk.gray('Service management commands:'));
console.log(chalk.gray(` Check status: ${chalk.white('pgforge service-status ' + name)}`));
console.log(chalk.gray(` Disable: ${chalk.white('pgforge disable-service ' + name)}`));

} catch (error) {
spinner.fail(`Failed to enable service: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
});

// Disable service command
program
.command('disable-service <name>')
.description('disable auto-start service for a PostgreSQL instance')
.option('--user', 'use user systemd service instead of system service')
.action(async (name, options) => {
const spinner = ora(`Disabling service for instance '${name}'...`).start();

try {
await instanceManager.disableService(name, options.user);
spinner.succeed(`Service disabled for instance '${name}'`);

console.log();
console.log(chalk.gray('The instance will no longer automatically start after system restart.'));
console.log(chalk.gray(`Use ${chalk.white('pgforge enable-service ' + name)} to re-enable auto-start.`));

} catch (error) {
spinner.fail(`Failed to disable service: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
});

// Service status command
program
.command('service-status <name>')
.description('show systemd service status for a PostgreSQL instance')
.option('--user', 'check user systemd service instead of system service')
.action(async (name, options) => {
const spinner = ora(`Checking service status for '${name}'...`).start();

try {
const status = await instanceManager.getServiceStatus(name, options.user);
spinner.stop();

console.log(chalk.bold(`Service Status for '${name}':`));
console.log(` Service Name: ${chalk.cyan('pgforge-' + name)}`);
console.log(` Enabled: ${status.enabled ? chalk.green('Yes') : chalk.red('No')}`);
console.log(` Active: ${status.active ? chalk.green('Yes') : chalk.red('No')}`);
console.log(` Status: ${chalk.yellow(status.status)}`);
console.log(` Service Type: ${options.user ? chalk.blue('User') : chalk.blue('System')}`);

if (status.enabled) {
console.log();
console.log(chalk.gray('This instance will automatically start after system restart.'));
} else {
console.log();
console.log(chalk.gray(`Use ${chalk.white('pgforge enable-service ' + name)} to enable auto-start.`));
}

} catch (error) {
spinner.fail(`Failed to get service status: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
});

// Error handling
program.configureOutput({
writeErr: (str) => process.stderr.write(chalk.red(str))
Expand Down
11 changes: 11 additions & 0 deletions src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ export interface PostgreSQLInstanceConfig {
format?: 'custom' | 'plain' | 'directory' | 'tar';
destination?: string;
};
service?: {
enabled: boolean;
autoStart: boolean;
restartPolicy?: 'always' | 'on-failure' | 'no';
restartSec?: number;
};
};
status?: {
state: 'stopped' | 'starting' | 'running' | 'stopping' | 'error';
Expand All @@ -72,6 +78,11 @@ export interface PostgreSQLInstanceConfig {
version?: string;
dataSize?: string;
connections?: number;
service?: {
enabled: boolean;
active: boolean;
status?: string;
};
};
}

Expand Down
160 changes: 160 additions & 0 deletions src/instance/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ import { randomBytes } from 'crypto';
import { access, mkdir, writeFile, readFile, readdir, rmdir, unlink, stat } from 'fs/promises';
import { join, dirname } from 'path';
import { ConfigManager } from '../config/manager.js';
import { ServiceManager } from '../service/manager.js';
import type { PostgreSQLInstanceConfig } from '../config/types.js';

const execAsync = promisify(exec);

export class InstanceManager {
private configManager: ConfigManager;
private serviceManager: ServiceManager;

constructor() {
this.configManager = new ConfigManager();
this.serviceManager = new ServiceManager();
}

async createInstance(
Expand Down Expand Up @@ -171,6 +174,38 @@ export class InstanceManager {
}
}

// Check service status if service is enabled
if (config.spec.service?.enabled && await this.serviceManager.isSystemdAvailable()) {
try {
const serviceStatus = await this.serviceManager.getServiceStatus(name, false);

// Update config with service status
if (!config.status) {
config.status = {
state: 'stopped',
version: config.spec.version,
connections: 0,
};
}
config.status.service = serviceStatus;

// If service is active but config shows stopped, update it
if (serviceStatus.active && config.status.state === 'stopped') {
config.status.state = 'running';
config.status.startTime = new Date().toISOString();
} else if (!serviceStatus.active && config.status.state === 'running') {
config.status.state = 'stopped';
config.status.lastRestart = config.status.startTime;
config.status.startTime = undefined;
}

await this.configManager.saveInstanceConfig(config);
} catch (error) {
// Service status check failed, don't fail the whole operation
console.warn(`Warning: Could not check service status for '${name}': ${error}`);
}
}

return config;
}

Expand Down Expand Up @@ -804,4 +839,129 @@ export class InstanceManager {
// TODO: Implement backup functionality
console.log('Backup functionality not yet implemented');
}

/**
* Enable service auto-start for an instance
*/
async enableService(name: string, useUserService = false): Promise<void> {
const config = await this.configManager.getInstanceConfig(name);
if (!config) {
throw new Error(`Instance '${name}' not found`);
}

// Check if systemd is available
if (!await this.serviceManager.isSystemdAvailable()) {
throw new Error('systemd is not available on this system. Service management requires systemd.');
}

// Update configuration to enable service
config.spec.service = {
enabled: true,
autoStart: true,
restartPolicy: 'on-failure',
restartSec: 5,
...config.spec.service
};

// Enable the service
await this.serviceManager.enableService(config, useUserService);

// Update instance configuration
await this.configManager.saveInstanceConfig(config);

console.log(`Service auto-start enabled for instance '${name}'`);
}

/**
* Disable service auto-start for an instance
*/
async disableService(name: string, useUserService = false): Promise<void> {
const config = await this.configManager.getInstanceConfig(name);
if (!config) {
throw new Error(`Instance '${name}' not found`);
}

// Disable the service
await this.serviceManager.disableService(name, useUserService);

// Update configuration to disable service
if (config.spec.service) {
config.spec.service.enabled = false;
config.spec.service.autoStart = false;
}

// Update instance configuration
await this.configManager.saveInstanceConfig(config);

console.log(`Service auto-start disabled for instance '${name}'`);
}

/**
* Get service status for an instance
*/
async getServiceStatus(name: string, useUserService = false): Promise<{
enabled: boolean;
active: boolean;
status: string;
}> {
return await this.serviceManager.getServiceStatus(name, useUserService);
}

/**
* Start instance using service (if enabled) or direct process
*/
async startInstanceWithService(name: string, useUserService = false): Promise<void> {
const config = await this.configManager.getInstanceConfig(name);
if (!config) {
throw new Error(`Instance '${name}' not found`);
}

if (config.spec.service?.enabled && await this.serviceManager.isSystemdAvailable()) {
// Start using systemd service
await this.serviceManager.startService(name, useUserService);

// Wait for service to start and update status
await new Promise(resolve => setTimeout(resolve, 2000));
const serviceStatus = await this.serviceManager.getServiceStatus(name, useUserService);

config.status = {
state: serviceStatus.active ? 'running' : 'stopped',
startTime: serviceStatus.active ? new Date().toISOString() : undefined,
version: config.spec.version,
connections: 0,
service: serviceStatus,
};
} else {
// Start using direct process (existing method)
await this.startInstance(name);
}
}

/**
* Stop instance using service (if enabled) or direct process
*/
async stopInstanceWithService(name: string, useUserService = false): Promise<void> {
const config = await this.configManager.getInstanceConfig(name);
if (!config) {
throw new Error(`Instance '${name}' not found`);
}

if (config.spec.service?.enabled && await this.serviceManager.isSystemdAvailable()) {
// Stop using systemd service
await this.serviceManager.stopService(name, useUserService);

// Update status
const serviceStatus = await this.serviceManager.getServiceStatus(name, useUserService);
config.status = {
state: 'stopped',
lastRestart: config.status?.startTime,
version: config.spec.version,
connections: 0,
service: serviceStatus,
};
} else {
// Stop using direct process (existing method)
await this.stopInstance(name);
}
}
}
Loading