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
173 changes: 173 additions & 0 deletions .github/workflows/postgres-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
name: PostgreSQL image

on:
push:
branches: [main]
paths:
- build/package/postgres.Dockerfile
- .github/workflows/postgres-image.yml
workflow_dispatch:

permissions: {}

concurrency:
group: postgres-18-image
cancel-in-progress: true

jobs:
verify:
name: Verify PostgreSQL 18 image
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3

- name: Build test image
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
file: build/package/postgres.Dockerfile
load: true
tags: onebox-postgres:test

- name: Verify bundled extensions
shell: bash
run: |
set -euo pipefail
container=onebox-postgres-test
trap 'docker rm -f "$container" >/dev/null 2>&1 || true' EXIT
docker run -d --name "$container" -e POSTGRES_PASSWORD=test onebox-postgres:test \
-c shared_preload_libraries=pg_cron,pgaudit,pg_stat_statements \
-c cron.database_name=postgres \
-c cron.use_background_workers=on
for attempt in $(seq 1 30); do
if docker exec "$container" pg_isready -U postgres -d postgres >/dev/null 2>&1; then
break
fi
if [ "$attempt" -eq 30 ]; then
docker logs "$container"
exit 1
fi
sleep 1
done
docker exec "$container" psql -U postgres -d postgres -v ON_ERROR_STOP=1 \
-c 'CREATE EXTENSION vector' \
-c 'CREATE EXTENSION vectorscale' \
-c 'CREATE EXTENSION postgis' \
-c 'CREATE EXTENSION pg_cron' \
-c 'CREATE EXTENSION pgaudit' \
-c 'CREATE EXTENSION pg_repack' \
-c 'CREATE EXTENSION pg_partman' \
-c 'CREATE EXTENSION hypopg' \
-c 'CREATE EXTENSION pg_stat_statements' \
-c "SELECT '[1,2,3]'::vector <-> '[1,2,4]'::vector" \
-c 'CREATE TABLE onebox_vectorscale(id bigint, embedding vector(3))' \
-c "INSERT INTO onebox_vectorscale VALUES (1, '[1,2,3]'), (2, '[4,5,6]'), (3, '[7,8,9]')" \
-c 'CREATE INDEX onebox_vectorscale_diskann ON onebox_vectorscale USING diskann (embedding vector_l2_ops)' \
-c "SELECT indexdef FROM pg_indexes WHERE indexname = 'onebox_vectorscale_diskann'" \
-c 'SET enable_seqscan = off' \
-c "SELECT id FROM onebox_vectorscale ORDER BY embedding <-> '[1,2,3]' LIMIT 1" \
-c "SELECT ST_AsText(ST_Point(1, 2))" \
-c "SELECT cron.schedule('onebox-smoke', '0 0 * * *', 'SELECT 1')" \
-c "SELECT cron.unschedule('onebox-smoke')" \
-c 'CREATE TABLE onebox_hypopg(id bigint)' \
-c "SELECT indexrelid IS NOT NULL FROM hypopg_create_index('CREATE INDEX ON onebox_hypopg(id)')" \
-c "SELECT extname, extversion FROM pg_extension WHERE extname IN ('vector', 'vectorscale', 'postgis', 'pg_cron', 'pgaudit', 'pg_repack', 'pg_partman', 'hypopg', 'pg_stat_statements') ORDER BY extname"
docker exec "$container" pg_repack --version

publish:
name: Publish PostgreSQL 18 image
needs: verify
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3

- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3

- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and publish commit-addressed candidate
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
file: build/package/postgres.Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ghcr.io/labstack/onebox-postgres:18-${{ github.sha }}
provenance: mode=max
sbom: true

