Skip to content
Open
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
35 changes: 31 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ jobs:
if: ${{ env.DOCKERHUB_PASSWORD && env.DOCKERHUB_USERNAME }}
run: |
echo "Login to Docker Hub";echo "$DOCKERHUB_PASSWORD" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Run Barge
working-directory: ${{ github.workspace }}/barge
run: |
Expand All @@ -145,6 +145,19 @@ jobs:
- name: docker logs
run: docker logs ocean-ocean-contracts-1 && docker logs ocean-typesense-1
if: ${{ failure() }}
- name: Set DOCKER_REGISTRY_AUTHS from Docker Hub secrets
if: env.DOCKERHUB_USERNAME && env.DOCKERHUB_PASSWORD
run: |
DOCKER_REGISTRY_AUTHS=$(jq -n \
--arg username "$DOCKERHUB_USERNAME" \
--arg password "$DOCKERHUB_PASSWORD" \
'{ "https://registry-1.docker.io": { "username": $username, "password": $password } }')
echo "DOCKER_REGISTRY_AUTHS<<EOF" >> $GITHUB_ENV
echo "$DOCKER_REGISTRY_AUTHS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: integration tests
run: npm run test:integration:cover
env:
Expand All @@ -162,6 +175,7 @@ jobs:
FEE_AMOUNT: '{ "amount": 1, "unit": "MB" }'
ASSET_PURGATORY_URL: 'https://raw.githubusercontent.com/oceanprotocol/list-purgatory/main/list-assets.json'
ACCOUNT_PURGATORY_URL: 'https://raw.githubusercontent.com/oceanprotocol/list-purgatory/main/list-accounts.json'
DOCKER_REGISTRY_AUTHS: ${{ env.DOCKER_REGISTRY_AUTHS }}
- name: docker logs
run: docker logs ocean-ocean-contracts-1 && docker logs ocean-typesense-1
if: ${{ failure() }}
Expand Down Expand Up @@ -237,7 +251,19 @@ jobs:
repository: 'oceanprotocol/ocean-node'
path: 'ocean-node'
ref: ${{ github.event_name == 'pull_request' && github.head_ref || 'main' }}

- name: Set DOCKER_REGISTRY_AUTHS from Docker Hub secrets
if: env.DOCKERHUB_USERNAME && env.DOCKERHUB_PASSWORD
run: |
DOCKER_REGISTRY_AUTHS=$(jq -n \
--arg username "$DOCKERHUB_USERNAME" \
--arg password "$DOCKERHUB_PASSWORD" \
'{ "https://registry-1.docker.io": { "username": $username, "password": $password } }')
echo "DOCKER_REGISTRY_AUTHS<<EOF" >> $GITHUB_ENV
echo "$DOCKER_REGISTRY_AUTHS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Start Ocean Node
working-directory: ${{ github.workspace }}/ocean-node
run: |
Expand Down Expand Up @@ -265,6 +291,7 @@ jobs:
MAX_REQ_PER_MINUTE: 320
MAX_CONNECTIONS_PER_MINUTE: 320
DOCKER_COMPUTE_ENVIRONMENTS: '[{"socketPath":"/var/run/docker.sock","resources":[{"id":"disk","total":10}],"storageExpiry":604800,"maxJobDuration":3600,"minJobDuration": 60,"fees":{"8996":[{"prices":[{"id":"cpu","price":1}]}]},"free":{"maxJobDuration":60,"minJobDuration": 10,"maxJobs":3,"resources":[{"id":"cpu","max":1},{"id":"ram","max":1},{"id":"disk","max":1}]}}]'
DOCKER_REGISTRY_AUTHS: ${{ env.DOCKER_REGISTRY_AUTHS }}
- name: Check Ocean Node is running
run: |
for i in $(seq 1 90); do
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
username: ${{ secrets.DOCKERHUB_PUSH_USERNAME }}
password: ${{ secrets.DOCKER_PUSH_TOKEN }}

