1pub mod admin;
22pub mod args;
23pub mod client;
24pub mod exit;
25
26use std::path::{Path, PathBuf};
27
28use args::{Invocation, Sink, Source, Verb};
29use base64::prelude::{BASE64_STANDARD, Engine as _};
30use client::Client;
31use exit::Failure;
32use gwk_domain::blob::{BLOB_CHUNK_BYTES, BlobAddress};
33use gwk_domain::command::KernelCommand;
34use gwk_domain::envelope::{Actor, CommandEnvelope, ENVELOPE_SCHEMA_VERSION, Origin};
35use gwk_domain::ids::{
36 AttentionItemId, AuthorityGrantId, ByteCount, CommandId, IdempotencyKey, ProjectId, Seq,
37 Timestamp,
38};
39use gwk_domain::protocol::{
40 CONTRACT_VERSION, KernelErrorCode, KernelRequest, KernelResult, ServerControl,
41};
42use serde_json::Value;
43use tokio::io::AsyncWriteExt;
44
45pub const PUBLIC_REVISION: Option<&str> = option_env!("GW_PUBLIC_REVISION");
51
52pub async fn run(argv: &[String]) -> u8 {
54 let invocation = match args::parse(argv) {
55 Ok(invocation) => invocation,
56 Err(failure) => return report(&failure, false),
57 };
58 let Invocation { verb, pretty } = invocation;
59 match execute(verb, pretty).await {
60 Ok(()) => exit::OK,
61 Err(failure) => report(&failure, pretty),
62 }
63}
64
65fn report(failure: &Failure, pretty: bool) -> u8 {
67 emit(&failure.to_json(), pretty);
68 failure.exit
69}
70
71fn emit(value: &Value, pretty: bool) {
72 let rendered = if pretty {
73 serde_json::to_string_pretty(value)
74 } else {
75 serde_json::to_string(value)
76 };
77 match rendered {
78 Ok(text) => println!("{text}"),
79 Err(e) => eprintln!("gw: could not render an answer: {e}"),
81 }
82}
83
84async fn execute(verb: Verb, pretty: bool) -> Result<(), Failure> {
85 match verb {
86 Verb::Help => {
87 print!("{}", args::HELP);
88 Ok(())
89 }
90 Verb::BuildInfo => {
91 emit(&build_info(), pretty);
92 Ok(())
93 }
94
95 Verb::Daemon => admin::daemon(pretty).await,
99 Verb::AdminInit => admin::init(pretty).await,
100 Verb::AdminVerify => admin::verify(pretty).await,
101 Verb::AdminRebuildProjections { scratch } => {
102 admin::rebuild_projections(&scratch, pretty).await
103 }
104 Verb::AdminBlob { what } => admin::retention(&what, pretty).await,
105
106 Verb::Health => ask(KernelRequest::Health {}, pretty).await,
108 Verb::Status => ask(KernelRequest::Status {}, pretty).await,
109 Verb::Watermark => ask(KernelRequest::Watermark {}, pretty).await,
110 Verb::VerifySealed => ask(KernelRequest::VerifySealed {}, pretty).await,
111
112 Verb::Activate {
113 cutover_id,
114 manifest_sha256,
115 } => {
116 let command = KernelCommand::ActivateKernel {
117 cutover_id: cutover_id.clone(),
118 archive_manifest_sha256: manifest_sha256,
119 };
120 submit(&command, &format!("kernel_activated:{cutover_id}"), pretty).await
124 }
125
126 Verb::CommandSubmit { source } => {
127 let envelope: CommandEnvelope = document(&source)?;
128 ask(KernelRequest::SubmitCommand { envelope }, pretty).await
129 }
130
131 Verb::ProjectionGet { kind, id } => {
132 ask(
133 KernelRequest::GetProjection {
134 projection: kind,
135 id,
136 },
137 pretty,
138 )
139 .await
140 }
141 Verb::ProjectionList {
142 kind,
143 cursor,
144 limit,
145 } => {
146 ask(
147 KernelRequest::ListProjection {
148 projection: kind,
149 cursor,
150 limit,
151 },
152 pretty,
153 )
154 .await
155 }
156
157 Verb::EventRead { cursor, limit } => {
158 ask(KernelRequest::ReadEvents { cursor, limit }, pretty).await
159 }
160 Verb::EventFollow { cursor } => follow(cursor, pretty).await,
161
162 Verb::AttentionResolve { id, resolution } => {
163 let command = KernelCommand::ResolveAttention {
164 attention_item_id: AttentionItemId::new(id.clone()),
165 resolution,
166 };
167 submit(&command, &format!("resolve_attention:{id}"), pretty).await
168 }
169 Verb::AuthorityGrant { source } => {
170 let envelope: CommandEnvelope = document(&source)?;
171 expect_command(&envelope, "grant_authority")?;
176 ask(KernelRequest::SubmitCommand { envelope }, pretty).await
177 }
178 Verb::AuthorityRevoke { id, reason } => {
179 let command = KernelCommand::RevokeAuthority {
180 authority_grant_id: AuthorityGrantId::new(id.clone()),
181 reason,
182 };
183 submit(&command, &format!("revoke_authority:{id}"), pretty).await
184 }
185
186 Verb::BlobPut { source, media_type } => blob_put(&source, media_type, pretty).await,
187 Verb::BlobGet { address, output } => blob_get(&address, &output, pretty).await,
188 Verb::BlobStat { address } => ask(KernelRequest::BlobStat { address }, pretty).await,
189
190 Verb::IngestSubmit {
191 kind,
192 source,
193 project,
194 key,
195 } => {
196 let payload: Value = document(&source)?;
197 let key =
198 key.unwrap_or_else(|| format!("{}:{}", kind.as_str(), digest_of_json(&payload)));
199 let command = KernelCommand::IngestRecord {
200 kind,
201 payload,
202 payload_ref: None,
203 };
204 let envelope = envelope(
205 &command,
206 &key,
207 project.as_deref().unwrap_or(gwk_kernel::SYSTEM_PROJECT),
208 );
209 ask(KernelRequest::SubmitCommand { envelope }, pretty).await
210 }
211 }
212}
213
214fn build_info() -> Value {
216 serde_json::json!({
217 "type": "build_info",
218 "crate_version": env!("CARGO_PKG_VERSION"),
219 "contract_version": CONTRACT_VERSION,
220 "public_revision": PUBLIC_REVISION,
224 "socket_path": socket_path().to_string_lossy(),
225 })
226}
227
228fn socket_path() -> PathBuf {
230 std::env::var_os(gwk_kernel::config::SOCKET_PATH_ENV)
231 .map(PathBuf::from)
232 .unwrap_or_else(|| PathBuf::from(gwk_kernel::DEFAULT_SOCKET_PATH))
233}
234
235async fn connect() -> Result<Client, Failure> {
236 let (client, _ack) = Client::connect(&socket_path()).await?;
237 Ok(client)
238}
239
240async fn ask(request: KernelRequest, pretty: bool) -> Result<(), Failure> {
242 let result = connect().await?.ask(request).await?;
243 answer(result, pretty)
244}
245
246fn answer(result: KernelResult, pretty: bool) -> Result<(), Failure> {
248 if let KernelResult::Error {
249 code,
250 message,
251 detail,
252 } = result
253 {
254 let mut failure = Failure::new(code, message);
255 if let Some(detail) = detail {
256 failure.message = format!("{}: {detail}", failure.message);
259 }
260 return Err(failure);
261 }
262 let value = serde_json::to_value(&result)
263 .map_err(|e| Failure::internal(format!("render an answer: {e}")))?;
264 emit(&value, pretty);
265 Ok(())
266}
267
268async fn submit(command: &KernelCommand, key: &str, pretty: bool) -> Result<(), Failure> {
276 let envelope = envelope(command, key, gwk_kernel::SYSTEM_PROJECT);
277 ask(KernelRequest::SubmitCommand { envelope }, pretty).await
278}
279
280fn envelope(command: &KernelCommand, key: &str, project: &str) -> CommandEnvelope {
281 CommandEnvelope {
282 command_id: CommandId::new(format!("cmd-{key}")),
283 project_id: ProjectId::new(project),
284 command_type: command.command_type().to_owned(),
285 schema_version: ENVELOPE_SCHEMA_VERSION,
286 issued_at: now(),
290 actor: Actor {
291 kind: "operator".to_owned(),
292 id: None,
293 },
294 origin: Origin {
295 system: "gw".to_owned(),
296 r#ref: None,
297 },
298 target_aggregate_type: None,
299 target_aggregate_id: None,
300 expected_version: None,
301 idempotency_key: IdempotencyKey::new(key),
302 causation_id: None,
303 correlation_id: None,
304 payload: serde_json::to_value(command).unwrap_or(Value::Null),
307 }
308}
309
310fn expect_command(envelope: &CommandEnvelope, wanted: &str) -> Result<(), Failure> {
311 if envelope.command_type == wanted {
312 return Ok(());
313 }
314 Err(Failure::usage(format!(
315 "this verb submits {wanted}, and the envelope is a {}",
316 envelope.command_type
317 )))
318}
319
320async fn follow(cursor: Option<Seq>, pretty: bool) -> Result<(), Failure> {
326 let mut client = connect().await?;
327 let stream = client.subscribe(cursor).await?;
328 loop {
329 let Some(control) = client.receive().await? else {
330 return Ok(());
333 };
334 match control {
335 ServerControl::EventBatch {
336 request_id, events, ..
337 } if request_id == stream => {
338 for event in &events {
339 let value = serde_json::to_value(event)
340 .map_err(|e| Failure::internal(format!("render an event: {e}")))?;
341 emit(&value, pretty);
342 }
343 }
344 ServerControl::StreamClosed {
345 request_id,
346 code,
347 last_cursor,
348 } if request_id == stream => {
349 let resume = last_cursor
353 .map(|seq| seq.value().to_string())
354 .unwrap_or_else(|| "the beginning".to_owned());
355 return Err(Failure::new(
356 code,
357 format!("the stream closed; resume from {resume}"),
358 ));
359 }
360 _ => {}
363 }
364 }
365}
366
367async fn blob_put(source: &Source, media_type: String, pretty: bool) -> Result<(), Failure> {
369 let plaintext = bytes(source)?;
370 let address = address_of(&plaintext);
371 let mut client = connect().await?;
372
373 let upload = match client
374 .ask(KernelRequest::BlobBegin {
375 media_type,
376 byte_size: ByteCount::new(plaintext.len() as u64),
377 })
378 .await?
379 {
380 KernelResult::BlobBegun { upload_id } => upload_id,
381 other => return answer(other, pretty),
382 };
383
384 let pieces: Vec<&[u8]> = if plaintext.is_empty() {
388 vec![&[]]
389 } else {
390 plaintext.chunks(BLOB_CHUNK_BYTES).collect()
391 };
392 for (sequence, chunk) in pieces.into_iter().enumerate() {
393 let sequence = u32::try_from(sequence)
394 .map_err(|_| Failure::usage("this blob has more chunks than the protocol counts"))?;
395 match client
396 .ask(KernelRequest::BlobChunk {
397 upload_id: upload.clone(),
398 sequence,
399 data_base64: BASE64_STANDARD.encode(chunk),
400 })
401 .await?
402 {
403 KernelResult::BlobChunkAccepted { .. } => {}
404 other => return answer(other, pretty),
405 }
406 }
407
408 let result = client
409 .ask(KernelRequest::BlobCommit {
410 upload_id: upload,
411 address,
412 })
413 .await?;
414 answer(result, pretty)
415}
416
417async fn blob_get(address: &BlobAddress, output: &Sink, pretty: bool) -> Result<(), Failure> {
419 let mut client = connect().await?;
420 let size = match client
424 .ask(KernelRequest::BlobStat {
425 address: address.clone(),
426 })
427 .await?
428 {
429 KernelResult::BlobStat { descriptor } => descriptor.byte_size.value(),
430 other => return answer(other, pretty),
431 };
432
433 let mut bytes: Vec<u8> = Vec::with_capacity(size as usize);
434 while (bytes.len() as u64) < size {
435 let result = client
436 .ask(KernelRequest::BlobRead {
437 address: address.clone(),
438 offset: ByteCount::new(bytes.len() as u64),
439 length: ByteCount::new(size - bytes.len() as u64),
440 })
441 .await?;
442 let KernelResult::BlobBytes { data_base64, .. } = result else {
443 return answer(result, pretty);
444 };
445 let part = BASE64_STANDARD
446 .decode(&data_base64)
447 .map_err(|e| Failure::new(KernelErrorCode::BlobIntegrity, format!("base64: {e}")))?;
448 if part.is_empty() {
449 return Err(Failure::new(
450 KernelErrorCode::BlobIntegrity,
451 format!("the read stalled at {} of {size} bytes", bytes.len()),
452 ));
453 }
454 bytes.extend_from_slice(&part);
455 }
456
457 match output {
458 Sink::Stdout => tokio::io::stdout()
461 .write_all(&bytes)
462 .await
463 .map_err(|e| Failure::internal(format!("write to standard output: {e}")))?,
464 Sink::File(path) => {
465 write_file(path, &bytes)?;
466 emit(
467 &serde_json::json!({
468 "type": "blob_written",
469 "address": address.as_str(),
470 "byte_size": size.to_string(),
471 "path": path.to_string_lossy(),
472 }),
473 pretty,
474 );
475 }
476 }
477 Ok(())
478}
479
480fn document<T: serde::de::DeserializeOwned>(source: &Source) -> Result<T, Failure> {
485 let raw = bytes(source)?;
486 serde_json::from_slice(&raw).map_err(|e| Failure::usage(format!("{source:?}: {e}")))
487}
488
489fn bytes(source: &Source) -> Result<Vec<u8>, Failure> {
490 match source {
491 Source::Stdin => {
492 use std::io::Read as _;
493 let mut buffer = Vec::new();
494 std::io::stdin()
495 .read_to_end(&mut buffer)
496 .map_err(|e| Failure::usage(format!("read standard input: {e}")))?;
497 Ok(buffer)
498 }
499 Source::File(path) => {
500 std::fs::read(path).map_err(|e| Failure::usage(format!("read {}: {e}", path.display())))
501 }
502 }
503}
504
505fn write_file(path: &Path, bytes: &[u8]) -> Result<(), Failure> {
506 std::fs::write(path, bytes)
507 .map_err(|e| Failure::usage(format!("write {}: {e}", path.display())))
508}
509
510fn address_of(plaintext: &[u8]) -> BlobAddress {
513 BlobAddress::from_digest(&hex_lower(plaintext)).expect("a sha256 digest is 64 lowercase hex")
519}
520
521fn digest_of_json(value: &Value) -> String {
522 hex_lower(serde_json::to_string(value).unwrap_or_default().as_bytes())
523}
524
525fn hex_lower(bytes: &[u8]) -> String {
526 use sha2::{Digest as _, Sha256};
527 let digest: [u8; 32] = Sha256::digest(bytes).into();
528 let mut out = String::with_capacity(64);
529 for byte in digest {
530 out.push(char::from_digit(u32::from(byte >> 4), 16).unwrap_or('0'));
532 out.push(char::from_digit(u32::from(byte & 0x0f), 16).unwrap_or('0'));
533 }
534 out
535}
536
537fn now() -> Timestamp {
544 let secs = std::time::SystemTime::now()
545 .duration_since(std::time::UNIX_EPOCH)
546 .map(|d| d.as_secs())
547 .unwrap_or_default();
548 Timestamp::new(rfc3339(secs))
549}
550
551fn rfc3339(secs: u64) -> String {
552 let days = (secs / 86_400) as i64;
553 let rest = secs % 86_400;
554 let (year, month, day) = civil_from_days(days);
555 format!(
556 "{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}Z",
557 rest / 3600,
558 (rest % 3600) / 60,
559 rest % 60
560 )
561}
562
563fn civil_from_days(days: i64) -> (i64, u32, u32) {
566 let z = days + 719_468;
567 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
568 let doe = z - era * 146_097;
569 let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
570 let y = yoe + era * 400;
571 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
572 let mp = (5 * doy + 2) / 153;
573 let d = doy - (153 * mp + 2) / 5 + 1;
574 let m = if mp < 10 { mp + 3 } else { mp - 9 };
575 (if m <= 2 { y + 1 } else { y }, m as u32, d as u32)
576}
577
578#[cfg(test)]
579mod tests {
580 use super::*;
581
582 #[test]
583 fn the_clock_renders_dates_a_database_will_accept() {
584 assert_eq!(rfc3339(0), "1970-01-01T00:00:00Z");
585 assert_eq!(rfc3339(86_399), "1970-01-01T23:59:59Z");
586 assert_eq!(rfc3339(86_400), "1970-01-02T00:00:00Z");
587 assert_eq!(rfc3339(951_782_400), "2000-02-29T00:00:00Z");
590 assert_eq!(rfc3339(951_868_800), "2000-03-01T00:00:00Z");
591 assert_eq!(rfc3339(4_107_456_000), "2100-02-28T00:00:00Z");
594 assert_eq!(rfc3339(4_107_456_000 + 86_400), "2100-03-01T00:00:00Z");
595 assert!(rfc3339(0) < rfc3339(86_400));
597 }
598
599 #[test]
600 fn a_blob_address_is_the_digest_of_its_own_plaintext() {
601 assert_eq!(
605 address_of(b"").as_str(),
606 "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
607 );
608 assert_ne!(address_of(b"a").as_str(), address_of(b"b").as_str());
609 }
610
611 #[test]
612 fn a_minted_envelope_is_the_same_envelope_on_a_retry() {
613 let command = KernelCommand::ResolveAttention {
614 attention_item_id: AttentionItemId::new("a-1"),
615 resolution: None,
616 };
617 let one = envelope(&command, "resolve_attention:a-1", "system");
618 let two = envelope(&command, "resolve_attention:a-1", "system");
619 assert_eq!(one.command_id, two.command_id);
623 assert_eq!(one.idempotency_key, two.idempotency_key);
624 assert_eq!(one.command_type, "resolve_attention");
625 assert_eq!(one.payload, two.payload);
626 }
627
628 #[test]
629 fn a_verb_that_promises_one_command_refuses_another() {
630 let command = KernelCommand::ResolveAttention {
631 attention_item_id: AttentionItemId::new("a-1"),
632 resolution: None,
633 };
634 let envelope = envelope(&command, "k", "system");
635 assert!(expect_command(&envelope, "resolve_attention").is_ok());
636 let wrong = expect_command(&envelope, "grant_authority").expect_err("refused");
637 assert_eq!(wrong.exit, exit::USAGE);
638 }
639}