Skip to content

Commit 269c94f

Browse files
committed
Support registering at a subpath
In addition, stop looking for active deployments by their ID; instead use the deployment id annotation which we store against a replicaset.
1 parent 0dd8cfa commit 269c94f

10 files changed

Lines changed: 126 additions & 64 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ reqwest = { version = "0.12.15", default-features = false, features = [
7777
"rustls-tls",
7878
] }
7979
fnv = "1.0.7"
80+
url = { version = "2.5.4", features = ["serde"] }
8081

8182
[dev-dependencies]
8283
assert-json-diff = "2.0.2"

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ This field contains Restate-specific configuration.
363363
| Field | Type | Description |
364364
|---|---|---|
365365
| `register` | `object` | **Required**. The location of the Restate Admin API to register this deployment against. See details below. |
366+
| `servicePath` | `string` | Optional path to append to the Service url when registering with Restate. |
366367

367368
The `register` field must specify exactly one of `cluster`, `service`, or `url`.
368369

crd/RestateDeployment.pkl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ class Spec {
5757
class Restate {
5858
/// The location of the Restate Admin API to register this deployment against
5959
register: Register
60+
61+
/// Optional path to append to the Service url when registering with Restate. If not provided, the
62+
/// service will be registered at the root path "/".
63+
servicePath: String?
6064
}
6165

6266
/// The location of the Restate Admin API to register this deployment against

crd/restatedeployments.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ spec:
104104
description: A url of the restate admin endpoint against which to register the deployment Exactly one of `cluster`, `service` or `url` must be specified
105105
type: string
106106
type: object
107+
servicePath:
108+
description: Optional path to append to the Service url when registering with Restate. If not provided, the service will be registered at the root path "/".
109+
nullable: true
110+
type: string
107111
required:
108112
- register
109113
type: object

src/controllers/mod.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::sync::Arc;
33
use chrono::{DateTime, Utc};
44
use serde::Serialize;
55
use tokio::sync::RwLock;
6+
use url::Url;
67

78
pub mod restatecluster;
89
pub mod restatedeployment;
@@ -83,3 +84,20 @@ impl State {
8384
}
8485
}
8586
}
87+
88+
pub fn service_url(
89+
service_name: &str,
90+
service_namespace: &str,
91+
port: i32,
92+
path: Option<&str>,
93+
) -> Result<Url, url::ParseError> {
94+
let mut url = Url::parse(&format!(
95+
"http://{service_name}.{service_namespace}.svc.cluster.local:{port}",
96+
))?;
97+
98+
if let Some(path) = path {
99+
url.set_path(path)
100+
}
101+
102+
Ok(url)
103+
}

src/controllers/restatedeployment/controller.rs

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ use serde::Deserialize;
2727
use serde_json::json;
2828
use tokio::sync::RwLock;
2929
use tracing::*;
30+
use url::Url;
3031

