184 lines
5.9 KiB
Dart
184 lines
5.9 KiB
Dart
import 'package:uuid/uuid.dart';
|
|
|
|
/// What kind of thing this service deploys.
|
|
enum ServiceKind { jarService, webApp, generic }
|
|
|
|
/// Front-end framework for a [ServiceKind.webApp].
|
|
enum WebFramework { angular, react, flutterWeb, staticFiles }
|
|
|
|
/// How a service's health is probed.
|
|
enum HealthCheckType { auto, http, tcp, command, none }
|
|
|
|
extension ServiceKindX on ServiceKind {
|
|
String get label => switch (this) {
|
|
ServiceKind.webApp => 'Web app (nginx)',
|
|
ServiceKind.generic => 'Service (any binary)',
|
|
ServiceKind.jarService => 'Jar service',
|
|
};
|
|
}
|
|
|
|
extension HealthCheckTypeX on HealthCheckType {
|
|
String get label => switch (this) {
|
|
HealthCheckType.auto => 'systemd (auto)',
|
|
HealthCheckType.http => 'HTTP request',
|
|
HealthCheckType.tcp => 'TCP port',
|
|
HealthCheckType.command => 'Command exit 0',
|
|
HealthCheckType.none => 'Disabled',
|
|
};
|
|
}
|
|
|
|
extension WebFrameworkX on WebFramework {
|
|
String get label => switch (this) {
|
|
WebFramework.angular => 'Angular',
|
|
WebFramework.react => 'React',
|
|
WebFramework.flutterWeb => 'Flutter web',
|
|
WebFramework.staticFiles => 'Static files',
|
|
};
|
|
|
|
/// Default build command (run through a shell). Blank = no build.
|
|
String get defaultBuild => switch (this) {
|
|
WebFramework.angular => 'npm install && npm run build',
|
|
WebFramework.react => 'npm install && npm run build',
|
|
WebFramework.flutterWeb => 'flutter build web --release',
|
|
WebFramework.staticFiles => '',
|
|
};
|
|
}
|
|
|
|
/// A single deployable service that lives on a [ServerProfile].
|
|
/// One server can host many of these — jar services and/or web apps.
|
|
class ServiceConfig {
|
|
final String id;
|
|
String name; // friendly label, e.g. "orders-api"
|
|
|
|
ServiceKind kind;
|
|
WebFramework framework;
|
|
|
|
/// Absolute target directory on the server.
|
|
/// - jar service: where the jar lives, e.g. /opt/orders
|
|
/// - web app: the nginx web root, e.g. /var/www/orders
|
|
String remoteDir;
|
|
|
|
/// The filename the uploaded jar should be saved as (jar services only).
|
|
String remoteJarName;
|
|
|
|
/// The systemd unit to (re)start for a jar service, e.g. "orders".
|
|
String serviceName;
|
|
|
|
/// The nginx unit to reload for a web app (web only). Default "nginx".
|
|
String nginxService;
|
|
|
|
/// Optional custom log command (WITHOUT sudo — the deployer adds it).
|
|
String logCommand;
|
|
|
|
/// Absolute path to the local source project. When set, PeachDeployments can build
|
|
/// from source before deploying.
|
|
String localProjectPath;
|
|
|
|
/// Optional custom build command. Blank auto-detects.
|
|
String buildCommand;
|
|
|
|
/// jar services: glob/path for the built jar (blank auto-detects).
|
|
String jarPattern;
|
|
|
|
/// web apps: build output directory relative to the project (blank
|
|
/// auto-detects, e.g. dist/, build/, build/web).
|
|
String outputSubdir;
|
|
|
|
/// How to probe this service's health.
|
|
HealthCheckType healthType;
|
|
|
|
/// Health target: a URL (http), port number (tcp), or shell command.
|
|
String healthTarget;
|
|
|
|
ServiceConfig({
|
|
String? id,
|
|
this.name = 'New service',
|
|
this.kind = ServiceKind.jarService,
|
|
this.framework = WebFramework.react,
|
|
this.remoteDir = '/opt/app',
|
|
this.remoteJarName = '',
|
|
this.serviceName = '',
|
|
this.nginxService = 'nginx',
|
|
this.logCommand = '',
|
|
this.localProjectPath = '',
|
|
this.buildCommand = '',
|
|
this.jarPattern = '',
|
|
this.outputSubdir = '',
|
|
this.healthType = HealthCheckType.auto,
|
|
this.healthTarget = '',
|
|
}) : id = id ?? const Uuid().v4();
|
|
|
|
bool get isWeb => kind == ServiceKind.webApp;
|
|
bool get isGeneric => kind == ServiceKind.generic;
|
|
bool get canBuild => localProjectPath.trim().isNotEmpty;
|
|
|
|
String get effectiveLogCommand {
|
|
if (logCommand.trim().isNotEmpty) return logCommand.trim();
|
|
if (isWeb) {
|
|
final unit = nginxService.trim().isEmpty ? 'nginx' : nginxService.trim();
|
|
return 'journalctl -u $unit -n 200 --no-pager';
|
|
}
|
|
return 'journalctl -u ${serviceName.isEmpty ? 'app' : serviceName} '
|
|
'-n 200 --no-pager';
|
|
}
|
|
|
|
/// Normalized remote directory (no trailing slash).
|
|
String get remoteRoot => remoteDir.endsWith('/')
|
|
? remoteDir.substring(0, remoteDir.length - 1)
|
|
: remoteDir;
|
|
|
|
String remotePath(String jarLabelFallback) {
|
|
final name = remoteJarName.trim().isNotEmpty
|
|
? remoteJarName.trim()
|
|
: jarLabelFallback;
|
|
return '$remoteRoot/$name';
|
|
}
|
|
|
|
ServiceConfig copy() => ServiceConfig.fromJson(toJson());
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'id': id,
|
|
'name': name,
|
|
'kind': kind.name,
|
|
'framework': framework.name,
|
|
'remoteDir': remoteDir,
|
|
'remoteJarName': remoteJarName,
|
|
'serviceName': serviceName,
|
|
'nginxService': nginxService,
|
|
'logCommand': logCommand,
|
|
'localProjectPath': localProjectPath,
|
|
'buildCommand': buildCommand,
|
|
'jarPattern': jarPattern,
|
|
'outputSubdir': outputSubdir,
|
|
'healthType': healthType.name,
|
|
'healthTarget': healthTarget,
|
|
};
|
|
|
|
factory ServiceConfig.fromJson(Map<String, dynamic> j) => ServiceConfig(
|
|
id: j['id'] as String?,
|
|
name: (j['name'] ?? 'service') as String,
|
|
kind: ServiceKind.values.firstWhere(
|
|
(k) => k.name == j['kind'],
|
|
orElse: () => ServiceKind.jarService,
|
|
),
|
|
framework: WebFramework.values.firstWhere(
|
|
(f) => f.name == j['framework'],
|
|
orElse: () => WebFramework.react,
|
|
),
|
|
remoteDir: (j['remoteDir'] ?? '/opt/app') as String,
|
|
remoteJarName: (j['remoteJarName'] ?? '') as String,
|
|
serviceName: (j['serviceName'] ?? '') as String,
|
|
nginxService: (j['nginxService'] ?? 'nginx') as String,
|
|
logCommand: (j['logCommand'] ?? '') as String,
|
|
localProjectPath: (j['localProjectPath'] ?? '') as String,
|
|
buildCommand: (j['buildCommand'] ?? '') as String,
|
|
jarPattern: (j['jarPattern'] ?? '') as String,
|
|
outputSubdir: (j['outputSubdir'] ?? '') as String,
|
|
healthType: HealthCheckType.values.firstWhere(
|
|
(h) => h.name == j['healthType'],
|
|
orElse: () => HealthCheckType.auto,
|
|
),
|
|
healthTarget: (j['healthTarget'] ?? '') as String,
|
|
);
|
|
}
|