mz_catalog/durable/upgrade/
v80_to_v81.rs1use crate::durable::upgrade::MigrationAction;
11use crate::durable::upgrade::json_compatible::{JsonCompatible, json_compatible};
12use crate::durable::upgrade::objects_v80 as v80;
13use crate::durable::upgrade::objects_v81 as v81;
14use mz_catalog_protos::objects_v80::{ClusterVariant, ManagedCluster};
15use mz_repr::adt::regex::Regex;
16
17json_compatible!(v80::RoleKey with v81::RoleKey);
18json_compatible!(v80::RoleMembership with v81::RoleMembership);
19json_compatible!(v80::RoleVars with v81::RoleVars);
20
21pub fn upgrade(
27 snapshot: Vec<v80::StateUpdateKind>,
28) -> Vec<MigrationAction<v80::StateUpdateKind, v81::StateUpdateKind>> {
29 let has_password_auth = snapshot.iter().any(|update| match update {
37 v80::StateUpdateKind::ServerConfiguration(config) => {
38 config.key.name == "enable_password_auth" && config.value.value == "on"
39 }
40 _ => false,
41 });
42
43 let is_cloud = snapshot.iter().any(|update| match update {
44 v80::StateUpdateKind::Cluster(cluster) if cluster.value.name == "mz_system" => {
45 if let ClusterVariant::Managed(ManagedCluster {
46 replication_factor, ..
47 }) = cluster.value.config.variant
48 {
49 replication_factor > 0 && !has_password_auth
50 } else {
51 false
52 }
53 }
54 _ => false,
55 });
56
57 if !is_cloud {
58 return Vec::new();
60 }
61
62 let mut migrations = Vec::new();
63 for update in snapshot {
64 let v80::StateUpdateKind::Role(role) = update else {
65 continue;
66 };
67
68 let email_regex_heuristic = Regex::new(r".+@.+\..+", true).expect("valid regex");
75 let auto_provision_source = if email_regex_heuristic.is_match(&role.value.name.clone()) {
76 Some(v81::AutoProvisionSource::Frontegg)
77 } else {
78 None
79 };
80
81 let new_role = v81::StateUpdateKind::Role(v81::Role {
82 key: JsonCompatible::convert(&role.key),
83 value: v81::RoleValue {
84 name: role.value.name.clone(),
85 attributes: v81::RoleAttributes {
86 inherit: role.value.attributes.inherit,
87 superuser: role.value.attributes.superuser,
88 login: role.value.attributes.login,
89 auto_provision_source,
90 },
91 membership: JsonCompatible::convert(&role.value.membership),
92 vars: JsonCompatible::convert(&role.value.vars),
93 oid: role.value.oid,
94 },
95 });
96
97 let old_role = v80::StateUpdateKind::Role(role);
98 migrations.push(MigrationAction::Update(old_role, new_role));
99 }
100 migrations
101}
102
103#[cfg(test)]
104mod tests {
105 use super::upgrade;
106 use crate::durable::upgrade::MigrationAction;
107 use crate::durable::upgrade::objects_v80 as v80;
108 use crate::durable::upgrade::objects_v81 as v81;
109
110 fn make_server_configuration(name: &str, value: &str) -> v80::StateUpdateKind {
111 v80::StateUpdateKind::ServerConfiguration(v80::ServerConfiguration {
112 key: v80::ServerConfigurationKey {
113 name: name.to_string(),
114 },
115 value: v80::ServerConfigurationValue {
116 value: value.to_string(),
117 },
118 })
119 }
120
121 fn make_mz_system_cluster(replication_factor: u32) -> v80::StateUpdateKind {
122 v80::StateUpdateKind::Cluster(v80::Cluster {
123 key: v80::ClusterKey {
124 id: v80::ClusterId::System(1),
125 },
126 value: v80::ClusterValue {
127 name: "mz_system".to_string(),
128 owner_id: v80::RoleId::System(1),
129 privileges: vec![],
130 config: v80::ClusterConfig {
131 workload_class: None,
132 variant: v80::ClusterVariant::Managed(v80::ManagedCluster {
133 size: "1".to_string(),
134 replication_factor,
135 availability_zones: vec![],
136 logging: v80::ReplicaLogging {
137 log_logging: false,
138 interval: None,
139 },
140 optimizer_feature_overrides: vec![],
141 schedule: v80::ClusterSchedule::Manual,
142 }),
143 },
144 },
145 })
146 }
147
148 fn make_role(id: u64, name: &str) -> v80::StateUpdateKind {
149 v80::StateUpdateKind::Role(v80::Role {
150 key: v80::RoleKey {
151 id: v80::RoleId::User(id),
152 },
153 value: v80::RoleValue {
154 name: name.to_string(),
155 attributes: v80::RoleAttributes {
156 inherit: true,
157 superuser: None,
158 login: None,
159 },
160 membership: v80::RoleMembership { map: vec![] },
161 vars: v80::RoleVars { entries: vec![] },
162 oid: id.try_into().expect("id fits into u32"),
163 },
164 })
165 }
166
167 #[mz_ore::test]
168 fn test_self_managed_returns_no_migrations() {
169 let snapshot = vec![
172 make_server_configuration("enable_password_auth", "on"),
173 make_mz_system_cluster(0),
174 make_role(1, "user@example.com"),
175 ];
176 let migrations = upgrade(snapshot);
177 assert!(migrations.is_empty());
178 }
179
180 #[mz_ore::test]
181 fn test_self_managed_no_password_auth_returns_no_migrations() {
182 let snapshot = vec![
186 make_server_configuration("enable_password_auth", "on"),
187 make_mz_system_cluster(1),
188 make_role(1, "user@example.com"),
189 ];
190 let migrations = upgrade(snapshot);
191 assert!(migrations.is_empty());
192 }
193
194 #[mz_ore::test]
195 fn test_cloud_mixed_roles() {
196 let snapshot = vec![
198 make_server_configuration("enable_password_auth", "off"),
199 make_mz_system_cluster(1),
200 make_role(1, "user@example.com"),
201 make_role(2, "manually_created_role"),
202 ];
203 let migrations = upgrade(snapshot);
204 assert_eq!(migrations.len(), 2);
205
206 let MigrationAction::Update(_, user_role_action) = &migrations[0] else {
207 panic!("Expected action for user role");
208 };
209 let v81::StateUpdateKind::Role(user_role) = user_role_action else {
210 panic!();
211 };
212 assert_eq!(
213 user_role.value.attributes.auto_provision_source,
214 Some(v81::AutoProvisionSource::Frontegg)
215 );
216
217 let MigrationAction::Update(_, manually_created_role_action) = &migrations[1] else {
218 panic!();
219 };
220 let v81::StateUpdateKind::Role(manually_created_role) = manually_created_role_action else {
221 panic!();
222 };
223 assert_eq!(
224 manually_created_role.value.attributes.auto_provision_source,
225 None
226 );
227 }
228
229 #[mz_ore::test]
230 fn test_non_role_updates_ignored() {
231 let snapshot = vec![
233 make_server_configuration("enable_password_auth", "off"),
234 make_mz_system_cluster(1),
235 ];
236 let migrations = upgrade(snapshot);
237 assert!(migrations.is_empty());
238 }
239}