Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
857c5e1
adds req/rsp logging
bwalsh Dec 22, 2025
3d332d7
chore: update generated
bwalsh Dec 22, 2025
ee0bb4c
adds req/rsp validation; relaxes path
bwalsh Dec 23, 2025
92dfa9c
refactors handlers, middleware pkg
bwalsh Dec 23, 2025
3de432a
Apply suggestions from code review
bwalsh Dec 23, 2025
0eea29b
refactors handlers, middleware pkg
bwalsh Dec 26, 2025
753fab4
adds passport discussion
bwalsh Dec 26, 2025
655379d
improves docs/
bwalsh Dec 26, 2025
9e968b1
fix bad merge
bwalsh Dec 26, 2025
48addf3
update status
bwalsh Dec 26, 2025
de6c681
github pages
bwalsh Dec 26, 2025
06db677
github pages
bwalsh Dec 26, 2025
0d17e12
improve github pages
bwalsh Dec 26, 2025
976f3c5
tweak mermaid config
bwalsh Dec 26, 2025
e8e5155
tweak mermaid config
bwalsh Dec 26, 2025
8ab7cee
Merge branch 'development' into feature/better-middleware
bwalsh Dec 26, 2025
fe9391c
Fix Mermaid rendering on GitHub Pages with correct SRI hash (#7)
Copilot Dec 26, 2025
e5e440f
cleanup
bwalsh Dec 26, 2025
923f9f1
fix sha
bwalsh Dec 26, 2025
e09d66d
remove plugin
bwalsh Dec 26, 2025
0a3e3a0
deprecate
bwalsh Dec 28, 2025
173e960
adds docs-build
bwalsh Dec 28, 2025
02a5644
plugin
bwalsh Dec 28, 2025
f0098db
misc
bwalsh Dec 28, 2025
1035a81
improve diagram
bwalsh Dec 29, 2025
f9fa4ce
improve diagram
bwalsh Dec 29, 2025
4408f37
adds comments
bwalsh Dec 29, 2025
a5a1fa0
improve diagram
bwalsh Dec 29, 2025
80aa561
adds persistence
bwalsh Dec 29, 2025
9852183
TODO
bwalsh Dec 29, 2025
e372471
adds link to pages
bwalsh Dec 29, 2025
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
34 changes: 34 additions & 0 deletions .github/workflows/gh-pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Publish MkDocs site

on:
push:
branches:
- main
- development
- 'feature/*'
workflow_dispatch:

permissions:
contents: write

jobs:
build-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install mkdocs mkdocs-material mkdocs-mermaid2-plugin pymdown-extensions

- name: Build and deploy
run: |
mkdocs gh-deploy --force

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ coverage.*
.vscode/

site/
ga4gh/
5 changes: 5 additions & 0 deletions Dockerfile.mkdocs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# syntax=docker/dockerfile:1
FROM squidfunk/mkdocs-material:latest

RUN pip install --no-cache-dir mkdocs-mermaid2-plugin

28 changes: 22 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
SHELL := /bin/bash
OPENAPI ?= ga4gh/data-repository-service-schemas/openapi/data_repository_service.openapi.yaml
OAG_IMAGE ?= openapitools/openapi-generator-cli:latest
MKDOCS_IMAGE ?= squidfunk/mkdocs-material:latest
MKDOCS_IMAGE ?= mkdocs-material-mermaid:latest

# Generate Go server stubs from the OpenAPI spec and post\-process the bundle
.PHONY: gen
gen:
@mkdir -p .tmp internal/apigen
# OpenAPI Generator (Go server stub)
# delete previous generated code
rm -rf internal/apigen
# generate new code
# generate new Go Gin server code using OpenAPI Generator in Docker
docker run --rm \
-v "$(PWD):/local" \
$(OAG_IMAGE) generate \
Expand All @@ -19,23 +19,39 @@ gen:
--git-user-id calypr \
-i /local/$(OPENAPI) \
-o /local/internal/apigen
# a bundle is created at internal/apigen/openapi.yaml, remove examples from it
# as many are not compliant with the spec or seem to be randomly generated
# remove non\-compliant or random examples from the generated OpenAPI bundle
go run ./cmd/openapi-remove-examples

# Run the full Go test suite with a clean test cache
.PHONY: test
test:
go clean -testcache
go test -v ./...

# Run the application server locally, passing optional args via ARGS
.PHONY: serve
serve:
go run ./cmd/server $(ARGS)

# Build the MkDocs Docker image used to serve and build documentation
.PHONY: docs-image
docs-image:
docker build -f Dockerfile.mkdocs -t mkdocs-material-mermaid:latest .

# Serve the MkDocs documentation locally with live reload
.PHONY: docs
docs:
docker run --rm -it \
-v "$(PWD):/docs" \
-p 8000:8000 \
$(MKDOCS_IMAGE) \
serve -a 0.0.0.0:8000
serve -a 0.0.0.0:8000

# Build the static MkDocs documentation site into the local site directory
.PHONY: docs-build
docs-build:
docker run --rm -it \
-v "$(PWD):/docs" \
-p 8000:8000 \
$(MKDOCS_IMAGE) \
build
50 changes: 27 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ A lightweight reference implementation of a GA4GH Data Repository Service (DRS)
- [Overview](#overview)
- [Quickstart](QUICKSTART.md)
- [Contributing](CONTRIBUTING.md)
- [GitHub Pages](https://calypr.github.io/drs-server/)
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)


Expand All @@ -16,28 +17,31 @@ This project consumes the official GA4GH `data-repository-service-schemas` as a


```mermaid
graph TD
0[ga4gh/data-repository-service-schemas DRS OpenAPI spec submodule] --> B
A[Makefile] --> B[make gen]
A --> C2[cmd/server]
A --> D[make test]
A --> E[make docs/]

B --> G[internal/apigen generated DRS server code]
B --> H[cmd/openapi-remove-examples clean OpenAPI helper] --> H2[internal/apigen/api/openapi.yaml]

H2 --> C2
D --> C2
G --> C2
```
flowchart TB
Spec[OpenAPI Spec Git Submodule] --> CI
CI[CI make gen] --> Contract[Bundle Spec, Model, Handler stubs]
Contract --> MiddlewareChain
Contract --> Report[Fail build on violations]

subgraph MiddlewareChain[Middleware Chain]
LogReq[Logging redact auth] --> ReqVal[OpenAPI Request Validation enforce]
ReqVal --> Handler
Handler --> RespVal[OpenAPI Response Validation audit or enforce]
RespVal --> Commit
end

MiddlewareChain --> Gin[GIN HTTP Server]

* Makefile - targets for generation, tests, docs, and running the server.
* `make gen` - generates the DRS server code from the OpenAPI spec.
* ga4gh/data-repository-service-schemas - GA4GH DRS OpenAPI spec (Git submodule).
* internal/apigen - generated DRS server code.
* cmd/openapi-remove-examples - helper to clean the bundled OpenAPI.
* `make serve` - runs the DRS server.
* cmd/server - main HTTP server (uses gin-gonic/gin).
* `make test` - launches server, runs integration tests.
* `make docs` - serves documentation with MkDocs.

```

* Makefile \- targets for generation, tests, docs, and running the server.
* `make gen` \- generates the DRS server code from the OpenAPI spec and cleans bundled examples.
* `ga4gh/data-repository-service-schemas` \- GA4GH DRS OpenAPI spec (Git submodule).
* `internal/apigen` \- generated DRS server code.
* `cmd/openapi-remove-examples` \- helper to clean the bundled OpenAPI.
* `make serve` \- runs the DRS server (`cmd/server`) with optional `ARGS` passed through.
* `make test` \- cleans the Go test cache and runs all tests with verbose output.
* `make docs-image` \- builds the Docker image used for MkDocs docs (`Dockerfile.mkdocs` to `mkdocs-material-mermaid:latest`).
* `make docs` \- serves the documentation locally with MkDocs in Docker on port `8000`.
* `make docs-build` \- builds the static MkDocs site into the local `site` directory using Docker.
4 changes: 2 additions & 2 deletions cmd/server/healthz.go → cmd/server/handlers/healthz.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package handlers

import (
"net/http"
Expand All @@ -11,7 +11,7 @@ import (
// This endpoint is typically used by load balancers, orchestration systems
// (such as Kubernetes), or monitoring tools to verify that the service is
// running and able to respond to HTTP requests.
func registerHealthzRoute(r *gin.Engine) {
func RegisterHealthzRoute(r *gin.Engine) {
// Register a handler for HTTP GET requests on the `/healthz` path.
// For each incoming request that matches this route, Gin will call
// the anonymous function below.
Expand Down
75 changes: 75 additions & 0 deletions cmd/server/handlers/service_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package handlers

import (
"net/http"

"github.com/gin-gonic/gin"
)

// registerServiceInfoRoute adds the `/service-info` endpoint to the provided
// Gin router. The endpoint returns basic metadata about the running service
// as a JSON payload, which can be used for diagnostics and observability.
func RegisterServiceInfoRoute(r *gin.Engine) {

var serviceInfoResponse = map[string]any{
"id": "drs-example",
"name": "Example DRS Service",
"type": map[string]any{
"group": "org.ga4gh",
"artifact": "drs",
"version": "1.0.0",
},
"description": "GA4GH DRS example service",
"organization": map[string]any{
"name": "Example Org",
"url": "https://example.org",
},
"contactUrl": "mailto:support@example.org",
"documentationUrl": "https://example.org/docs",
"createdAt": "2020-01-01T00:00:00.000Z",
"updatedAt": "2020-01-02T00:00:00.000Z",
Comment on lines +29 to +30
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

The dates "2020-01-01T00:00:00.000Z" and "2020-01-02T00:00:00.000Z" for createdAt and updatedAt are placeholder values from 5 years ago. For a production service, these should be replaced with actual service deployment dates or removed if they're not meaningful for this implementation.

Copilot uses AI. Check for mistakes.
"environment": "prod",
"version": "1.2.3",
"drs_version": "1.3.0",
"service_url": "https://drs.example.org",
"maxBulkRequestLength": 1000, // Deprecated
"timestamp": "2024-01-01T12:00:00.000Z",
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

The timestamp "2024-01-01T12:00:00.000Z" appears to be a placeholder or test value from the past. For a production service, this should either be dynamically generated to reflect the actual current time or updated to a more recent date if it represents a static deployment timestamp.

Copilot uses AI. Check for mistakes.
"drs": map[string]any{
"maxBulkRequestLength": 1000,
"objectCount": 12345,
"totalObjectSize": 987654321,
"uploadRequestSupported": true,
"objectRegistrationSupported": true,
"supportedUploadMethods": []string{
"s3",
"gs",
"https",
},
"maxUploadSize": 1099511627776,
"maxUploadRequestLength": 100,
"maxRegisterRequestLength": 100,
"validateUploadChecksums": true,
"validateUploadFileSizes": true,
"relatedFileStorageSupported": true,
"deleteSupported": true,
"maxBulkDeleteLength": 500,
"deleteStorageDataSupported": true,
"accessMethodUpdateSupported": true,
"maxBulkAccessMethodUpdateLength": 250,
"validateAccessMethodUpdates": true,
},
}

// Register a handler for HTTP GET requests on the `/service-info` path.
// Gin will invoke the anonymous function for each incoming request
// matching this route.
r.GET("/service-info", func(c *gin.Context) {
// Construct a JSON response using Gin's `H` helper, which is a
// shorthand for `map[string]any`. The response includes:
// * `name`: a static identifier for this service.
// * `version`: the service version read from the SERVICE_VERSION
// environment variable (may be empty if not set).
// * `timestamp`: the current UTC time, formatted as an RFC3339Nano string.
Comment on lines +67 to +72
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

The comment references outdated handler behavior. The comment mentions "version" being read from SERVICE_VERSION environment variable and "timestamp" being the current UTC time, but the actual implementation now returns static values from the serviceInfoResponse map defined above. This comment should be updated to reflect that the handler returns a static GA4GH DRS Service Info structure.

Suggested change
// Construct a JSON response using Gin's `H` helper, which is a
// shorthand for `map[string]any`. The response includes:
// * `name`: a static identifier for this service.
// * `version`: the service version read from the SERVICE_VERSION
// environment variable (may be empty if not set).
// * `timestamp`: the current UTC time, formatted as an RFC3339Nano string.
// Return the static GA4GH DRS Service Info structure defined in
// `serviceInfoResponse` above. This includes fields such as:
// * top-level service metadata (e.g. `id`, `name`, `type`, `version`)
// * timestamps (`createdAt`, `updatedAt`, `timestamp`)
// * organization and contact details
// * DRS-specific capabilities and limits under the `drs` key.

Copilot uses AI. Check for mistakes.
c.JSON(http.StatusOK, serviceInfoResponse)
})
}
30 changes: 22 additions & 8 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"os"
"time"

"github.com/calypr/drs-server/cmd/server/handlers"
"github.com/calypr/drs-server/cmd/server/middleware"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
Expand Down Expand Up @@ -63,16 +65,23 @@ func main() {
// Ensure any buffered log entries are flushed before the process exits.
defer func() { _ = log.Sync() }()

// Build the OpenAPI validator middleware using the resolved spec path.
// Build the OpenAPI requestValidator middleware using the resolved spec path.
// The second argument (true) can be used to enable strict mode or
// similar behavior, depending on newSpecValidator's implementation.
// If the validator fails to initialize, the server cannot safely start,
// If the requestValidator fails to initialize, the server cannot safely start,
// so the process is terminated with a fatal log.
validator, err := newSpecValidator(specPath, true)
requestValidator, err := middleware.NewSpecValidator(specPath, true)
if err != nil {
log.Fatal("openapi validator", zap.Error(err))
log.Fatal("openapi requestValidator", zap.Error(err))
}

// Build the response validator middleware using the default configuration.
// The validator runs in audit mode by default (use Enforce in CI).
// If the responseValidator fails to initialize, the server cannot safely start,
respCfg := middleware.DefaultResponseValidatorConfig()
respCfg.Mode = middleware.ResponseValidationAudit // prod default; use Enforce in CI
responseValidator := middleware.NewOpenAPIResponseValidator(respCfg)

// Create a new Gin engine instance. Gin provides routing, middleware,
// and HTTP handler abstractions.
r := gin.New()
Expand All @@ -81,20 +90,25 @@ func main() {
// handlers from crashing the server by recovering and returning a 500.
r.Use(gin.Recovery())

// Attach the OpenAPI validator middleware so that all incoming requests
// Attach the OpenAPI requestValidator middleware so that all incoming requests
// are validated against the OpenAPI specification before reaching the
// actual endpoint handlers.
r.Use(validator)
r.Use(requestValidator)
r.Use(responseValidator)

// Add middleware AFTER NewRouter (it returns *gin.Engine)
//var requestLogger = RequestLogRedactingAuth()
//r.Use(requestLogger)

// Register HTTP routes on the Gin engine.

// Health endpoint: typically used by Kubernetes or other systems to
// check if the service is alive and ready to receive traffic.
registerHealthzRoute(r)
handlers.RegisterHealthzRoute(r)

// Service info endpoint: exposes basic metadata about the service,
// such as name, version, and current timestamp.
registerServiceInfoRoute(r)
handlers.RegisterServiceInfoRoute(r)

// Construct the HTTP server using the Gin engine as the handler.
// ReadHeaderTimeout limits the time allowed to read request headers,
Expand Down
Loading