controller/app_service/types.rs
1use std::collections::BTreeMap;
2
3use k8s_openapi::{
4 api::core::v1::{ResourceRequirements, Volume, VolumeMount},
5 apimachinery::pkg::api::resource::Quantity,
6};
7
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10use utoipa::ToSchema;
11
12pub const COMPONENT_NAME: &str = "appService";
13
14/// StorageConfig is used to configure the storage for the appService.
15/// This uses the `Volume` and `VolumeMount` types from the Kubernetes API.
16///
17/// See the [Kubernetes docs](https://kubernetes.io/docs/concepts/storage/volumes/).
18#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, JsonSchema, PartialEq)]
19pub struct StorageConfig {
20 pub volumes: Option<Vec<Volume>>,
21 #[serde(rename = "volumeMounts")]
22 pub volume_mounts: Option<Vec<VolumeMount>>,
23}
24
25/// AppService significantly extends the functionality of your Tembo Postgres
26/// instance by running tools and software built by the Postgres open source community.
27///
28/// **Example**: This will configure and install a PostgREST container along side
29/// the Postgres instance, install pg_graphql extension, and configure the
30/// ingress routing to expose the PostgREST service.
31///
32/// ```yaml
33/// apiVersion: coredb.io/v1alpha1
34/// kind: CoreDB
35/// metadata:
36/// name: test-db
37/// spec:
38/// trunk_installs:
39/// - name: pg_graphql
40/// version: 1.2.0
41/// extensions:
42/// - name: pg_graphql
43/// locations:
44/// - database: postgres
45/// enabled: true
46///
47/// appServices:
48/// - name: postgrest
49/// image: postgrest/postgrest:v12.2.8
50/// routing:
51/// # only expose /rest/v1 and /graphql/v1
52/// - port: 3000
53/// ingressPath: /rest/v1
54/// middlewares:
55/// - my-headers
56/// - port: 3000
57/// ingressPath: /graphql/v1
58/// middlewares:
59/// - map-gql
60/// - my-headers
61/// middlewares:
62/// - customRequestHeaders:
63/// name: my-headers
64/// config:
65/// # removes auth header from request
66/// Authorization: ""
67/// Content-Profile: graphql
68/// Accept-Profile: graphql
69/// - stripPrefix:
70/// name: my-strip-prefix
71/// config:
72/// - /rest/v1
73/// # reroute gql and rest requests
74/// - replacePathRegex:
75/// name: map-gql
76/// config:
77/// regex: \/graphql\/v1\/?
78/// replacement: /rpc/resolve
79/// env:
80/// - name: PGRST_DB_URI
81/// valueFromPlatform: ReadWriteConnection
82/// - name: PGRST_DB_SCHEMA
83/// value: "public, graphql"
84/// - name: PGRST_DB_ANON_ROLE
85/// value: postgres
86/// - name: PGRST_LOG_LEVEL
87/// value: info
88/// ```
89#[derive(Clone, Debug, Default, Serialize, Deserialize, ToSchema, JsonSchema, PartialEq)]
90pub struct AppService {
91 /// Defines the name of the appService.
92 pub name: String,
93
94 /// Defines the container image to use for the appService.
95 pub image: String,
96
97 /// Defines the arguments to pass into the container if needed.
98 /// You define this in the same manner as you would for all Kubernetes containers.
99 /// See the [Kubernetes docs](https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container).
100 pub args: Option<Vec<String>>,
101
102 /// Defines the command into the container if needed.
103 /// You define this in the same manner as you would for all Kubernetes containers.
104 /// See the [Kubernetes docs](https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container).
105 pub command: Option<Vec<String>>,
106
107 /// Defines the environment variables to pass into the container if needed.
108 /// You define this in the same manner as you would for all Kubernetes containers.
109 /// See the [Kubernetes docs](https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container).
110 pub env: Option<Vec<EnvVar>>,
111
112 /// Defines the resources to allocate to the container.
113 /// You define this in the same manner as you would for all Kubernetes containers.
114 /// See the [Kubernetes docs](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/).
115 #[serde(default = "default_resources")]
116 pub resources: ResourceRequirements,
117
118 /// Defines the probes to use for the container.
119 /// You define this in the same manner as you would for all Kubernetes containers.
120 /// See the [Kubernetes docs](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/).
121 pub probes: Option<Probes>,
122
123 /// Defines the metrics endpoints to be scraped by Prometheus.
124 /// This implements a subset of features available by PodMonitorPodMetricsEndpoints.
125 pub metrics: Option<AppMetrics>,
126
127 /// Defines the ingress middeware configuration for the appService.
128 /// This is specifically configured for the ingress controller Traefik.
129 pub middlewares: Option<Vec<Middleware>>,
130
131 /// Defines the routing configuration for the appService.
132 pub routing: Option<Vec<Routing>>,
133
134 /// Defines the storage configuration for the appService.
135 pub storage: Option<StorageConfig>,
136}
137
138pub fn default_resources() -> ResourceRequirements {
139 let limits: BTreeMap<String, Quantity> = BTreeMap::from([
140 ("cpu".to_owned(), Quantity("400m".to_string())),
141 ("memory".to_owned(), Quantity("256Mi".to_string())),
142 ]);
143 let requests: BTreeMap<String, Quantity> = BTreeMap::from([
144 ("cpu".to_owned(), Quantity("100m".to_string())),
145 ("memory".to_owned(), Quantity("256Mi".to_string())),
146 ]);
147 ResourceRequirements {
148 limits: Some(limits),
149 requests: Some(requests),
150 claims: None,
151 }
152}
153
154#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, JsonSchema, PartialEq)]
155pub struct AppMetrics {
156 /// port must be also exposed in one of AppService.routing[]
157 pub port: u16,
158 /// path to scrape metrics
159 pub path: String,
160}
161
162// Secrets are injected into the container as environment variables
163// ths allows users to map these secrets to environment variable of their choice
164#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, JsonSchema, PartialEq)]
165pub struct EnvVar {
166 pub name: String,
167 #[serde(skip_serializing_if = "Option::is_none")]
168 pub value: Option<String>,
169 #[serde(rename = "valueFromPlatform", skip_serializing_if = "Option::is_none")]
170 pub value_from_platform: Option<EnvVarRef>,
171}
172
173// we will map these from secrets to env vars, if desired
174#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, JsonSchema, PartialEq)]
175pub enum EnvVarRef {
176 ReadOnlyConnection,
177 ReadWriteConnection,
178}
179
180/// Routing is used if there is a routing port, then a service is created using
181/// that Port when ingress_path is present, an ingress is created. Otherwise, no
182/// ingress is created
183#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, JsonSchema)]
184pub struct Routing {
185 pub port: u16,
186 #[serde(rename = "ingressPath")]
187 pub ingress_path: Option<String>,
188
189 /// provide name of the middleware resources to apply to this route
190 pub middlewares: Option<Vec<String>>,
191 #[serde(rename = "entryPoints")]
192 #[serde(default = "default_entry_points")]
193 pub entry_points: Option<Vec<String>>,
194 #[serde(rename = "ingressType")]
195 #[serde(default = "default_ingress_type")]
196 pub ingress_type: Option<IngressType>,
197}
198
199#[allow(non_camel_case_types)]
200#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, JsonSchema)]
201pub enum IngressType {
202 http,
203 tcp,
204}
205
206pub fn default_ingress_type() -> Option<IngressType> {
207 Some(IngressType::http)
208}
209
210pub fn default_entry_points() -> Option<Vec<String>> {
211 Some(vec!["websecure".to_owned()])
212}
213
214/// Probes are used to determine the health of a container.
215/// You define this in the same manner as you would for all Kubernetes containers.
216/// See the [Kubernetes docs](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/).
217#[allow(non_snake_case)]
218#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, JsonSchema, PartialEq)]
219pub struct Probes {
220 pub readiness: Probe,
221 pub liveness: Probe,
222}
223
224#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, JsonSchema, PartialEq)]
225pub struct Probe {
226 pub path: String,
227 pub port: i32,
228 // this should never be negative
229 #[serde(rename = "initialDelaySeconds")]
230 pub initial_delay_seconds: u32,
231}
232
233#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, JsonSchema, PartialEq)]
234pub struct Ingress {
235 pub enabled: bool,
236 pub path: Option<String>,
237}
238
239/// Midddleware is used to configure the middleware for the appService.
240/// This is specifically configured for the ingress controller Traefik.
241///
242/// Please refer to the example in the `AppService` documentation.
243#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, JsonSchema, PartialEq)]
244pub enum Middleware {
245 #[serde(rename = "customRequestHeaders")]
246 CustomRequestHeaders(HeaderConfig),
247 #[serde(rename = "stripPrefix")]
248 StripPrefix(StripPrefixConfig),
249 #[serde(rename = "replacePathRegex")]
250 ReplacePathRegex(ReplacePathRegexConfig),
251}
252
253#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, JsonSchema, PartialEq)]
254pub struct HeaderConfig {
255 pub name: String,
256 #[schemars(schema_with = "preserve_arbitrary")]
257 pub config: BTreeMap<String, String>,
258}
259
260#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, JsonSchema, PartialEq)]
261pub struct StripPrefixConfig {
262 pub name: String,
263 pub config: Vec<String>,
264}
265#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, JsonSchema, PartialEq)]
266pub struct ReplacePathRegexConfig {
267 pub name: String,
268 pub config: ReplacePathRegexConfigType,
269}
270
271#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, JsonSchema, PartialEq)]
272pub struct ReplacePathRegexConfigType {
273 pub regex: String,
274 pub replacement: String,
275}
276
277// source: https://github.com/kube-rs/kube/issues/844
278fn preserve_arbitrary(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
279 let mut obj = schemars::schema::SchemaObject::default();
280 obj.extensions
281 .insert("x-kubernetes-preserve-unknown-fields".into(), true.into());
282 schemars::schema::Schema::Object(obj)
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn test_middleware_config() {
291 let middleware = serde_json::json!([
292 {
293 "customRequestHeaders": {
294 "name": "my-custom-headers",
295 "config":
296 {
297 //remove a header
298 "Authorization": "",
299 // add a header
300 "My-New-Header": "yolo"
301 }
302 },
303 },
304 {
305 "stripPrefix": {
306 "name": "strip-my-prefix",
307 "config": [
308 "/removeMe"
309 ]
310 },
311 },
312 {
313 "replacePathRegex": {
314 "name": "replace-my-regex",
315 "config":
316 {
317 "regex": "/replace/me",
318 "replacement": "/with/me"
319 }
320 },
321 }
322 ]);
323
324 let mws = serde_json::from_value::<Vec<Middleware>>(middleware).unwrap();
325 for mw in mws {
326 match mw {
327 Middleware::CustomRequestHeaders(mw) => {
328 assert_eq!(mw.name, "my-custom-headers");
329 assert_eq!(mw.config["My-New-Header"], "yolo");
330 assert_eq!(mw.config["Authorization"], "");
331 }
332 Middleware::StripPrefix(mw) => {
333 assert_eq!(mw.name, "strip-my-prefix");
334 assert_eq!(mw.config[0], "/removeMe");
335 }
336 Middleware::ReplacePathRegex(mw) => {
337 assert_eq!(mw.name, "replace-my-regex");
338 assert_eq!(mw.config.regex, "/replace/me");
339 assert_eq!(mw.config.replacement, "/with/me");
340 }
341 }
342 }
343
344 // malformed middlewares
345 let unsupported_mw = serde_json::json!({
346 "unsupportedMiddlewareType": {
347 "name": "my-custom-headers",
348 "config":
349 {
350 //remove a header
351 "Authorization": "",
352 // add a header
353 "My-New-Header": "yolo"
354 }
355 },
356 });
357 let failed = serde_json::from_value::<Middleware>(unsupported_mw);
358 assert!(failed.is_err());
359
360 // provide a supported middleware but with malformed configuration
361 let supported_bad_config = serde_json::json!({
362 "replacePath": {
363 "name": "my-custom-headers",
364 "config":
365 {
366 "replacePath": "expects_a_vec<string>",
367 }
368 },
369 });
370 let failed = serde_json::from_value::<Middleware>(supported_bad_config);
371 assert!(failed.is_err());
372 }
373}