From 66c81b9cc9bab59ae029495acc15a55b401f9196 Mon Sep 17 00:00:00 2001 From: alexcos20 Date: Fri, 6 Feb 2026 15:34:06 +0200 Subject: [PATCH] use local image --- src/components/c2d/compute_engine_docker.ts | 54 +++++- src/test/integration/compute.test.ts | 204 ++++++++++++++++++++ 2 files changed, 256 insertions(+), 2 deletions(-) diff --git a/src/components/c2d/compute_engine_docker.ts b/src/components/c2d/compute_engine_docker.ts index 37c1bd269..8b7444eed 100644 --- a/src/components/c2d/compute_engine_docker.ts +++ b/src/components/c2d/compute_engine_docker.ts @@ -556,15 +556,65 @@ export class C2DEngineDocker extends C2DEngine { } /** - * Checks the docker image by looking at the manifest + * Checks the docker image by looking at local images first, then remote manifest * @param image name or tag - * @returns boolean + * @param encryptedDockerRegistryAuth optional encrypted auth for remote registry + * @param platform optional platform to validate against + * @returns ValidateParams with valid flag and platform validation result */ public async checkDockerImage( image: string, encryptedDockerRegistryAuth?: string, platform?: RunningPlatform ): Promise { + // Step 1: Try to check local image first + if (this.docker) { + try { + const dockerImage = this.docker.getImage(image) + const imageInfo = await dockerImage.inspect() + + // Extract platform information from local image + const localPlatform = { + architecture: imageInfo.Architecture || 'amd64', + os: imageInfo.Os || 'linux' + } + + // Normalize architecture (amd64 -> x86_64 for compatibility) + if (localPlatform.architecture === 'amd64') { + localPlatform.architecture = 'x86_64' + } + + // Validate platform if required + const isValidPlatform = platform + ? checkManifestPlatform(localPlatform, platform) + : true + + if (isValidPlatform) { + CORE_LOGGER.debug(`Image ${image} found locally and platform is valid`) + return { valid: true } + } else { + CORE_LOGGER.warn( + `Image ${image} found locally but platform mismatch: ` + + `local=${localPlatform.architecture}/${localPlatform.os}, ` + + `required=${platform.architecture}/${platform.os}` + ) + return { + valid: false, + status: 400, + reason: + `Platform mismatch: image is ${localPlatform.architecture}/${localPlatform.os}, ` + + `but environment requires ${platform.architecture}/${platform.os}` + } + } + } catch (localErr: any) { + // Image not found locally or error inspecting - fall through to remote check + CORE_LOGGER.debug( + `Image ${image} not found locally (${localErr.message}), checking remote registry` + ) + } + } + + // Step 2: Fall back to remote registry check (existing behavior) try { const manifest = await this.getDockerManifest(image, encryptedDockerRegistryAuth) diff --git a/src/test/integration/compute.test.ts b/src/test/integration/compute.test.ts index 765e130c6..96dae67ec 100644 --- a/src/test/integration/compute.test.ts +++ b/src/test/integration/compute.test.ts @@ -75,6 +75,8 @@ import { import { freeComputeStartPayload } from '../data/commands.js' import { DDOManager } from '@oceanprotocol/ddo-js' +import Dockerode from 'dockerode' +import { C2DEngineDocker } from '../../components/c2d/compute_engine_docker.js' const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) @@ -1855,6 +1857,208 @@ describe('Compute', () => { }) }) + describe('Local Docker image checking', () => { + let docker: Dockerode + let dockerEngine: C2DEngineDocker + + before(async function () { + // Skip if Docker not available + try { + docker = new Dockerode() + await docker.info() + } catch (e) { + this.skip() + } + + // Get the Docker engine from oceanNode + const c2dEngines = oceanNode.getC2DEngines() + const engines = (c2dEngines as any).engines as C2DEngineDocker[] + dockerEngine = engines.find((e) => e instanceof C2DEngineDocker) + if (!dockerEngine) { + this.skip() + } + }) + + it('should check local image when it exists locally', async function () { + // Skip if Docker not available + try { + await docker.info() + } catch (e) { + this.skip() + } + + const testImage = 'alpine:3.18' + + // Ensure image exists locally + try { + await docker.pull(testImage) + } catch (e) { + // If pull fails, skip test + this.skip() + } + + // Check the image - should find it locally + const result = await dockerEngine.checkDockerImage(testImage) + + assert(result, 'Result should exist') + assert(result.valid === true, 'Image should be valid') + }).timeout(30000) + + it('should validate platform for local images', async function () { + // Skip if Docker not available + try { + await docker.info() + } catch (e) { + this.skip() + } + + const testImage = 'alpine:3.18' + + // Ensure image exists locally + try { + await docker.pull(testImage) + } catch (e) { + this.skip() + } + + // Get the platform from the local image + const imageInfo = await docker.getImage(testImage).inspect() + const localArch = imageInfo.Architecture || 'amd64' + const localOs = imageInfo.Os || 'linux' + + // Check with matching platform + const matchingPlatform = { + architecture: localArch === 'amd64' ? 'x86_64' : localArch, + os: localOs + } + const resultMatching = await dockerEngine.checkDockerImage( + testImage, + undefined, + matchingPlatform + ) + + assert(resultMatching, 'Result should exist') + assert( + resultMatching.valid === true, + 'Image should be valid with matching platform' + ) + }).timeout(30000) + + it('should detect platform mismatch for local images', async function () { + // Skip if Docker not available + try { + await docker.info() + } catch (e) { + this.skip() + } + + const testImage = 'alpine:3.18' + + // Ensure image exists locally + try { + await docker.pull(testImage) + } catch (e) { + this.skip() + } + + // Check with mismatched platform (assuming local is linux/amd64 or linux/x86_64) + const mismatchedPlatform = { + architecture: 'arm64', // Different architecture + os: 'linux' + } + const resultMismatch = await dockerEngine.checkDockerImage( + testImage, + undefined, + mismatchedPlatform + ) + + assert(resultMismatch, 'Result should exist') + assert( + resultMismatch.valid === false, + 'Image should be invalid with mismatched platform' + ) + assert(resultMismatch.status === 400, 'Status should be 400 for platform mismatch') + assert( + resultMismatch.reason.includes('Platform mismatch'), + 'Reason should include platform mismatch message' + ) + }).timeout(30000) + + it('should fall back to remote registry when local image not found', async function () { + // Skip if Docker not available + try { + await docker.info() + } catch (e) { + this.skip() + } + + const nonExistentLocalImage = 'nonexistent-local-image:latest' + + // Ensure image doesn't exist locally + try { + const image = docker.getImage(nonExistentLocalImage) + await image.inspect() + // If we get here, image exists - remove it for test + await image.remove({ force: true }) + } catch (e) { + // Image doesn't exist locally, which is what we want + } + + // Check the image - should fall back to remote check + // This will likely fail with 404, but we're testing the fallback behavior + const result = await dockerEngine.checkDockerImage(nonExistentLocalImage) + + assert(result, 'Result should exist') + // Should have attempted remote check (will fail, but that's expected) + assert(result.valid === false, 'Image should be invalid (not found)') + assert(result.status === 404, 'Status should be 404 for not found') + }).timeout(30000) + + it('should work without platform validation when platform not specified', async function () { + // Skip if Docker not available + try { + await docker.info() + } catch (e) { + this.skip() + } + + const testImage = 'alpine:3.18' + + // Ensure image exists locally + try { + await docker.pull(testImage) + } catch (e) { + this.skip() + } + + // Check without platform - should succeed if image exists + const result = await dockerEngine.checkDockerImage(testImage) + + assert(result, 'Result should exist') + assert(result.valid === true, 'Image should be valid without platform check') + }).timeout(30000) + + after(async function () { + // Clean up test images if needed + try { + await docker.info() + } catch (e) { + // Docker not available, skip cleanup + } + + // Optionally remove test images to save space + // (commented out to avoid breaking other tests that might use these images) + /* + try { + const image = docker.getImage('alpine:3.18') + await image.remove({ force: true }) + } catch (e) { + // Ignore errors during cleanup + } + */ + }) + }) + after(async () => { await tearDownEnvironment(previousConfiguration) indexer.stopAllChainIndexers()