Migrate Drone SQLite database to PostgreSQL
As part of my recent homelab overhaul, I was unsatisfied with my Drone CI/CD deployment. The main reason for this was the deployment needed to have a persistent volume, despite not storing any data - the volume was only needed for the SQLite database. SQLite is great for getting started, but proves to be inflexible when you have multiple machines and want to move your workloads around.
Since I recently set up a PostgreSQL database with Pulumi and Drone supports PostgreSQL as a database backend, I decided it was time to do something about it. Luckily, someone already documented the steps in the Drone forums and the procedure is fairly straightforward, though there are a some small pitfalls I discovered along the way, which I want to share with you in this post.
For future reference, I’m using Drone 1.10.1, PGLoader 3.6.3 and PostgreSQL 14.1 at the time of writing.
The first step is obtaining the current SQLite database. Basically, we just need to make a copy of the database file, but it is recommended to turn off the Drone server before that to avoid any writes to the database while we are copying it. The exact procedure will depend on your deployment environment (binary, Docker, Kubernetes), of course.
Following that, immediately make a backup copy of it, just to be sure:
cp database.sqlite drone.sqlite.bak
The next step is to install PGLoader. As the name suggests, it’s a tool for migrating your data other data formats (e.g. CSV) and databases (e.g. MSSQL, MySQL or SQLite) into PostgreSQL. The author gave a nice introductory presentation about the tool at PGConf 2017. You can find installation instructions in the repository.
Then, we need to create a PostgreSQL database and user, if not already present. I’m doing this with my Pulumi Infrastructure-as-Code setup, but I can of course also simply be done with the command line:
postgres=# CREATE DATABASE drone;
postgres=# CREATE USER drone WITH ENCRYPTED PASSWORD 'changemeplease';
postgres=# GRANT ALL PRIVILEGES ON DATABASE drone to drone;
And then we’re already set! Import the database with the following command (substituting the variables as appropriate):
pgloader database.sqlite pgsql://USERNAME:PASSWORD@HOST:PORT/DB_NAME
Unfortunately, you are likely to encounter this error, which is already tracked in the upstream issues:
ERROR Database error 42704: type "number" does not exist
We can workaround this error by creating a second table with the same schema as builds
but different data types,
after checking the current data types and indices with:
# open original SQLite database
$ sqlite datbase.sqlite
# check the current schema and indices
sqlite> .schema builds
# create a second table with postgres-compatible types
sqlite>
CREATE TABLE buildsnew(
build_id bigserial,
build_repo_id bigint,
build_trigger text,
build_number bigint,
build_parent bigint,
build_status text,
build_error text,
build_event text,
build_action text,
build_link text,
build_timestamp bigint,
build_title text,
build_message text,
build_before text,
build_after text,
build_ref text,
build_source_repo text,
build_source text,
build_target text,
build_author text,
build_author_name text,
build_author_email text,
build_author_avatar text,
build_sender text,
build_deploy text,
build_params text,
build_started bigint,
build_finished bigint,
build_created bigint,
build_updated bigint,
build_version bigint,
build_cron text default '',
build_deploy_id bigint default '0',
build_debug boolean default '0',
UNIQUE(build_repo_id, build_number)
);
INSERT INTO buildsnew SELECT * FROM builds;
DROP TABLE builds;
ALTER TABLE `buildsnew` RENAME TO `builds`;
CREATE INDEX ix_build_repo ON builds (build_repo_id);
CREATE INDEX ix_build_author ON builds (build_author);
CREATE INDEX ix_build_sender ON builds (build_sender);
CREATE INDEX ix_build_ref ON builds (build_repo_id, build_ref);
CREATE INDEX ix_build_incomplete ON builds (build_status) WHERE build_status IN ('pending', 'running');
Press Ctrl-D to exit the SQLite shell. The data will be saved upon exiting.
Now we can retry the database import:
$ pgloader database.sqlite pgsql://USERNAME:PASSWORD@HOST/DB_NAME
2022-02-11T21:19:10.016668+01:00 LOG pgloader version "3.6.62d83f2"
2022-02-11T21:19:10.593394+01:00 LOG Migrating from #<SQLITE-CONNECTION sqlite:///home/jack/database.sqlite {1006FF52F3}>
2022-02-11T21:19:10.593394+01:00 LOG Migrating into #<PGSQL-CONNECTION pgsql://USER@HOST/DB_NAME {10072DC4F3}>
2022-02-11T21:19:26.941747+01:00 LOG report summary reset
table name errors rows bytes total time
----------------------- --------- --------- --------- --------------
fetch 0 0 0.000s
fetch meta data 0 50 0.067s
Create Schemas 0 0 0.053s
Create SQL Types 0 0 0.143s
Create tables 0 26 4.884s
Set Table OIDs 0 13 0.067s
----------------------- --------- --------- --------- --------------
migrations 0 36 1.0 kB 0.557s
users 0 1 1.8 kB 0.540s
repos 0 28 7.5 kB 0.543s
stages 0 233 33.9 kB 1.113s
logs 0 658 9.5 MB 1.924s
secrets 0 14 4.4 kB 1.737s
orgsecrets 0 0 2.287s
builds 0 233 102.0 kB 2.410s
perms 0 28 1.0 kB 0.100s
steps 0 709 39.3 kB 0.590s
cron 0 0 0.607s
nodes 0 0 1.227s
latest 0 8 0.4 kB 1.237s
----------------------- --------- --------- --------- --------------
COPY Threads Completion 0 4 2.447s
Create Indexes 0 32 6.424s
Index Build Completion 0 32 3.594s
Reset Sequences 0 6 0.810s
Primary Keys 0 11 2.000s
Create Foreign Keys 1 4 0.920s
Create Triggers 0 0 0.123s
Install Comments 0 0 0.000s
----------------------- --------- --------- --------- --------------
Total import time ✓ 1948 9.7 MB 16.318s
This indicates that the import was successful! In my case there was one error due to a missing UNIQUE constraint, however this was automatically corrected by PGLoader. As an additional verification step, we can connect to the PostgreSQL instance and inspect the tables:
$ psql -U USER -h HOST -d DB_NAME
drone=> \dt
List of relations
Schema | Name | Type | Owner
--------+------------+-------+-------
public | builds | table | drone
public | cron | table | drone
public | latest | table | drone
public | logs | table | drone
public | migrations | table | drone
public | nodes | table | drone
public | orgsecrets | table | drone
public | perms | table | drone
public | repos | table | drone
public | secrets | table | drone
public | stages | table | drone
public | steps | table | drone
public | users | table | drone
(13 rows)
drone=> SELECT * FROM users LIMIT 3;
drone=> SELECT * FROM builds LIMIT 3;
The only thing left to do is set the new configuration for Drone in the environment variables:
DRONE_DATABASE_DRIVER=postgres
DRONE_DATABASE_DATASOURCE=postgres://USER:PASSWORD@HOST:PORT/DB_NAME?sslmode=disable
Finally, you can start your Drone instance again and visit the webpage to check if everything works as expected.
Happy importing!