Skip to main content

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,
46        Clone,
47        Debug,
48        Default,
49        PartialEq,
50        Deserialize,
51        Serialize,
52        JsonSchema
53    )]
54    #[serde(rename_all = "camelCase")]
55    #[kube(
56        namespaced,
57        group = "materialize.cloud",
58        version = "v1alpha1",
59        kind = "Balancer",
60        singular = "balancer",
61        plural = "balancers",
62        status = "BalancerStatus",
63        printcolumn = r#"{"name": "ImageRef", "type": "string", "description": "Reference to the Docker image.", "jsonPath": ".spec.balancerdImageRef", "priority": 1}"#,
64        printcolumn = r#"{"name": "Ready", "type": "string", "description": "Whether the deployment is ready", "jsonPath": ".status.conditions[?(@.type==\"Ready\")].status", "priority": 1}"#
65    )]
66    pub struct BalancerSpec {
67        /// The balancerd image to run.
68        pub balancerd_image_ref: String,
69        /// The name of an externally managed ConfigMap in this namespace containing
70        /// dynamic configuration as a JSON object in `config.json`.
71        pub configmap_name: Option<String>,
72        // Resource requirements for the balancerd pod
73        pub resource_requirements: Option<ResourceRequirements>,
74        // Number of balancerd pods to create
75        pub replicas: Option<i32>,
76        /// Leave the balancerd Deployment's replica count to an external
77        /// autoscaler, such as a HorizontalPodAutoscaler. `replicas` then only
78        /// seeds the count when the Deployment is first created.
79        pub externally_scaled: Option<bool>,
80        // The configuration for generating an x509 certificate using cert-manager for balancerd
81        // to present to incoming connections.
82        // The dns_names and issuer_ref fields are required.
83        pub external_certificate_spec: Option<MaterializeCertSpec>,
84        // The configuration for generating an x509 certificate using cert-manager for balancerd
85        // to use to communicate with environmentd.
86        // The dns_names and issuer_ref fields are required.
87        pub internal_certificate_spec: Option<MaterializeCertSpec>,
88        // Annotations to apply to the pods
89        pub pod_annotations: Option<BTreeMap<String, String>>,
90        // Labels to apply to the pods
91        pub pod_labels: Option<BTreeMap<String, String>>,
92
93        // Configuration for statically routing traffic
94        pub static_routing: Option<StaticRoutingConfig>,
95        // Configuration for routing traffic via Frontegg
96        pub frontegg_routing: Option<FronteggRoutingConfig>,
97
98        // This can be set to override the randomly chosen resource id
99        pub resource_id: Option<String>,
100    }
101
102    impl Balancer {
103        pub fn name_prefixed(&self, suffix: &str) -> String {
104            format!("mz{}-{}", self.resource_id(), suffix)
105        }
106
107        pub fn resource_id(&self) -> &str {
108            &self.status.as_ref().unwrap().resource_id
109        }
110
111        pub fn namespace(&self) -> String {
112            self.meta().namespace.clone().unwrap()
113        }
114
115        pub fn deployment_name(&self) -> String {
116            self.name_prefixed("balancerd")
117        }
118
119        /// The replica count to apply, or `None` if replicas are externally scaled.
120        pub fn replicas(&self) -> Option<i32> {
121            if self.spec.externally_scaled.unwrap_or(false) {
122                None
123            } else {
124                Some(self.initial_replicas())
125            }
126        }
127
128        /// The replica count to create the Deployment with. When externally
129        /// scaled, the autoscaler owns the count from then on.
130        pub fn initial_replicas(&self) -> i32 {
131            self.spec.replicas.unwrap_or(2)
132        }
133
134        pub fn app_name(&self) -> String {
135            "balancerd".to_owned()
136        }
137
138        pub fn service_name(&self) -> String {
139            self.name_prefixed("balancerd")
140        }
141
142        pub fn external_certificate_name(&self) -> String {
143            self.name_prefixed("balancerd-external")
144        }
145
146        pub fn external_certificate_secret_name(&self) -> String {
147            self.name_prefixed("balancerd-external-tls")
148        }
149
150        pub fn routing(&self) -> anyhow::Result<Routing<'_>> {
151            match (&self.spec.static_routing, &self.spec.frontegg_routing) {
152                (Some(config), None) => Ok(Routing::Static(config)),
153                (None, Some(config)) => Ok(Routing::Frontegg(config)),
154                (None, None) => bail!("no routing configuration present"),
155                _ => bail!("multiple routing configurations present"),
156            }
157        }
158
159        pub fn status(&self) -> BalancerStatus {
160            self.status.clone().unwrap_or_else(|| BalancerStatus {
161                resource_id: self
162                    .spec
163                    .resource_id
164                    .clone()
165                    .unwrap_or_else(new_resource_id),
166                conditions: vec![],
167            })
168        }
169    }
170
171    #[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq)]
172    #[serde(rename_all = "camelCase")]
173    pub struct BalancerStatus {
174        /// Resource identifier used as a name prefix to avoid pod name collisions.
175        pub resource_id: String,
176
177        pub conditions: Vec<Condition>,
178    }
179
180    impl ManagedResource for Balancer {
181        fn default_labels(&self) -> BTreeMap<String, String> {
182            BTreeMap::from_iter([
183                (
184                    "materialize.cloud/organization-name".to_owned(),
185                    self.name_unchecked(),
186                ),
187                (
188                    "materialize.cloud/organization-namespace".to_owned(),
189                    self.namespace(),
190                ),
191                (
192                    "materialize.cloud/mz-resource-id".to_owned(),
193                    self.resource_id().to_owned(),
194                ),
195                ("materialize.cloud/app".to_owned(), "balancerd".to_owned()),
196            ])
197        }
198
199        fn app_name(&self) -> Option<&str> {
200            Some("balancerd")
201        }
202    }
203}