feat: display platform agent status and version
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -11,6 +11,7 @@ AGENT_PORT=3100
|
||||
AGENT_GATEWAY_TOKEN=
|
||||
HEADSCALE_BASE_URL=https://atnmy-hs.duckdns.org
|
||||
HEADSCALE_API_KEY=
|
||||
HEADSCALE_GATEWAY_USERS=local-platform-gateway,platform-gateway
|
||||
HEADSCALE_TEACHING_DEVICE_OWNER=tagged-devices
|
||||
LMS_GATEWAY_TOKEN=change-me
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ export const config = {
|
||||
headscaleBaseUrl: required('HEADSCALE_BASE_URL', 'http://localhost:8080'),
|
||||
headscaleApiKey: process.env.HEADSCALE_API_KEY ?? '',
|
||||
headscaleTeachingDeviceOwner: (process.env.HEADSCALE_TEACHING_DEVICE_OWNER ?? 'tagged-devices').trim().replace(/@+$/, ''),
|
||||
headscaleGatewayUsers: (process.env.HEADSCALE_GATEWAY_USERS ?? process.env.HEADSCALE_GATEWAY_USER ?? 'local-platform-gateway')
|
||||
.split(',')
|
||||
.map((user) => user.trim().replace(/@+$/, ''))
|
||||
.filter(Boolean),
|
||||
lmsGatewayToken: process.env.LMS_GATEWAY_TOKEN ?? process.env.PLATFORM_GATEWAY_TOKEN ?? '',
|
||||
launcherGatewayToken: process.env.LAUNCHER_GATEWAY_TOKEN ?? process.env.LMS_GATEWAY_TOKEN ?? process.env.PLATFORM_GATEWAY_TOKEN ?? '',
|
||||
adminGatewayToken: process.env.ADMIN_GATEWAY_TOKEN ?? process.env.PLATFORM_GATEWAY_TOKEN ?? '',
|
||||
|
||||
@@ -143,6 +143,8 @@ lmsRoutes.get('/lms/platforms/:id/status', async (c) => {
|
||||
sshUser: platform.sshUser ?? null,
|
||||
workspaceRoot: platform.workspaceRoot ?? null,
|
||||
lastSeenAt: platform.lastSeenAt,
|
||||
agentStartedAt: agentHealth.startedAt ?? null,
|
||||
agentUptimeSeconds: agentHealth.uptimeSeconds ?? null,
|
||||
state: platform.state,
|
||||
message: agentHealth.message ?? null,
|
||||
})
|
||||
|
||||
@@ -2,7 +2,12 @@ import { config } from '../config'
|
||||
import type { Platform } from '../types'
|
||||
|
||||
export class AgentClient {
|
||||
async health(platform: Platform): Promise<{ ok: boolean; message?: string }> {
|
||||
async health(platform: Platform): Promise<{
|
||||
ok: boolean;
|
||||
message?: string;
|
||||
startedAt?: string | null;
|
||||
uptimeSeconds?: number | null;
|
||||
}> {
|
||||
if (!platform.agentUrl) return { ok: false, message: 'platform_agent_url_missing' }
|
||||
|
||||
const controller = new AbortController()
|
||||
@@ -14,8 +19,14 @@ export class AgentClient {
|
||||
signal: controller.signal
|
||||
})
|
||||
if (!response.ok) return { ok: false, message: `agent_health_failed:${response.status}` }
|
||||
const payload = await response.json().catch(() => ({ ok: true }))
|
||||
return { ok: payload?.ok !== false }
|
||||
const payload = await response.json().catch(() => ({ ok: true })) as Record<string, unknown>
|
||||
return {
|
||||
ok: payload?.ok !== false,
|
||||
startedAt: typeof payload?.startedAt === 'string' ? payload.startedAt : null,
|
||||
uptimeSeconds: typeof payload?.uptimeSeconds === 'number' && Number.isFinite(payload.uptimeSeconds)
|
||||
? Math.max(0, Math.floor(payload.uptimeSeconds))
|
||||
: null,
|
||||
}
|
||||
} catch (error) {
|
||||
return { ok: false, message: error instanceof Error ? error.message : String(error) }
|
||||
} finally {
|
||||
|
||||
@@ -28,6 +28,7 @@ export type HeadscalePolicyStatus = {
|
||||
}
|
||||
|
||||
const generatedPrefix = 'group:lms-platform-'
|
||||
const gatewayGroup = 'group:lms-platform-gateway'
|
||||
|
||||
export class HeadscaleAdapter {
|
||||
async applyGrant(_grant: AccessGrant): Promise<{ applied: boolean; mode: 'noop' | 'headscale' }> {
|
||||
@@ -77,8 +78,24 @@ export class HeadscaleAdapter {
|
||||
}> {
|
||||
const nodes = await this.listNodes()
|
||||
const unresolved: string[] = []
|
||||
const groups: Record<string, string[]> = {}
|
||||
const acls: HeadscalePolicy['acls'] = []
|
||||
const groups: Record<string, string[]> = {
|
||||
[gatewayGroup]: [...new Set(config.headscaleGatewayUsers.map((user) => this.toHeadscaleUserName(user)))],
|
||||
}
|
||||
const acls: HeadscalePolicy['acls'] = [
|
||||
{
|
||||
// The local bridge initiates discovery from the trusted gateway host.
|
||||
action: 'accept',
|
||||
src: [gatewayGroup],
|
||||
dst: ['tag:teaching-device:*'],
|
||||
},
|
||||
{
|
||||
// DDS discovery and topic payloads use independent UDP flows, so the
|
||||
// teaching device must also be able to reach the bridge on the gateway.
|
||||
action: 'accept',
|
||||
src: ['tag:teaching-device'],
|
||||
dst: [`${gatewayGroup}:*`],
|
||||
},
|
||||
]
|
||||
|
||||
assignments.forEach((assignment, index) => {
|
||||
const users = [...new Set(assignment.studentEmails.map((email) => this.toHeadscaleUserName(email)).filter(Boolean))]
|
||||
|
||||
Reference in New Issue
Block a user