Relax workspace prechecks and add post-deploy validation
This commit is contained in:
parent
b06a27a3cf
commit
03296b4e21
@ -39,15 +39,24 @@ echo "CADDY=\$(caddy version 2>/dev/null || echo missing)"
|
|||||||
echo "ANSIBLE=\$(ansible --version 2>/dev/null | head -1 || echo missing)"
|
echo "ANSIBLE=\$(ansible --version 2>/dev/null | head -1 || echo missing)"
|
||||||
echo "GIT=\$(git --version 2>/dev/null || echo missing)"
|
echo "GIT=\$(git --version 2>/dev/null || echo missing)"
|
||||||
echo "DNS_OK=\$(getent hosts $domain 2>/dev/null | wc -l | tr -d ' ')"
|
echo "DNS_OK=\$(getent hosts $domain 2>/dev/null | wc -l | tr -d ' ')"
|
||||||
|
echo "PORT_80_LISTENERS=\$(ss -ltn '( sport = :80 )' 2>/dev/null | tail -n +2 | wc -l | tr -d ' ')"
|
||||||
echo "PORT_443_LISTENERS=\$(ss -ltn '( sport = :443 )' 2>/dev/null | tail -n +2 | wc -l | tr -d ' ')"
|
echo "PORT_443_LISTENERS=\$(ss -ltn '( sport = :443 )' 2>/dev/null | tail -n +2 | wc -l | tr -d ' ')"
|
||||||
echo "BRIDGE_DNS_OK=\$(getent hosts $bridge 2>/dev/null | wc -l | tr -d ' ')"
|
echo "BRIDGE_DNS_OK=\$(getent hosts $bridge 2>/dev/null | wc -l | tr -d ' ')"
|
||||||
|
echo "BRIDGE_PORT_80_LISTENERS=\$(ss -ltn '( sport = :80 )' 2>/dev/null | tail -n +2 | wc -l | tr -d ' ')"
|
||||||
echo "BRIDGE_PORT_443_LISTENERS=\$(ss -ltn '( sport = :443 )' 2>/dev/null | tail -n +2 | wc -l | tr -d ' ')"
|
echo "BRIDGE_PORT_443_LISTENERS=\$(ss -ltn '( sport = :443 )' 2>/dev/null | tail -n +2 | wc -l | tr -d ' ')"
|
||||||
|
PORT_80_OPEN=yes
|
||||||
PORT_443_OPEN=yes
|
PORT_443_OPEN=yes
|
||||||
if command -v ufw >/dev/null 2>&1; then
|
if command -v ufw >/dev/null 2>&1; then
|
||||||
UFW_STATUS="\$(ufw status 2>/dev/null || sudo -n ufw status 2>/dev/null || echo unavailable)"
|
UFW_STATUS="\$(ufw status 2>/dev/null || sudo -n ufw status 2>/dev/null || echo unavailable)"
|
||||||
if printf '%s' "\$UFW_STATUS" | grep -qi 'Status: inactive'; then
|
if printf '%s' "\$UFW_STATUS" | grep -qi 'Status: inactive'; then
|
||||||
|
PORT_80_OPEN=yes
|
||||||
PORT_443_OPEN=yes
|
PORT_443_OPEN=yes
|
||||||
elif printf '%s' "\$UFW_STATUS" | grep -Eqi '(^|[[:space:]])(443(/tcp)?|https)[[:space:]]+ALLOW'; then
|
elif printf '%s' "\$UFW_STATUS" | grep -Eqi '(^|[[:space:]])(80(/tcp)?|http)[[:space:]]+ALLOW'; then
|
||||||
|
PORT_80_OPEN=yes
|
||||||
|
else
|
||||||
|
PORT_80_OPEN=no
|
||||||
|
fi
|
||||||
|
if printf '%s' "\$UFW_STATUS" | grep -Eqi '(^|[[:space:]])(443(/tcp)?|https)[[:space:]]+ALLOW'; then
|
||||||
PORT_443_OPEN=yes
|
PORT_443_OPEN=yes
|
||||||
else
|
else
|
||||||
PORT_443_OPEN=no
|
PORT_443_OPEN=no
|
||||||
@ -55,6 +64,14 @@ if command -v ufw >/dev/null 2>&1; then
|
|||||||
elif command -v firewall-cmd >/dev/null 2>&1; then
|
elif command -v firewall-cmd >/dev/null 2>&1; then
|
||||||
FIREWALL_STATE="\$(firewall-cmd --state 2>/dev/null || sudo -n firewall-cmd --state 2>/dev/null || echo not-running)"
|
FIREWALL_STATE="\$(firewall-cmd --state 2>/dev/null || sudo -n firewall-cmd --state 2>/dev/null || echo not-running)"
|
||||||
if [ "\$FIREWALL_STATE" = "running" ]; then
|
if [ "\$FIREWALL_STATE" = "running" ]; then
|
||||||
|
if firewall-cmd --quiet --query-service=http 2>/dev/null ||
|
||||||
|
sudo -n firewall-cmd --quiet --query-service=http 2>/dev/null ||
|
||||||
|
firewall-cmd --quiet --query-port=80/tcp 2>/dev/null ||
|
||||||
|
sudo -n firewall-cmd --quiet --query-port=80/tcp 2>/dev/null; then
|
||||||
|
PORT_80_OPEN=yes
|
||||||
|
else
|
||||||
|
PORT_80_OPEN=no
|
||||||
|
fi
|
||||||
if firewall-cmd --quiet --query-service=https 2>/dev/null ||
|
if firewall-cmd --quiet --query-service=https 2>/dev/null ||
|
||||||
sudo -n firewall-cmd --quiet --query-service=https 2>/dev/null ||
|
sudo -n firewall-cmd --quiet --query-service=https 2>/dev/null ||
|
||||||
firewall-cmd --quiet --query-port=443/tcp 2>/dev/null ||
|
firewall-cmd --quiet --query-port=443/tcp 2>/dev/null ||
|
||||||
@ -64,10 +81,13 @@ if command -v ufw >/dev/null 2>&1; then
|
|||||||
PORT_443_OPEN=no
|
PORT_443_OPEN=no
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
PORT_80_OPEN=yes
|
||||||
PORT_443_OPEN=yes
|
PORT_443_OPEN=yes
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
echo "PORT_80_OPEN=\$PORT_80_OPEN"
|
||||||
echo "PORT_443_OPEN=\$PORT_443_OPEN"
|
echo "PORT_443_OPEN=\$PORT_443_OPEN"
|
||||||
|
echo "BRIDGE_PORT_80_OPEN=\$PORT_80_OPEN"
|
||||||
echo "BRIDGE_PORT_443_OPEN=\$PORT_443_OPEN"
|
echo "BRIDGE_PORT_443_OPEN=\$PORT_443_OPEN"
|
||||||
''';
|
''';
|
||||||
}
|
}
|
||||||
@ -91,11 +111,20 @@ echo "BRIDGE_PORT_443_OPEN=\$PORT_443_OPEN"
|
|||||||
ansibleVersion: values['ANSIBLE'] ?? 'missing',
|
ansibleVersion: values['ANSIBLE'] ?? 'missing',
|
||||||
gitVersion: values['GIT'] ?? 'missing',
|
gitVersion: values['GIT'] ?? 'missing',
|
||||||
dnsAddressCount: int.tryParse(values['DNS_OK'] ?? '') ?? 0,
|
dnsAddressCount: int.tryParse(values['DNS_OK'] ?? '') ?? 0,
|
||||||
|
port80ListenerCount:
|
||||||
|
int.tryParse(values['PORT_80_LISTENERS'] ?? '') ?? 0,
|
||||||
|
port80Open: (values['PORT_80_OPEN'] ?? '').toLowerCase() != 'no',
|
||||||
port443ListenerCount:
|
port443ListenerCount:
|
||||||
int.tryParse(values['PORT_443_LISTENERS'] ?? '') ?? 0,
|
int.tryParse(values['PORT_443_LISTENERS'] ?? '') ?? 0,
|
||||||
port443Open: (values['PORT_443_OPEN'] ?? '').toLowerCase() != 'no',
|
port443Open: (values['PORT_443_OPEN'] ?? '').toLowerCase() != 'no',
|
||||||
bridgeDnsAddressCount:
|
bridgeDnsAddressCount:
|
||||||
int.tryParse(values['BRIDGE_DNS_OK'] ?? '') ?? 0,
|
int.tryParse(values['BRIDGE_DNS_OK'] ?? '') ?? 0,
|
||||||
|
bridgePort80ListenerCount:
|
||||||
|
int.tryParse(values['BRIDGE_PORT_80_LISTENERS'] ?? '') ?? 0,
|
||||||
|
bridgePort80Open:
|
||||||
|
(values['BRIDGE_PORT_80_OPEN'] ?? values['PORT_80_OPEN'] ?? '')
|
||||||
|
.toLowerCase() !=
|
||||||
|
'no',
|
||||||
bridgePort443ListenerCount:
|
bridgePort443ListenerCount:
|
||||||
int.tryParse(values['BRIDGE_PORT_443_LISTENERS'] ?? '') ?? 0,
|
int.tryParse(values['BRIDGE_PORT_443_LISTENERS'] ?? '') ?? 0,
|
||||||
bridgePort443Open:
|
bridgePort443Open:
|
||||||
|
|||||||
@ -32,11 +32,13 @@ class _WorkspaceManagementFormState extends State<WorkspaceManagementForm> {
|
|||||||
late final TextEditingController _sudoController;
|
late final TextEditingController _sudoController;
|
||||||
late final TextEditingController _installPathController;
|
late final TextEditingController _installPathController;
|
||||||
final List<_ExtraRowControllers> _extraRows = <_ExtraRowControllers>[];
|
final List<_ExtraRowControllers> _extraRows = <_ExtraRowControllers>[];
|
||||||
|
bool _syncingFromController = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final c = widget.controller;
|
final c = widget.controller;
|
||||||
|
widget.controller.addListener(_handleControllerUpdate);
|
||||||
_serverController = TextEditingController(text: c.serverAddress);
|
_serverController = TextEditingController(text: c.serverAddress);
|
||||||
_domainController = TextEditingController(text: c.workspaceDomain);
|
_domainController = TextEditingController(text: c.workspaceDomain);
|
||||||
_userController = TextEditingController(text: c.sshUsername);
|
_userController = TextEditingController(text: c.sshUsername);
|
||||||
@ -57,8 +59,67 @@ class _WorkspaceManagementFormState extends State<WorkspaceManagementForm> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleControllerUpdate() {
|
||||||
|
if (!mounted || _syncingFromController) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_syncingFromController = true;
|
||||||
|
try {
|
||||||
|
_syncText(_serverController, widget.controller.serverAddress);
|
||||||
|
_syncText(_domainController, widget.controller.workspaceDomain);
|
||||||
|
_syncText(_userController, widget.controller.sshUsername);
|
||||||
|
_syncText(_passwordController, widget.controller.sshPassword ?? '');
|
||||||
|
_syncText(_keyController, widget.controller.sshKeyContent ?? '');
|
||||||
|
_syncText(_keyPathController, widget.controller.sshKeyPath ?? '');
|
||||||
|
_syncText(_portController, widget.controller.sshPort.toString());
|
||||||
|
_syncText(_sudoController, widget.controller.sudoPassword ?? '');
|
||||||
|
_syncText(_installPathController, widget.controller.installPath);
|
||||||
|
_syncExtraRows(widget.controller.extraConfigs);
|
||||||
|
} finally {
|
||||||
|
_syncingFromController = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _syncText(TextEditingController controller, String value) {
|
||||||
|
if (controller.text != value) {
|
||||||
|
controller.value = controller.value.copyWith(
|
||||||
|
text: value,
|
||||||
|
selection: TextSelection.collapsed(offset: value.length),
|
||||||
|
composing: TextRange.empty,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _syncExtraRows(List<WorkspaceExtraConfig> configs) {
|
||||||
|
if (_extraRows.length != configs.length) {
|
||||||
|
for (final row in _extraRows) {
|
||||||
|
row.dispose();
|
||||||
|
}
|
||||||
|
_extraRows
|
||||||
|
..clear()
|
||||||
|
..addAll(
|
||||||
|
configs.map(
|
||||||
|
(row) => _ExtraRowControllers(
|
||||||
|
keyController: TextEditingController(text: row.key),
|
||||||
|
valueController: TextEditingController(text: row.value),
|
||||||
|
noteController: TextEditingController(text: row.note),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (var i = 0; i < configs.length; i++) {
|
||||||
|
final source = configs[i];
|
||||||
|
final row = _extraRows[i];
|
||||||
|
_syncText(row.keyController, source.key);
|
||||||
|
_syncText(row.valueController, source.value);
|
||||||
|
_syncText(row.noteController, source.note);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
widget.controller.removeListener(_handleControllerUpdate);
|
||||||
_serverController.dispose();
|
_serverController.dispose();
|
||||||
_domainController.dispose();
|
_domainController.dispose();
|
||||||
_userController.dispose();
|
_userController.dispose();
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:yaml/yaml.dart';
|
import 'package:yaml/yaml.dart';
|
||||||
|
|
||||||
@ -11,12 +13,15 @@ class WorkspaceProvisionController extends ChangeNotifier {
|
|||||||
WorkspaceProvisionController({
|
WorkspaceProvisionController({
|
||||||
WorkspaceSshExecutor? executor,
|
WorkspaceSshExecutor? executor,
|
||||||
String initialWorkspaceDomain = '',
|
String initialWorkspaceDomain = '',
|
||||||
|
Future<void> Function(String host)? externalPortProbe,
|
||||||
}) : executor = executor ?? const DartSshExecutor(),
|
}) : executor = executor ?? const DartSshExecutor(),
|
||||||
workspaceDomain = initialWorkspaceDomain {
|
workspaceDomain = initialWorkspaceDomain,
|
||||||
|
externalPortProbe = externalPortProbe ?? _probeExternalPorts {
|
||||||
steps = defaultProvisionSteps();
|
steps = defaultProvisionSteps();
|
||||||
}
|
}
|
||||||
|
|
||||||
final WorkspaceSshExecutor executor;
|
final WorkspaceSshExecutor executor;
|
||||||
|
final Future<void> Function(String host) externalPortProbe;
|
||||||
|
|
||||||
String serverAddress = '';
|
String serverAddress = '';
|
||||||
String workspaceDomain = '';
|
String workspaceDomain = '';
|
||||||
@ -155,6 +160,7 @@ class WorkspaceProvisionController extends ChangeNotifier {
|
|||||||
onStepUpdate: _setStep,
|
onStepUpdate: _setStep,
|
||||||
onLog: _appendLog,
|
onLog: _appendLog,
|
||||||
);
|
);
|
||||||
|
await _verifyDeploymentReadiness();
|
||||||
for (final step in steps) {
|
for (final step in steps) {
|
||||||
if (step.status == StepStatus.pending || step.status == StepStatus.running) {
|
if (step.status == StepStatus.pending || step.status == StepStatus.running) {
|
||||||
_setStep(step.id, StepStatus.success, null);
|
_setStep(step.id, StepStatus.success, null);
|
||||||
@ -358,21 +364,100 @@ class WorkspaceProvisionController extends ChangeNotifier {
|
|||||||
'The target server cannot resolve $bridgeDomain. Add an A record for this host at your DNS provider, then confirm dig/getent returns an address on the VPS.',
|
'The target server cannot resolve $bridgeDomain. Add an A record for this host at your DNS provider, then confirm dig/getent returns an address on the VPS.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!info.bridgePort443Open) {
|
|
||||||
return appText(
|
|
||||||
'$bridgeDomain 的 443 端口未开放,请先放通 HTTPS 访问。',
|
|
||||||
'Port 443 is not open for $bridgeDomain. Allow HTTPS traffic first.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!info.isBridgePort443Available) {
|
|
||||||
return appText(
|
|
||||||
'$bridgeDomain 的 443 端口已被占用,请先释放。',
|
|
||||||
'Port 443 is already in use for $bridgeDomain.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _verifyDeploymentReadiness() async {
|
||||||
|
final result = await executor.execute(
|
||||||
|
sshConfig(),
|
||||||
|
_coreServicesCheckCommand(),
|
||||||
|
);
|
||||||
|
for (final line in result.combinedOutput.split(RegExp(r'\r?\n'))) {
|
||||||
|
if (line.trim().isNotEmpty) {
|
||||||
|
_appendLog(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!result.success) {
|
||||||
|
throw PlaybookRunException(
|
||||||
|
appText(
|
||||||
|
'部署完成后核心服务校验失败,请查看日志。',
|
||||||
|
'Core service validation failed after deployment. Check logs.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final serviceStates = <String, String>{};
|
||||||
|
for (final raw in result.combinedOutput.split(RegExp(r'\r?\n'))) {
|
||||||
|
final match = RegExp(r'^SERVICE_(.+?)=(.+)$').firstMatch(raw.trim());
|
||||||
|
if (match != null) {
|
||||||
|
serviceStates[match.group(1)!] = match.group(2)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final unhealthy = serviceStates.entries
|
||||||
|
.where((entry) => entry.value.trim().toLowerCase() != 'active')
|
||||||
|
.map((entry) => entry.key)
|
||||||
|
.toList();
|
||||||
|
if (unhealthy.isNotEmpty) {
|
||||||
|
throw PlaybookRunException(
|
||||||
|
appText(
|
||||||
|
'部署完成后核心服务未处于 active:${unhealthy.join(', ')}。',
|
||||||
|
'Core services are not active after deployment: ${unhealthy.join(', ')}.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await externalPortProbe(bridgeDomain.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
String _coreServicesCheckCommand() {
|
||||||
|
final services = <String>[
|
||||||
|
'caddy',
|
||||||
|
'xworkmate-bridge',
|
||||||
|
'openclaw-gateway',
|
||||||
|
'hermes-gateway',
|
||||||
|
];
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
buffer.writeln('set +e');
|
||||||
|
for (final service in services) {
|
||||||
|
final unit = shellQuote('$service.service');
|
||||||
|
final envKey = service.toUpperCase().replaceAll('-', '_');
|
||||||
|
buffer.writeln('if systemctl list-unit-files $unit >/dev/null 2>&1 || systemctl status $unit >/dev/null 2>&1; then');
|
||||||
|
buffer.writeln(' STATE=\$(systemctl is-active $unit 2>/dev/null || echo inactive)');
|
||||||
|
buffer.writeln(' echo SERVICE_$envKey=\$STATE');
|
||||||
|
buffer.writeln('fi');
|
||||||
|
}
|
||||||
|
buffer.writeln('exit 0');
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> _probeExternalPorts(String host) async {
|
||||||
|
final bridgeHost = host.trim();
|
||||||
|
if (bridgeHost.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final probeFailures = <String>[];
|
||||||
|
for (final port in <int>[80, 443]) {
|
||||||
|
try {
|
||||||
|
final socket = await Socket.connect(
|
||||||
|
bridgeHost,
|
||||||
|
port,
|
||||||
|
timeout: const Duration(seconds: 3),
|
||||||
|
);
|
||||||
|
socket.destroy();
|
||||||
|
} catch (_) {
|
||||||
|
probeFailures.add('$port');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (probeFailures.isNotEmpty) {
|
||||||
|
throw PlaybookRunException(
|
||||||
|
appText(
|
||||||
|
'部署已完成,但外部探测仍然不通:$bridgeHost 的 ${probeFailures.join(' / ')} 端口未真正放行。',
|
||||||
|
'Deployment finished, but external probes still fail: $bridgeHost ports ${probeFailures.join(' / ')} are not truly open.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
sshPassword = null;
|
sshPassword = null;
|
||||||
|
|||||||
@ -56,9 +56,13 @@ class ServerInfo {
|
|||||||
required this.ansibleVersion,
|
required this.ansibleVersion,
|
||||||
required this.gitVersion,
|
required this.gitVersion,
|
||||||
required this.dnsAddressCount,
|
required this.dnsAddressCount,
|
||||||
|
required this.port80ListenerCount,
|
||||||
|
required this.port80Open,
|
||||||
required this.port443ListenerCount,
|
required this.port443ListenerCount,
|
||||||
required this.port443Open,
|
required this.port443Open,
|
||||||
required this.bridgeDnsAddressCount,
|
required this.bridgeDnsAddressCount,
|
||||||
|
required this.bridgePort80ListenerCount,
|
||||||
|
required this.bridgePort80Open,
|
||||||
required this.bridgePort443ListenerCount,
|
required this.bridgePort443ListenerCount,
|
||||||
required this.bridgePort443Open,
|
required this.bridgePort443Open,
|
||||||
});
|
});
|
||||||
@ -72,9 +76,13 @@ class ServerInfo {
|
|||||||
final String ansibleVersion;
|
final String ansibleVersion;
|
||||||
final String gitVersion;
|
final String gitVersion;
|
||||||
final int dnsAddressCount;
|
final int dnsAddressCount;
|
||||||
|
final int port80ListenerCount;
|
||||||
|
final bool port80Open;
|
||||||
final int port443ListenerCount;
|
final int port443ListenerCount;
|
||||||
final bool port443Open;
|
final bool port443Open;
|
||||||
final int bridgeDnsAddressCount;
|
final int bridgeDnsAddressCount;
|
||||||
|
final int bridgePort80ListenerCount;
|
||||||
|
final bool bridgePort80Open;
|
||||||
final int bridgePort443ListenerCount;
|
final int bridgePort443ListenerCount;
|
||||||
final bool bridgePort443Open;
|
final bool bridgePort443Open;
|
||||||
|
|
||||||
@ -82,8 +90,10 @@ class ServerInfo {
|
|||||||
bool get ansibleMissing => _isMissing(ansibleVersion);
|
bool get ansibleMissing => _isMissing(ansibleVersion);
|
||||||
bool get hasMissingPrerequisites => gitMissing || ansibleMissing;
|
bool get hasMissingPrerequisites => gitMissing || ansibleMissing;
|
||||||
bool get dnsResolved => dnsAddressCount > 0;
|
bool get dnsResolved => dnsAddressCount > 0;
|
||||||
|
bool get isPort80Available => port80ListenerCount == 0;
|
||||||
bool get isPort443Available => port443ListenerCount == 0;
|
bool get isPort443Available => port443ListenerCount == 0;
|
||||||
bool get bridgeDnsResolved => bridgeDnsAddressCount > 0;
|
bool get bridgeDnsResolved => bridgeDnsAddressCount > 0;
|
||||||
|
bool get isBridgePort80Available => bridgePort80ListenerCount == 0;
|
||||||
bool get isBridgePort443Available => bridgePort443ListenerCount == 0;
|
bool get isBridgePort443Available => bridgePort443ListenerCount == 0;
|
||||||
|
|
||||||
String get displaySummary {
|
String get displaySummary {
|
||||||
@ -93,9 +103,13 @@ class ServerInfo {
|
|||||||
if (arch.trim().isNotEmpty) arch.trim(),
|
if (arch.trim().isNotEmpty) arch.trim(),
|
||||||
sudo,
|
sudo,
|
||||||
dnsResolved ? 'dns=ok' : 'dns=missing',
|
dnsResolved ? 'dns=ok' : 'dns=missing',
|
||||||
|
port80Open ? '80=open' : '80=blocked',
|
||||||
|
isPort80Available ? '80=free' : '80=busy',
|
||||||
port443Open ? '443=open' : '443=blocked',
|
port443Open ? '443=open' : '443=blocked',
|
||||||
isPort443Available ? '443=free' : '443=busy',
|
isPort443Available ? '443=free' : '443=busy',
|
||||||
bridgeDnsResolved ? 'bridge-dns=ok' : 'bridge-dns=missing',
|
bridgeDnsResolved ? 'bridge-dns=ok' : 'bridge-dns=missing',
|
||||||
|
bridgePort80Open ? 'bridge-80=open' : 'bridge-80=blocked',
|
||||||
|
isBridgePort80Available ? 'bridge-80=free' : 'bridge-80=busy',
|
||||||
bridgePort443Open ? 'bridge-443=open' : 'bridge-443=blocked',
|
bridgePort443Open ? 'bridge-443=open' : 'bridge-443=blocked',
|
||||||
isBridgePort443Available ? 'bridge-443=free' : 'bridge-443=busy',
|
isBridgePort443Available ? 'bridge-443=free' : 'bridge-443=busy',
|
||||||
].join(', ');
|
].join(', ');
|
||||||
|
|||||||
@ -47,10 +47,14 @@ CADDY=missing
|
|||||||
ANSIBLE=missing
|
ANSIBLE=missing
|
||||||
GIT=git version 2.34.1
|
GIT=git version 2.34.1
|
||||||
DNS_OK=1
|
DNS_OK=1
|
||||||
|
PORT_80_LISTENERS=0
|
||||||
PORT_443_LISTENERS=0
|
PORT_443_LISTENERS=0
|
||||||
|
PORT_80_OPEN=yes
|
||||||
PORT_443_OPEN=yes
|
PORT_443_OPEN=yes
|
||||||
BRIDGE_DNS_OK=1
|
BRIDGE_DNS_OK=1
|
||||||
|
BRIDGE_PORT_80_LISTENERS=0
|
||||||
BRIDGE_PORT_443_LISTENERS=0
|
BRIDGE_PORT_443_LISTENERS=0
|
||||||
|
BRIDGE_PORT_80_OPEN=yes
|
||||||
BRIDGE_PORT_443_OPEN=yes
|
BRIDGE_PORT_443_OPEN=yes
|
||||||
''');
|
''');
|
||||||
|
|
||||||
@ -60,9 +64,13 @@ BRIDGE_PORT_443_OPEN=yes
|
|||||||
expect(info.ansibleMissing, isTrue);
|
expect(info.ansibleMissing, isTrue);
|
||||||
expect(info.gitMissing, isFalse);
|
expect(info.gitMissing, isFalse);
|
||||||
expect(info.dnsResolved, isTrue);
|
expect(info.dnsResolved, isTrue);
|
||||||
|
expect(info.port80Open, isTrue);
|
||||||
|
expect(info.isPort80Available, isTrue);
|
||||||
expect(info.port443Open, isTrue);
|
expect(info.port443Open, isTrue);
|
||||||
expect(info.isPort443Available, isTrue);
|
expect(info.isPort443Available, isTrue);
|
||||||
expect(info.bridgeDnsResolved, isTrue);
|
expect(info.bridgeDnsResolved, isTrue);
|
||||||
|
expect(info.bridgePort80Open, isTrue);
|
||||||
|
expect(info.isBridgePort80Available, isTrue);
|
||||||
expect(info.bridgePort443Open, isTrue);
|
expect(info.bridgePort443Open, isTrue);
|
||||||
expect(info.isBridgePort443Available, isTrue);
|
expect(info.isBridgePort443Available, isTrue);
|
||||||
});
|
});
|
||||||
@ -147,10 +155,14 @@ CADDY=missing
|
|||||||
ANSIBLE=ansible [core 2.16]
|
ANSIBLE=ansible [core 2.16]
|
||||||
GIT=git version 2.43.0
|
GIT=git version 2.43.0
|
||||||
DNS_OK=1
|
DNS_OK=1
|
||||||
|
PORT_80_LISTENERS=0
|
||||||
PORT_443_LISTENERS=0
|
PORT_443_LISTENERS=0
|
||||||
|
PORT_80_OPEN=yes
|
||||||
PORT_443_OPEN=yes
|
PORT_443_OPEN=yes
|
||||||
BRIDGE_DNS_OK=1
|
BRIDGE_DNS_OK=1
|
||||||
|
BRIDGE_PORT_80_LISTENERS=0
|
||||||
BRIDGE_PORT_443_LISTENERS=0
|
BRIDGE_PORT_443_LISTENERS=0
|
||||||
|
BRIDGE_PORT_80_OPEN=yes
|
||||||
BRIDGE_PORT_443_OPEN=yes
|
BRIDGE_PORT_443_OPEN=yes
|
||||||
''',
|
''',
|
||||||
stderr: '',
|
stderr: '',
|
||||||
@ -180,13 +192,26 @@ BRIDGE_PORT_443_OPEN=yes
|
|||||||
commandResults: [
|
commandResults: [
|
||||||
const SshResult(exitCode: 0, stdout: 'pulled', stderr: ''),
|
const SshResult(exitCode: 0, stdout: 'pulled', stderr: ''),
|
||||||
const SshResult(exitCode: 0, stdout: 'wrote', stderr: ''),
|
const SshResult(exitCode: 0, stdout: 'wrote', stderr: ''),
|
||||||
|
const SshResult(
|
||||||
|
exitCode: 0,
|
||||||
|
stdout: '''
|
||||||
|
SERVICE_CADDY=active
|
||||||
|
SERVICE_XWORKMATE_BRIDGE=active
|
||||||
|
SERVICE_OPENCLAW_GATEWAY=active
|
||||||
|
SERVICE_HERMES_GATEWAY=active
|
||||||
|
''',
|
||||||
|
stderr: '',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
streamingChunks: [
|
streamingChunks: [
|
||||||
'TASK [Install desktop packages]\nok: [localhost]\n',
|
'TASK [Install desktop packages]\nok: [localhost]\n',
|
||||||
'TASK [Configure caddy TLS]\nchanged: [localhost]\n',
|
'TASK [Configure caddy TLS]\nchanged: [localhost]\n',
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
final controller = WorkspaceProvisionController(executor: executor);
|
final controller = WorkspaceProvisionController(
|
||||||
|
executor: executor,
|
||||||
|
externalPortProbe: (_) async {},
|
||||||
|
);
|
||||||
addTearDown(controller.dispose);
|
addTearDown(controller.dispose);
|
||||||
controller.updateForm(
|
controller.updateForm(
|
||||||
serverAddress: '203.0.113.10',
|
serverAddress: '203.0.113.10',
|
||||||
@ -203,9 +228,13 @@ BRIDGE_PORT_443_OPEN=yes
|
|||||||
ansibleVersion: 'ansible [core 2.14]',
|
ansibleVersion: 'ansible [core 2.14]',
|
||||||
gitVersion: 'git version 2.34.1',
|
gitVersion: 'git version 2.34.1',
|
||||||
dnsAddressCount: 1,
|
dnsAddressCount: 1,
|
||||||
|
port80ListenerCount: 0,
|
||||||
|
port80Open: true,
|
||||||
port443ListenerCount: 0,
|
port443ListenerCount: 0,
|
||||||
port443Open: true,
|
port443Open: true,
|
||||||
bridgeDnsAddressCount: 1,
|
bridgeDnsAddressCount: 1,
|
||||||
|
bridgePort80ListenerCount: 0,
|
||||||
|
bridgePort80Open: true,
|
||||||
bridgePort443ListenerCount: 0,
|
bridgePort443ListenerCount: 0,
|
||||||
bridgePort443Open: true,
|
bridgePort443Open: true,
|
||||||
);
|
);
|
||||||
@ -221,7 +250,7 @@ BRIDGE_PORT_443_OPEN=yes
|
|||||||
expect(executor.commands.join('\n'), contains('ansible-playbook'));
|
expect(executor.commands.join('\n'), contains('ansible-playbook'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('precheck blocks when 443 is not open', () async {
|
test('precheck does not block when 443 is not open', () async {
|
||||||
final controller = WorkspaceProvisionController(executor: _FakeSshExecutor());
|
final controller = WorkspaceProvisionController(executor: _FakeSshExecutor());
|
||||||
addTearDown(controller.dispose);
|
addTearDown(controller.dispose);
|
||||||
controller.updateForm(
|
controller.updateForm(
|
||||||
@ -239,17 +268,18 @@ BRIDGE_PORT_443_OPEN=yes
|
|||||||
ansibleVersion: 'ansible [core 2.14]',
|
ansibleVersion: 'ansible [core 2.14]',
|
||||||
gitVersion: 'git version 2.34.1',
|
gitVersion: 'git version 2.34.1',
|
||||||
dnsAddressCount: 1,
|
dnsAddressCount: 1,
|
||||||
|
port80ListenerCount: 0,
|
||||||
|
port80Open: true,
|
||||||
port443ListenerCount: 0,
|
port443ListenerCount: 0,
|
||||||
port443Open: false,
|
port443Open: false,
|
||||||
bridgeDnsAddressCount: 1,
|
bridgeDnsAddressCount: 1,
|
||||||
|
bridgePort80ListenerCount: 0,
|
||||||
|
bridgePort80Open: true,
|
||||||
bridgePort443ListenerCount: 0,
|
bridgePort443ListenerCount: 0,
|
||||||
bridgePort443Open: false,
|
bridgePort443Open: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(controller.validatePrecheckBlockingIssue(), isNull);
|
||||||
controller.validatePrecheckBlockingIssue(),
|
|
||||||
contains('443'),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('precheck blocks when bridge DNS is missing', () async {
|
test('precheck blocks when bridge DNS is missing', () async {
|
||||||
@ -270,9 +300,13 @@ BRIDGE_PORT_443_OPEN=yes
|
|||||||
ansibleVersion: 'ansible [core 2.14]',
|
ansibleVersion: 'ansible [core 2.14]',
|
||||||
gitVersion: 'git version 2.34.1',
|
gitVersion: 'git version 2.34.1',
|
||||||
dnsAddressCount: 1,
|
dnsAddressCount: 1,
|
||||||
|
port80ListenerCount: 0,
|
||||||
|
port80Open: true,
|
||||||
port443ListenerCount: 0,
|
port443ListenerCount: 0,
|
||||||
port443Open: true,
|
port443Open: true,
|
||||||
bridgeDnsAddressCount: 0,
|
bridgeDnsAddressCount: 0,
|
||||||
|
bridgePort80ListenerCount: 0,
|
||||||
|
bridgePort80Open: true,
|
||||||
bridgePort443ListenerCount: 0,
|
bridgePort443ListenerCount: 0,
|
||||||
bridgePort443Open: true,
|
bridgePort443Open: true,
|
||||||
);
|
);
|
||||||
@ -301,9 +335,13 @@ BRIDGE_PORT_443_OPEN=yes
|
|||||||
ansibleVersion: 'ansible [core 2.14]',
|
ansibleVersion: 'ansible [core 2.14]',
|
||||||
gitVersion: 'git version 2.34.1',
|
gitVersion: 'git version 2.34.1',
|
||||||
dnsAddressCount: 1,
|
dnsAddressCount: 1,
|
||||||
|
port80ListenerCount: 0,
|
||||||
|
port80Open: true,
|
||||||
port443ListenerCount: 0,
|
port443ListenerCount: 0,
|
||||||
port443Open: true,
|
port443Open: true,
|
||||||
bridgeDnsAddressCount: 1,
|
bridgeDnsAddressCount: 1,
|
||||||
|
bridgePort80ListenerCount: 0,
|
||||||
|
bridgePort80Open: true,
|
||||||
bridgePort443ListenerCount: 0,
|
bridgePort443ListenerCount: 0,
|
||||||
bridgePort443Open: true,
|
bridgePort443Open: true,
|
||||||
);
|
);
|
||||||
@ -348,6 +386,62 @@ extra_configs:
|
|||||||
expect(controller.extraConfigs.first.value, 'deepseek-new');
|
expect(controller.extraConfigs.first.value, 'deepseek-new');
|
||||||
expect(controller.extraConfigs.last.value, '');
|
expect(controller.extraConfigs.last.value, '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('post deploy verification fails when external probe does not connect', () async {
|
||||||
|
final controller = WorkspaceProvisionController(
|
||||||
|
executor: _FakeSshExecutor(
|
||||||
|
commandResults: [
|
||||||
|
const SshResult(exitCode: 0, stdout: 'pulled', stderr: ''),
|
||||||
|
const SshResult(exitCode: 0, stdout: 'wrote', stderr: ''),
|
||||||
|
const SshResult(
|
||||||
|
exitCode: 0,
|
||||||
|
stdout: '''
|
||||||
|
SERVICE_CADDY=active
|
||||||
|
SERVICE_XWORKMATE_BRIDGE=active
|
||||||
|
''',
|
||||||
|
stderr: '',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
streamingChunks: [
|
||||||
|
'TASK [Configure caddy TLS]\nchanged: [localhost]\n',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
externalPortProbe: (host) async {
|
||||||
|
throw PlaybookRunException('probe failed for $host');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
addTearDown(controller.dispose);
|
||||||
|
controller.updateForm(
|
||||||
|
serverAddress: '203.0.113.10',
|
||||||
|
workspaceDomain: 'workspace.example.com',
|
||||||
|
sshKeyContent: 'key',
|
||||||
|
);
|
||||||
|
controller.serverInfo = const ServerInfo(
|
||||||
|
os: 'Ubuntu 22.04',
|
||||||
|
arch: 'x86_64',
|
||||||
|
sudoAvailable: true,
|
||||||
|
dockerVersion: 'missing',
|
||||||
|
systemdVersion: 'systemd 249',
|
||||||
|
caddyVersion: 'missing',
|
||||||
|
ansibleVersion: 'ansible [core 2.14]',
|
||||||
|
gitVersion: 'git version 2.34.1',
|
||||||
|
dnsAddressCount: 1,
|
||||||
|
port80ListenerCount: 0,
|
||||||
|
port80Open: true,
|
||||||
|
port443ListenerCount: 0,
|
||||||
|
port443Open: true,
|
||||||
|
bridgeDnsAddressCount: 1,
|
||||||
|
bridgePort80ListenerCount: 0,
|
||||||
|
bridgePort80Open: true,
|
||||||
|
bridgePort443ListenerCount: 0,
|
||||||
|
bridgePort443Open: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.createWorkspace();
|
||||||
|
|
||||||
|
expect(controller.phase, ProvisionPhase.failed);
|
||||||
|
expect(controller.errorMessage, contains('probe failed'));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user