mz_cloud_resources/
crd.rs1use std::collections::BTreeMap;
13use std::time::Duration;
14
15use futures::future::join_all;
16use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::{
17 CustomResourceConversion, CustomResourceDefinition,
18};
19use k8s_openapi::apimachinery::pkg::apis::meta::v1::OwnerReference;
20use kube::{
21 Api, Client, Resource, ResourceExt,
22 api::{ObjectMeta, Patch, PatchParams},
23 core::crd::merge_crds,
24 runtime::{conditions, wait::await_condition},
25};
26use rand::{Rng, distr::Uniform};
27use schemars::JsonSchema;
28use serde::{Deserialize, Serialize};
29use tracing::{info, warn};
30
31use crate::crd::generated::cert_manager::certificates::{
32 CertificateIssuerRef, CertificatePrivateKeyAlgorithm, CertificateSecretTemplate,
33};
34use mz_ore::retry::Retry;
35
36pub mod balancer;
37pub mod console;
38pub mod generated;
39pub mod materialize;
40#[cfg(feature = "vpc-endpoints")]
41pub mod vpc_endpoint;
42
43#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize, JsonSchema)]
47#[serde(rename_all = "camelCase")]
48pub struct MaterializeCertSpec {
49 pub dns_names: Option<Vec<String>>,
51 pub duration: Option<String>,
55 pub renew_before: Option<String>,
59 pub issuer_ref: Option<CertificateIssuerRef>,
61 pub secret_template: Option<CertificateSecretTemplate>,
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub private_key_algorithm: Option<CertificatePrivateKeyAlgorithm>,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub private_key_size: Option<i64>,
69}
70
71pub trait ManagedResource: Resource<DynamicType = ()> + Sized {
72 fn default_labels(&self) -> BTreeMap<String, String> {
73 BTreeMap::new()
74 }
75
76 fn app_name(&self) -> Option<&str> {
77 None
78 }
79
80 fn managed_resource_meta(&self, name: String) -> ObjectMeta {
81 let mut labels = self.default_labels();
82 labels.extend(recommended_k8s_labels(self.app_name()));
83 ObjectMeta {
84 namespace: Some(self.meta().namespace.clone().unwrap()),
85 name: Some(name),
86 labels: Some(labels),
87 owner_references: Some(vec![owner_reference(self)]),
88 ..Default::default()
89 }
90 }
91}
92
93pub fn recommended_k8s_labels(app_name: Option<&str>) -> BTreeMap<String, String> {
96 let mut labels = BTreeMap::new();
97 labels.insert(
98 "app.kubernetes.io/managed-by".to_owned(),
99 "materialize-operator".to_owned(),
100 );
101 labels.insert(
102 "app.kubernetes.io/part-of".to_owned(),
103 "materialize".to_owned(),
104 );
105 if let Some(app) = app_name {
106 labels.insert("app.kubernetes.io/name".to_owned(), app.to_owned());
107 labels.insert("app".to_owned(), app.to_owned());
109 }
110 labels
111}
112
113fn owner_reference<T: Resource<DynamicType = ()>>(t: &T) -> OwnerReference {
114 OwnerReference {
115 api_version: T::api_version(&()).to_string(),
116 kind: T::kind(&()).to_string(),
117 name: t.name_unchecked(),
118 uid: t.uid().unwrap(),
119 block_owner_deletion: Some(true),
120 ..Default::default()
121 }
122}
123
124#[derive(Debug, Clone)]
125pub struct VersionedCrd {
126 pub crds: Vec<CustomResourceDefinition>,
127 pub stored_version: String,
128 pub conversion: Option<CustomResourceConversion>,
131}
132
133pub async fn register_versioned_crds(
134 kube_client: Client,
135 versioned_crds: Vec<VersionedCrd>,
136 field_manager: &str,
137) -> Result<(), anyhow::Error> {
138 let crd_futures = versioned_crds
139 .into_iter()
140 .map(|versioned_crd| register_w_retry(kube_client.clone(), versioned_crd, field_manager));
141 for res in join_all(crd_futures).await {
142 if res.is_err() {
143 return res;
144 }
145 }
146 Ok(())
147}
148
149async fn register_w_retry(
150 kube_client: Client,
151 versioned_crds: VersionedCrd,
152 field_manager: &str,
153) -> Result<(), anyhow::Error> {
154 Retry::default()
155 .max_duration(Duration::from_secs(30))
156 .clamp_backoff(Duration::from_secs(5))
157 .retry_async(|_| async {
158 let res = register_custom_resource(
159 kube_client.clone(),
160 versioned_crds.clone(),
161 field_manager,
162 )
163 .await;
164 if let Err(err) = &res {
165 warn!(err = %err);
166 }
167 res
168 })
169 .await?;
170 Ok(())
171}
172
173async fn register_custom_resource(
176 kube_client: Client,
177 versioned_crds: VersionedCrd,
178 field_manager: &str,
179) -> Result<(), anyhow::Error> {
180 let crds = versioned_crds.crds;
181 let crd_name = format!("{}.{}", &crds[0].spec.names.plural, &crds[0].spec.group);
182 info!("Registering {} crd", &crd_name);
183 let crd_api = Api::<CustomResourceDefinition>::all(kube_client);
184 let mut crd = merge_crds(crds, &versioned_crds.stored_version).unwrap();
185 if let Some(conversion) = versioned_crds.conversion {
186 crd.spec.conversion = Some(conversion);
187 }
188 let crd_json = serde_json::to_string(&serde_json::json!(&crd))?;
189 info!(crd_json = %crd_json);
190 crd_api
191 .patch(
192 &crd_name,
193 &PatchParams::apply(field_manager).force(),
194 &Patch::Apply(crd),
195 )
196 .await?;
197 await_condition(crd_api, &crd_name, conditions::is_crd_established()).await?;
198 info!("Done registering {} crd", &crd_name);
199 Ok(())
200}
201
202pub fn new_resource_id() -> String {
203 const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
208 rand::rng()
209 .sample_iter(Uniform::new(0, CHARSET.len()).expect("valid range"))
210 .take(10)
211 .map(|i| char::from(CHARSET[i]))
212 .collect()
213}