mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-04-04 15:56:16 +02:00
feat(Docker): container URL resolution util and networking improvs
This commit is contained in:
parent
e31f956289
commit
64e6e11389
|
|
@ -3,6 +3,7 @@ import { QueueService } from '#services/queue_service'
|
||||||
import { BenchmarkService } from '#services/benchmark_service'
|
import { BenchmarkService } from '#services/benchmark_service'
|
||||||
import type { RunBenchmarkJobParams } from '../../types/benchmark.js'
|
import type { RunBenchmarkJobParams } from '../../types/benchmark.js'
|
||||||
import logger from '@adonisjs/core/services/logger'
|
import logger from '@adonisjs/core/services/logger'
|
||||||
|
import { DockerService } from '#services/docker_service'
|
||||||
|
|
||||||
export class RunBenchmarkJob {
|
export class RunBenchmarkJob {
|
||||||
static get queue() {
|
static get queue() {
|
||||||
|
|
@ -18,7 +19,8 @@ export class RunBenchmarkJob {
|
||||||
|
|
||||||
logger.info(`[RunBenchmarkJob] Starting benchmark ${benchmark_id} of type ${benchmark_type}`)
|
logger.info(`[RunBenchmarkJob] Starting benchmark ${benchmark_id} of type ${benchmark_type}`)
|
||||||
|
|
||||||
const benchmarkService = new BenchmarkService()
|
const dockerService = new DockerService()
|
||||||
|
const benchmarkService = new BenchmarkService(dockerService)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result
|
let result
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { inject } from '@adonisjs/core'
|
import { inject } from '@adonisjs/core'
|
||||||
import logger from '@adonisjs/core/services/logger'
|
import logger from '@adonisjs/core/services/logger'
|
||||||
import transmit from '@adonisjs/transmit/services/main'
|
import transmit from '@adonisjs/transmit/services/main'
|
||||||
import Docker from 'dockerode'
|
|
||||||
import si from 'systeminformation'
|
import si from 'systeminformation'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
|
|
@ -24,6 +23,7 @@ import type {
|
||||||
RepositoryStats,
|
RepositoryStats,
|
||||||
} from '../../types/benchmark.js'
|
} from '../../types/benchmark.js'
|
||||||
import { randomUUID } from 'node:crypto'
|
import { randomUUID } from 'node:crypto'
|
||||||
|
import { DockerService } from './docker_service.js'
|
||||||
|
|
||||||
// Re-export default weights for use in service
|
// Re-export default weights for use in service
|
||||||
const SCORE_WEIGHTS = {
|
const SCORE_WEIGHTS = {
|
||||||
|
|
@ -44,10 +44,6 @@ const BENCHMARK_CHANNEL = 'benchmark-progress'
|
||||||
const AI_BENCHMARK_MODEL = 'llama3.2:1b'
|
const AI_BENCHMARK_MODEL = 'llama3.2:1b'
|
||||||
const AI_BENCHMARK_PROMPT = 'Explain recursion in programming in exactly 100 words.'
|
const AI_BENCHMARK_PROMPT = 'Explain recursion in programming in exactly 100 words.'
|
||||||
|
|
||||||
// Ollama API URL - configurable for Docker environments where localhost doesn't reach the host
|
|
||||||
// In Docker, use host.docker.internal (Docker Desktop) or the host gateway IP (Linux)
|
|
||||||
const OLLAMA_API_URL = process.env.OLLAMA_API_URL || 'http://host.docker.internal:11434'
|
|
||||||
|
|
||||||
// Reference scores for normalization (calibrated to 0-100 scale)
|
// Reference scores for normalization (calibrated to 0-100 scale)
|
||||||
// These represent "expected" scores for a mid-range system (score ~50)
|
// These represent "expected" scores for a mid-range system (score ~50)
|
||||||
const REFERENCE_SCORES = {
|
const REFERENCE_SCORES = {
|
||||||
|
|
@ -61,18 +57,10 @@ const REFERENCE_SCORES = {
|
||||||
|
|
||||||
@inject()
|
@inject()
|
||||||
export class BenchmarkService {
|
export class BenchmarkService {
|
||||||
private docker: Docker
|
|
||||||
private currentBenchmarkId: string | null = null
|
private currentBenchmarkId: string | null = null
|
||||||
private currentStatus: BenchmarkStatus = 'idle'
|
private currentStatus: BenchmarkStatus = 'idle'
|
||||||
|
|
||||||
constructor() {
|
constructor(private dockerService: DockerService) {}
|
||||||
const isWindows = process.platform === 'win32'
|
|
||||||
if (isWindows) {
|
|
||||||
this.docker = new Docker({ socketPath: '//./pipe/docker_engine' })
|
|
||||||
} else {
|
|
||||||
this.docker = new Docker({ socketPath: '/var/run/docker.sock' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a full benchmark suite
|
* Run a full benchmark suite
|
||||||
|
|
@ -366,18 +354,25 @@ export class BenchmarkService {
|
||||||
* Run AI benchmark using Ollama
|
* Run AI benchmark using Ollama
|
||||||
*/
|
*/
|
||||||
private async _runAIBenchmark(): Promise<AIScores> {
|
private async _runAIBenchmark(): Promise<AIScores> {
|
||||||
|
try {
|
||||||
|
|
||||||
this._updateStatus('running_ai', 'Running AI benchmark...')
|
this._updateStatus('running_ai', 'Running AI benchmark...')
|
||||||
|
|
||||||
|
const ollamaAPIURL = await this.dockerService.getServiceURL(DockerService.OLLAMA_SERVICE_NAME)
|
||||||
|
if (!ollamaAPIURL) {
|
||||||
|
throw new Error('AI Assistant service location could not be determined. Ensure AI Assistant is installed and running.')
|
||||||
|
}
|
||||||
|
|
||||||
// Check if Ollama is available
|
// Check if Ollama is available
|
||||||
try {
|
try {
|
||||||
await axios.get(`${OLLAMA_API_URL}/api/tags`, { timeout: 5000 })
|
await axios.get(`${ollamaAPIURL}/api/tags`, { timeout: 5000 })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorCode = error.code || error.response?.status || 'unknown'
|
const errorCode = error.code || error.response?.status || 'unknown'
|
||||||
throw new Error(`Ollama is not running or not accessible (${errorCode}). Ensure AI Assistant is installed and running.`)
|
throw new Error(`Ollama is not running or not accessible (${errorCode}). Ensure AI Assistant is installed and running.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the benchmark model is available, pull if not
|
// Check if the benchmark model is available, pull if not
|
||||||
const modelsResponse = await axios.get(`${OLLAMA_API_URL}/api/tags`)
|
const modelsResponse = await axios.get(`${ollamaAPIURL}/api/tags`)
|
||||||
const models = modelsResponse.data.models || []
|
const models = modelsResponse.data.models || []
|
||||||
const hasModel = models.some((m: any) => m.name === AI_BENCHMARK_MODEL || m.name.startsWith(AI_BENCHMARK_MODEL.split(':')[0]))
|
const hasModel = models.some((m: any) => m.name === AI_BENCHMARK_MODEL || m.name.startsWith(AI_BENCHMARK_MODEL.split(':')[0]))
|
||||||
|
|
||||||
|
|
@ -387,7 +382,7 @@ export class BenchmarkService {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Model pull can take several minutes, use longer timeout
|
// Model pull can take several minutes, use longer timeout
|
||||||
await axios.post(`${OLLAMA_API_URL}/api/pull`, { name: AI_BENCHMARK_MODEL }, { timeout: 600000 })
|
await axios.post(`${ollamaAPIURL}/api/pull`, { name: AI_BENCHMARK_MODEL }, { timeout: 600000 })
|
||||||
logger.info(`[BenchmarkService] Model ${AI_BENCHMARK_MODEL} downloaded successfully`)
|
logger.info(`[BenchmarkService] Model ${AI_BENCHMARK_MODEL} downloaded successfully`)
|
||||||
} catch (pullError) {
|
} catch (pullError) {
|
||||||
throw new Error(`Failed to download AI benchmark model (${AI_BENCHMARK_MODEL}): ${pullError.message}`)
|
throw new Error(`Failed to download AI benchmark model (${AI_BENCHMARK_MODEL}): ${pullError.message}`)
|
||||||
|
|
@ -397,9 +392,8 @@ export class BenchmarkService {
|
||||||
// Run inference benchmark
|
// Run inference benchmark
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${OLLAMA_API_URL}/api/generate`,
|
`${ollamaAPIURL}/api/generate`,
|
||||||
{
|
{
|
||||||
model: AI_BENCHMARK_MODEL,
|
model: AI_BENCHMARK_MODEL,
|
||||||
prompt: AI_BENCHMARK_PROMPT,
|
prompt: AI_BENCHMARK_PROMPT,
|
||||||
|
|
@ -519,11 +513,11 @@ export class BenchmarkService {
|
||||||
*/
|
*/
|
||||||
private async _ensureSysbenchImage(): Promise<void> {
|
private async _ensureSysbenchImage(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.docker.getImage(SYSBENCH_IMAGE).inspect()
|
await this.dockerService.docker.getImage(SYSBENCH_IMAGE).inspect()
|
||||||
} catch {
|
} catch {
|
||||||
this._updateStatus('starting', `Pulling sysbench image...`)
|
this._updateStatus('starting', `Pulling sysbench image...`)
|
||||||
const pullStream = await this.docker.pull(SYSBENCH_IMAGE)
|
const pullStream = await this.dockerService.docker.pull(SYSBENCH_IMAGE)
|
||||||
await new Promise((resolve) => this.docker.modem.followProgress(pullStream, resolve))
|
await new Promise((resolve) => this.dockerService.docker.modem.followProgress(pullStream, resolve))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -641,7 +635,7 @@ export class BenchmarkService {
|
||||||
private async _runSysbenchCommand(cmd: string[]): Promise<string> {
|
private async _runSysbenchCommand(cmd: string[]): Promise<string> {
|
||||||
try {
|
try {
|
||||||
// Create container with TTY to avoid multiplexed output
|
// Create container with TTY to avoid multiplexed output
|
||||||
const container = await this.docker.createContainer({
|
const container = await this.dockerService.docker.createContainer({
|
||||||
Image: SYSBENCH_IMAGE,
|
Image: SYSBENCH_IMAGE,
|
||||||
Cmd: cmd,
|
Cmd: cmd,
|
||||||
name: `${SYSBENCH_CONTAINER_NAME}_${Date.now()}`,
|
name: `${SYSBENCH_CONTAINER_NAME}_${Date.now()}`,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ export class DockerService {
|
||||||
public static FLATNOTES_SERVICE_NAME = 'nomad_flatnotes'
|
public static FLATNOTES_SERVICE_NAME = 'nomad_flatnotes'
|
||||||
public static KOLIBRI_SERVICE_NAME = 'nomad_kolibri'
|
public static KOLIBRI_SERVICE_NAME = 'nomad_kolibri'
|
||||||
public static BENCHMARK_SERVICE_NAME = 'nomad_benchmark'
|
public static BENCHMARK_SERVICE_NAME = 'nomad_benchmark'
|
||||||
|
public static NOMAD_NETWORK = 'project-nomad_default'
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Support both Linux (production) and Windows (development with Docker Desktop)
|
// Support both Linux (production) and Windows (development with Docker Desktop)
|
||||||
|
|
@ -124,11 +125,58 @@ export class DockerService {
|
||||||
status: container.State,
|
status: container.State,
|
||||||
}))
|
}))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching services status: ${error.message}`)
|
logger.error(`Error fetching services status: ${error.message}`)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the URL to access a service based on its configuration.
|
||||||
|
* Attempts to return a docker-internal URL using the service name and exposed port.
|
||||||
|
* @param serviceName - The name of the service to get the URL for.
|
||||||
|
* @returns - The URL as a string, or null if it cannot be determined.
|
||||||
|
*/
|
||||||
|
async getServiceURL(serviceName: string): Promise<string | null> {
|
||||||
|
if (!serviceName || serviceName.trim() === '') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = await Service.query()
|
||||||
|
.where('service_name', serviceName)
|
||||||
|
.andWhere('installed', true)
|
||||||
|
.first()
|
||||||
|
|
||||||
|
if (!service) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostname = process.env.NODE_ENV === 'production' ? serviceName : 'localhost'
|
||||||
|
|
||||||
|
// First, check if ui_location is set and is a valid port number
|
||||||
|
if (service.ui_location && parseInt(service.ui_location, 10)) {
|
||||||
|
return `http://${hostname}:${service.ui_location}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, try to extract a host port from container_config
|
||||||
|
const parsedConfig = this._parseContainerConfig(service.container_config)
|
||||||
|
if (parsedConfig?.HostConfig?.PortBindings) {
|
||||||
|
const portBindings = parsedConfig.HostConfig.PortBindings
|
||||||
|
const hostPorts = Object.values(portBindings)
|
||||||
|
if (!hostPorts || !Array.isArray(hostPorts) || hostPorts.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostPortsArray = hostPorts.flat() as { HostPort: string }[]
|
||||||
|
const hostPortsStrings = hostPortsArray.map((binding) => binding.HostPort)
|
||||||
|
if (hostPortsStrings.length > 0) {
|
||||||
|
return `http://${hostname}:${hostPortsStrings[0]}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, return null if we can't determine a URL
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
async createContainerPreflight(
|
async createContainerPreflight(
|
||||||
serviceName: string
|
serviceName: string
|
||||||
): Promise<{ success: boolean; message: string }> {
|
): Promise<{ success: boolean; message: string }> {
|
||||||
|
|
@ -411,6 +459,14 @@ export class DockerService {
|
||||||
...(containerConfig?.ExposedPorts && { ExposedPorts: containerConfig.ExposedPorts }),
|
...(containerConfig?.ExposedPorts && { ExposedPorts: containerConfig.ExposedPorts }),
|
||||||
...(containerConfig?.Env && { Env: containerConfig.Env }),
|
...(containerConfig?.Env && { Env: containerConfig.Env }),
|
||||||
...(service.container_command ? { Cmd: service.container_command.split(' ') } : {}),
|
...(service.container_command ? { Cmd: service.container_command.split(' ') } : {}),
|
||||||
|
// Ensure container is attached to the Nomad docker network in production
|
||||||
|
...(process.env.NODE_ENV === 'production' && {
|
||||||
|
NetworkingConfig: {
|
||||||
|
EndpointsConfig: {
|
||||||
|
[DockerService.NOMAD_NETWORK]: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
this._broadcast(
|
this._broadcast(
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,10 @@ export default class BenchmarkResults extends BaseCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
const { DockerService } = await import('#services/docker_service')
|
||||||
const { BenchmarkService } = await import('#services/benchmark_service')
|
const { BenchmarkService } = await import('#services/benchmark_service')
|
||||||
const benchmarkService = new BenchmarkService()
|
const dockerService = new DockerService()
|
||||||
|
const benchmarkService = new BenchmarkService(dockerService)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let results
|
let results
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,10 @@ export default class BenchmarkRun extends BaseCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
const { DockerService } = await import('#services/docker_service')
|
||||||
const { BenchmarkService } = await import('#services/benchmark_service')
|
const { BenchmarkService } = await import('#services/benchmark_service')
|
||||||
const benchmarkService = new BenchmarkService()
|
const dockerService = new DockerService()
|
||||||
|
const benchmarkService = new BenchmarkService(dockerService)
|
||||||
|
|
||||||
// Determine benchmark type
|
// Determine benchmark type
|
||||||
let benchmarkType: 'full' | 'system' | 'ai' = 'full'
|
let benchmarkType: 'full' | 'system' | 'ai' = 'full'
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,10 @@ export default class BenchmarkSubmit extends BaseCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
const { DockerService } = await import('#services/docker_service')
|
||||||
const { BenchmarkService } = await import('#services/benchmark_service')
|
const { BenchmarkService } = await import('#services/benchmark_service')
|
||||||
const benchmarkService = new BenchmarkService()
|
const dockerService = new DockerService()
|
||||||
|
const benchmarkService = new BenchmarkService(dockerService)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the result to submit
|
// Get the result to submit
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ services:
|
||||||
- DB_SSL=false
|
- DB_SSL=false
|
||||||
- REDIS_HOST=redis
|
- REDIS_HOST=redis
|
||||||
- REDIS_PORT=6379
|
- REDIS_PORT=6379
|
||||||
- OLLAMA_API_URL=http://host.docker.internal:11434
|
|
||||||
depends_on:
|
depends_on:
|
||||||
mysql:
|
mysql:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user