- name: Set Docker metadata
Expand Down Expand Up @@ -111,7 +111,7 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
username: ${{ secrets.DOCKERHUB_PUSH_USERNAME }}
password: ${{ secrets.DOCKER_PUSH_TOKEN }}
- name: Create manifest list and push
working-directory: /tmp/digests
Expand Down
35 changes: 35 additions & 0 deletions docs/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,38 @@ The `DOCKER_COMPUTE_ENVIRONMENTS` environment variable should be a JSON array of
- **total**: Total number of the resource available.
- **min**: Minimum number of the resource needed for a job.
- **max**: Maximum number of the resource for a job.

### Docker Registry Authentication

- `DOCKER_REGISTRY_AUTHS`: JSON object mapping Docker registry URLs to authentication credentials. Used for accessing private Docker/OCI registries when validating and pulling Docker images. Each registry entry must provide either `username`+`password` or `auth`. Example:

```json
{
"https://registry-1.docker.io": {
"username": "myuser",
"password": "mypassword"
},
"https://ghcr.io": {
"username": "myuser",
"password": "ghp_..."
},
"https://registry.gitlab.com": {
"auth": "glpat-..."
}
}
```

**Configuration Options:**

- **Registry URL** (key): The full registry URL including protocol (e.g., `https://registry-1.docker.io`, `https://ghcr.io`, `https://registry.gitlab.com`)
- **username** (optional): Username for registry authentication. Required if using password-based auth.
- **password** (optional): Password or personal access token for registry authentication. Required if using username-based auth.
- **auth** (optional): Authentication token (alternative to username+password). Required if not using username+password.

**Notes:**

- For Docker Hub (`registry-1.docker.io`), you can use your Docker Hub username and password, or a personal access token (PAT) as the password.
- For GitHub Container Registry (GHCR), use your GitHub username with a personal access token (PAT) as the password, or use a token directly.
- For GitLab Container Registry, use a personal access token (PAT) or deploy token.
- The registry URL must match exactly (including protocol) with the registry used in the Docker image reference.
- If no credentials are configured for a registry, the node will attempt unauthenticated access (works for public images only).
10 changes: 10 additions & 0 deletions src/@types/OceanNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,18 @@ export interface AccessListContract {
[chainId: string]: string[]
}

export interface dockerRegistryAuth {
username?: string
password?: string
auth?: string
}
export interface dockerRegistrysAuth {
[registry: string]: dockerRegistryAuth
}

