Skip to content
Draft
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
6 changes: 6 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,10 @@ Configure PostgreSQL with the following options:
: Password for the PostgreSQL database user.
Can be set using the `${WAVE_DB_PASSWORD}` environment variable.

`wave.db.schema` *(optional)*
: PostgreSQL schema used for Wave tables (default: `wave`).
Can be set using the `${WAVE_DB_SCHEMA}` environment variable.

`wave.db.uri` *(required)*
: JDBC connection string for the PostgreSQL database.
For example, `jdbc:postgresql://localhost:5432/wave`.
Expand All @@ -436,6 +440,8 @@ Configure PostgreSQL with the following options:
: Username for authenticating with the PostgreSQL database.
Can be set using the `${WAVE_DB_USER}` environment variable.

For isolated deployments that share a PostgreSQL database, provision one role and schema per Wave instance, then set `WAVE_DB_USER` and `WAVE_DB_SCHEMA` to that pair (for example, `wave_pr_123`). Wave sets the datasource schema from `wave.db.schema`, so role and schema can differ if the role owns or can create the configured schema.

## Blob cache

Configure how Wave caches container blobs to improve client performance and optionally delegates transfer tasks to Kubernetes pods for scalability with the following options:
Expand Down
5 changes: 5 additions & 0 deletions docs/install/_templates/wave.env
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ WAVE_DB_URI=jdbc:postgresql://host.docker.internal:5432/changeme
#
WAVE_DB_USER=changeme

#
# PostgreSQL schema used by Wave.
#
WAVE_DB_SCHEMA=wave

#
# Password required to access the PostgreSQL database
# used by the Wave service.
Expand Down
3 changes: 2 additions & 1 deletion docs/install/configure-wave-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,9 @@ data:
# Existing database, redis, and platform configuration...
db:
uri: "jdbc:postgresql://your-postgres-host:5432/wave"
user: "wave_user"
user: "wave"
password: "your_secure_password_here"
schema: "wave"

redis:
uri: "redis://your-redis-host:6379"
Expand Down
31 changes: 11 additions & 20 deletions docs/install/docker-compose.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,34 +33,25 @@ The minimum system requirements for self-hosted Wave in Docker Compose are:

Wave requires a PostgreSQL database to operate.

Create a dedicated `wave` database and user account with the appropriate privileges:
Create a dedicated `wave` database and application role with the appropriate privileges:

```sql
-- Create a dedicated user for Wave
CREATE ROLE wave_user LOGIN PASSWORD 'your_secure_password';
-- Create a dedicated role for Wave.
CREATE ROLE wave LOGIN PASSWORD 'your_secure_password';

-- Create the Wave database
CREATE DATABASE wave;
-- Create the Wave database owned by that role.
-- On managed PostgreSQL, grant role membership to the admin user first if required.
GRANT wave TO CURRENT_USER;
CREATE DATABASE wave OWNER wave;

-- Connect to the wave database
-- Connect to the wave database.
\c wave;

-- Grant basic schema access
GRANT USAGE, CREATE ON SCHEMA public TO wave_user;

-- Grant privileges on existing tables and sequences
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO wave_user;
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO wave_user;

-- Grant privileges on future tables and sequences
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO wave_user;

ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO wave_user;
-- Let Wave create and own its dedicated schema on first startup.
GRANT CONNECT, CREATE ON DATABASE wave TO wave;
```

Wave will automatically handle schema migrations on startup and create the required database objects.
Wave creates its dedicated PostgreSQL schema on startup and creates the required database objects there. The default schema is `wave`; set `WAVE_DB_SCHEMA` to use a different per-instance schema.

## Wave config

Expand Down
32 changes: 12 additions & 20 deletions docs/install/kubernetes.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,31 +42,22 @@ This guide assumes:

Wave requires a PostgreSQL database to operate.

Create a dedicated `wave` database and user account with the appropriate privileges:
Create a dedicated `wave` database and application role with the appropriate privileges:

```sql
-- Create a dedicated user for Wave
CREATE ROLE wave_user LOGIN PASSWORD 'your_secure_password';
-- Create a dedicated role for Wave.
CREATE ROLE wave LOGIN PASSWORD 'your_secure_password';

-- Create the Wave database
CREATE DATABASE wave;
-- Create the Wave database owned by that role.
-- On managed PostgreSQL, grant role membership to the admin user first if required.
GRANT wave TO CURRENT_USER;
CREATE DATABASE wave OWNER wave;

-- Connect to the wave database
-- Connect to the wave database.
\c wave;

-- Grant basic schema access
GRANT USAGE, CREATE ON SCHEMA public TO wave_user;

-- Grant privileges on existing tables and sequences
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO wave_user;
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO wave_user;

-- Grant privileges on future tables and sequences
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO wave_user;

ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO wave_user;
-- Let Wave create and own its dedicated schema on first startup.
GRANT CONNECT, CREATE ON DATABASE wave TO wave;
```

## Create namespace
Expand Down Expand Up @@ -128,8 +119,9 @@ data:
# Database connection settings
db:
uri: "jdbc:postgresql://your-postgres-host:5432/wave"
user: "wave_user"
user: "wave"
password: "your_secure_password"
schema: "wave"

