mz_testdrive/action/
schema_registry.rs1use std::sync::atomic::{AtomicBool, Ordering};
11use std::time::Duration;
12
13use anyhow::{Context, bail};
14use mz_ccsr::{SchemaReference, SchemaType};
15use mz_ore::retry::Retry;
16use mz_ore::str::StrExt;
17use serde_json::Value as JsonValue;
18
19use crate::action::{ControlFlow, State};
20use crate::format::avro;
21use crate::parser::BuiltinCommand;
22
23fn extract_avro_fullname(schema_json: &str) -> anyhow::Result<String> {
26 let value: JsonValue =
27 serde_json::from_str(schema_json).context("parsing schema JSON to extract fullname")?;
28
29 let name = value
30 .get("name")
31 .and_then(|v| v.as_str())
32 .ok_or_else(|| anyhow::anyhow!("schema missing 'name' field"))?;
33
34 let namespace = value.get("namespace").and_then(|v| v.as_str());
35
36 if name.contains('.') {
38 Ok(name.to_string())
39 } else if let Some(ns) = namespace {
40 Ok(format!("{}.{}", ns, name))
41 } else {
42 Ok(name.to_string())
43 }
44}
45
46pub async fn run_publish(
47 mut cmd: BuiltinCommand,
48 state: &State,
49) -> Result<ControlFlow, anyhow::Error> {
50 let subject = cmd.args.string("subject")?;
52 let schema_type = match cmd.args.string("schema-type")?.as_str() {
53 "avro" => SchemaType::Avro,
54 "json" => SchemaType::Json,
55 "protobuf" => SchemaType::Protobuf,
56 s => bail!("unknown schema type: {}", s),
57 };
58 let references_in = match cmd.args.opt_string("references") {
59 None => vec![],
60 Some(s) => s.split(',').map(|s| s.to_string()).collect(),
61 };
62 cmd.args.done()?;
63 let schema = cmd.input.join("\n");
64
65 println!(
67 "Publishing schema for subject {} to the schema registry...",
68 subject.quoted(),
69 );
70 let mut references = vec![];
71 for reference in references_in {
72 let subject = state
73 .ccsr_client
74 .get_subject_latest(&reference)
75 .await
76 .with_context(|| format!("fetching reference {}", reference))?;
77 let type_name = match schema_type {
78 SchemaType::Avro => extract_avro_fullname(&subject.schema.raw).with_context(|| {
82 format!("extracting type name from reference schema {}", reference)
83 })?,
84 SchemaType::Protobuf | SchemaType::Json => subject.name,
85 };
86
87 references.push(SchemaReference {
88 name: type_name,
89 subject: reference.to_string(),
90 version: subject.version,
91 })
92 }
93 state
94 .ccsr_client
95 .publish_schema(&subject, &schema, schema_type, &references)
96 .await
97 .context("publishing schema")?;
98 Ok(ControlFlow::Continue)
99}
100
101pub async fn run_verify(
102 mut cmd: BuiltinCommand,
103 state: &State,
104) -> Result<ControlFlow, anyhow::Error> {
105 let subject = cmd.args.string("subject")?;
107 match cmd.args.string("schema-type")?.as_str() {
108 "avro" => (),
109 f => bail!("unknown format: {}", f),
110 };
111 let compatibility_level = cmd.args.opt_string("compatibility-level");
112 cmd.args.done()?;
113 let expected_schema = match &cmd.input[..] {
114 [expected_schema] => {
115 avro::parse_schema(expected_schema, &[]).context("parsing expected avro schema")?
116 }
117 _ => bail!("unable to read expected schema input"),
118 };
119
120 println!(
122 "Verifying contents of latest schema for subject {} in the schema registry...",
123 subject.quoted(),
124 );
125
126 let actual_schema = mz_ore::retry::Retry::default()
130 .max_duration(state.timeout)
131 .retry_async(|_| async {
132 match state.ccsr_client.get_schema_by_subject(&subject).await {
133 Ok(s) => mz_ore::retry::RetryResult::Ok(s.raw),
134 Err(
135 e @ mz_ccsr::GetBySubjectError::SubjectNotFound
136 | e @ mz_ccsr::GetBySubjectError::VersionNotFound(_),
137 ) => mz_ore::retry::RetryResult::RetryableErr(e),
138 Err(e) => mz_ore::retry::RetryResult::FatalErr(e),
139 }
140 })
141 .await
142 .context("fetching schema")?;
143
144 let actual_schema =
145 avro::parse_schema(&actual_schema, &[]).context("parsing actual avro schema")?;
146
147 if expected_schema != actual_schema {
148 bail!(
149 "schema did not match\nexpected:\n{:?}\n\nactual:\n{:?}",
150 expected_schema,
151 actual_schema,
152 );
153 }
154
155 if let Some(compatibility_level) = compatibility_level {
156 println!(
157 "Verifying compatibility level of subject {} in the schema registry...",
158 subject.quoted(),
159 );
160 let res = state.ccsr_client.get_subject_config(&subject).await?;
161 if compatibility_level != res.compatibility_level.to_string() {
162 bail!(
163 "compatibility level did not match\nexpected: {}\nactual: {}",
164 compatibility_level,
165 res.compatibility_level,
166 );
167 }
168 }
169 Ok(ControlFlow::Continue)
170}
171
172pub async fn run_wait(
173 mut cmd: BuiltinCommand,
174 state: &State,
175) -> Result<ControlFlow, anyhow::Error> {
176 let topic = cmd.args.string("topic")?;
178 let subjects = [format!("{}-value", topic), format!("{}-key", topic)];
179
180 cmd.args.done()?;
181 cmd.assert_no_input()?;
182
183 let waiting_for_kafka = AtomicBool::new(false);
189 let waiting_for_kafka = &waiting_for_kafka;
190
191 println!(
192 "Waiting for schema for subjects {:?} to become available in the schema registry...",
193 subjects
194 );
195
196 let topic = &topic;
197 let subjects = &subjects;
198 Retry::default()
199 .initial_backoff(Duration::from_millis(50))
200 .factor(1.5)
201 .max_duration(state.timeout)
202 .retry_async_canceling(|_| async move {
203 if !waiting_for_kafka.load(Ordering::Relaxed) {
204 futures::future::try_join_all(subjects.iter().map(|subject| async move {
205 state
206 .ccsr_client
207 .get_schema_by_subject(subject)
210 .await
211 .context("fetching schema")
212 .and(Ok(()))
213 }))
214 .await?;
215
216 waiting_for_kafka.store(true, Ordering::Relaxed);
217 println!("Waiting for Kafka topic {} to exist", topic);
218 }
219
220 if waiting_for_kafka.load(Ordering::Relaxed) {
221 super::kafka::check_topic_exists(topic, state).await?
222 }
223
224 Ok::<(), anyhow::Error>(())
225 })
226 .await?;
227
228 Ok(ControlFlow::Continue)
229}