export interface OceanNodeConfig {
dockerComputeEnvironments: C2DDockerConfig[]
dockerRegistrysAuth: dockerRegistrysAuth
authorizedDecrypters: string[]
authorizedDecryptersList: AccessListContract | null
allowedValidators: string[]
Expand Down
20 changes: 18 additions & 2 deletions src/components/c2d/compute_engine_base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,27 @@ import { C2DClusterType } from '../../@types/C2D/C2D.js'
import { C2DDatabase } from '../database/C2DDatabase.js'
import { Escrow } from '../core/utils/escrow.js'
import { KeyManager } from '../KeyManager/index.js'

import { dockerRegistryAuth, dockerRegistrysAuth } from '../../@types/OceanNode.js'
import { ValidateParams } from '../httpRoutes/validateCommands.js'
export abstract class C2DEngine {
private clusterConfig: C2DClusterInfo
public db: C2DDatabase
public escrow: Escrow
public keyManager: KeyManager
public dockerRegistryAuths: dockerRegistrysAuth

public constructor(
cluster: C2DClusterInfo,
db: C2DDatabase,
escrow: Escrow,
keyManager: KeyManager
keyManager: KeyManager,
dockerRegistryAuths: dockerRegistrysAuth
) {
this.clusterConfig = cluster
this.db = db
this.escrow = escrow
this.keyManager = keyManager
this.dockerRegistryAuths = dockerRegistryAuths
}

getKeyManager(): KeyManager {
Expand Down Expand Up @@ -66,6 +71,9 @@ export abstract class C2DEngine {
return null
}

// eslint-disable-next-line require-await
public abstract checkDockerImage(image: string, platform?: any): Promise<ValidateParams>

public abstract startComputeJob(
assets: ComputeAsset[],
algorithm: ComputeAlgorithm,
Expand Down Expand Up @@ -523,4 +531,12 @@ export abstract class C2DEngine {
}
return cost
}

public getDockerRegistryAuth(registry: string): dockerRegistryAuth | null {
if (!this.dockerRegistryAuths) return null
if (this.dockerRegistryAuths[registry]) {
return this.dockerRegistryAuths[registry]
}
return null
}
}
89 changes: 79 additions & 10 deletions src/components/c2d/compute_engine_docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { decryptFilesObject, omitDBComputeFieldsFromComputeJob } from './index.j
import { ValidateParams } from '../httpRoutes/validateCommands.js'
import { Service } from '@oceanprotocol/ddo-js'
import { getOceanTokenAddressForChain } from '../../utils/address.js'
import { dockerRegistrysAuth } from '../../@types/OceanNode.js'

export class C2DEngineDocker extends C2DEngine {
private envs: ComputeEnvironment[] = []
Expand All @@ -69,9 +70,10 @@ export class C2DEngineDocker extends C2DEngine {
clusterConfig: C2DClusterInfo,
db: C2DDatabase,
escrow: Escrow,
keyManager: KeyManager
keyManager: KeyManager,
dockerRegistryAuths: dockerRegistrysAuth
) {
super(clusterConfig, db, escrow, keyManager)
super(clusterConfig, db, escrow, keyManager, dockerRegistryAuths)

this.docker = null
if (clusterConfig.connection.socketPath) {
Expand Down Expand Up @@ -438,7 +440,7 @@ export class C2DEngineDocker extends C2DEngine {
return filteredEnvs
}

private static parseImage(image: string) {
private parseImage(image: string) {
let registry = C2DEngineDocker.DEFAULT_DOCKER_REGISTRY
let name = image
let ref = 'latest'
Expand Down Expand Up @@ -472,13 +474,32 @@ export class C2DEngineDocker extends C2DEngine {
return { registry, name, ref }
}

public static async getDockerManifest(image: string): Promise<any> {
const { registry, name, ref } = C2DEngineDocker.parseImage(image)
public async getDockerManifest(image: string): Promise<any> {
const { registry, name, ref } = this.parseImage(image)
const url = `${registry}/v2/${name}/manifests/${ref}`

// Get registry auth from parent class
const dockerRegistryAuth = this.getDockerRegistryAuth(registry)

let headers: Record<string, string> = {
Accept:
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.index.v1+json'
}

// If we have auth credentials, add Basic auth header to initial request
if (dockerRegistryAuth) {
// Use auth string if available, otherwise encode username:password
const authString = dockerRegistryAuth.auth
? dockerRegistryAuth.auth
: Buffer.from(
`${dockerRegistryAuth.username}:${dockerRegistryAuth.password}`
).toString('base64')
headers.Authorization = `Basic ${authString}`
CORE_LOGGER.debug(
`Using docker registry auth for ${registry} to get manifest for image ${image}`
)
}

let response = await fetch(url, { headers })

if (response.status === 401) {
Expand All @@ -489,7 +510,22 @@ export class C2DEngineDocker extends C2DEngine {
const tokenUrl = new URL(match[1])
tokenUrl.searchParams.set('service', match[2])
tokenUrl.searchParams.set('scope', `repository:${name}:pull`)
const { token } = (await fetch(tokenUrl.toString()).then((r) => r.json())) as {

// Add Basic auth to token request if we have credentials
const tokenHeaders: Record<string, string> = {}
if (dockerRegistryAuth) {
// Use auth string if available, otherwise encode username:password
const authString = dockerRegistryAuth.auth
? dockerRegistryAuth.auth
: Buffer.from(
`${dockerRegistryAuth.username}:${dockerRegistryAuth.password}`
).toString('base64')
tokenHeaders.Authorization = `Basic ${authString}`
}

const { token } = (await fetch(tokenUrl.toString(), {
headers: tokenHeaders
}).then((r) => r.json())) as {
token: string
}
headers = { ...headers, Authorization: `Bearer ${token}` }
Expand All @@ -511,12 +547,12 @@ export class C2DEngineDocker extends C2DEngine {
* @param image name or tag
* @returns boolean
*/
public static async checkDockerImage(
public async checkDockerImage(
image: string,
platform?: RunningPlatform
): Promise<ValidateParams> {
try {
const manifest = await C2DEngineDocker.getDockerManifest(image)
const manifest = await this.getDockerManifest(image)

const platforms = Array.isArray(manifest.manifests)
? manifest.manifests.map((entry: any) => entry.platform)
Expand Down Expand Up @@ -647,7 +683,7 @@ export class C2DEngineDocker extends C2DEngine {
}
} else {
// already built, we need to validate it
const validation = await C2DEngineDocker.checkDockerImage(image, env.platform)
const validation = await this.checkDockerImage(image, env.platform)
if (!validation.valid)
throw new Error(
`Cannot find image ${image} for ${env.platform.architecture}. Maybe it does not exist or it's build for other arhitectures.`
Expand Down Expand Up @@ -1644,7 +1680,40 @@ export class C2DEngineDocker extends C2DEngine {
const imageLogFile =
this.getC2DConfig().tempFolder + '/' + job.jobId + '/data/logs/image.log'
try {
const pullStream = await this.docker.pull(job.containerImage)
// Get registry auth for the image
const { registry } = this.parseImage(job.containerImage)
const dockerRegistryAuth = this.getDockerRegistryAuth(registry)

// Prepare authconfig for Dockerode if credentials are available
const pullOptions: any = {}
if (dockerRegistryAuth) {
// Extract hostname from registry URL (remove protocol)
const registryUrl = new URL(registry)
const serveraddress =
registryUrl.hostname + (registryUrl.port ? `:${registryUrl.port}` : '')

// Use auth string if available, otherwise encode username:password
const authString = dockerRegistryAuth.auth
? dockerRegistryAuth.auth
: Buffer.from(
`${dockerRegistryAuth.username}:${dockerRegistryAuth.password}`
).toString('base64')

pullOptions.authconfig = {
serveraddress,
...(dockerRegistryAuth.auth
? { auth: authString }
: {
username: dockerRegistryAuth.username,
password: dockerRegistryAuth.password
})
}
CORE_LOGGER.debug(
`Using docker registry auth for ${registry} to pull image ${job.containerImage}`
)
}

const pullStream = await this.docker.pull(job.containerImage, pullOptions)
await new Promise((resolve, reject) => {
let wroteStatusBanner = false
this.docker.modem.followProgress(
Expand Down
12 changes: 10 additions & 2 deletions src/components/c2d/compute_engines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { OceanNodeConfig } from '../../@types/OceanNode.js'
import { C2DDatabase } from '../database/C2DDatabase.js'
import { Escrow } from '../core/utils/escrow.js'
import { KeyManager } from '../KeyManager/index.js'

export class C2DEngines {
public engines: C2DEngine[]

public constructor(
config: OceanNodeConfig,
db: C2DDatabase,
Expand All @@ -23,7 +23,15 @@ export class C2DEngines {
this.engines = []
for (const cluster of config.c2dClusters) {
if (cluster.type === C2DClusterType.DOCKER) {
this.engines.push(new C2DEngineDocker(cluster, db, escrow, keyManager))
this.engines.push(
new C2DEngineDocker(
cluster,
db,
escrow,
keyManager,
config.dockerRegistrysAuth
)
)
}
}
}
Expand Down
Loading
Loading