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
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,34 @@ jobs:

- name: Build
run: cd cli && cargo build --verbose

docker-readonly:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Build image (local)
run: docker build -f docker/Dockerfile -t biosynth:ci .

- name: Run read-only container
run: |
mkdir -p out
docker run --rm --read-only \
--tmpfs /tmp:rw,exec,mode=1777 \
-e BVS_READ_ONLY_DB=1 \
-v "${{ github.workspace }}/out:/out" \
biosynth:ci \
synthetic \
--output /out/genotypes/{index}.txt \
--count 1 \
--seed 42

docker run --rm --read-only \
--tmpfs /tmp:rw,exec,mode=1777 \
-e BVS_READ_ONLY_DB=1 \
-v "${{ github.workspace }}/out:/out" \
biosynth:ci \
genotype-to-vcf \
--input /out/genotypes/0001.txt \
--output /out/out.vcf
60 changes: 0 additions & 60 deletions .github/workflows/docker.yml

This file was deleted.

49 changes: 49 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,55 @@ jobs:
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

publish-docker:
name: Publish Docker image
runs-on: ubuntu-latest
needs: [build, publish-crate]
if: needs.build.result == 'success'
steps:
- uses: actions/checkout@v4
with:
ref: main

- name: Read version
id: version
run: echo "version=$(grep '^version = ' cli/Cargo.toml | head -n1 | cut -d'\"' -f2)" >> "$GITHUB_OUTPUT"

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/openmined/biosynth
tags: |
type=raw,value=latest
type=raw,value=${{ steps.version.outputs.version }}
type=sha

- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
with:
tag_name: v${{ steps.version.outputs.version }}
release_name: Release v${{ steps.version.outputs.version }}
Expand Down
12 changes: 11 additions & 1 deletion cli/src/commands/genotype_to_vcf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub fn run_genotype_to_vcf(args: GenotypeToVcfArgs) -> Result<()> {
bail!("--missing-log can only be used with a single input file");
}

let sqlite_path = ensure_reference_db(Some(&args.sqlite), args.force_download)?;
let sqlite_path = resolve_sqlite_path(&args)?;
let store = if read_only_db_requested() {
StatsStore::connect_read_only(&sqlite_path)?
} else {
Expand Down Expand Up @@ -110,6 +110,16 @@ fn read_only_db_requested() -> bool {
}
}

fn resolve_sqlite_path(args: &GenotypeToVcfArgs) -> Result<PathBuf> {
if read_only_db_requested() {
let baked_in = PathBuf::from("/app/data/genostats.sqlite");
if baked_in.exists() {
return Ok(baked_in);
}
}
ensure_reference_db(Some(&args.sqlite), args.force_download)
}

fn convert_file(
input: &Path,
output_paths: &OutputPaths,
Expand Down
28 changes: 26 additions & 2 deletions cli/src/commands/synthetic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,12 @@ pub fn run_synthetic(args: SyntheticArgs) -> Result<()> {
bail!("Day range must be between 1 and 31");
}

let sqlite_path = ensure_reference_db(Some(&args.sqlite), args.force_download)?;
let store = StatsStore::connect(&sqlite_path)?;
let sqlite_path = resolve_sqlite_path(&args)?;
let store = if read_only_db_requested() {
StatsStore::connect_read_only(&sqlite_path)?
} else {
StatsStore::connect(&sqlite_path)?
};
let references = store.all_references(args.limit)?;
if references.is_empty() {
bail!(
Expand Down Expand Up @@ -123,6 +127,26 @@ pub fn run_synthetic(args: SyntheticArgs) -> Result<()> {
Ok(())
}

fn read_only_db_requested() -> bool {
match std::env::var("BVS_READ_ONLY_DB") {
Ok(value) => matches!(
value.to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
),
Err(_) => false,
}
}

fn resolve_sqlite_path(args: &SyntheticArgs) -> Result<PathBuf> {
if read_only_db_requested() {
let baked_in = PathBuf::from("/app/data/genostats.sqlite");
if baked_in.exists() {
return Ok(baked_in);
}
}
ensure_reference_db(Some(&args.sqlite), args.force_download)
}

fn write_single_file(
path: &PathBuf,
references: &[ReferenceVariant],
Expand Down
21 changes: 17 additions & 4 deletions cli/src/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,7 @@ impl StatsStore {
}

pub fn connect_read_only(path: &Path) -> Result<Self> {
let conn = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY)
.with_context(|| format!("Open database at {:?} (read-only)", path))?;
let conn = open_read_only_connection(path)?;
configure_connection_read_only(&conn)?;
Ok(Self {
sqlite_path: path.to_path_buf(),
Expand All @@ -70,8 +69,7 @@ impl StatsStore {

pub fn open_connection(&self) -> Result<Connection> {
let conn = if self.read_only {
Connection::open_with_flags(&self.sqlite_path, OpenFlags::SQLITE_OPEN_READ_ONLY)
.with_context(|| format!("Open database at {:?} (read-only)", self.sqlite_path))?
open_read_only_connection(&self.sqlite_path)?
} else {
Connection::open(&self.sqlite_path)
.with_context(|| format!("Open database at {:?}", self.sqlite_path))?
Expand Down Expand Up @@ -332,3 +330,18 @@ fn configure_connection_read_only(conn: &Connection) -> Result<()> {
conn.pragma_update(None, "query_only", "ON")?;
Ok(())
}

fn open_read_only_connection(path: &Path) -> Result<Connection> {
let uri = read_only_uri(path);
Connection::open_with_flags(
uri,
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_URI,
)
.with_context(|| format!("Open database at {:?} (read-only)", path))
}

fn read_only_uri(path: &Path) -> String {
let raw = path.to_string_lossy();
let escaped = raw.replace(' ', "%20");
format!("file:{escaped}?immutable=1")
}
28 changes: 28 additions & 0 deletions test-docker.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env sh
set -eu

IMAGE_TAG="${IMAGE_TAG:-biosynth:ci}"
OUTPUT_FILE="${OUTPUT_FILE:-/out/out.vcf}"

docker build -f docker/Dockerfile -t "${IMAGE_TAG}" .

mkdir -p out

docker run --rm --read-only \
--tmpfs /tmp:rw,exec,mode=1777 \
-e BVS_READ_ONLY_DB=1 \
-v "$PWD/out:/out" \
"${IMAGE_TAG}" \
synthetic \
--output /out/genotypes/{index}.txt \
--count 1 \
--seed 42

docker run --rm --read-only \
--tmpfs /tmp:rw,exec,mode=1777 \
-e BVS_READ_ONLY_DB=1 \
-v "$PWD/out:/out" \
"${IMAGE_TAG}" \
genotype-to-vcf \
--input /out/genotypes/0001.txt \
--output "${OUTPUT_FILE}"