A Kubernetes-native Database-as-a-Service that provisions managed PostgreSQL instances on Harvester HCI v1.7.1.
Architecture: Async CRD + Controller model — the REST API creates CRDs and returns immediately (HTTP 202), a controller reconciles desired state to Harvester via phase-based provisioning.
- RDS-compatible REST API — field names, response shapes, and status strings match AWS RDS
- Async CRD model —
kubectl applyaDBInstanceYAML orPOSTto the REST API; controller handles the rest - Phase-based reconciliation — crash-safe, idempotent, resumable from any step
- LUKS2 encryption at rest — every PGDATA volume is encrypted via cloud-init
- SSL-only connections — self-signed CA,
pg_hba.confrejects all non-SSL - Kube-OVN VPC isolation — each database gets its own VPC with controlled cross-VLAN access
- Prometheus + Grafana monitoring — auto-deployed ServiceMonitor and dashboard per instance
- pgBackRest backups to S3 — configurable retention and schedule
- Stop/Start — pause instances without deleting (maps to KubeVirt
spec.running) - Live resize — change instance class or storage size on a running database
- Deletion protection — prevents accidental
kubectl delete
- Harvester HCI 1.7.1 cluster
- Kube-OVN enabled (for VPC isolation)
kubectlconfigured to the Harvester cluster- VM image
ubuntu-22.04-server-cloudimg-amd64.imgin the Harvester image store - (Optional) Prometheus Operator for monitoring
- (Optional) S3/MinIO endpoint for backups
# Validate the source locally
go test ./...
go build ./...
# Apply the sample custom resource after your CRD/controller manifests are installed
kubectl apply -f config/samples/dbinstance.yamlGenerated deployment manifests such as config/crd/, config/rbac/, and config/manager/ are not currently checked into this repository.
External callers of the REST API should not be given direct Harvester credentials. Instead, run the controller/gateway pod with a dedicated Kubernetes ServiceAccount, and grant that service account the permissions it needs to create and manage Harvester resources on behalf of API callers.
Because this controller manages a cluster-scoped DBInstance CRD and also creates cluster-scoped resources such as namespaces, Kube-OVN VPCs, and subnets, the practical deployment model is a ClusterRole plus ClusterRoleBinding.
The following example is the minimum practical RBAC for the code in this repository:
apiVersion: v1
kind: ServiceAccount
metadata:
name: dbaas-controller
namespace: dbaas-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: dbaas-controller
rules:
- apiGroups: ["dbaas.wso2.com"]
resources: ["dbinstances"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["dbaas.wso2.com"]
resources: ["dbinstances/status"]
verbs: ["get", "update", "patch"]
- apiGroups: ["dbaas.wso2.com"]
resources: ["dbinstances/finalizers"]
verbs: ["update"]
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "create"]
- apiGroups: ["kubeovn.io"]
resources: ["vpcs"]
verbs: ["get", "create", "update", "delete"]
- apiGroups: ["kubeovn.io"]
resources: ["subnets"]
verbs: ["create", "delete"]
- apiGroups: ["kubeovn.io"]
resources: ["vpc-peerings"]
verbs: ["get", "create", "delete"]
- apiGroups: ["k8s.cni.cncf.io"]
resources: ["network-attachment-definitions"]
verbs: ["create", "delete"]
- apiGroups: ["cdi.kubevirt.io"]
resources: ["datavolumes"]
verbs: ["get", "create", "update", "delete"]
- apiGroups: ["kubevirt.io"]
resources: ["virtualmachines"]
verbs: ["get", "create", "update", "delete"]
- apiGroups: ["kubevirt.io"]
resources: ["virtualmachineinstances"]
verbs: ["get"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["create", "delete"]
- apiGroups: [""]
resources: ["services"]
verbs: ["create"]
- apiGroups: ["monitoring.coreos.com"]
resources: ["servicemonitors"]
verbs: ["create", "delete"]
- apiGroups: ["coordination.k8s.io"]
resources: ["leases"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: dbaas-controller
subjects:
- kind: ServiceAccount
name: dbaas-controller
namespace: dbaas-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: dbaas-controllerNotes:
secretsaccess is used to create the generated database credentials and LUKS encryption key.servicesandservicemonitorsare only needed if you enable the built-in monitoring flow.leasesare only needed because leader election is enabled by default.- External API authorization is a separate concern: if this API is exposed outside the cluster, place it behind your normal auth layer (for example, an ingress or API gateway with user authentication and authorization).
This design uses two separate trust relationships:
- Client to DBaaS API
- DBaaS API/controller to Harvester
Clients should trust the DBaaS API through normal HTTPS and whatever authentication/authorization layer you place in front of it.
The DBaaS API/controller should trust Harvester through the Kubernetes API server, using:
- TLS to the cluster API endpoint
- the cluster CA certificate to verify the API server
- a dedicated Kubernetes
ServiceAccounttoken for authentication - RBAC for authorization
In other words, external callers should not be given Harvester credentials directly. The controller pod should run with a scoped service account, and Harvester should allow only that identity to create and manage the resources listed above.
The code in this repository already follows that model by loading cluster configuration and creating Kubernetes clients from it.
Recommended deployment pattern:
- run the controller and REST gateway inside the Harvester cluster
- assign
serviceAccountName: dbaas-controllerto the pod - expose the REST API separately behind TLS and your preferred auth layer
Example Deployment snippet:
apiVersion: apps/v1
kind: Deployment
metadata:
name: dbaas-controller
namespace: dbaas-system
spec:
replicas: 1
selector:
matchLabels:
app: dbaas-controller
template:
metadata:
labels:
app: dbaas-controller
spec:
serviceAccountName: dbaas-controller
containers:
- name: controller
image: your-registry/dbaas-controller:latest
args:
- --gateway-address=:8080
- --metrics-bind-address=:8081
- --health-probe-bind-address=:8082When running inside the cluster, Kubernetes automatically provides the service account token and cluster CA certificate to the pod. The controller uses those credentials to call the Harvester/Kubernetes API securely.
If you run the API outside the cluster, provide a dedicated low-privilege kubeconfig that contains:
- the Harvester API server URL
- the cluster CA certificate
- credentials for a tightly scoped identity
The REST API should authenticate every caller before accepting create, modify, stop/start, or delete requests.
Recommended approach:
- terminate TLS at your ingress or API gateway
- require an OAuth2/OIDC bearer token for each request
- validate the token before forwarding traffic to the DBaaS API
- authorize actions based on the caller identity, group, tenant, or namespace policy
Good production options include:
- OIDC/JWT for user or service authentication
- mTLS for service-to-service calls inside a private network
- an API gateway or ingress controller that enforces auth centrally
Recommended behavior:
- return
401 Unauthorizedwhen the caller is missing or has an invalid token - return
403 Forbiddenwhen the caller is authenticated but is not allowed to manage the requested database
Important:
- callers should only be authenticated to the DBaaS API
- callers should not receive Harvester credentials directly
- the DBaaS controller should continue to use its own Kubernetes
ServiceAccountwhen talking to Harvester
The examples below assume a bearer token is required by the external API.
Option A: kubectl (GitOps-friendly)
apiVersion: dbaas.wso2.com/v1alpha1
kind: DBInstance
metadata:
name: orders-prod
spec:
dbInstanceClass: db.m5.large # 2 CPU, 8GB RAM
allocatedStorage: 100 # 100 GiB
dbName: orders
masterUsername: orders_admin
manageMasterUserPassword: true
dbSubnetGroupName: "10.50.0.0/24"
backupRetentionPeriod: 7
deletionProtection: truekubectl apply -f config/samples/dbinstance.yaml
# Watch provisioning progress
kubectl get dbi orders-prod -w
# Output:
# NAME STATUS PHASE CLASS ENDPOINT AGE
# orders-prod creating NamespaceCreated db.m5.large 5s
# orders-prod creating NetworkProvisioned db.m5.large 8s
# orders-prod creating StorageProvisioned db.m5.large 15s
# orders-prod creating VMCreated db.m5.large 20s
# orders-prod creating WaitingForCloudInit db.m5.large 25s
# orders-prod creating DatabaseReady db.m5.large 10.100.42.5 90s
# orders-prod available Available db.m5.large 10.100.42.5 95sOption B: External REST API
DBAAS_API=http://dbaas-controller.dbaas-system:8080/rds/v1/db-instances
DBAAS_TOKEN=<your-access-token>
# Create a database
curl -X POST "${DBAAS_API}" \
-H "Authorization: Bearer ${DBAAS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"DBInstanceIdentifier": "orders-prod",
"DBInstanceClass": "db.m5.large",
"AllocatedStorage": 100,
"DBName": "orders",
"MasterUsername": "orders_admin",
"DBSubnetGroupName": "10.50.0.0/24",
"BackupRetentionPeriod": 7,
"DeletionProtection": true
}'
# Example HTTP 202 response:
# {
# "DBInstance": {
# "DBInstanceIdentifier": "orders-prod",
# "DBInstanceStatus": "creating"
# }
# }
# Poll for status:
curl "${DBAAS_API}/orders-prod" \
-H "Authorization: Bearer ${DBAAS_TOKEN}"
# Example HTTP 200 response while provisioning:
# {
# "DBInstance": {
# "DBInstanceIdentifier": "orders-prod",
# "DBInstanceStatus": "creating"
# }
# }
# Example HTTP 200 response when available:
# {
# "DBInstance": {
# "DBInstanceIdentifier": "orders-prod",
# "DBInstanceStatus": "available",
# "Endpoint": {
# "Address": "10.100.42.5",
# "Port": 5432,
# "JDBCURL": "jdbc:postgresql://10.100.42.5:5432/orders?ssl=true&sslmode=verify-full"
# }
# }
# }
# Update an existing database (resize class/storage, change backup window)
curl -X PATCH "${DBAAS_API}/orders-prod" \
-H "Authorization: Bearer ${DBAAS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"DBInstanceClass": "db.m5.xlarge",
"AllocatedStorage": 200,
"BackupRetentionPeriod": 14,
"PreferredBackupWindow": "02:00-03:00"
}'
# Example HTTP 202 response:
# {
# "DBInstance": {
# "DBInstanceIdentifier": "orders-prod",
# "DBInstanceStatus": "modifying"
# }
# }
# Stop the database through the external API
curl -X PATCH "${DBAAS_API}/orders-prod" \
-H "Authorization: Bearer ${DBAAS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"Running": false
}'
# Example HTTP 202 response:
# {
# "DBInstance": {
# "DBInstanceIdentifier": "orders-prod",
# "DBInstanceStatus": "stopping"
# }
# }
# Start it again
curl -X PATCH "${DBAAS_API}/orders-prod" \
-H "Authorization: Bearer ${DBAAS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"Running": true
}'
# Example HTTP 202 response:
# {
# "DBInstance": {
# "DBInstanceIdentifier": "orders-prod",
# "DBInstanceStatus": "starting"
# }
# }
# Delete requires deletion protection to be disabled first
curl -X PATCH "${DBAAS_API}/orders-prod" \
-H "Authorization: Bearer ${DBAAS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"DeletionProtection": false
}'
# Delete the database
curl -X DELETE "${DBAAS_API}/orders-prod" \
-H "Authorization: Bearer ${DBAAS_TOKEN}"
# Example HTTP 202 response:
# {
# "DBInstance": {
# "DBInstanceIdentifier": "orders-prod",
# "DBInstanceStatus": "deleting"
# }
# }POST, PATCH, and DELETE are asynchronous: the gateway accepts the request, updates the backing DBInstance, and the controller reconciles the change in the background. Use GET /rds/v1/db-instances/{id} to watch the latest status. In production, these endpoints should be protected by caller authentication such as OIDC bearer tokens or mTLS.
# Get the JDBC URL
kubectl get dbi orders-prod -o jsonpath='{.status.endpoint.jdbcUrl}'
# jdbc:postgresql://10.100.42.5:5432/orders?ssl=true&sslmode=verify-full
# Get the admin password
kubectl get secret pg-orders-prod-credentials -n dbaas-orders-prod \
-o jsonpath='{.data.admin_password}' | base64 -d
# Get the CA certificate for SSL verification
kubectl get dbi orders-prod -o jsonpath='{.status.caCertPem}' > ca.crt# Stop (preserves storage, frees compute)
kubectl patch dbi orders-prod --type merge -p '{"spec":{"running":false}}'
# Start
kubectl patch dbi orders-prod --type merge -p '{"spec":{"running":true}}'
# Resize (live)
kubectl patch dbi orders-prod --type merge -p \
'{"spec":{"dbInstanceClass":"db.m5.xlarge","allocatedStorage":200}}'
# Delete (must disable protection first)
kubectl patch dbi orders-prod --type merge -p '{"spec":{"deletionProtection":false}}'
kubectl delete dbi orders-prod┌──────────────────────────────────────────────────────────┐
│ REST API Gateway (:8080) │
│ POST/PATCH/DELETE → manage DBInstance CRDs async │
│ GET → read CRD status → RDS-style output │
└──────────────┬───────────────────────────────────────────┘
│ kubectl apply / HTTP POST
┌──────────────▼───────────────────────────────────────────┐
│ Kubernetes CRDs (etcd) │
│ ┌──────────────┐ ┌────────────┐ ┌──────────────────┐ │
│ │ DBInstance │ │ DBSnapshot │ │ DBParameterGroup │ │
│ │ spec + status │ │ │ │ │ │
│ └──────────────┘ └────────────┘ └──────────────────┘ │
└──────────────┬───────────────────────────────────────────┘
│ watch + reconcile loop
┌──────────────▼───────────────────────────────────────────┐
│ DBInstance Controller (controller-runtime) │
│ Phase: Namespace → Network → Storage → VM → DB → Mon │
│ Each phase: idempotent, retryable, resumable │
└──────────────┬───────────────────────────────────────────┘
│ Harvester APIs (per phase)
┌──────────────▼───────────────────────────────────────────┐
│ Harvester 1.7.1 │
│ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌───────────┐ │
│ │ Kube-OVN │ │ CDI │ │ KubeVirt │ │ Prometheus│ │
│ │ VPC │ │ DataVol │ │ VM │ │ + Grafana │ │
│ └──────────┘ └───────────┘ └──────────┘ └───────────┘ │
└──────────────────────────────────────────────────────────┘
Each database instance is placed inside its own Kube-OVN VPC with two NICs: a management NIC on the pod network (for internet access and monitoring) and an isolated VPC NIC (for database traffic). An external consumer VLAN is explicitly allowed through allowSubnets.
┌─────────────────────────────────────────────────────────────────────────────────┐
│ Harvester 1.7.1 Cluster │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Namespace: dbaas-system │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ dbaas-controller Pod │ │ │
│ │ │ :8080 REST Gateway ◄── kubectl / curl │ │ │
│ │ │ :8081 Metrics ◄── Prometheus scrape │ │ │
│ │ │ :8082 Health probes ◄── kubelet liveness/readiness │ │ │
│ │ │ Watches: DBInstance CRDs (etcd) │ │ │
│ │ │ Creates: VPC, Subnet, NAD, DV, VM, Secret, SM │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ │ Reconcile loop (one phase per iteration) │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ Kube-OVN VPC: dbaas-orders-prod-vpc │ │
│ │ staticRoutes: [{cidr: "10.50.0.0/24", nextHop: autodetect}] │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ Kube-OVN Subnet: dbaas-orders-prod-subnet │ │ │
│ │ │ CIDR: 10.227.122.0/24 Gateway: 10.227.122.1 │ │ │
│ │ │ provider: dbaas-orders-prod-nad.dbaas-orders-prod.ovn │ │ │
│ │ │ private: true allowSubnets: ["10.50.0.0/24"] │ │ │
│ │ │ │ │ │
│ │ │ Namespace: dbaas-orders-prod │ │ │
│ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ NAD: dbaas-orders-prod-nad │ │ │ │
│ │ │ │ Labels: type=OverlayNetwork, clusternetwork=mgmt │ │ │ │
│ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ KubeVirt VM: pg-orders-prod │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ NIC 1: mgmt-net (pod network, masquerade) │ │ │ │
│ │ │ │ IP: 10.52.1.160 ◄── internet, apt, DNS, monitoring │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ NIC 2: vpc-net (Kube-OVN bridge via NAD) │ │ │ │
│ │ │ │ IP: 10.227.122.3 ◄── PostgreSQL application traffic │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ │
│ │ │ │ │ PostgreSQL 14 │ │ │ │ │
│ │ │ │ │ listen_addresses = '*' port = 5432 │ │ │ │ │
│ │ │ │ │ DB: orders User: orders_admin │ │ │ │ │
│ │ │ │ │ pg_hba: host all all 0.0.0.0/0 scram-sha-256 │ │ │ │ │
│ │ │ │ └────────────────────────────────────────────────────────┘ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ Disks: │ │ │ │
│ │ │ │ os-disk ← DV: pg-orders-prod-os (20Gi, image clone) │ │ │ │
│ │ │ │ pgdata ← DV: pg-orders-prod-data (20Gi, Longhorn) │ │ │ │
│ │ │ │ cloudinit ← Secret: pg-orders-prod-credentials │ │ │ │
│ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ Monitoring │ │ │ │
│ │ │ │ Service: pg-orders-prod-metrics (ClusterIP: None, :9187) │ │ │ │
│ │ │ │ ServiceMonitor: pg-orders-prod-monitor (15s scrape) │ │ │ │
│ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │
│ │ └────────────────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ═══════════════════════ VPC Boundary (isolated) ═══════════════════════════ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ Consumer VLAN: 10.50.0.0/24 (from spec.dbSubnetGroupName) │ │
│ │ │ │
│ │ Application pods / RKE2 workloads connect to: │ │
│ │ jdbc:postgresql://10.227.122.3:5432/orders?ssl=true │ │
│ │ │ │
│ │ Traffic allowed via: │ │
│ │ subnet.spec.allowSubnets + VPC static route │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ═══════════════════════ Isolation Test ════════════════════════════════════ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ test-external-vpc (10.99.0.0/24) │ │
│ │ pgtest-external pod → nc 10.227.122.3:5432 → TIMEOUT ✗ │ │
│ │ (not in allowSubnets — VPC isolation blocks traffic) │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ Same VPC (dbaas-orders-prod namespace) │ │
│ │ pgtest pod → pg_isready 10.52.1.160:5432 → accepting connections ✓ │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
Data flow summary:
| Path | Source → Destination | Network |
|---|---|---|
| App → DB | Consumer VLAN (10.50.0.0/24) → VM vpc-net (10.227.122.3:5432) | Kube-OVN VPC, allowed via allowSubnets |
| Cloud-init | VM mgmt-net (10.52.1.160) → internet | Pod network (masquerade NAT) |
| Monitoring | Prometheus → VM mgmt-net:9187 | Pod network via headless Service |
| API | User → controller :8080 → CRD → reconciler → Harvester APIs | Cluster internal |
| Console | Harvester UI → VM serial console (ubuntu / vmPassword) | KubeVirt subresource |
When application pods run in a separate VPC (for example, an RKE2 cluster), they cannot reach the database VPC by default. The optional vpcPeering field in the DBInstance spec creates a Kube-OVN VpcPeering resource that establishes bidirectional routing between the two VPCs.
The following diagram uses the orders-prod sample, where the RKE2 application cluster lives in rke2-cluster-vpc and the database is provisioned in dbaas-orders-prod-vpc:
┌─────────────────────────────────────────┐ ┌─────────────────────────────────────────┐
│ RKE2 Cluster VPC: rke2-cluster-vpc │ │ DBaaS VPC: dbaas-orders-prod-vpc │
│ │ │ │
│ ┌───────────────────────────────────┐ │ │ ┌───────────────────────────────────┐ │
│ │ Subnet: rke2-cluster-subnet │ │ │ │ Subnet: dbaas-orders-prod-subnet │ │
│ │ CIDR: 10.16.0.0/16 │ │ │ │ CIDR: 10.100.X.0/24 │ │
│ │ │ │ │ │ │ │
│ │ ┌─────────────────────────────┐ │ │ │ │ ┌─────────────────────────────┐ │ │
│ │ │ App Pod: orders-service │ │ │ │ │ │ KubeVirt VM: pg-orders-prod │ │ │
│ │ │ IP: 10.16.5.12 │ │ │ │ │ │ IP: 10.100.X.Y │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ psql -h 10.100.X.Y ──────────────────────────▶│ PostgreSQL :5432 │ │ │
│ │ │ │ │ │ │ │ │ (SSL-only, LUKS-encrypted) │ │ │
│ │ └─────────────────────────────┘ │ │ │ │ └─────────────────────────────┘ │ │
│ │ │ │ │ │ │ │
│ └───────────────────────────────────┘ │ │ └───────────────────────────────────┘ │
│ │ │ │
└──────────────────┬──────────────────────┘ └──────────────────┬──────────────────────┘
│ │
│ Kube-OVN VpcPeering │
│ dbaas-orders-prod-peering │
│ │
│ ┌───────────────────────────────────────────┐ │
└──│ localVpc: dbaas-orders-prod-vpc │──┘
│ remoteVpc: rke2-cluster-vpc │
│ localSubnets: [dbaas-orders-prod-subnet] │
│ remoteSubnets:[rke2-cluster-subnet] │
└───────────────────────────────────────────┘
How it works:
- The controller creates the DBaaS VPC and subnet during normal provisioning (phase
NetworkProvisioned). - After monitoring is deployed, the
VpcPeeringphase creates aVpcPeeringresource nameddbaas-{id}-peering. - Kube-OVN installs bidirectional routes so that pods in
rke2-cluster-subnetcan reach IPs indbaas-orders-prod-subnetand vice versa. - Application pods connect directly to the PostgreSQL VM IP — no LoadBalancer or NodePort required.
To enable VPC peering in a DBInstance, add the vpcPeering block to the spec:
spec:
vpcPeering:
remoteVpc: rke2-cluster-vpc
remoteSubnet: rke2-cluster-subnetIf vpcPeering is omitted, the database is only reachable from the consumer VLAN specified by dbSubnetGroupName.
The controller advances one phase per reconcile loop iteration:
| Phase | What it does | Harvester API |
|---|---|---|
| Pending → NamespaceCreated | Creates dbaas-{id} namespace |
POST /api/v1/namespaces |
| → NetworkProvisioned | Creates Kube-OVN VPC + Subnet + NAD | POST kubeovn.io/v1/vpcs, subnets, k8s.cni.cncf.io/v1/network-attachment-definitions |
| → StorageProvisioned | Creates CDI DataVolume (blank block for LUKS) | POST cdi.kubevirt.io/v1beta1/datavolumes |
| → VMCreated | Creates KubeVirt VM with cloud-init + credentials Secret | POST kubevirt.io/v1/virtualmachines, v1/secrets |
| → WaitingForCloudInit | Watches VMI status for Running + IP | WATCH kubevirt.io/v1/virtualmachineinstances |
| → DatabaseReady | PostgreSQL accepts connections | KubeVirt exec subresource |
| → MonitoringDeployed | Creates ServiceMonitor + Grafana dashboard | POST monitoring.coreos.com/v1/servicemonitors |
| → Available | Done. Endpoint populated. | — |
Crash recovery: If the controller restarts, it reads status.provisioningPhase and status.resources from the CRD to determine what's already created, and resumes from the next phase.
| Class | vCPU | RAM | Max Connections |
|---|---|---|---|
| db.t3.micro | 1 | 1 GB | 50 |
| db.t3.small | 1 | 2 GB | 100 |
| db.t3.medium | 2 | 4 GB | 150 |
| db.t3.large | 2 | 8 GB | 200 |
| db.m5.large | 2 | 8 GB | 200 |
| db.m5.xlarge | 4 | 16 GB | 400 |
| db.m5.2xlarge | 8 | 32 GB | 600 |
| db.m5.4xlarge | 16 | 64 GB | 1000 |
| db.r5.large | 2 | 16 GB | 300 |
| db.r5.xlarge | 4 | 32 GB | 500 |
| db.r5.2xlarge | 8 | 64 GB | 800 |
# Build
make build
# Run locally against your kubeconfig
make install # install CRDs
make run # start controller + REST gateway
# Test
make smoke-test # creates a database via REST API
make status # kubectl get dbidbaas/
├── api/v1alpha1/ # CRD Go types + deepcopy
│ ├── types.go # DBInstance, DBSnapshot, DBParameterGroup
│ ├── groupversion_info.go
│ └── zz_generated.deepcopy.go
├── cmd/controller/ # Main entry point
│ └── main.go # Starts controller-runtime manager + REST gateway
├── internal/
│ ├── controller/ # Reconciler (the core async logic)
│ │ └── dbinstance_reconciler.go
│ ├── gateway/ # Thin REST API → CRD translator
│ │ └── handler.go
│ └── harvester/ # Harvester API client (KubeVirt, CDI, Kube-OVN)
│ ├── client.go # Dynamic client wrapper
│ └── cloudinit.go # PostgreSQL cloud-init generator
├── config/
│ ├── crd/ # CRD YAML manifests
│ ├── rbac/ # ServiceAccount, ClusterRole, ClusterRoleBinding
│ ├── manager/ # Controller Deployment + Service
│ └── samples/ # Example DBInstance YAMLs
├── Dockerfile
├── Makefile
└── go.mod
This component is designed to fit into the WSO2 Open Cloud Datacenter initiative, providing managed database services on Harvester HCI for Choreo and Asgardeo workloads.
Apache-2.0