- name: Verify exact published platform images
shell: bash
run: |
set -euo pipefail
candidate="ghcr.io/labstack/onebox-postgres:18-${GITHUB_SHA}"
for platform in linux/amd64 linux/arm64; do
architecture="${platform#linux/}"
container="onebox-postgres-${architecture}"
trap 'docker rm -f "$container" >/dev/null 2>&1 || true' EXIT
docker run -d --platform "$platform" --name "$container" -e POSTGRES_PASSWORD=test "$candidate" \
-c shared_preload_libraries=pg_cron,pgaudit,pg_stat_statements \
-c cron.database_name=postgres \
-c cron.use_background_workers=on
for attempt in $(seq 1 60); do
if docker exec "$container" pg_isready -U postgres -d postgres >/dev/null 2>&1; then
break
fi
if [ "$attempt" -eq 60 ]; then
docker logs "$container"
exit 1
fi
sleep 1
done
docker exec "$container" psql -U postgres -d postgres -v ON_ERROR_STOP=1 \
-c 'CREATE EXTENSION vector' \
-c 'CREATE EXTENSION vectorscale' \
-c 'CREATE EXTENSION postgis' \
-c 'CREATE EXTENSION pg_cron' \
-c 'CREATE EXTENSION pgaudit' \
-c 'CREATE EXTENSION pg_repack' \
-c 'CREATE EXTENSION pg_partman' \
-c 'CREATE EXTENSION hypopg' \
-c 'CREATE EXTENSION pg_stat_statements' \
-c "SELECT '[1,2,3]'::vector <-> '[1,2,4]'::vector" \
-c 'CREATE TABLE onebox_vectorscale(id bigint, embedding vector(3))' \
-c "INSERT INTO onebox_vectorscale VALUES (1, '[1,2,3]'), (2, '[4,5,6]'), (3, '[7,8,9]')" \
-c 'CREATE INDEX onebox_vectorscale_diskann ON onebox_vectorscale USING diskann (embedding vector_l2_ops)' \
-c "SELECT indexdef FROM pg_indexes WHERE indexname = 'onebox_vectorscale_diskann'" \
-c 'SET enable_seqscan = off' \
-c "SELECT id FROM onebox_vectorscale ORDER BY embedding <-> '[1,2,3]' LIMIT 1" \
-c "SELECT ST_AsText(ST_Point(1, 2))" \
-c "SELECT cron.schedule('onebox-smoke', '0 0 * * *', 'SELECT 1')" \
-c "SELECT cron.unschedule('onebox-smoke')" \
-c 'CREATE TABLE onebox_hypopg(id bigint)' \
-c "SELECT indexrelid IS NOT NULL FROM hypopg_create_index('CREATE INDEX ON onebox_hypopg(id)')"
docker exec "$container" pg_repack --version
docker rm -f "$container"
trap - EXIT
done

- name: Promote tested manifest to PostgreSQL 18
run: docker buildx imagetools create --tag ghcr.io/labstack/onebox-postgres:18 "ghcr.io/labstack/onebox-postgres:18-${GITHUB_SHA}"

- name: Verify public anonymous access
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
visibility=$(gh api orgs/labstack/packages/container/onebox-postgres --jq .visibility)
if [ "$visibility" != public ]; then
echo "::error::Make the onebox-postgres package public at https://github.com/orgs/labstack/packages/container/onebox-postgres/settings and re-run this workflow."
exit 1
fi
docker logout ghcr.io
docker buildx imagetools inspect ghcr.io/labstack/onebox-postgres:18
11 changes: 11 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@ jobs:
exit 1
fi

- name: Require public PostgreSQL 18 distribution
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
visibility=$(gh api orgs/labstack/packages/container/onebox-postgres --jq .visibility)
if [ "$visibility" != public ]; then
echo "The onebox-postgres package must be public before a release can reference it." >&2
exit 1
fi
docker manifest inspect ghcr.io/labstack/onebox-postgres:18 >/dev/null

- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3

- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
Expand Down
83 changes: 83 additions & 0 deletions build/package/postgres.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# syntax=docker/dockerfile:1

ARG PG_MAJOR=18
ARG DEBIAN_CODENAME=trixie
FROM postgres:${PG_MAJOR}-${DEBIAN_CODENAME}

ARG PG_MAJOR
ARG TARGETARCH
ARG PGVECTOR_COMMIT=8ee86c96f0fd72390f890aa8a336fda6d3ab4c6c
ARG PGVECTOR_VERSION=0.8.6
ARG PGVECTORSCALE_COMMIT=c66cae4b621664b68546587da9fafd80b791e643
ARG PGVECTORSCALE_VERSION=0.9.0
ARG PGVECTORSCALE_AMD64_SHA256=7a5450b81a7403ca20ff5e5a2f81aa13c81795ddd1fdfe9b986c42c48b12ed67
ARG PGVECTORSCALE_ARM64_SHA256=8d0916df999f082ceb3d019bdfa72f5df395c31b152f7906de59e429ee11edc7
ARG POSTGIS_PACKAGE_VERSION=3.6.4+dfsg-2.pgdg13+1
ARG PG_CRON_PACKAGE_VERSION=1.6.7-3.pgdg13+1
ARG PGAUDIT_PACKAGE_VERSION=18.0-3.pgdg13+1
ARG PG_REPACK_PACKAGE_VERSION=1.5.3-1.pgdg13+1
ARG PG_PARTMAN_PACKAGE_VERSION=5.5.0-1.pgdg13+1
ARG HYPOPG_PACKAGE_VERSION=1.4.3-1.pgdg13+1