31-
use crate::controllers::{Diagnostics, State};
32+
use crate::controllers::{service_url, Diagnostics, State};
3233
use crate::metrics::Metrics;
3334
use crate::resources::restateclusters::RestateCluster;
3435
use crate::resources::restatedeployments::{
@@ -151,7 +152,7 @@ impl RestateDeployment {
151152
let rs_api = Api::<ReplicaSet>::namespaced(ctx.client.clone(), namespace);
152153
let svc_api = Api::<Service>::namespaced(ctx.client.clone(), namespace);
153154

154-
let admin_endpoint = self.spec.restate.register.url()?;
155+
let admin_endpoint = self.spec.restate.register.admin_url()?;
155156

156157
let pod_template_annotation = reconcilers::replicaset::pod_template_annotation(self);
157158

@@ -277,15 +278,28 @@ impl RestateDeployment {
277278
)
278279
.await?;
279280

280-
let service_endpoint =
281-
format!("http://{versioned_name}.{namespace}.svc.cluster.local:9080/");
281+
let service_endpoint = service_url(
282+
&versioned_name,
283+
namespace,
284+
9080,
285+
self.spec.restate.service_path.as_deref(),
286+
)?;
282287

283-
let mut endpoints = self
284-
.list_endpoints(&ctx.http_client, &admin_endpoint)
288+
let mut deployments = self
289+
.list_deployments(&ctx.http_client, &admin_endpoint)
285290
.await?;
286291

287-
// if the endpoint is not active, register it
288-
if !endpoints.get(&service_endpoint).cloned().unwrap_or(false) {
292+
let existing_deployment_id = replicaset
293+
.annotations()
294+
.get(RESTATE_DEPLOYMENT_ID_ANNOTATION);
295+
296+
// if the repliceset doesn't have a deployment id, or its deployment id is not active, register it
297+
if existing_deployment_id.is_none_or(|existing_deployment_id| {
298+
!deployments
299+
.get(existing_deployment_id)
300+
.cloned()
301+
.unwrap_or_default()
302+
}) {
289303
if let Some(cluster_name) = &self.spec.restate.register.cluster {
290304
// wait for the cluster to be ready before registering to it
291305
validate_cluster_status(rsc_api, cluster_name).await?;
@@ -302,7 +316,8 @@ impl RestateDeployment {
302316
)
303317
.await?;
304318
// if registration succeeded, treat this as an active endpoint
305-
endpoints.insert(service_endpoint.clone(), true);
319+
// if we fail after this point we will re-register and should get the same deployment id
320+
deployments.insert(deployment_id.clone(), true);
306321

307322
debug!("Updating deployment-id annotation of ReplicaSet/Service {versioned_name} in namespace {namespace}");
308323

@@ -338,7 +353,7 @@ impl RestateDeployment {
338353
&ctx.http_client,
339354
&admin_endpoint,
340355
self,
341-
&endpoints,
356+
&deployments,
342357
)
343358
.await?;
344359

@@ -475,8 +490,8 @@ impl RestateDeployment {
475490
/// Register a service version with the Restate cluster
476491
async fn register_service_with_restate(
477492
client: &reqwest::Client,
478-
admin_endpoint: &str,
479-
service_endpoint: &str,
493+
admin_endpoint: &Url,
494+
service_endpoint: &Url,
480495
) -> Result<String> {
481496
debug!("Registering endpoint '{service_endpoint}' to Restate at '{admin_endpoint}'",);
482497

@@ -486,7 +501,7 @@ impl RestateDeployment {
486501
}
487502

488503
let resp: DeploymentResponse = client
489-
.post(format!("{}/deployments", admin_endpoint))
504+
.post(admin_endpoint.join("/deployments")?)
490505
.json(&serde_json::json!({
491506
"uri": service_endpoint,
492507
}))
@@ -502,14 +517,14 @@ impl RestateDeployment {
502517
Ok(resp.id)
503518
}
504519

505-
async fn list_endpoints(
520+
pub async fn list_deployments(
506521
&self,
507522
http_client: &reqwest::Client,
508-
admin_endpoint: &str,
523+
admin_endpoint: &Url,
509524
) -> Result<HashMap<String, bool>> {
510-
// This query finds endpoint urls, noting those that are the latest for a particular service, or have an active invocation
525+
// This query finds deployments, noting those that are the latest for a particular service, or have an active invocation
511526
let sql_query = r#"
512-
SELECT d.endpoint, (s.name IS NOT NULL OR i.id IS NOT NULL) as active
527+
SELECT d.id as deployment_id, (s.name IS NOT NULL OR i.id IS NOT NULL) as active
513528
FROM sys_deployment d
514529
LEFT JOIN sys_service s ON (d.id = s.deployment_id)
515530
LEFT JOIN sys_invocation_status i ON (d.id = i.pinned_deployment_id);
@@ -522,12 +537,12 @@ impl RestateDeployment {
522537

523538
#[derive(Deserialize)]
524539
struct DeploymentQueryResultRow {
525-
endpoint: String,
540+
deployment_id: String,
526541
active: bool,
527542
}
528543

529544
let response: DeploymentQueryResult = http_client
530-
.post(format!("{}/query", admin_endpoint))
545+
.post(admin_endpoint.join("/query")?)
531546
.header(reqwest::header::ACCEPT, "application/json")
532547
.json(&serde_json::json!({
533548
"query": sql_query
@@ -544,10 +559,10 @@ impl RestateDeployment {
544559
let mut endpoints = HashMap::with_capacity(response.rows.len());
545560

546561
for row in response.rows {
547-
match endpoints.entry(row.endpoint) {
562+
match endpoints.entry(row.deployment_id) {
548563
std::collections::hash_map::Entry::Occupied(mut entry) => {
549-
// technically there can be multiple deployments with the same endpoint url (via different headers, etc)
550-
// we treat the endpoint as active if any such deployment is active
564+
// two rows with same deployment id shouldnt happen...
565+
// we treat the deployment as active if any row is active
551566
if !entry.get() {
552567
entry.insert(row.active);
553568
}
@@ -595,10 +610,10 @@ impl RestateDeployment {
595610
};
596611
}
597612

598-
let admin_endpoint = self.spec.restate.register.url()?;
613+
let admin_endpoint = self.spec.restate.register.admin_url()?;
599614

600-
let endpoints = self
601-
.list_endpoints(&ctx.http_client, &admin_endpoint)
615+
let deployments = self
616+
.list_deployments(&ctx.http_client, &admin_endpoint)
602617
.await?;
603618

604619
let (active_count, next_removal) = reconcilers::replicaset::cleanup_old_replicasets(
@@ -608,7 +623,7 @@ impl RestateDeployment {
608623
&ctx.http_client,
609624
&admin_endpoint,
610625
self,
611-
&endpoints,
626+
&deployments,
612627
)
613628
.await?;
614629

src/controllers/restatedeployment/reconcilers/replicaset.rs

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use kube::runtime::reflector::Store;
1111
use kube::{Resource, ResourceExt};
1212
use serde_json::json;
1313
use tracing::*;
14+
use url::Url;
1415

1516
use crate::controllers::restatedeployment::controller::{
1617
APP_MANAGED_BY_LABEL, OWNED_BY_LABEL, RESTATE_DEPLOYMENT_ID_ANNOTATION,
@@ -114,6 +115,11 @@ pub fn generate_pod_template_hash(rsd: &RestateDeployment, pod_template: &str) -
114115
hasher.write(cluster.as_bytes());
115116
}
116117

118+
// if you change the path, it creates a new deployment, which means we want a new replicaset too to keep things 1:1
119+
if let Some(service_path) = rsd.spec.restate.service_path.as_deref() {
120+
hasher.write(service_path.as_bytes());
121+
}
122+
117123
if let Some(collision_count) = rsd.status.as_ref().and_then(|s| s.collision_count) {
118124
hasher.write(&collision_count.to_be_bytes());
119125
}
@@ -150,9 +156,9 @@ pub async fn cleanup_old_replicasets(
150156
rs_api: &Api<ReplicaSet>,
151157
replicasets_store: &Store<ReplicaSet>,
152158
http_client: &reqwest::Client,
153-
admin_endpoint: &str,
159+
admin_endpoint: &Url,
154160
rsd: &RestateDeployment,
155-
endpoints: &HashMap<String, bool>,
161+
deployments: &HashMap<String, bool>,
156162
) -> Result<(i32, Option<chrono::DateTime<chrono::Utc>>)> {
157163
let owner_name = rsd.name_any();
158164

@@ -191,14 +197,16 @@ pub async fn cleanup_old_replicasets(
191197

192198
for rs in replicasets {
193199
let rs_name = rs.name_any();
194-
let service_endpoint = format!("http://{}.{}.svc.cluster.local:9080/", rs_name, namespace);
195200

196-
// Skip active versions
197-
let endpoint = endpoints.get(&service_endpoint).cloned();
198-
let endpoint_exists = endpoint.is_some();
199-
let endpoint_active = endpoint.unwrap_or(false);
201+
let rs_deployment_id = rs.annotations().get(RESTATE_DEPLOYMENT_ID_ANNOTATION);
200202

201-
if endpoint_active {
203+
// Skip active deployments
204+
let deployment = rs_deployment_id
205+
.and_then(|rs_deployment_id| deployments.get(rs_deployment_id).cloned());
206+
let deployment_exists = deployment.is_some();
207+
let deployment_active = deployment.unwrap_or(false);
208+
209+
if deployment_active {
202210
active_count += 1;
203211

204212
if rs
@@ -251,7 +259,7 @@ pub async fn cleanup_old_replicasets(
251259
match (
252260
current_remove_at,
253261
current_remove_at_in_past,
254-
endpoint_exists,
262+
deployment_exists,
255263
) {
256264
(_, true, _) | (_, _, false) => {
257265
// we are past the remove at time, or the endpoint was removed by other means; can now scale it down
@@ -290,24 +298,22 @@ pub async fn cleanup_old_replicasets(
290298
continue;
291299
}
292300

293-
if endpoint_exists {
294-
if let Some(deployment_id) =
295-
rs.annotations().get(RESTATE_DEPLOYMENT_ID_ANNOTATION)
296-
{
297-
debug!("Force-deleting Restate deployment {deployment_id} as its associated with old ReplicaSet {rs_name} in namespace {namespace}");
298-
299-
let resp = http_client
300-
.delete(format!(
301-
"{admin_endpoint}/deployments/{deployment_id}?force=true"
302-
))
303-
.send()
304-
.await
305-
.map_err(Error::AdminCallFailed)?;
306-
307-
// for idempotency we have to allow 404
308-
if resp.status() != reqwest::StatusCode::NOT_FOUND {
309-
let _ = resp.error_for_status().map_err(Error::AdminCallFailed)?;
310-
}
301+
if deployment_exists {
302+
let rs_deployment_id = rs_deployment_id.unwrap();
303+
304+
debug!("Force-deleting Restate deployment {rs_deployment_id} as its associated with old ReplicaSet {rs_name} in namespace {namespace}");
305+
306+
let resp = http_client
307+
.delete(format!(
308+
"{admin_endpoint}/deployments/{rs_deployment_id}?force=true"
309+
))
310+
.send()
311+
.await
312+
.map_err(Error::AdminCallFailed)?;
313+
314+
// for idempotency we have to allow 404
315+
if resp.status() != reqwest::StatusCode::NOT_FOUND {
316+
let _ = resp.error_for_status().map_err(Error::AdminCallFailed)?;
311317
}
312318
}
313319

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ pub enum Error {
5151

5252
#[error("This RestateDeployment is backing recently-active versions in Restate. It will be removed after the drain delay period.")]
5353
DeploymentDraining { requeue_after: Option<Duration> },
54+
55+
#[error(transparent)]
56+
InvalidUrl(#[from] url::ParseError),
5457
}
5558

5659
pub type Result<T, E = Error> = std::result::Result<T, E>;
@@ -70,6 +73,7 @@ impl Error {
7073
Error::HashCollision => "HashCollision",
7174
Error::DeploymentInUse => "DeploymentInUse",
7275
Error::DeploymentDraining { .. } => "DeploymentDraining",
76+
Error::InvalidUrl { .. } => "InvalidUrl",
7377
}
7478
}
7579
}

0 commit comments

Comments
 (0)