Skip to content

Commit d73fbd6

Browse files
authored
feat(canopy): bump bestool-canopy 0.7.0 / kopia 0.4.0 and thread run_id through restore runs (#94)
2 parents 13ab62e + b069a46 commit d73fbd6

15 files changed

Lines changed: 330 additions & 82 deletions

File tree

Cargo.lock

Lines changed: 30 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ publish = false
88
[dependencies]
99
anyhow = "1.0.102"
1010
axum = "0.8.9"
11-
bestool-canopy = "0.6.1"
12-
bestool-kopia = { version = "0.3.4", features = ["proxy"] }
11+
bestool-canopy = "0.7.0"
12+
bestool-kopia = { version = "0.4.0", features = ["proxy"] }
1313
cronexpr = "1.5.0"
1414
futures = "0.3.31"
1515
globset = "0.4.18"

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ Deleting this resource will drop the restored database and prompt the Replica to
214214
| Field | Type | Description |
215215
|-------|------|-------------|
216216
| `phase` | `Pending` \| `Restoring` \| `Ready` \| `Switching` \| `Active` \| `Failed` | Current phase of the restore. |
217+
| `runId` | `string` | Canopy run-uuid minted when the restore Job is created, reused for the run's credential requests and verification report so canopy can correlate them. Canopy-backed restores only. |
217218
| `postgresVersion` | `string` | Detected PostgreSQL major version from the restored data. |
218219
| `createdAt` | `Time` | When the restore resource was created. |
219220
| `restoredAt` | `Time` | When the restore job completed. |

crds.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,6 +1124,15 @@ spec:
11241124
format: date-time
11251125
nullable: true
11261126
type: string
1127+
runId:
1128+
description: |-
1129+
Canopy run-uuid for this restore run, minted when the restore Job is
1130+
created and reused for the run's credential requests and its
1131+
verification report so canopy can correlate them. Stored as a string
1132+
(parsed to `Uuid` when sent) to avoid pulling a schemars uuid feature
1133+
into the CRD schema.
1134+
nullable: true
1135+
type: string
11271136
type: object
11281137
required:
11291138
- spec

src/bin/canopy_proxy.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ struct Config {
3434
broker_url: String,
3535
group: String,
3636
backup_type: String,
37+
/// Canopy run-uuid for this restore run (`PGRO_RUN_ID`), forwarded with
38+
/// every creds request so canopy attributes the grant to the run. Optional
39+
/// so a sidecar scheduled by an older operator (no env set) still works.
40+
run_id: Option<String>,
3741
region: String,
3842
port_file: PathBuf,
3943
/// URL the operator serves the stats-callback on; the sidecar POSTs
@@ -49,6 +53,7 @@ impl Config {
4953
broker_url: env_required("PGRO_BROKER_URL")?,
5054
group: env_required("PGRO_GROUP")?,
5155
backup_type: env_required("PGRO_TYPE")?,
56+
run_id: std::env::var("PGRO_RUN_ID").ok().filter(|s| !s.is_empty()),
5257
region: env_required("PGRO_REGION")?,
5358
port_file: env_or("PGRO_PROXY_PORT_FILE", "/var/run/pgro/proxy-port").into(),
5459
stats_callback_url: env_required("PGRO_STATS_CALLBACK_URL")?,
@@ -71,11 +76,12 @@ struct BrokerCredentialProvider {
7176
url: String,
7277
group: String,
7378
backup_type: String,
79+
run_id: Option<String>,
7480
cache: Mutex<Option<BackupCredentials>>,
7581
}
7682

7783
impl BrokerCredentialProvider {
78-
fn new(broker_url: &str, group: &str, backup_type: &str) -> Self {
84+
fn new(broker_url: &str, group: &str, backup_type: &str, run_id: Option<&str>) -> Self {
7985
Self {
8086
http: reqwest::Client::builder()
8187
.timeout(Duration::from_secs(10))
@@ -87,6 +93,7 @@ impl BrokerCredentialProvider {
8793
),
8894
group: group.to_string(),
8995
backup_type: backup_type.to_string(),
96+
run_id: run_id.map(str::to_string),
9097
cache: Mutex::new(None),
9198
}
9299
}
@@ -96,13 +103,16 @@ impl BrokerCredentialProvider {
96103
struct Req<'a> {
97104
group: &'a str,
98105
r#type: &'a str,
106+
#[serde(skip_serializing_if = "Option::is_none")]
107+
run_id: Option<&'a str>,
99108
}
100109
let resp = self
101110
.http
102111
.post(&self.url)
103112
.json(&Req {
104113
group: &self.group,
105114
r#type: &self.backup_type,
115+
run_id: self.run_id.as_deref(),
106116
})
107117
.send()
108118
.await?
@@ -207,6 +217,7 @@ async fn run(cfg: Config) -> Result<(), String> {
207217
&cfg.broker_url,
208218
&group,
209219
&backup_type,
220+
cfg.run_id.as_deref(),
210221
));
211222
let s3_cfg = S3ProxyConfig {
212223
upstream: format!("https://s3.{}.amazonaws.com", cfg.region),

src/bin/operator.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -698,10 +698,15 @@ async fn register_capabilities(ctx: Arc<Context>) {
698698
/// State passed to the credential-broker Router. Holds the operator's canopy
699699
/// client and a per-(group, type) cache so concurrent Job sidecars don't
700700
/// multiply upstream canopy calls.
701+
/// Broker creds cache key: `(group, type, run_id)`. Including the run_id keeps
702+
/// within-run STS refreshes on a cache hit while giving each restore run its
703+
/// own canopy-attributed credentials.
704+
type CredsCacheKey = (String, String, Option<uuid::Uuid>);
705+
701706
#[derive(Clone)]
702707
struct BrokerState {
703708
ctx: Arc<Context>,
704-
cache: Arc<tokio::sync::Mutex<std::collections::HashMap<(String, String), CachedCreds>>>,
709+
cache: Arc<tokio::sync::Mutex<std::collections::HashMap<CredsCacheKey, CachedCreds>>>,
705710
}
706711

707712
#[derive(Clone)]
@@ -736,6 +741,11 @@ fn broker_router(state: BrokerState) -> Router {
736741
struct BrokerCredsRequest {
737742
group: uuid::Uuid,
738743
r#type: String,
744+
/// Canopy run-uuid of the restore run this sidecar serves, forwarded to
745+
/// canopy so its credential grant is attributed to the run. Optional to
746+
/// tolerate an older sidecar (mid-rollout) that doesn't send one.
747+
#[serde(default)]
748+
run_id: Option<uuid::Uuid>,
739749
}
740750

741751
/// Broker endpoint the proxy sidecar hits to refresh its STS creds. Forwards
@@ -756,7 +766,7 @@ async fn post_restore_creds(
756766
);
757767
};
758768

759-
let key = (req.group.to_string(), req.r#type.clone());
769+
let key = (req.group.to_string(), req.r#type.clone(), req.run_id);
760770
{
761771
let cache = state.cache.lock().await;
762772
if let Some(cached) = cache.get(&key)
@@ -766,7 +776,10 @@ async fn post_restore_creds(
766776
}
767777
}
768778

769-
match canopy.restore_credentials(&req.r#type, req.group).await {
779+
match canopy
780+
.restore_credentials(&req.r#type, req.group, req.run_id)
781+
.await
782+
{
770783
Ok(resp) => {
771784
let expires_at = resp
772785
.credentials

src/canopy.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ impl Client {
109109
/// carries the intent name, the canopy semantics it opts into, and its
110110
/// typed parameter schema.
111111
pub async fn restore_capabilities(&self, intents: &[IntentDescriptor]) -> Result<()> {
112-
let body = RestoreCapabilitiesArgs {
113-
intents: intents.to_vec(),
114-
};
112+
let body = RestoreCapabilitiesArgs::builder()
113+
.intents(intents.to_vec())
114+
.build();
115115
self.inner
116116
.restore_capabilities(&body)
117117
.await
@@ -128,16 +128,21 @@ impl Client {
128128
}
129129

130130
/// Fetch short-lived read-only STS creds plus the repo password for a
131-
/// `(group, type)`. Authorized iff a declaration covers it.
131+
/// `(group, type)`. Authorized iff a declaration covers it. `run_id` is the
132+
/// canopy run-uuid when the fetch belongs to a restore run (the restore
133+
/// job's sidecar); `None` for non-run fetches such as the reconcile-time
134+
/// repo-password poll.
132135
pub async fn restore_credentials(
133136
&self,
134137
backup_type: &str,
135138
group: Uuid,
139+
run_id: Option<Uuid>,
136140
) -> Result<RestoreCredentials> {
137-
let body = RestoreCredentialsArgs {
138-
group,
139-
type_: backup_type.to_string(),
140-
};
141+
let body = RestoreCredentialsArgs::builder()
142+
.group(group)
143+
.type_(backup_type.to_string())
144+
.maybe_run_id(run_id)
145+
.build();
141146
self.inner.restore_credentials(&body).await.map_err(|err| {
142147
Error::Canopy(format!(
143148
"restore_credentials({backup_type}, {group}): {err}"

src/controllers/canopy.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,8 +259,10 @@ async fn reconcile_entry(ctx: &Context, ns_name: &str, entry: &WorklistEntry) ->
259259
let Some(canopy) = ctx.canopy.as_ref() else {
260260
return Ok(());
261261
};
262+
// This per-tick fetch exists only to read the repo password for the creds
263+
// Secret; it is not a restore run, so it carries no run_id.
262264
let creds = canopy
263-
.restore_credentials(&entry.type_, entry.group_id)
265+
.restore_credentials(&entry.type_, entry.group_id, None)
264266
.await?;
265267

266268
ensure_canopy_creds_secret(ctx, ns_name, entry, &creds.repo_password.0).await?;

src/controllers/canopy/intent.rs

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ const DEFAULT_ANALYTICS_MINIMUM_TTL_SECS: i64 = 7200;
7272
const DEFAULT_ANALYTICS_SWITCHOVER_GRACE_SECS: i64 = 120;
7373

7474
fn param(type_: ParamType, default: Option<Value>) -> ParamSpec {
75-
ParamSpec { type_, default }
75+
ParamSpec::builder()
76+
.type_(type_)
77+
.maybe_default(default)
78+
.build()
7679
}
7780

7881
/// The `analytics` intent's parameter schema (name → typed spec + default).
@@ -112,23 +115,28 @@ fn analytics_param_schema() -> ParamSchema {
112115
/// operators, dispatches only these, and applies the semantics' behaviours.
113116
pub fn descriptors() -> Vec<IntentDescriptor> {
114117
vec![
115-
IntentDescriptor {
116-
intent: "verify".to_string(),
117-
description: Some(
118+
IntentDescriptor::builder()
119+
.intent("verify".to_string())
120+
.description(
118121
"Restore the snapshot to prove it is restorable, then discard it.".to_string(),
119-
),
120-
semantics: vec![semantics::CHECK.to_string(), semantics::ONCE.to_string()],
121-
params: None,
122-
},
123-
IntentDescriptor {
124-
intent: "analytics".to_string(),
125-
description: Some(
122+
)
123+
.semantics(vec![
124+
semantics::CHECK.to_string(),
125+
semantics::ONCE.to_string(),
126+
])
127+
.build(),
128+
IntentDescriptor::builder()
129+
.intent("analytics".to_string())
130+
.description(
126131
"Keep a long-lived read-only query replica restored from the latest snapshot."
127132
.to_string(),
128-
),
129-
semantics: vec![semantics::CHECK.to_string(), semantics::URL.to_string()],
130-
params: Some(analytics_param_schema()),
131-
},
133+
)
134+
.semantics(vec![
135+
semantics::CHECK.to_string(),
136+
semantics::URL.to_string(),
137+
])
138+
.params(analytics_param_schema())
139+
.build(),
132140
]
133141
}
134142

0 commit comments

Comments
 (0)