# Redis configuration for caching and session management
redis:
Expand Down
25 changes: 25 additions & 0 deletions docs/migrations/1-36-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
title: Migrating to 1.36.0
tags: [migration notes, wave]
---

Wave 1.36.0 moves PostgreSQL tables out of the `public` schema and into a dedicated Wave schema.

## Mandatory steps

If your deployment already uses PostgreSQL, move the legacy `public.wave_*` tables before deploying this version:

```bash
psql "postgresql://<admin-user>@<host>:5432/wave" \
-v target_schema="${WAVE_DB_SCHEMA:-wave}" \
-v owner_role="${WAVE_DB_USER:-wave}" \
-f migrations/2026-07-move-wave-schema.sql
```

The `target_schema` value must match the `WAVE_DB_SCHEMA` value used by the new Wave deployment. The migration is a metadata-only move (`ALTER TABLE ... SET SCHEMA`), not a row copy.

If the old Wave build is still running during the migration, use a target schema that matches the current database role so the default PostgreSQL search path (`"$user", public`) can still resolve the moved tables. Otherwise, run the script during the deployment window and start the new build with `WAVE_DB_SCHEMA` set to the target schema.

The application now fails startup if any `wave_*` tables remain in `public`, to prevent creating empty shadow tables in the new schema.

New deployments do not need to run the migration script. Wave creates the configured schema automatically on startup. The default schema is `wave`; set `WAVE_DB_SCHEMA` for isolated deployments such as PR previews.
1 change: 1 addition & 0 deletions docs/migrations/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ tags: [migration notes, wave]

This section covers mandatory steps for migrating to new versions of Wave.

- [Migrating to 1.36.0](./1-36-0.md)
- [Migrating to 1.25.0](./1-25-0.md)
- [Migrating to 1.24.0](./1-24-0.md)
- [Migrating to 1.21.0](./1-21-0.md)
5 changes: 5 additions & 0 deletions lite/wave.env
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ WAVE_DB_URI=jdbc:postgresql://db:5432/wave
#
WAVE_DB_USER=wave

#
# PostgreSQL schema used by Wave.
#
WAVE_DB_SCHEMA=wave

#
# Password required to access the PostgreSQL database
# used by the Wave service.
Expand Down
100 changes: 100 additions & 0 deletions migrations/2026-07-move-wave-schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
-- =============================================================================
-- Wave PostgreSQL schema migration: public -> dedicated schema
-- =============================================================================
--
-- PURPOSE
-- Move Wave's legacy public.wave_* tables into a dedicated schema.
-- ALTER TABLE ... SET SCHEMA is metadata-only; table data is not rewritten.
--
-- USAGE
-- psql "postgresql://<admin-user>@<host>:5432/wave" \
-- -v target_schema=wave \
-- -v owner_role=wave \
-- -f migrations/2026-07-move-wave-schema.sql
--
-- REQUIRED PRIVILEGES
-- Run as a role that can create schemas and alter the legacy public.wave_*
-- tables. On managed PostgreSQL, this is typically the DB owner/admin.
--
-- IDEMPOTENCY
-- Re-running is safe after the tables have moved: ALTER TABLE IF EXISTS
-- public.<table> becomes a no-op once the public table is gone.
-- If both public.<table> and <target_schema>.<table> exist, the script fails
-- before moving anything. Do not merge by hand; inspect which table has real
-- data and remove only confirmed empty shadow tables.
--
-- DOWNTIME / SEARCH PATH
-- Moving tables into a schema that does not match the currently running app
-- role can break old app connections whose search_path is "$user", public.
-- For live compatibility, use a target_schema matching the app role. Otherwise
-- run this during the deploy window and start the new build with WAVE_DB_SCHEMA
-- set to the same target_schema.
--
-- ROLLBACK
-- BEGIN;
-- ALTER TABLE IF EXISTS :"target_schema".wave_build SET SCHEMA public;
-- ALTER TABLE IF EXISTS :"target_schema".wave_request SET SCHEMA public;
-- ALTER TABLE IF EXISTS :"target_schema".wave_mirror SET SCHEMA public;
-- ALTER TABLE IF EXISTS :"target_schema".wave_scan SET SCHEMA public;
-- COMMIT;
--
-- =============================================================================

\if :{?target_schema}
\else
\set target_schema wave
\endif

\if :{?owner_role}
\else
\set owner_role wave
\endif

\set ON_ERROR_STOP on

SELECT CASE WHEN EXISTS (
SELECT 1
FROM pg_tables public_tables
JOIN pg_tables target_tables
ON target_tables.tablename = public_tables.tablename
WHERE public_tables.schemaname = 'public'
AND target_tables.schemaname = :'target_schema'
AND public_tables.tablename IN ('wave_build', 'wave_request', 'wave_mirror', 'wave_scan')
) THEN 'true' ELSE 'false' END AS has_target_collision
\gset

\if :has_target_collision
\echo 'Refusing to migrate: at least one wave_* table exists in both public and target schema.'
\echo 'Inspect target tables before retrying; drop only confirmed empty shadow tables.'
\quit 1
\endif

