Skip to content

Commit 1704311

Browse files
authored
feat(operator): apply own CRDs on startup (#84)
2 parents 0d1015e + 593576b commit 1704311

5 files changed

Lines changed: 73 additions & 5 deletions

File tree

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@ Monitors a Kopia backup repository for "physical" backups of PostgreSQL database
77
88
## Install
99

10-
Generate the CRDs:
10+
Apply the operator manifest:
1111

1212
```
13-
cargo run --bin gen-crds > crds.yaml
13+
kubectl apply -f operator.yaml
1414
```
1515

16-
Apply both the CRDs and the operator:
16+
The operator applies its own CRDs on startup, so no separate `crds.yaml`
17+
step is needed. If you'd rather manage CRD lifecycle out-of-band (e.g.
18+
gating schema changes at install time), generate + apply them manually:
1719

1820
```
21+
cargo run --bin gen-crds > crds.yaml
1922
kubectl apply -f crds.yaml
20-
kubectl apply -f operator.yaml
2123
```
2224

2325
## Quick start

operator.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# postgres-restore-operator install manifest
2-
# CRDs are not included — generate them with: cargo run --bin gen-crds
2+
# CRDs are installed by the operator at startup. Generate them
3+
# manually with `cargo run --bin gen-crds` if you want to apply them
4+
# out-of-band (e.g. install-time gating on schema changes).
35
---
46
apiVersion: v1
57
kind: Namespace
@@ -17,6 +19,10 @@ kind: ClusterRole
1719
metadata:
1820
name: postgres-restore-operator
1921
rules:
22+
# CRD install (operator applies its own CRDs on startup).
23+
- apiGroups: ["apiextensions.k8s.io"]
24+
resources: ["customresourcedefinitions"]
25+
verbs: ["get", "list", "create", "update", "patch"]
2026
# CRD access
2127
- apiGroups: ["pgro.bes.au"]
2228
resources:

src/bin/operator.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ async fn main() -> anyhow::Result<()> {
151151

152152
let client = Client::try_default().await?;
153153

154+
postgres_restore_operator::crd_install::ensure_crds(&client).await?;
155+
154156
let namespace = operator_namespace();
155157
let (max_concurrent_restores, kopia_image, use_port_forward) =
156158
read_config(&client, &namespace).await;

src/crd_install.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//! Server-side-apply the operator's own CRDs at startup.
2+
//!
3+
//! Same source of truth as the `gen-crds` binary — both call
4+
//! `CustomResourceExt::crd()` on the derived types. The operator applies
5+
//! them on boot so a fresh cluster only needs `kubectl apply -f
6+
//! operator.yaml` to be functional; `crds.yaml` stays around for CI /
7+
//! debugging / operators who prefer to manage CRD lifecycle out-of-band.
8+
9+
use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition;
10+
use kube::{
11+
Api, Client, CustomResourceExt, ResourceExt,
12+
api::{Patch, PatchParams},
13+
};
14+
use tracing::{info, warn};
15+
16+
use crate::{
17+
error::Result,
18+
types::{PostgresPhysicalReplica, PostgresPhysicalRestore},
19+
};
20+
21+
/// Field manager used for the SSA patch. Distinct from
22+
/// `postgres-restore-operator` so a manual `kubectl edit` doesn't fight
23+
/// the operator on CRD ownership — humans overriding CRD fields at
24+
/// runtime is legitimate (e.g. temporary preservation policy changes).
25+
const FIELD_MANAGER: &str = "postgres-restore-operator/crd";
26+
27+
/// SSA-apply the two pgro CRDs. Idempotent — safe to call every startup
28+
/// and safe to run concurrently with itself across replicas.
29+
///
30+
/// Fails hard on RBAC / apiserver errors: without the CRDs the operator
31+
/// can't watch anything, so surfacing the error at boot beats a
32+
/// mysteriously-silent operator later.
33+
pub async fn ensure_crds(client: &Client) -> Result<()> {
34+
let api: Api<CustomResourceDefinition> = Api::all(client.clone());
35+
for crd in [
36+
PostgresPhysicalReplica::crd(),
37+
PostgresPhysicalRestore::crd(),
38+
] {
39+
let name = crd.name_any();
40+
let value = serde_json::to_value(&crd)?;
41+
match api
42+
.patch(
43+
&name,
44+
&PatchParams::apply(FIELD_MANAGER).force(),
45+
&Patch::Apply(&value),
46+
)
47+
.await
48+
{
49+
Ok(_) => info!(crd = %name, "applied CRD"),
50+
Err(err) => {
51+
warn!(crd = %name, error = %err, "failed to apply CRD");
52+
return Err(err.into());
53+
}
54+
}
55+
}
56+
Ok(())
57+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub mod canopy;
22
pub mod context;
33
pub mod controllers;
4+
pub mod crd_install;
45
pub mod error;
56
pub mod kopia;
67
pub mod metrics;

0 commit comments

Comments
 (0)