diff --git a/Dockerfile b/Dockerfile index bad42ea..70657c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,9 +3,10 @@ # Stage 1 builds the neo4j-browser static frontend from source. # Stage 2 is the Apache AGE image (PostgreSQL with the AGE extension) plus the # peo4c bridge, which speaks the Bolt protocol to the browser and translates -# Cypher into AGE `cypher()` calls. +# Cypher into AGE `cypher()` calls, and pgAdmin as a SQL interface. ARG AGE_IMAGE=docker.io/apache/age:release_PG18_1.8.0 +ARG PGADMIN_VERSION=9.18 FROM docker.io/library/node:16.20.2-bullseye AS browser @@ -23,13 +24,23 @@ RUN yarn build && test -f dist/index.html FROM ${AGE_IMAGE} +ARG PGADMIN_VERSION + RUN apt-get update \ - && apt-get install -y --no-install-recommends python3 python3-psycopg \ + && apt-get install -y --no-install-recommends python3 python3-psycopg python3-venv \ && rm -rf /var/lib/apt/lists/* +RUN python3 -m venv /opt/pgadmin \ + && /opt/pgadmin/bin/pip install --no-cache-dir "pgadmin4==${PGADMIN_VERSION}" gunicorn \ + && ln -s "$(/opt/pgadmin/bin/python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')/pgadmin4" \ + /opt/pgadmin/app \ + && mkdir -p /var/lib/pgadmin \ + && chown postgres:postgres /var/lib/pgadmin + COPY --from=browser /src/dist /opt/peo4c/browser COPY bridge /opt/peo4c/bridge COPY docker/initdb/ /docker-entrypoint-initdb.d/ +COPY docker/pgadmin/config_distro.py /opt/peo4c/pgadmin/config_distro.py COPY docker/peo4c-entrypoint.sh /usr/local/bin/peo4c-entrypoint.sh ENV PEO4C_DEFAULT_GRAPH=neo4j \ @@ -37,10 +48,13 @@ ENV PEO4C_DEFAULT_GRAPH=neo4j \ PEO4C_HTTP_PORT=7474 \ PEO4C_BOLT_PORT=7687 \ PEO4C_PG_HOST=127.0.0.1 \ - PEO4C_PG_PORT=5432 + PEO4C_PG_PORT=5432 \ + PEO4C_PGADMIN=true \ + PEO4C_PGADMIN_PORT=5050 \ + CONFIG_DISTRO_FILE_PATH=/opt/peo4c/pgadmin/config_distro.py -# 7474: neo4j-browser (HTTP), 7687: Bolt, 5432: PostgreSQL -EXPOSE 7474 7687 5432 +# 7474: neo4j-browser (HTTP), 7687: Bolt, 5050: pgAdmin, 5432: PostgreSQL +EXPOSE 7474 7687 5050 5432 ENTRYPOINT ["/usr/local/bin/peo4c-entrypoint.sh"] CMD ["postgres", "-c", "shared_preload_libraries=age"] diff --git a/README.md b/README.md index de05a93..4917d57 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ [neo4j-browser](https://github.com/neo4j/neo4j-browser) running on PostgreSQL, using [Apache AGE](https://github.com/apache/age) as the graph engine. -Everything ships in one container image: +Everything ships in one container image, together with +[pgAdmin](https://www.pgadmin.org/) for working with the same data in SQL: ``` neo4j-browser (static files) peo4c bridge (Python) PostgreSQL + AGE @@ -19,14 +20,20 @@ Everything ships in one container image: ```sh podman build -t peo4c . podman run -d --name peo4c -e POSTGRES_PASSWORD=secret \ - -p 7474:7474 -p 7687:7687 -p 5432:5432 \ - -v peo4c-data:/var/lib/postgresql peo4c + -p 7474:7474 -p 7687:7687 -p 5050:5050 -p 5432:5432 \ + -v peo4c-data:/var/lib/postgresql -v peo4c-pgadmin:/var/lib/pgadmin peo4c ``` -Open and log in with your **PostgreSQL** credentials: -user `postgres`, password the value of `POSTGRES_PASSWORD`. Bolt clients (the Neo4j -drivers, cypher-shell) can connect to `neo4j://localhost:7687` the same way, and -`psql` works on port 5432. +| Port | Service | Log in with | +| --- | --- | --- | +| 7474 | neo4j-browser: | PostgreSQL user `postgres` and `POSTGRES_PASSWORD` | +| 7687 | Bolt, for Neo4j drivers and cypher-shell (`neo4j://localhost:7687`) | same as above | +| 5050 | pgAdmin: | `admin@example.com` and `POSTGRES_PASSWORD` | +| 5432 | PostgreSQL, for `psql` and other SQL clients | same as neo4j-browser | + +pgAdmin comes with the bundled PostgreSQL server already registered as "peo4c"; +it asks for the PostgreSQL password the first time you open it. AGE stores each +graph as a schema of the same name, with one table per label. `docker` works as a drop-in replacement for `podman` in the commands above. @@ -59,9 +66,16 @@ drivers, cypher-shell) can connect to `neo4j://localhost:7687` the same way, and | `PEO4C_ADVERTISED_BOLT_PORT` | `7687` | Bolt port advertised to the browser; set it when you publish 7687 on another host port. | | `PEO4C_ADVERTISED_HOST` | `localhost` | Host reported in `SHOW DATABASES` and to clients that do not send a routing address. | | `PEO4C_LOG_LEVEL` | `INFO` | `DEBUG` logs every generated SQL statement. | +| `PGADMIN_DEFAULT_EMAIL` | `admin@example.com` | pgAdmin login, created on first start. | +| `PGADMIN_DEFAULT_PASSWORD` | `$POSTGRES_PASSWORD` | pgAdmin login password, set on first start. | +| `PEO4C_PGADMIN` | `true` | Set to `false` to not start pgAdmin. | +| `PEO4C_PGADMIN_PORT` | `5050` | Port pgAdmin listens on inside the container. | -Build arguments: `AGE_IMAGE` (base image, default `apache/age:release_PG18_1.8.0`) and -`NEO4J_BROWSER_REF` (neo4j-browser commit to build). +Build arguments: `AGE_IMAGE` (base image, default `apache/age:release_PG18_1.8.0`), +`NEO4J_BROWSER_REF` (neo4j-browser commit to build) and `PGADMIN_VERSION` (default `9.18`). + +pgAdmin keeps its users and settings in `/var/lib/pgadmin`; changing +`PGADMIN_DEFAULT_*` after the first start has no effect unless that directory is empty. ## Limitations diff --git a/docker/peo4c-entrypoint.sh b/docker/peo4c-entrypoint.sh index 9f93e74..0e6458c 100755 --- a/docker/peo4c-entrypoint.sh +++ b/docker/peo4c-entrypoint.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash -# Runs PostgreSQL (through the stock postgres entrypoint) and the peo4c bridge -# side by side, and stops the container when either of them exits. +# Runs PostgreSQL (through the stock postgres entrypoint), the peo4c bridge and +# pgAdmin side by side. The container stops when PostgreSQL or the bridge exits; +# pgAdmin is optional and its failure does not take the others down. set -euo pipefail if [ "${1:-}" != "postgres" ]; then @@ -9,6 +10,31 @@ fi export PEO4C_PG_DATABASE="${PEO4C_PG_DATABASE:-${POSTGRES_DB:-${POSTGRES_USER:-postgres}}}" +start_pgadmin() { + local app=/opt/pgadmin/app + local email="${PGADMIN_DEFAULT_EMAIL:-admin@example.com}" + + if [ ! -f /var/lib/pgadmin/pgadmin4.db ]; then + # First start: create the login user and register the bundled PostgreSQL. + cat > /var/lib/pgadmin/servers.json <&2 +fi + shutdown() { - kill -TERM "$bridge_pid" 2>/dev/null || true - kill -TERM "$pg_pid" 2>/dev/null || true + for pid in "$bridge_pid" $pgadmin_pid "$pg_pid"; do + kill -TERM "$pid" 2>/dev/null || true + done } trap shutdown TERM INT -# Wait until one of the two processes exits, then stop the other one too. +# Wait until PostgreSQL or the bridge exits, then stop everything. set +e wait -n "$pg_pid" "$bridge_pid" status=$? shutdown -wait "$pg_pid" 2>/dev/null +wait 2>/dev/null exit "$status" diff --git a/docker/pgadmin/config_distro.py b/docker/pgadmin/config_distro.py new file mode 100644 index 0000000..0ee0039 --- /dev/null +++ b/docker/pgadmin/config_distro.py @@ -0,0 +1,18 @@ +# pgAdmin settings for the peo4c image, loaded via CONFIG_DISTRO_FILE_PATH. +# Paths derived from DATA_DIR in pgAdmin's config.py are computed before this +# file is applied, so they are all set explicitly here. + +DATA_DIR = '/var/lib/pgadmin' +SQLITE_PATH = DATA_DIR + '/pgadmin4.db' +SESSION_DB_PATH = DATA_DIR + '/sessions' +STORAGE_DIR = DATA_DIR + '/storage' +KERBEROS_CCACHE_DIR = DATA_DIR + '/krbccache' +AZURE_CREDENTIAL_CACHE_DIR = DATA_DIR + '/azurecredentialcache' +LOG_FILE = '/dev/null' + +SERVER_MODE = True +UPGRADE_CHECK_ENABLED = False + +# From the config_distro.py shipped in the pgadmin4 wheel, which this replaces. +HELP_PATH = '../../docs/en_US/_build/html/' +MINIFY_HTML = False