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
186 changes: 186 additions & 0 deletions .github/workflows/drs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
name: DRS Integration Tests

on:
push:
branches:
- 'main'
pull_request:
types: [opened, reopened, synchronize]
workflow_dispatch:

jobs:
drs-integration:
name: DRS integration (Java ${{ matrix.java_version }})
runs-on: ubuntu-latest
timeout-minutes: 20

strategy:
fail-fast: false
matrix:
java_version: [21]

env:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASS: minioadmin
MINIO_BUCKET: nf-ga4gh-test
SYFON_USER: drs-user
SYFON_PASS: drs-pass
SYFON_ORG: test-org
SYFON_PROJECT: test-project
SYFON_BASE_URL: http://localhost:8080
MINIO_BASE_URL: http://localhost:9000

steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 1
submodules: true

- name: Setup Java ${{ matrix.java_version }}
uses: actions/setup-java@v5
with:
java-version: ${{ matrix.java_version }}
distribution: temurin
cache: gradle

# -----------------------------------------------------------------------
# Start MinIO
#
# MINIO_DOMAIN is required so MinIO accepts virtual-hosted-style
# requests (Host: <bucket>.localhost) β€” the syfon CLI's S3 client
# always addresses buckets this way unless given an IP-literal
# endpoint, and without MINIO_DOMAIN set, MinIO responds 400 Bad
# Request to any such request.
# -----------------------------------------------------------------------
- name: Start MinIO
run: |
docker run -d --name minio \
--network host \
-e MINIO_ROOT_USER=$MINIO_ROOT_USER \
-e MINIO_ROOT_PASSWORD=$MINIO_ROOT_PASS \
-e MINIO_DOMAIN=localhost \
minio/minio:latest server /data

- name: Wait for MinIO
run: |
for i in $(seq 1 30); do
if curl -fsS http://localhost:9000/minio/health/live >/dev/null 2>&1; then
echo "MinIO ready"; exit 0
fi
sleep 1
done
echo "MinIO failed to start"; docker logs minio; exit 1

# -----------------------------------------------------------------------
# Install mc and create the test bucket
# -----------------------------------------------------------------------
- name: Install mc
run: |
curl -fsSL https://dl.min.io/client/mc/release/linux-amd64/mc -o /usr/local/bin/mc
chmod +x /usr/local/bin/mc

- name: Create MinIO bucket
run: |
mc alias set local http://localhost:9000 $MINIO_ROOT_USER $MINIO_ROOT_PASS
mc mb --ignore-existing local/$MINIO_BUCKET

# -----------------------------------------------------------------------
# Start Syfon
# -----------------------------------------------------------------------
- name: Write Syfon config
run: |
cat > /tmp/syfon-config.yaml <<EOF
port: 8080

auth:
mode: local
basic:
username: $SYFON_USER
password: $SYFON_PASS

database:
sqlite:
file: /tmp/syfon.db

credential_encryption:
local_key_file: /tmp/syfon-kek.key
EOF

- name: Start Syfon
run: |
docker run -d --name syfon \
--network host \
-v /tmp/syfon-config.yaml:/syfon-config.yaml \
quay.io/ohsu-comp-bio/syfon:development \
serve --config /syfon-config.yaml

- name: Wait for Syfon
run: |
for i in $(seq 1 30); do
if curl -fsS http://localhost:8080/healthz >/dev/null 2>&1; then
echo "Syfon ready"; exit 0
fi
sleep 1
done
echo "Syfon failed to start"; docker logs syfon; exit 1

# -----------------------------------------------------------------------
# Install the syfon CLI (used to register the bucket scope below, and
# by the integration tests to upload objects). The `development` image
# is musl-linked (Alpine), so its binary won't run directly on the
# glibc-based runner β€” instead, wrap `docker run` so every `syfon`
# invocation transparently runs inside a container of the same image.
# --network host lets the container reach Syfon/MinIO on localhost.
# /tmp is mounted since that's where the integration tests' uploaded
# files (java.io.tmpdir) and this job's workspace both live.
# -----------------------------------------------------------------------
- name: Install syfon CLI wrapper
run: |
cat > /usr/local/bin/syfon <<'EOF'
#!/usr/bin/env bash
exec docker run --rm --network host \
-v /tmp:/tmp \
-v "$(pwd)":"$(pwd)" -w "$(pwd)" \
quay.io/ohsu-comp-bio/syfon:development "$@"
EOF
chmod +x /usr/local/bin/syfon

- name: Register Syfon bucket, organization, and project scope
run: |
syfon --server $SYFON_BASE_URL --username $SYFON_USER --password $SYFON_PASS \
bucket add $MINIO_BUCKET \
--access-key $MINIO_ROOT_USER --secret-key $MINIO_ROOT_PASS \
--endpoint $MINIO_BASE_URL --region us-east-1

syfon --server $SYFON_BASE_URL --username $SYFON_USER --password $SYFON_PASS \
bucket add-organization $SYFON_ORG --path s3://$MINIO_BUCKET/$SYFON_ORG

syfon --server $SYFON_BASE_URL --username $SYFON_USER --password $SYFON_PASS \
bucket add-project $SYFON_ORG $SYFON_PROJECT --path s3://$MINIO_BUCKET/$SYFON_ORG/$SYFON_PROJECT