BEGIN;

CREATE SCHEMA IF NOT EXISTS :"target_schema" AUTHORIZATION :"owner_role";
ALTER SCHEMA :"target_schema" OWNER TO :"owner_role";

ALTER TABLE IF EXISTS public.wave_build SET SCHEMA :"target_schema";
ALTER TABLE IF EXISTS public.wave_request SET SCHEMA :"target_schema";
ALTER TABLE IF EXISTS public.wave_mirror SET SCHEMA :"target_schema";
ALTER TABLE IF EXISTS public.wave_scan SET SCHEMA :"target_schema";

ALTER TABLE IF EXISTS :"target_schema".wave_build OWNER TO :"owner_role";
ALTER TABLE IF EXISTS :"target_schema".wave_request OWNER TO :"owner_role";
ALTER TABLE IF EXISTS :"target_schema".wave_mirror OWNER TO :"owner_role";
ALTER TABLE IF EXISTS :"target_schema".wave_scan OWNER TO :"owner_role";

SELECT CASE WHEN EXISTS (
SELECT 1
FROM pg_tables
WHERE schemaname = 'public'
AND tablename IN ('wave_build', 'wave_request', 'wave_mirror', 'wave_scan')
) THEN 'true' ELSE 'false' END AS has_public_leftovers
\gset

\if :has_public_leftovers
\echo 'Migration incomplete: one or more wave_* tables remain in public.'
\quit 1
\endif

COMMIT;
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import io.micronaut.context.annotation.Primary
import io.micronaut.context.annotation.Requires
import io.micronaut.runtime.event.ApplicationStartupEvent
import io.micronaut.runtime.event.annotation.EventListener
import io.micronaut.scheduling.TaskExecutors
import io.seqera.wave.core.ContainerDigestPair
import io.seqera.wave.service.mirror.MirrorResult
Expand Down Expand Up @@ -57,9 +55,6 @@ import jakarta.inject.Named
@CompileStatic
class PostgresPersistentService implements PersistenceService {

@Inject
private PostgresSchemaService dbInitService

@Inject
private BuildRepository buildRepository

Expand All @@ -76,11 +71,6 @@ class PostgresPersistentService implements PersistenceService {
@Named(TaskExecutors.BLOCKING)
private ExecutorService ioExecutor

@EventListener
void onApplicationStartup(ApplicationStartupEvent event) {
dbInitService.create()
}

// ===== --- build records ---- =====
private Runnable safeRun(Runnable action, GString msg) {
new Runnable() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@
package io.seqera.wave.service.persistence.postgres

import java.sql.SQLException
import java.util.regex.Pattern

import groovy.transform.CompileStatic
import io.micronaut.context.annotation.Context
import io.micronaut.context.annotation.Requires
import io.micronaut.context.annotation.Value
import io.micronaut.data.jdbc.runtime.JdbcOperations
import jakarta.annotation.PostConstruct
import jakarta.inject.Inject
import jakarta.inject.Singleton

Expand All @@ -32,13 +36,19 @@ import jakarta.inject.Singleton
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
@Requires(env='postgres')
@Context
@Singleton
@CompileStatic
class PostgresSchemaService {

final static private Pattern SAFE_IDENT = Pattern.compile('[a-z_][a-z0-9_]*')

@Inject
private JdbcOperations jdbcOperations

@Value('${wave.db.schema:wave}')
private String schemaName

final static private String ddl = '''
-- BUILD entity
CREATE TABLE IF NOT EXISTS wave_build (
Expand Down Expand Up @@ -94,10 +104,18 @@ class PostgresSchemaService {

'''.stripIndent()

@PostConstruct
void init() {
create()
}

void create() {
assertSafeIdentifier(schemaName)
assertNoLegacyTablesInPublic()
jdbcOperations.execute((conn)-> {
try(final stmt = conn.createStatement()) {
stmt.execute("CREATE SCHEMA IF NOT EXISTS ${schemaName} AUTHORIZATION CURRENT_USER")
stmt.execute("SET search_path = ${schemaName}")
stmt.execute(ddl)
}
catch (SQLException e) {
Expand All @@ -123,4 +141,23 @@ class PostgresSchemaService {
}
})
}

private void assertNoLegacyTablesInPublic() {
final count = jdbcOperations.prepareStatement(
"SELECT count(*) FROM pg_tables WHERE schemaname = 'public' AND tablename LIKE 'wave\\_%' ESCAPE '\\'"
) { stmt ->
final resultSet = stmt.executeQuery()
return resultSet.next() ? resultSet.getInt(1) : 0
}
if( count>0 ) {
throw new IllegalStateException(
"${count} legacy wave_* table(s) in 'public' require migration to '${schemaName}' before starting this build. Contact support.")
}
}

static private void assertSafeIdentifier(String name) {
if( !name || !SAFE_IDENT.matcher(name).matches() ) {
throw new IllegalStateException("Invalid wave.db.schema: must match ${SAFE_IDENT.pattern()} (got: ${name})")
}
}
}
Loading
Loading