NAME
DBIO::Test::Kubernetes - Provision temporary database pods in Kubernetes for DBIO testing
VERSION
version 0.900004
SYNOPSIS
my $k8s = DBIO::Test::Kubernetes->new(
kubeconfig => $path,
databases => [qw(pg mysql)],
);
$k8s->deploy_databases;
$k8s->wait_for_ready;
# Local mode: port-forward and get env vars
$k8s->setup_port_forwards;
my %env = $k8s->env_vars;
local @ENV{keys %env} = values %env;
system('prove', '-Ilib', 't/');
$k8s->cleanup;
# In a namespace that is already there (and stays there)
my $k8s = DBIO::Test::Kubernetes->new(
existing_namespace => 'dev-dbio',
databases => [qw(pg mysql)],
);
See t/kubernetes/smoke.t for a runnable example.
DESCRIPTION
DBIO::Test::Kubernetes provisions temporary test databases inside Kubernetes and exposes the corresponding DBIO_TEST_* environment variables for local or in-cluster test runs.
It is intended for integration-style test runs that need short-lived database instances without hand-managed local services or bespoke local containers.
databases takes any of the following names and defaults to pg and mysql; any other name croaks. env_vars returns <prefix>_DSN, <prefix>_USER and <prefix>_PASS for each database, with the prefix given here:
pg-- imagepostgres:16, prefixDBIO_TEST_PGpg-ext-- imagesrc.ci/srv/postgres:18, prefixDBIO_TEST_PG_EXTmysql-- imagemysql:8.0.30, prefixDBIO_TEST_MYSQLduckdb-- imageduckdb/duckdb:latest, prefixDBIO_TEST_DUCKDB
METHODS
new
my $k8s = DBIO::Test::Kubernetes->new(
kubeconfig => $path, # optional
databases => [qw(pg mysql)], # optional
namespace => 'my-run', # or:
existing_namespace => 'dev-dbio', # one of the two
);
Builds the run; nothing is created in the cluster yet. The kubeconfig is kubeconfig, else $ENV{DBIO_TEST_KUBECONFIG}, else $ENV{KUBECONFIG}, else ~/.kube/config; a file that is not there croaks. databases names what to deploy (see "DESCRIPTION") and an unknown name croaks. namespace and existing_namespace are the two kinds described under "NAMESPACES"; giving both, or an empty existing_namespace, throws a DBIO::Exception. Without either, a namespace of its own is named from a random suffix.
namespace
The name of the namespace this run works in: the one it created, or the existing_namespace it was given.
databases
The arrayref of database names this run deploys.
api
The Kubernetes::REST client built from the kubeconfig.
kubeconfig
Path of the kubeconfig file in use.
uses_existing_namespace
True when the instance was built with existing_namespace: it works in a namespace it did not create and will not delete. See "NAMESPACES".
created_objects
my @objects = $k8s->created_objects; # ({ kind => 'Pod', name => '...' }, ...)
The objects this run has created and not yet deleted, in creation order, each as a hashref with kind and name. An object is listed once the API has accepted it, so the list is what "cleanup" will remove -- and what is left behind when cleanup is not called.
deploy_databases
$k8s->deploy_databases;
Creates the namespace (unless it is an existing_namespace) and one pod and one ClusterIP service per database, every one with a cpu and memory limit (see "RESOURCE LIMITS"). Each object is recorded as it is created, so "cleanup" can remove it even when this call dies halfway. Returns once the objects are created; use "wait_for_ready" to wait for the pods.
wait_for_ready
$k8s->wait_for_ready( timeout => 120, interval => 3 );
Polls each database pod until its Ready condition is true. timeout (seconds, default 120) is one deadline for all databases together; interval (default 3) is the sleep between polls. Croaks when "deploy_databases" was not called first, and on timeout. A failure to read a pod other than "not there yet" is thrown, not waited out.
connection_info
my $info = $k8s->connection_info( mode => 'local' );
# { pg => { dsn, user, pass, host, port }, ... }
Returns a hashref, keyed by database name, of what a test needs to connect. mode is local (the default: 127.0.0.1 and the port "setup_port_forwards" chose, which croaks if that was not called) or cluster (the service's cluster DNS name and its own port, for a runner inside the cluster).
env_vars
my %env = $k8s->env_vars( mode => 'local' );
Flattens "connection_info" into the DBIO_TEST_*_DSN, _USER and _PASS environment variables the driver test suites read. Takes the same mode option.
setup_port_forwards
$k8s->setup_port_forwards;
Starts one kubectl port-forward per database to a free local port, for local mode. Croaks unless "deploy_databases" and "wait_for_ready" came first, and when a forward died within its first two seconds. The processes are stopped by "cleanup" (and, as a last resort, when the object is destroyed).
deploy_test_job
my $job = $k8s->deploy_test_job(
image => 'dbio-test:latest',
prove_args => ['-l', 't/'],
resources => { requests => {...}, limits => {...} },
);
Creates a Job that runs prove inside the cluster against the databases in cluster mode and returns its name. resources follows "RESOURCE LIMITS": it must set a cpu and a memory limit or the call throws.
wait_for_job
my $exit = $k8s->wait_for_job( name => $job, timeout => 3600, interval => 5 );
Polls the Job until it has succeeded (returns 0) or failed (returns 1). name defaults to this run's job. Croaks on timeout.
fetch_job_logs
my $logs = $k8s->fetch_job_logs( name => $job );
# [ { pod => $name, log => $text }, ... ]
Returns the log of each pod the Job ran, read with kubectl logs.
cleanup
$k8s->cleanup;
Stops the port-forwards and deletes what this run created: its own namespace, or, in an existing namespace, each recorded object and nothing else. A delete that fails warns and leaves the object in created_objects. Safe to call after a failed deploy_databases, and more than once.
NAMESPACES
A run works in one namespace, and the constructor is told which kind it is. It is never inferred from what the cluster happens to permit.
A namespace of its own -- the default
Without a namespace argument a name
dbio-test-XXXXXXXXis generated;namespace => $namenames it instead.deploy_databasescreates the namespace, andcleanupdeletes it, which takes everything in it along. This needs a cluster account that may create and delete namespaces. Objects carry plain names: a podpgbehind a servicepg-svc, a jobdbio-test-runner.An existing namespace --
existing_namespace => $nameFor an account that is confined to one namespace, usually shared with other workloads. No namespace is created and none is ever deleted. Because the namespace is not this run's alone, three things differ:
Every object name carries the id of the run: a pod
dbio-test-XXXXXXXX-pgbehind a servicedbio-test-XXXXXXXX-pg-svc, a jobdbio-test-XXXXXXXX-runner.Pods carry only the labels
dbio-test-run(the run id) anddbio-test-db(the database name), and each service selects on both. Services and the job carry the labeldbio-test-runas well, so that it finds every object of a run. No selector written for another workload in the namespace matches these pods, so no service that was there before starts routing to them, and two runs at once do not see each other's pods.cleanupdeletes exactly the objects this run created, each by kind and name, as recorded when the API accepted it -- never by label selector.
namespace and existing_namespace together throw. In both kinds cleanup works from what was recorded at creation, so it also clears up after a deploy_databases that died halfway; created_objects returns that record.
The databases are reached the same way in both kinds: a ClusterIP service per database, port-forwarded to a free local port by setup_port_forwards, or addressed by its cluster DNS name in cluster mode.
RESOURCE LIMITS
Every container this module creates carries a resources block with both requests and limits, and the limits always constrain cpu and memory. Test databases routinely run on small shared machines next to other services; an unlimited container that is forgotten about can starve or OOM the whole host, so the unlimited shape is not reachable from here.
The default envelope is 250m/256Mi requested, 1 core / 1Gi limited, matching the checked-in manifests of the driver distributions.
Database pods always get this default envelope: the database specs are built in, and none of them sets a resources of its own. deploy_test_job accepts a resources option to raise the ceiling for the test runner:
resources => {
requests => { cpu => '500m', memory => '512Mi' },
limits => { cpu => '2', memory => '4Gi' },
},
A supplied resources that does not set both a cpu and a memory limit is a hard error, not a silent fallback to the default: asking for an unlimited (or half-limited) container is treated as a mistake worth stopping for.
AUTHOR
DBIO & DBIx::Class Authors
COPYRIGHT AND LICENSE
Copyright (C) 2026 DBIO Authors Portions Copyright (C) 2005-2025 DBIx::Class Authors Based on DBIx::Class, heavily modified.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.