# -----------------------------------------------------------------------
# Run the integration tests
# -----------------------------------------------------------------------
- name: Run DRS integration tests
run: ./gradlew integrationTest
env:
GRADLE_OPTS: '-Dorg.gradle.daemon=false'
SYFON_BIN: /usr/local/bin/syfon

# -----------------------------------------------------------------------
# Diagnostics and artifacts
# -----------------------------------------------------------------------
- name: Show container logs on failure
if: failure()
run: |
echo "=== MinIO logs ===" && docker logs minio || true
echo "=== Syfon logs ===" && docker logs syfon || true

- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: drs-integration-results-java-${{ matrix.java_version }}
path: build/reports/tests/integrationTest/
retention-days: 7
40 changes: 39 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

This plugin implements the support for GA4GH APIs for Nextflow.

It currently only supports the [Task Execution Service (TES) API](https://github.com/ga4gh/task-execution-schemas).
It currently supports the [Task Execution Service (TES) API](https://github.com/ga4gh/task-execution-schemas) and the [Data Repository Service (DRS) API](https://github.com/ga4gh/data-repository-service-schemas).

The [Task Execution Schema](https://github.com/ga4gh/task-execution-schemas) (TES) project by the [GA4GH](https://www.ga4gh.org) standardization initiative is an effort to define a standardized schema and API for describing batch execution tasks in a portable manner.

The [Data Repository Service](https://github.com/ga4gh/data-repository-service-schemas) (DRS) API by the [GA4GH](https://www.ga4gh.org) standardization initiative provides a generic interface for retrieving scientific data objects (genomic files, etc.) via a persistent, resolvable identifier, regardless of which repository hosts them.

## Get Started

To use this plugin, add it to your `nextflow.config`:
Expand Down Expand Up @@ -115,10 +117,46 @@ curl -fsSL https://ohsu-comp-bio.github.io/funnel/install.sh | bash
funnel server run
```

## DRS (Data Repository Service)

`drs://` URIs can be used directly as process inputs or outputs, without any extra configuration:

```groovy
workflow {
align(file("drs://drs.example.org/object-id"))
}
```

Under the TES executor, the `drs://` URI is passed straight through to the TES backend as the task input `url` β€” resolution is left entirely to the TES server. For the local executor (or when Nextflow itself needs to read a DRS object, e.g. staging into a shared work directory), the plugin resolves the URI via the standard two-hop DRS protocol and streams the object over HTTPS.

### Configuration

```groovy
drs {
// Bearer token sent on every DRS request. Takes precedence over
// username/password if both are set.
accessToken = '<token>'

// HTTP Basic auth credentials, for DRS servers that gate access this way
// instead of (or in addition to) a bearer token.
username = '<username>'
password = '<password>'

// Scheme used to contact the DRS metadata API. Defaults to 'https';
// set to 'http' to target a plain-HTTP DRS server (e.g. local development).
metadataScheme = 'https'
}
```

> [!NOTE]
>
> The DRS filesystem is read-only β€” write operations (`newOutputStream`, `delete`, `copy`, `move`, etc.) are not supported.

## Resources

- [GA4GH Homepage](https://www.ga4gh.org)
- [Task Execution Service (TES)](https://www.ga4gh.org/product/task-execution-service-tes)
- [Data Repository Service (DRS)](https://github.com/ga4gh/data-repository-service-schemas)

## License

Expand Down
41 changes: 38 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,60 @@ dependencies {
api "io.swagger.core.v3:swagger-annotations:2.0.0"
api "javax.annotation:javax.annotation-api:1.3.2"
api "org.threeten:threetenbp:1.3.5"

// Unit tests: WireMock for in-process HTTP stubs
testImplementation "org.wiremock:wiremock:3.13.1"
}

version = project.file('VERSION').text.trim()

// Enable test output
test {
testLogging {
events 'passed', 'skipped', 'failed'
}
}

// -----------------------------------------------------------------------
// integrationTest β€” exercises the DRS plugin against real, running Syfon
// and MinIO services (see .github/workflows/drs.yml). Unlike `test`
// (WireMock-stubbed), this suite is skipped unless SYFON_BASE_URL is set,
// so it never runs as part of a plain `./gradlew test` or `./gradlew build`.
// -----------------------------------------------------------------------
sourceSets {
integrationTest {
groovy.srcDirs = ['src/integrationTest/groovy']
resources.srcDirs = ['src/integrationTest/resources']
compileClasspath += sourceSets.main.output + sourceSets.test.output
runtimeClasspath += sourceSets.main.output + sourceSets.test.output
}
}

configurations {
integrationTestImplementation.extendsFrom testImplementation
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
}

tasks.register('integrationTest', Test) {
description = 'Runs DRS integration tests against live Syfon + MinIO services.'
group = 'verification'
useJUnitPlatform()
testClassesDirs = sourceSets.integrationTest.output.classesDirs
classpath = sourceSets.integrationTest.runtimeClasspath
testLogging {
events 'passed', 'skipped', 'failed'
}
shouldRunAfter test
}

nextflowPlugin {
nextflowVersion = '25.10.0'

provider = 'nextflow'
className = 'nextflow.ga4gh.Ga4ghPlugin'
extensionPoints = [
'nextflow.ga4gh.drs.DrsPathFactory',
'nextflow.ga4gh.drs.DrsPathRegistrant',
'nextflow.ga4gh.tes.config.TesConfig',
'nextflow.ga4gh.tes.executor.TesExecutor',
'nextflow.ga4gh.tes.config.TesConfig'
]
}

Loading
Loading