LABEL org.opencontainers.image.source="https://github.com/labstack/onebox" \
org.opencontainers.image.description="PostgreSQL for Onebox-managed applications" \
org.opencontainers.image.licenses="PostgreSQL AND GPL-2.0-or-later"

# The full commit is immutable. BuildKit checks out that exact source instead
# of trusting a movable release tag, and the final image contains no compiler.
ADD https://github.com/pgvector/pgvector.git#${PGVECTOR_COMMIT} /tmp/pgvector
RUN apt-get update && \
apt-mark hold locales && \
apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
curl \
postgresql-server-dev-${PG_MAJOR} \
postgresql-${PG_MAJOR}-postgis-3=${POSTGIS_PACKAGE_VERSION} \
postgresql-${PG_MAJOR}-cron=${PG_CRON_PACKAGE_VERSION} \
postgresql-${PG_MAJOR}-pgaudit=${PGAUDIT_PACKAGE_VERSION} \
postgresql-${PG_MAJOR}-repack=${PG_REPACK_PACKAGE_VERSION} \
postgresql-${PG_MAJOR}-partman=${PG_PARTMAN_PACKAGE_VERSION} \
postgresql-${PG_MAJOR}-hypopg=${HYPOPG_PACKAGE_VERSION} \
unzip && \
cd /tmp/pgvector && \
make clean && \
make OPTFLAGS="" && \
make install && \
grep -Fqx "default_version = '${PGVECTOR_VERSION}'" vector.control && \
install -d /usr/share/doc/pgvector && \
install -m 0644 LICENSE README.md /usr/share/doc/pgvector/ && \
printf '%s\n' "${PGVECTOR_VERSION}" > /usr/share/doc/pgvector/VERSION && \
case "${TARGETARCH}" in \
amd64) pgvectorscale_sha="${PGVECTORSCALE_AMD64_SHA256}" ;; \
arm64) pgvectorscale_sha="${PGVECTORSCALE_ARM64_SHA256}" ;; \
*) echo "unsupported pgvectorscale architecture: ${TARGETARCH}" >&2; exit 1 ;; \
esac && \
pgvectorscale_archive="pgvectorscale-${PGVECTORSCALE_VERSION}-pg${PG_MAJOR}-${TARGETARCH}.zip" && \
curl -fsSL \
"https://github.com/timescale/pgvectorscale/releases/download/${PGVECTORSCALE_VERSION}/${pgvectorscale_archive}" \
-o "/tmp/${pgvectorscale_archive}" && \
echo "${pgvectorscale_sha} /tmp/${pgvectorscale_archive}" | sha256sum -c - && \
install -d /tmp/pgvectorscale-package && \
unzip -j "/tmp/${pgvectorscale_archive}" \
"pgvectorscale-postgresql-${PG_MAJOR}_${PGVECTORSCALE_VERSION}-Linux_${TARGETARCH}.deb" \
-d /tmp/pgvectorscale-package && \
apt-get install -y --no-install-recommends \
"/tmp/pgvectorscale-package/pgvectorscale-postgresql-${PG_MAJOR}_${PGVECTORSCALE_VERSION}-Linux_${TARGETARCH}.deb" && \
grep -Fqx "default_version = '${PGVECTORSCALE_VERSION}'" \
"/usr/share/postgresql/${PG_MAJOR}/extension/vectorscale.control" && \
install -d /usr/share/doc/pgvectorscale && \
curl -fsSL \
"https://raw.githubusercontent.com/timescale/pgvectorscale/${PGVECTORSCALE_COMMIT}/LICENSE" \
-o /usr/share/doc/pgvectorscale/LICENSE && \
curl -fsSL \
"https://raw.githubusercontent.com/timescale/pgvectorscale/${PGVECTORSCALE_COMMIT}/NOTICE" \
-o /usr/share/doc/pgvectorscale/NOTICE && \
echo "df34f0384d53261f4dc47b3d834a13b570f177b8ab1c8a00266a98f30de2e117 /usr/share/doc/pgvectorscale/LICENSE" | sha256sum -c - && \
echo "4e204f7b0aa175af0a3b38c3bc56852954adf0110e25babe94eab6e35eeef114 /usr/share/doc/pgvectorscale/NOTICE" | sha256sum -c - && \
cd / && \
rm -rf /tmp/pgvector /tmp/pgvectorscale-package "/tmp/${pgvectorscale_archive}" && \
apt-get remove -y build-essential ca-certificates curl postgresql-server-dev-${PG_MAJOR} unzip && \
apt-get autoremove -y && \
apt-mark unhold locales && \
rm -rf /var/lib/apt/lists/*
25 changes: 25 additions & 0 deletions docs/onebox.run-v1.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1586,6 +1586,31 @@
"pattern": "^[a-z]([a-z0-9-]{0,38}[a-z0-9])?$",
"type": "string"
},
"features": {
"additionalProperties": false,
"description": "Capabilities Onebox must establish before application workloads run.",
"patternProperties": {
"^x-": {}
},
"properties": {
"extensions": {
"additionalProperties": {
"additionalProperties": false,
"patternProperties": {
"^x-": {}
},
"properties": {},
"type": "object"
},
"description": "PostgreSQL extensions Onebox installs in the managed application database before application migrations run.",
"propertyNames": {
"pattern": "^[a-z][a-z0-9_-]*$"
},
"type": "object"
}
},
"type": "object"
},
"persistence": {
"additionalProperties": false,
"description": "Data-lifetime declaration for this supporting service.",
Expand Down
20 changes: 7 additions & 13 deletions e2e/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,16 @@ func (s *server) requireDocker(t *testing.T) {
}, "\n"))
}

// primeDriverImage puts the PostgreSQL image on the server from a mirror.
//
// The driver names `postgres:18` and the tag is not overridable, so every run
// would otherwise pull it from Docker Hub — which rate-limits anonymous
// requests per source address, and a CI runner shares its address with
// everyone else on it. The image content is identical across registries, so
// retagging it locally is the same bytes by the same digest: `docker image
// inspect` reports postgres@sha256:… first, which is what ob reads when it
// pins the protected image.
// primeDriverImage makes the public Onebox PostgreSQL distribution available
// before bootstrap. Release publication is gated on this exact tag being
// anonymously pullable.
func (s *server) primeDriverImage(t *testing.T) {
t.Helper()
if err := s.try(t, "docker image inspect postgres:18 >/dev/null 2>&1"); err == nil {
const image = "ghcr.io/labstack/onebox-postgres:18"
if err := s.try(t, "docker image inspect "+image+" >/dev/null 2>&1"); err == nil {
return
}
s.run(t, "docker pull -q public.ecr.aws/docker/library/postgres:18 >/dev/null && "+
"docker tag public.ecr.aws/docker/library/postgres:18 postgres:18")
s.run(t, "docker pull -q "+image+" >/dev/null")
}

// psql runs a query in the protected server and returns the single value.
Expand Down Expand Up @@ -351,7 +345,7 @@ HTTPServer(("127.0.0.1", 18080), Handler).handle_request()

// Issue #88: enable established archiving and then failed every upload
// with "x509: certificate signed by unknown authority", because wal-g runs
// inside the driver's image and postgres:18 carries no certificate
// inside the driver's image and the PostgreSQL 18 distribution carries no certificate
// authorities. It failed after the base backup, leaving archiving on.
//
// This endpoint is signed by an authority that exists only on this server,
Expand Down
3 changes: 3 additions & 0 deletions internal/app/constraints.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ var (
gSettingKey = grammar{"driver setting", regexp.MustCompile(`^[a-z][a-z0-9_-]*$`),
"a lowercase setting name such as appendonly, maxmemory-policy or shared_buffers"}

gExtension = grammar{"PostgreSQL extension", regexp.MustCompile(`^[a-z][a-z0-9_-]*$`),
"a lowercase PostgreSQL extension name such as vector, pg_trgm or uuid-ossp"}

// Docker admits plugin log drivers with arbitrary names, so this cannot be
// an enum without refusing a legitimate one. A grammar still catches the
// typo and the metacharacter, which is what reaches the generated runtime.
Expand Down
3 changes: 3 additions & 0 deletions internal/app/jsonschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,9 @@ var schemaConstraints = []struct {
{[]string{"api_version"}, map[string]any{"const": APIVersion}},
{[]string{"app"}, appNameConstraint()},
{[]string{"base_path"}, pattern(gAbsPath)},
{[]string{"services", "*", "features", "extensions"}, map[string]any{
"propertyNames": map[string]any{"pattern": gExtension.pattern.String()},
}},

{[]string{"environments", "*", "base_path"}, pattern(gAbsPath)},
{[]string{"environments", "*", "policy", "min_onebox_version"}, pattern(gCalVer)},
Expand Down
Loading