mz_cloud_resources/crd/
balancer.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use std::collections::BTreeMap;
11
12use anyhow::bail;
13use k8s_openapi::{
14    api::core::v1::ResourceRequirements, apimachinery::pkg::apis::meta::v1::Condition,
15};
16use kube::{CustomResource, Resource, ResourceExt};
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19
20use crate::crd::{ManagedResource, MaterializeCertSpec, new_resource_id};
21
22pub mod v1alpha1 {
23    use super::*;
24
25    #[derive(Clone, Debug)]
26    pub enum Routing<'a> {
27        Static(&'a StaticRoutingConfig),
28        Frontegg(&'a FronteggRoutingConfig),
29    }
30
31    #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)]
32    #[serde(rename_all = "camelCase")]
33    pub struct StaticRoutingConfig {
34        pub environmentd_namespace: String,
35        pub environmentd_service_name: String,
36    }
37
38    #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)]
39    #[serde(rename_all = "camelCase")]
40    pub struct FronteggRoutingConfig {
41        // TODO
42    }
43
44    #[derive(
45        CustomResource, Clone, Debug, Default, PartialEq, Deserialize, Serialize, JsonSchema,
46    )]
47    #[serde(rename_all = "camelCase")]
48    #[kube(
49        namespaced,
50        group = "materialize.cloud",
51        version = "v1alpha1",
52        kind = "Balancer",
53        singular = "balancer",
54        plural = "balancers",
55        status = "BalancerStatus",
56        printcolumn = r#"{"name": "ImageRef", "type": "string", "description": "Reference to the Docker image.", "jsonPath": ".spec.balancerdImageRef", "priority": 1}"#,
57        printcolumn = r#"{"name": "Ready", "type": "string", "description": "Whether the deployment is ready", "jsonPath": ".status.conditions[?(@.type==\"Ready\")].status", "priority": 1}"#
58    )]
59    pub struct BalancerSpec {
60        /// The balancerd image to run.
61        pub balancerd_image_ref: String,
62        // Resource requirements for the balancerd pod
63        pub resource_requirements: Option<ResourceRequirements>,
64        // Number of balancerd pods to create
65        pub replicas: Option<i32>,
66        // The configuration for generating an x509 certificate using cert-manager for balancerd
67        // to present to incoming connections.
68        // The dns_names and issuer_ref fields are required.
69        pub external_certificate_spec: Option<MaterializeCertSpec>,
70        // The configuration for generating an x509 certificate using cert-manager for balancerd
71        // to use to communicate with environmentd.
72        // The dns_names and issuer_ref fields are required.
73        pub internal_certificate_spec: Option<MaterializeCertSpec>,
74        // Annotations to apply to the pods
75        pub pod_annotations: Option<BTreeMap<String, String>>,
76        // Labels to apply to the pods
77        pub pod_labels: Option<BTreeMap<String, String>>,
78
79        // Configuration for statically routing traffic
80        pub static_routing: Option<StaticRoutingConfig>,
81        // Configuration for routing traffic via Frontegg
82        pub frontegg_routing: Option<FronteggRoutingConfig>,
83
84        // This can be set to override the randomly chosen resource id
85        pub resource_id: Option<String>,
86    }
87
88    impl Balancer {
89        pub fn name_prefixed(&self, suffix: &str) -> String {
90            format!("mz{}-{}", self.resource_id(), suffix)
91        }
92
93        pub fn resource_id(&self) -> &str {
94            &self.status.as_ref().unwrap().resource_id
95        }
96
97        pub fn namespace(&self) -> String {
98            self.meta().namespace.clone().unwrap()
99        }
100
101        pub fn deployment_name(&self) -> String {
102            self.name_prefixed("balancerd")
103        }
104
105        pub fn replicas(&self) -> i32 {
106            self.spec.replicas.unwrap_or(2)
107        }
108
109        pub fn app_name(&self) -> String {
110            "balancerd".to_owned()
111        }
112
113        pub fn service_name(&self) -> String {
114            self.name_prefixed("balancerd")
115        }
116
117        pub fn external_certificate_name(&self) -> String {
118            self.name_prefixed("balancerd-external")
119        }
120
121        pub fn external_certificate_secret_name(&self) -> String {
122            self.name_prefixed("balancerd-external-tls")
123        }
124
125        pub fn routing(&self) -> anyhow::Result<Routing<'_>> {
126            match (&self.spec.static_routing, &self.spec.frontegg_routing) {
127                (Some(config), None) => Ok(Routing::Static(config)),
128                (None, Some(config)) => Ok(Routing::Frontegg(config)),
129                (None, None) => bail!("no routing configuration present"),
130                _ => bail!("multiple routing configurations present"),
131            }
132        }
133
134        pub fn status(&self) -> BalancerStatus {
135            self.status.clone().unwrap_or_else(|| BalancerStatus {
136                resource_id: self
137                    .spec
138                    .resource_id
139                    .clone()
140                    .unwrap_or_else(new_resource_id),
141                conditions: vec![],
142            })
143        }
144    }
145
146    #[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq)]
147    #[serde(rename_all = "camelCase")]
148    pub struct BalancerStatus {
149        /// Resource identifier used as a name prefix to avoid pod name collisions.
150        pub resource_id: String,
151
152        pub conditions: Vec<Condition>,
153    }
154
155    impl ManagedResource for Balancer {
156        fn default_labels(&self) -> BTreeMap<String, String> {
157            BTreeMap::from_iter([
158                (
159                    "materialize.cloud/organization-name".to_owned(),
160                    self.name_unchecked(),
161                ),
162                (
163                    "materialize.cloud/organization-namespace".to_owned(),
164                    self.namespace(),
165                ),
166                (
167                    "materialize.cloud/mz-resource-id".to_owned(),
168                    self.resource_id().to_owned(),
169                ),
170                ("materialize.cloud/app".to_owned(), "balancerd".to_owned()),
171            ])
172        }
173    }
174}