Kubebird is a Kubernetes operator based on Python and kopf to install and manage Firebird RDBMS instances
Project uses the namespaced CR Instances that defines Firebird instance.
This is a sample of Instances:
apiVersion: kubebird.github.io/v1
kind: Instance
metadata:
name: test
spec:
image: firebirdsql/firebird
version: 3.0.14
databases:
- name: "instance.fdb"
shadow: false
pageSize: 8192 # defaults to 8192; one of 4096, 8192, 16384
charset: UTF8 # defaults to UTF8
collation: UTF8 # defaults to UTF8
- name: "shadowed.fdb"
alias: "enforced" # if not specified, uses database name as alias
shadow: true
service:
type: ClusterIP
port: 3050 # port the Service exposes the instance on; defaults to 3050
storage:
primary:
class: "" # if empty, uses the default storage class
size: 3Gi
shadow: # can be omitted if no database below has "shadow: true"
class: ""
size: 3Gi
authentication:
sysdba:
secretRef: ""With this CR, Kubebird can:
- Deploy an instance of Firebird, in a StatefulSet mode using
imageandversionspecified, in whichever namespace theInstanceitself is created in. - Create a service for the instance. Default service type is
ClusterIP, exposed onservice.port(defaults to3050); the pod's container port is always3050regardless of this setting. - Define the PVC used for the instance's primary data (
storage.primary) with specified size and storage class. If storage class isn't specified, it uses the default storage class. Size must be a valid Kubernetes quantity (e.g.3Gi,500Mi); the CRD rejects anything else. - Declare a list of the databases managed by instance. Based by of the configuration, database can be instantiated in shadow mode; shadow files live on a second, separate PVC (
storage.shadow), which is required if any database hasshadow: true. Each database can also setpageSize(one of4096,8192,16384; defaults to8192),charsetandcollation(both default toUTF8). - Register a Firebird alias for each database in
/opt/firebird/databases.conf, so clients can connect using that alias instead of the in-pod filesystem path. Usesaliasif set, otherwise falls back to the database's ownname(e.g.instance.fdb). - Authentication section is optional. If is specified, you can:
- Declare SYSDBA database password using a secret. If secrets isn't specified, operator create a
<instance-name>-sysdbasecret with a random password. The secret hasusername(alwaysSYSDBA) andpasswordkeys.
- Declare SYSDBA database password using a secret. If secrets isn't specified, operator create a
- Label every object it creates (PVCs, Service, StatefulSet, and the SYSDBA secret) with
kubebird.github.io/instance: <name>, sokubectl get all,pvc,secrets -l kubebird.github.io/instance=<name>finds everything for oneInstance. - Report the most recent error, if any, in
status.error— visible directly viakubectl get instances(anErrorcolumn) without needing to check the operator's own logs. It's cleared automatically once theInstancereconciles successfully again.
When an Instance is created, Kubebird creates the objects below in order (steps 1-6); each one is
owned by the Instance and removed automatically when the Instance is deleted. Kubernetes then
creates the Pod from the StatefulSet, and Kubebird waits for it to become ready before creating
the actual database files inside it (steps 7-8):
flowchart TD
User(["kubectl apply -f cr.yaml"]) --> CR[/"Instance"/]
CR --> Kubebird["Kubebird"]
Kubebird -->|"1"| Secret["Secret<br/><name>-sysdba"]
Kubebird -->|"2"| CM["ConfigMap<br/><name>-databases-conf"]
Kubebird -->|"3"| PVCPrimary["PVC<br/><name>-data"]
Kubebird -->|"4, optional"| PVCShadow["PVC<br/><name>-shadow"]
Kubebird -->|"5"| Service["Service<br/><name>"]
Kubebird -->|"6"| STS["StatefulSet<br/><name>"]
Secret -.->|SYSDBA password| STS
CM -.->|database aliases| STS
PVCPrimary -.->|data volume| STS
PVCShadow -.->|shadow volume, optional| STS
Service -.->|routes traffic to| STS
STS -->|Kubernetes creates| Pod["Pod<br/><name>-0"]
Kubebird -->|"7: waits for readiness"| Pod
Kubebird -->|"8: creates the databases"| Pod
classDef owned fill:#e6ecff,stroke:#3355ff,color:#000
class Secret,CM,PVCPrimary,PVCShadow,Service,STS owned
The dotted arrows show how the StatefulSet uses the other objects (the SYSDBA password from the
secret, database aliases from the ConfigMap, storage from the PVCs, traffic routing from the
Service) rather than a separate creation step. The shadow PVC only exists when storage.shadow
is set on the Instance.
Kubebird also reacts to updates on an existing Instance:
- Changing
spec.service.type,spec.service.port, orspec.versionreconciles theService/StatefulSetin place. - Adding an entry to
spec.databasesprovisions just that new database (existing ones are left alone) and registers its alias immediately, without needing a pod restart. - Rotating the SYSDBA secret's password (the auto-generated one, or a user-provided
authentication.sysdba.secretRef) pushes the new password to the live server automatically, so the secret and the running instance never drift apart.
Deleting an Instance relies on Kubernetes garbage collection of the objects Kubebird created for
it (they're all owned by the Instance); the operator itself just logs the deletion and reports
status.phase: Deleting while that garbage collection runs.
To install Kubebird in the Kubernetes cluster, you can use these commands:
kubectl apply -f deploy/crd.yaml
kubectl apply -f deploy/operator.yamldeploy/operator.yaml creates its own kubebird-system namespace and deploys the operator into it
(Deployment, ServiceAccount, and the RBAC it needs) — no -n <namespace> needed. Instance CRs
must be created in kubebird-system too, since the namespaced Role/RoleBinding only grant
access there.
The operator itself runs via the kubebird-operator console script (on uvloop):
uv run kubebird-operator
# or, to scope it to a single namespace instead of the whole cluster:
NAMESPACE=kubebird-system uv run kubebird-operator
# or, to control log verbosity (DEBUG/INFO/WARNING/ERROR/CRITICAL; defaults to INFO):
LOG_LEVEL=DEBUG uv run kubebird-operatordeploy/operator.yaml's Deployment sets a fixed LOG_LEVEL: INFO — edit that value directly to
change verbosity for an in-cluster deployment.
To build the container image instead:
docker build --build-arg VERSION=0.1.0 -t kubebird:0.1.0 .The image runs as a non-root appuser (uid 8877) on a Red Hat UBI10 base and starts
kubebird-operator by default.
# Setup
$ uv init --name Kubebird --app --description "Kubebird - A Kubernetes operator for Firebird" --build-backend uv --no-readme
$ uv add kopf kubernetes uvloop
$ uv add --dev pytest pytest-cov tox ruff mypy pyyaml types-pyyaml
$ uv add --dev testcontainersFor e2e tests, testcontainers is used to run a k3s cluster: tests/conftest.py defines a
session-scoped k3s fixture (testcontainers.community.k3s.K3SContainer), tested on its own in
tests/test_k3s.py. A kubeconfig fixture builds on it to point the kubernetes client library
at the container.
tests/test_create.py uses that fixture, together with kopf's testing
utilities, to apply deploy/crd.yaml and
deploy/cr.yaml against the k3s cluster and run the operator in-process via
kopf.testing.KopfRunner. These are real end-to-end runs: each waits for status.phase to reach
Ready (StatefulSet + real Firebird image pull + database provisioning over isql), then execs
into the pod to confirm the relevant database file actually exists on disk before deleting the
Instance. There are two such tests: test_create_instance checks the primary database and that
its alias (plus the version-specific security.db alias) landed in /opt/firebird/databases.conf,
and test_create_instance_shadow_database checks that a database with shadow: true gets its
shadow file on the separate shadow PVC.
tests/test_update.py covers the update-reconciliation behaviour the same way: patching an
already-Ready Instance's service.type/service.port/version/databases, and rotating its
SYSDBA secret's password (both the auto-generated one and a user-provided secretRef), then
confirming the change actually took effect against the live Service/StatefulSet/pod.
tests/test_delete.py covers deletion the same way: deletes a Ready Instance and confirms it
actually disappears (not just that the delete call returned) and that Kubernetes garbage-collects
the StatefulSet it owned.
tests/test_k3s.py::test_operator_yaml_deploys_and_grants_expected_rbac applies deploy/operator.yaml
(Namespace, ServiceAccount, ClusterRole/ClusterRoleBinding, Role/RoleBinding, Deployment) and
checks, via SubjectAccessReview, that the resulting ServiceAccount actually gets every permission
the operator's code calls for — much faster than the other suites since it doesn't need the
container image to actually be pullable or any pod to schedule.
.github/workflows/ci.yml runs the full tox suite above on every branch push, whenever a pull
request is opened or updated, and on every "approved" pull request review — no image is built for
any of these. Pushing a tag (e.g. v1.2.3) instead skips the test suite and only builds and pushes
an image to quay.io/kubebird/operator, tagged both :latest and with the tag name itself.
Requires repo secrets QUAY_USERNAME/QUAY_PASSWORD.
Kubebird is licensed under the Apache License 2.0.