Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 52 additions & 2 deletions src/components/c2d/compute_engine_docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ValidateParams> {
// 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we check remote for compatible images?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is only for local images. i am not sure if you can pull images for other architecture, so my guess would be if you have the image locally, it's compatible. but better to be sure and check it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, you can pull image for another arhitecture..

You can force Docker to pull a specific architecture:

docker pull --platform linux/arm64 myimage:tag

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)

Expand Down
204 changes: 204 additions & 0 deletions src/test/integration/compute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down Expand Up @@ -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()
Expand Down
Loading