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 "GIT=\$(git --version 2>/dev/null || echo missing)"
|
||||
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 "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 ' ')"
|
||||
PORT_80_OPEN=yes
|
||||
PORT_443_OPEN=yes
|
||||
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)"
|
||||
if printf '%s' "\$UFW_STATUS" | grep -qi 'Status: inactive'; then
|
||||
PORT_80_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
|
||||
else
|
||||
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
|
||||
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-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 ||
|
||||
sudo -n firewall-cmd --quiet --query-service=https 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
|
||||
fi
|
||||
else
|
||||
PORT_80_OPEN=yes
|
||||
PORT_443_OPEN=yes
|
||||
fi
|
||||
fi
|
||||
echo "PORT_80_OPEN=\$PORT_80_OPEN"
|
||||
echo "PORT_443_OPEN=\$PORT_443_OPEN"
|
||||
echo "BRIDGE_PORT_80_OPEN=\$PORT_80_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',
|
||||
gitVersion: values['GIT'] ?? 'missing',
|
||||
dnsAddressCount: int.tryParse(values['DNS_OK'] ?? '') ?? 0,
|
||||
port80ListenerCount:
|
||||
int.tryParse(values['PORT_80_LISTENERS'] ?? '') ?? 0,
|
||||
port80Open: (values['PORT_80_OPEN'] ?? '').toLowerCase() != 'no',
|
||||
port443ListenerCount:
|
||||
int.tryParse(values['PORT_443_LISTENERS'] ?? '') ?? 0,
|
||||
port443Open: (values['PORT_443_OPEN'] ?? '').toLowerCase() != 'no',
|
||||
bridgeDnsAddressCount:
|
||||
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:
|
||||
int.tryParse(values['BRIDGE_PORT_443_LISTENERS'] ?? '') ?? 0,
|
||||
bridgePort443Open:
|
||||
|
||||
@ -32,11 +32,13 @@ class _WorkspaceManagementFormState extends State<WorkspaceManagementForm> {
|
||||
late final TextEditingController _sudoController;
|
||||
late final TextEditingController _installPathController;
|
||||
final List<_ExtraRowControllers> _extraRows = <_ExtraRowControllers>[];
|
||||
bool _syncingFromController = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final c = widget.controller;
|
||||
widget.controller.addListener(_handleControllerUpdate);
|
||||
_serverController = TextEditingController(text: c.serverAddress);
|
||||
_domainController = TextEditingController(text: c.workspaceDomain);
|
||||
_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
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_handleControllerUpdate);
|
||||
_serverController.dispose();
|
||||
_domainController.dispose();
|
||||
_userController.dispose();
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
@ -11,12 +13,15 @@ class WorkspaceProvisionController extends ChangeNotifier {
|
||||
WorkspaceProvisionController({
|
||||
WorkspaceSshExecutor? executor,
|
||||
String initialWorkspaceDomain = '',
|
||||
Future<void> Function(String host)? externalPortProbe,
|
||||
}) : executor = executor ?? const DartSshExecutor(),
|
||||
workspaceDomain = initialWorkspaceDomain {
|
||||
workspaceDomain = initialWorkspaceDomain,
|
||||
externalPortProbe = externalPortProbe ?? _probeExternalPorts {
|
||||
steps = defaultProvisionSteps();
|
||||
}
|
||||
|
||||
final WorkspaceSshExecutor executor;
|
||||
final Future<void> Function(String host) externalPortProbe;
|
||||
|
||||
String serverAddress = '';
|
||||
String workspaceDomain = '';
|
||||
@ -155,6 +160,7 @@ class WorkspaceProvisionController extends ChangeNotifier {
|
||||
onStepUpdate: _setStep,
|
||||
onLog: _appendLog,
|
||||
);
|
||||
await _verifyDeploymentReadiness();
|
||||
for (final step in steps) {
|
||||
if (step.status == StepStatus.pending || step.status == StepStatus.running) {
|
||||
_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.',
|
||||
);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
void dispose() {
|
||||
sshPassword = null;
|
||||
|
||||
@ -56,9 +56,13 @@ class ServerInfo {
|
||||
required this.ansibleVersion,
|
||||
required this.gitVersion,
|
||||
required this.dnsAddressCount,
|
||||
required this.port80ListenerCount,
|
||||
required this.port80Open,
|
||||
required this.port443ListenerCount,
|
||||
required this.port443Open,
|
||||
required this.bridgeDnsAddressCount,
|
||||
required this.bridgePort80ListenerCount,
|
||||
required this.bridgePort80Open,
|
||||
required this.bridgePort443ListenerCount,
|
||||
required this.bridgePort443Open,
|
||||
});
|
||||
@ -72,9 +76,13 @@ class ServerInfo {
|
||||
final String ansibleVersion;
|
||||
final String gitVersion;
|
||||
final int dnsAddressCount;
|
||||
final int port80ListenerCount;
|
||||
final bool port80Open;
|
||||
final int port443ListenerCount;
|
||||
final bool port443Open;
|
||||
final int bridgeDnsAddressCount;
|
||||
final int bridgePort80ListenerCount;
|
||||
final bool bridgePort80Open;
|
||||
final int bridgePort443ListenerCount;
|
||||
final bool bridgePort443Open;
|
||||
|
||||
@ -82,8 +90,10 @@ class ServerInfo {
|
||||
bool get ansibleMissing => _isMissing(ansibleVersion);
|
||||
bool get hasMissingPrerequisites => gitMissing || ansibleMissing;
|
||||
bool get dnsResolved => dnsAddressCount > 0;
|
||||
bool get isPort80Available => port80ListenerCount == 0;
|
||||
bool get isPort443Available => port443ListenerCount == 0;
|
||||
bool get bridgeDnsResolved => bridgeDnsAddressCount > 0;
|
||||
bool get isBridgePort80Available => bridgePort80ListenerCount == 0;
|
||||
bool get isBridgePort443Available => bridgePort443ListenerCount == 0;
|
||||
|
||||
String get displaySummary {
|
||||
@ -93,9 +103,13 @@ class ServerInfo {
|
||||
if (arch.trim().isNotEmpty) arch.trim(),
|
||||
sudo,
|
||||
dnsResolved ? 'dns=ok' : 'dns=missing',
|
||||
port80Open ? '80=open' : '80=blocked',
|
||||
isPort80Available ? '80=free' : '80=busy',
|
||||
port443Open ? '443=open' : '443=blocked',
|
||||
isPort443Available ? '443=free' : '443=busy',
|
||||
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',
|
||||
isBridgePort443Available ? 'bridge-443=free' : 'bridge-443=busy',
|
||||
].join(', ');
|
||||
|
||||
@ -47,10 +47,14 @@ CADDY=missing
|
||||
ANSIBLE=missing
|
||||
GIT=git version 2.34.1
|
||||
DNS_OK=1
|
||||
PORT_80_LISTENERS=0
|
||||
PORT_443_LISTENERS=0
|
||||
PORT_80_OPEN=yes
|
||||
PORT_443_OPEN=yes
|
||||
BRIDGE_DNS_OK=1
|
||||
BRIDGE_PORT_80_LISTENERS=0
|
||||
BRIDGE_PORT_443_LISTENERS=0
|
||||
BRIDGE_PORT_80_OPEN=yes
|
||||
BRIDGE_PORT_443_OPEN=yes
|
||||
''');
|
||||
|
||||
@ -60,9 +64,13 @@ BRIDGE_PORT_443_OPEN=yes
|
||||
expect(info.ansibleMissing, isTrue);
|
||||
expect(info.gitMissing, isFalse);
|
||||
expect(info.dnsResolved, isTrue);
|
||||
expect(info.port80Open, isTrue);
|
||||
expect(info.isPort80Available, isTrue);
|
||||
expect(info.port443Open, isTrue);
|
||||
expect(info.isPort443Available, isTrue);
|
||||
expect(info.bridgeDnsResolved, isTrue);
|
||||
expect(info.bridgePort80Open, isTrue);
|
||||
expect(info.isBridgePort80Available, isTrue);
|
||||
expect(info.bridgePort443Open, isTrue);
|
||||
expect(info.isBridgePort443Available, isTrue);
|
||||
});
|
||||
@ -147,10 +155,14 @@ CADDY=missing
|
||||
ANSIBLE=ansible [core 2.16]
|
||||
GIT=git version 2.43.0
|
||||
DNS_OK=1
|
||||
PORT_80_LISTENERS=0
|
||||
PORT_443_LISTENERS=0
|
||||
PORT_80_OPEN=yes
|
||||
PORT_443_OPEN=yes
|
||||
BRIDGE_DNS_OK=1
|
||||
BRIDGE_PORT_80_LISTENERS=0
|
||||
BRIDGE_PORT_443_LISTENERS=0
|
||||
BRIDGE_PORT_80_OPEN=yes
|
||||
BRIDGE_PORT_443_OPEN=yes
|
||||
''',
|
||||
stderr: '',
|
||||
@ -180,13 +192,26 @@ BRIDGE_PORT_443_OPEN=yes
|
||||
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
|
||||
SERVICE_OPENCLAW_GATEWAY=active
|
||||
SERVICE_HERMES_GATEWAY=active
|
||||
''',
|
||||
stderr: '',
|
||||
),
|
||||
],
|
||||
streamingChunks: [
|
||||
'TASK [Install desktop packages]\nok: [localhost]\n',
|
||||
'TASK [Configure caddy TLS]\nchanged: [localhost]\n',
|
||||
],
|
||||
);
|
||||
final controller = WorkspaceProvisionController(executor: executor);
|
||||
final controller = WorkspaceProvisionController(
|
||||
executor: executor,
|
||||
externalPortProbe: (_) async {},
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
controller.updateForm(
|
||||
serverAddress: '203.0.113.10',
|
||||
@ -203,9 +228,13 @@ BRIDGE_PORT_443_OPEN=yes
|
||||
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,
|
||||
);
|
||||
@ -221,7 +250,7 @@ BRIDGE_PORT_443_OPEN=yes
|
||||
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());
|
||||
addTearDown(controller.dispose);
|
||||
controller.updateForm(
|
||||
@ -239,17 +268,18 @@ BRIDGE_PORT_443_OPEN=yes
|
||||
ansibleVersion: 'ansible [core 2.14]',
|
||||
gitVersion: 'git version 2.34.1',
|
||||
dnsAddressCount: 1,
|
||||
port80ListenerCount: 0,
|
||||
port80Open: true,
|
||||
port443ListenerCount: 0,
|
||||
port443Open: false,
|
||||
bridgeDnsAddressCount: 1,
|
||||
bridgePort80ListenerCount: 0,
|
||||
bridgePort80Open: true,
|
||||
bridgePort443ListenerCount: 0,
|
||||
bridgePort443Open: false,
|
||||
);
|
||||
|
||||
expect(
|
||||
controller.validatePrecheckBlockingIssue(),
|
||||
contains('443'),
|
||||
);
|
||||
expect(controller.validatePrecheckBlockingIssue(), isNull);
|
||||
});
|
||||
|
||||
test('precheck blocks when bridge DNS is missing', () async {
|
||||
@ -270,9 +300,13 @@ BRIDGE_PORT_443_OPEN=yes
|
||||
ansibleVersion: 'ansible [core 2.14]',
|
||||
gitVersion: 'git version 2.34.1',
|
||||
dnsAddressCount: 1,
|
||||
port80ListenerCount: 0,
|
||||
port80Open: true,
|
||||
port443ListenerCount: 0,
|
||||
port443Open: true,
|
||||
bridgeDnsAddressCount: 0,
|
||||
bridgePort80ListenerCount: 0,
|
||||
bridgePort80Open: true,
|
||||
bridgePort443ListenerCount: 0,
|
||||
bridgePort443Open: true,
|
||||
);
|
||||
@ -301,9 +335,13 @@ BRIDGE_PORT_443_OPEN=yes
|
||||
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,
|
||||
);
|
||||
@ -348,6 +386,62 @@ extra_configs:
|
||||
expect(controller.extraConfigs.first.value, 'deepseek-new');
|
||||
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