Skip to content

Commit 8d69f3b

Browse files
committed
feat: Add Scaler CRD
1 parent a4a204b commit 8d69f3b

4 files changed

Lines changed: 120 additions & 0 deletions

File tree

crates/stackable-operator/src/crd/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub mod authentication;
88
pub mod git_sync;
99
pub mod listener;
1010
pub mod s3;
11+
pub mod scaler;
1112

1213
/// A reference to a product cluster (for example, a `ZookeeperCluster`)
1314
///
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
use k8s_openapi::apimachinery::pkg::apis::meta::v1::Time;
2+
use kube::CustomResource;
3+
use schemars::JsonSchema;
4+
use serde::{Deserialize, Serialize};
5+
6+
#[cfg(doc)]
7+
use crate::kvp::Annotation;
8+
use crate::versioned::versioned;
9+
10+
#[versioned(version(name = "v1alpha1"))]
11+
pub mod versioned {
12+
#[versioned(crd(
13+
group = "autoscaling.stackable.tech",
14+
status = ScalerStatus,
15+
scale(
16+
spec_replicas_path = ".spec.replicas",
17+
status_replicas_path = ".status.replicas",
18+
label_selector_path = ".status.selector"
19+
),
20+
namespaced
21+
))]
22+
#[derive(Clone, Debug, PartialEq, CustomResource, Deserialize, Serialize, JsonSchema)]
23+
pub struct ScalerSpec {
24+
/// Desired replica count.
25+
///
26+
/// Written by the horizontal pod autoscaling mechanism via the /scale subresource.
27+
///
28+
/// NOTE: This and other replica fields)use a [`u16`] instead of a [`i32`] used by
29+
/// [`k8s_openapi`] types to force a non-negative replica count. All [`u16`]s can be
30+
/// converted losslessly to [`i32`]s where needed.
31+
///
32+
/// Upstream issues:
33+
///
34+
/// - https://github.com/kubernetes/kubernetes/issues/105533
35+
/// - https://github.com/Arnavion/k8s-openapi/issues/136
36+
pub replicas: u16,
37+
}
38+
}
39+
40+
/// Status of a StackableScaler.
41+
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
42+
#[serde(rename_all = "camelCase")]
43+
pub struct ScalerStatus {
44+
/// The current total number of replicas targeted by the managed StatefulSet.
45+
///
46+
/// Exposed via the `/scale` subresource for horizontal pod autoscaling consumption.
47+
pub replicas: u16,
48+
49+
/// Label selector string for HPA pod counting. Written at `.status.selector`.
50+
#[serde(skip_serializing_if = "Option::is_none")]
51+
pub selector: Option<String>,
52+
53+
/// The current state of the scaler state machine.
54+
pub state: ScalerState,
55+
56+
/// Timestamp indicating when the scaler state last transitioned.
57+
pub last_transition_time: Time,
58+
}
59+
60+
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, strum::Display)]
61+
#[serde(rename_all = "camelCase", rename_all_fields = "camelCase")]
62+
#[strum(serialize_all = "camelCase")]
63+
pub enum ScalerState {
64+
/// No scaling operation is in progress.
65+
Idle,
66+
67+
/// Running the `pre_scale` hook (e.g. data offload).
68+
PreScaling,
69+
70+
/// Waiting for the StatefulSet to converge to the new replica count.
71+
///
72+
/// This stage additionally tracks the previous replica count to be able derive the direction
73+
/// of the scaling operation.
74+
Scaling { previous_replicas: u16 },
75+
76+
/// Running the `post_scale` hook (e.g. cluster rebalance).
77+
///
78+
/// This stage additionally tracks the previous replica count to be able derive the direction
79+
/// of the scaling operation.
80+
PostScaling { previous_replicas: u16 },
81+
82+
/// A hook returned an error.
83+
///
84+
/// The scaler stays here until the user applies the [`Annotation::autoscaling_retry`] annotation
85+
/// to trigger a reset to [`ScalerState::Idle`].
86+
Failed {
87+
/// Which stage produced the error.
88+
failed_in: FailedInState,
89+
90+
/// Human-readable error message from the hook.
91+
reason: String,
92+
},
93+
}
94+
95+
/// Which stage of a scaling operation failed.
96+
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
97+
#[serde(rename_all = "camelCase")]
98+
pub enum FailedInState {
99+
/// The `pre_scale` hook returned an error.
100+
PreScaling,
101+
102+
/// The StatefulSet failed to reach the desired replica count.
103+
Scaling,
104+
105+
/// The `post_scale` hook returned an error.
106+
PostScaling,
107+
}

crates/stackable-operator/src/kvp/annotation/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,16 @@ impl Annotation {
156156
))?;
157157
Ok(Self(kvp))
158158
}
159+
160+
/// Constructs a `autoscaling.stackable.tech/retry` annotation.
161+
pub fn autoscaling_retry(retry: bool) -> Self {
162+
// SAFETY: We use expect here, because the input parameter can only be one of two possible
163+
// values: true or false. This fact in combination with the known annotation key length
164+
// allows use to use expect here, instead of bubbling up the error.
165+
let kvp = KeyValuePair::try_from(("autoscaling.stackable.tech/retry", retry.to_string()))
166+
.expect("autoscaling retry annotation must be valid");
167+
Self(kvp)
168+
}
159169
}
160170

161171
/// A validated set/list of Kubernetes annotations.

crates/xtask/src/crd/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use stackable_operator::{
1010
PodListenersVersion,
1111
},
1212
s3::{S3Bucket, S3BucketVersion, S3Connection, S3ConnectionVersion},
13+
scaler::{Scaler, ScalerVersion},
1314
},
1415
kube::core::crd::MergeError,
1516
};
@@ -77,6 +78,7 @@ pub fn generate_preview() -> Result<(), Error> {
7778
write_crd!(path, PodListeners, V1Alpha1);
7879
write_crd!(path, S3Bucket, V1Alpha1);
7980
write_crd!(path, S3Connection, V1Alpha1);
81+
write_crd!(path, Scaler, V1Alpha1);
8082

8183
write_crd!(path, DummyCluster, V1Alpha1);
8284

0 commit comments

Comments
 (0)