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}