Skip to content

Commit 619cd1a

Browse files
committed
[omdb] add webhook deliveries list
1 parent 60c8565 commit 619cd1a

File tree

4 files changed

+295
-26
lines changed

4 files changed

+295
-26
lines changed

dev-tools/omdb/src/bin/omdb/db.rs

+238-25
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ use nexus_db_model::Volume;
104104
use nexus_db_model::VolumeRepair;
105105
use nexus_db_model::VolumeResourceUsage;
106106
use nexus_db_model::VpcSubnet;
107+
use nexus_db_model::WebhookDelivery;
108+
use nexus_db_model::WebhookEventClass;
109+
use nexus_db_model::WebhookReceiver;
107110
use nexus_db_model::Zpool;
108111
use nexus_db_model::to_db_typed_uuid;
109112
use nexus_db_queries::context::OpContext;
@@ -154,6 +157,7 @@ use omicron_uuid_kinds::PhysicalDiskUuid;
154157
use omicron_uuid_kinds::PropolisUuid;
155158
use omicron_uuid_kinds::SledUuid;
156159
use omicron_uuid_kinds::VolumeUuid;
160+
use omicron_uuid_kinds::WebhookEventUuid;
157161
use omicron_uuid_kinds::ZpoolUuid;
158162
use sled_agent_client::VolumeConstructionRequest;
159163
use std::borrow::Cow;
@@ -1162,14 +1166,19 @@ struct WebhookArgs {
11621166

11631167
#[derive(Debug, Subcommand, Clone)]
11641168
enum WebhookCommands {
1165-
/// Get information on webhook receivers.
1169+
/// Get information on webhook receivers
11661170
#[clap(alias = "rx")]
11671171
Receiver {
11681172
#[command(subcommand)]
11691173
command: WebhookRxCommands,
11701174
},
11711175
/// Get information on webhook events
11721176
Event,
1177+
/// Get information on webhook delivieries
1178+
Delivery {
1179+
#[command(subcommand)]
1180+
command: WebhookDeliveryCommands,
1181+
},
11731182
}
11741183

11751184
#[derive(Debug, Subcommand, Clone)]
@@ -1189,10 +1198,44 @@ struct WebhookRxInfoArgs {
11891198

11901199
#[derive(Debug, Args, Clone)]
11911200
struct WebhookRxListArgs {
1192-
#[clap(long, short)]
1201+
#[clap(long, short = 'a')]
11931202
start_at: Option<Uuid>,
11941203
}
11951204

1205+
#[derive(Debug, Subcommand, Clone)]
1206+
enum WebhookDeliveryCommands {
1207+
/// List webhook deliveries
1208+
#[clap(alias = "ls")]
1209+
List(WebhookDeliveryListArgs),
1210+
}
1211+
1212+
#[derive(Debug, Args, Clone)]
1213+
struct WebhookDeliveryListArgs {
1214+
/// If present, show only deliveries to this receiver.
1215+
#[clap(long, short, alias = "rx")]
1216+
receiver: Option<NameOrId>,
1217+
1218+
/// If present, select only deliveries for the given event.
1219+
#[clap(long, short)]
1220+
event: Option<WebhookEventUuid>,
1221+
1222+
/// If present, select only deliveries in the provided state(s)
1223+
#[clap(long = "state", short)]
1224+
states: Vec<db::model::WebhookDeliveryState>,
1225+
1226+
/// If present, select only deliveries with the provided trigger(s)
1227+
#[clap(long = "trigger", short)]
1228+
triggers: Vec<db::model::WebhookDeliveryTrigger>,
1229+
1230+
/// Include only delivery entries created before this timestamp
1231+
#[clap(long, short)]
1232+
before: Option<DateTime<Utc>>,
1233+
1234+
/// Include only delivery entries created after this timestamp
1235+
#[clap(long, short)]
1236+
after: Option<DateTime<Utc>>,
1237+
}
1238+
11961239
impl DbArgs {
11971240
/// Run a `omdb db` subcommand.
11981241
///
@@ -8084,6 +8127,9 @@ async fn cmd_db_webhook(
80848127
WebhookCommands::Receiver {
80858128
command: WebhookRxCommands::Info(args),
80868129
} => cmd_db_webhook_rx_info(datastore, fetch_opts, args).await,
8130+
WebhookCommands::Delivery {
8131+
command: WebhookDeliveryCommands::List(args),
8132+
} => cmd_db_webhook_delivery_list(datastore, fetch_opts, args).await,
80878133
WebhookCommands::Event => {
80888134
Err(anyhow::anyhow!("not yet implemented, sorry!"))
80898135
}
@@ -8156,33 +8202,16 @@ async fn cmd_db_webhook_rx_info(
81568202
fetch_opts: &DbFetchOptions,
81578203
args: &WebhookRxInfoArgs,
81588204
) -> anyhow::Result<()> {
8159-
use nexus_db_schema::schema::webhook_receiver::dsl as rx_dsl;
81608205
use nexus_db_schema::schema::webhook_rx_event_glob::dsl as glob_dsl;
81618206
use nexus_db_schema::schema::webhook_rx_subscription::dsl as subscription_dsl;
81628207
use nexus_db_schema::schema::webhook_secret::dsl as secret_dsl;
81638208

81648209
let conn = datastore.pool_connection_for_tests().await?;
8165-
let mut query = match args.receiver {
8166-
NameOrId::Id(id) => {
8167-
rx_dsl::webhook_receiver.filter(rx_dsl::id.eq(id)).into_boxed()
8168-
}
8169-
NameOrId::Name(ref name) => rx_dsl::webhook_receiver
8170-
.filter(rx_dsl::name.eq(name.to_string()))
8171-
.into_boxed(),
8172-
};
8173-
if !fetch_opts.include_deleted {
8174-
query = query.filter(rx_dsl::time_deleted.is_null());
8175-
}
8176-
8177-
let rx = query
8178-
.limit(1)
8179-
.select(db::model::WebhookReceiver::as_select())
8180-
.get_result_async(&*conn)
8210+
let rx = lookup_webhook_rx(datastore, &args.receiver)
81818211
.await
8182-
.optional()
81838212
.with_context(|| format!("loading webhook receiver {}", args.receiver))?
81848213
.ok_or_else(|| {
8185-
anyhow::anyhow!("no instance {} exists", args.receiver)
8214+
anyhow::anyhow!("no webhook receiver {} exists", args.receiver)
81868215
})?;
81878216

81888217
const ID: &'static str = "ID";
@@ -8217,9 +8246,9 @@ async fn cmd_db_webhook_rx_info(
82178246
GLOB_EXACT,
82188247
]);
82198248

8220-
let db::model::WebhookReceiver {
8249+
let WebhookReceiver {
82218250
identity:
8222-
db::model::WebhookReceiverIdentity {
8251+
nexus_db_model::WebhookReceiverIdentity {
82238252
id,
82248253
name,
82258254
description,
@@ -8305,7 +8334,7 @@ async fn cmd_db_webhook_rx_info(
83058334
.filter(subscription_dsl::rx_id.eq(id.into_untyped_uuid()))
83068335
.filter(subscription_dsl::glob.is_null())
83078336
.select(subscription_dsl::event_class)
8308-
.load_async::<db::model::WebhookEventClass>(&*conn)
8337+
.load_async::<WebhookEventClass>(&*conn)
83098338
.await;
83108339
match exact {
83118340
Ok(exact) => {
@@ -8349,7 +8378,7 @@ async fn cmd_db_webhook_rx_info(
83498378
.filter(subscription_dsl::rx_id.eq(id.into_untyped_uuid()))
83508379
.filter(subscription_dsl::glob.eq(glob))
83518380
.select(subscription_dsl::event_class)
8352-
.load_async::<db::model::WebhookEventClass>(&*conn)
8381+
.load_async::<WebhookEventClass>(&*conn)
83538382
.await;
83548383
match exact {
83558384
Ok(exact) => {
@@ -8372,6 +8401,190 @@ async fn cmd_db_webhook_rx_info(
83728401
Ok(())
83738402
}
83748403

8404+
async fn cmd_db_webhook_delivery_list(
8405+
datastore: &DataStore,
8406+
fetch_opts: &DbFetchOptions,
8407+
args: &WebhookDeliveryListArgs,
8408+
) -> anyhow::Result<()> {
8409+
use nexus_db_schema::schema::webhook_delivery::dsl as delivery_dsl;
8410+
let conn = datastore.pool_connection_for_tests().await?;
8411+
let mut query = delivery_dsl::webhook_delivery
8412+
.limit(fetch_opts.fetch_limit.get().into())
8413+
.order_by(delivery_dsl::time_created.desc())
8414+
.into_boxed();
8415+
8416+
if let (Some(before), Some(after)) = (args.before, args.after) {
8417+
anyhow::ensure!(
8418+
after < before,
8419+
"if both after and before are included, after must be earlier than before"
8420+
);
8421+
}
8422+
8423+
if let Some(before) = args.before {
8424+
query = query.filter(delivery_dsl::time_created.lt(before));
8425+
}
8426+
8427+
if let Some(after) = args.before {
8428+
query = query.filter(delivery_dsl::time_created.gt(after));
8429+
}
8430+
8431+
if let Some(ref receiver) = args.receiver {
8432+
let rx =
8433+
lookup_webhook_rx(datastore, receiver).await?.ok_or_else(|| {
8434+
anyhow::anyhow!("no webhook receiver {receiver} found")
8435+
})?;
8436+
query = query.filter(delivery_dsl::rx_id.eq(rx.identity.id));
8437+
}
8438+
8439+
if !args.states.is_empty() {
8440+
query = query.filter(delivery_dsl::state.eq_any(args.states.clone()));
8441+
}
8442+
8443+
if !args.triggers.is_empty() {
8444+
query = query
8445+
.filter(delivery_dsl::triggered_by.eq_any(args.triggers.clone()));
8446+
}
8447+
8448+
let ctx = || "listing webhook receivers";
8449+
8450+
let deliveries = query
8451+
.select(WebhookDelivery::as_select())
8452+
.load_async(&*conn)
8453+
.await
8454+
.with_context(ctx)?;
8455+
8456+
check_limit(&deliveries, fetch_opts.fetch_limit, ctx);
8457+
8458+
#[derive(Tabled)]
8459+
struct DeliveryRow {
8460+
id: Uuid,
8461+
trigger: nexus_db_model::WebhookDeliveryTrigger,
8462+
state: nexus_db_model::WebhookDeliveryState,
8463+
attempts: u8,
8464+
#[tabled(display_with = "datetime_rfc3339_concise")]
8465+
time_created: DateTime<Utc>,
8466+
#[tabled(display_with = "datetime_opt_rfc3339_concise")]
8467+
time_completed: Option<DateTime<Utc>>,
8468+
}
8469+
8470+
#[derive(Tabled)]
8471+
struct WithEventId<T: Tabled> {
8472+
#[tabled(inline)]
8473+
inner: T,
8474+
event_id: Uuid,
8475+
}
8476+
8477+
#[derive(Tabled)]
8478+
struct WithRxId<T: Tabled> {
8479+
#[tabled(inline)]
8480+
inner: T,
8481+
receiver_id: Uuid,
8482+
}
8483+
8484+
impl From<&'_ WebhookDelivery> for DeliveryRow {
8485+
fn from(d: &WebhookDelivery) -> Self {
8486+
let WebhookDelivery {
8487+
id,
8488+
// event and receiver UUIDs are toggled on and off based on
8489+
// whether or not we are filtering by receiver and event, so
8490+
// ignore them here.
8491+
event_id: _,
8492+
rx_id: _,
8493+
attempts,
8494+
state,
8495+
time_created,
8496+
time_completed,
8497+
// ignore these as they are used for runtime coordination and
8498+
// aren't very useful for showing delivery history
8499+
deliverator_id: _,
8500+
time_leased: _,
8501+
triggered_by,
8502+
} = d;
8503+
Self {
8504+
id: id.into_untyped_uuid(),
8505+
trigger: *triggered_by,
8506+
state: *state,
8507+
attempts: attempts.0,
8508+
time_created: *time_created,
8509+
time_completed: *time_completed,
8510+
}
8511+
}
8512+
}
8513+
8514+
impl<'d, T> From<&'d WebhookDelivery> for WithEventId<T>
8515+
where
8516+
T: From<&'d WebhookDelivery> + Tabled,
8517+
{
8518+
fn from(d: &'d WebhookDelivery) -> Self {
8519+
Self { event_id: d.event_id.into_untyped_uuid(), inner: T::from(d) }
8520+
}
8521+
}
8522+
8523+
impl<'d, T> From<&'d WebhookDelivery> for WithRxId<T>
8524+
where
8525+
T: From<&'d WebhookDelivery> + Tabled,
8526+
{
8527+
fn from(d: &'d WebhookDelivery) -> Self {
8528+
Self { receiver_id: d.rx_id.into_untyped_uuid(), inner: T::from(d) }
8529+
}
8530+
}
8531+
8532+
let mut table = match (args.receiver.as_ref(), args.event) {
8533+
// Filtered by both receiver and event, so don't display either.
8534+
(Some(_), Some(_)) => {
8535+
tabled::Table::new(deliveries.iter().map(DeliveryRow::from))
8536+
}
8537+
// Filtered by neither receiver nor event, so include both.
8538+
(None, None) => tabled::Table::new(
8539+
deliveries.iter().map(WithRxId::<WithEventId<DeliveryRow>>::from),
8540+
),
8541+
// Filtered by receiver ID only
8542+
(Some(_), None) => tabled::Table::new(
8543+
deliveries.iter().map(WithEventId::<DeliveryRow>::from),
8544+
),
8545+
// Filtered by event ID only
8546+
(None, Some(_)) => tabled::Table::new(
8547+
deliveries.iter().map(WithRxId::<DeliveryRow>::from),
8548+
),
8549+
};
8550+
table
8551+
.with(tabled::settings::Style::empty())
8552+
.with(tabled::settings::Padding::new(0, 1, 0, 0));
8553+
8554+
println!("{table}");
8555+
Ok(())
8556+
}
8557+
8558+
/// Helper function to look up a webhook receiver with the given name or ID
8559+
async fn lookup_webhook_rx(
8560+
datastore: &DataStore,
8561+
name_or_id: &NameOrId,
8562+
) -> anyhow::Result<Option<WebhookReceiver>> {
8563+
use nexus_db_schema::schema::webhook_receiver::dsl;
8564+
8565+
let conn = datastore.pool_connection_for_tests().await?;
8566+
match name_or_id {
8567+
NameOrId::Id(id) => {
8568+
dsl::webhook_receiver
8569+
.filter(dsl::id.eq(*id))
8570+
.limit(1)
8571+
.select(WebhookReceiver::as_select())
8572+
.get_result_async(&*conn)
8573+
.await
8574+
}
8575+
NameOrId::Name(ref name) => {
8576+
dsl::webhook_receiver
8577+
.filter(dsl::name.eq(name.to_string()))
8578+
.limit(1)
8579+
.select(WebhookReceiver::as_select())
8580+
.get_result_async(&*conn)
8581+
.await
8582+
}
8583+
}
8584+
.optional()
8585+
.with_context(|| format!("loading webhook_receiver {name_or_id}"))
8586+
}
8587+
83758588
// Format a `chrono::DateTime` in RFC3339 with milliseconds precision and using
83768589
// `Z` rather than the UTC offset for UTC timestamps, to save a few characters
83778590
// of line width in tabular output.

nexus/db-model/src/webhook_delivery_state.rs

+8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use nexus_types::external_api::views;
77
use serde::Deserialize;
88
use serde::Serialize;
99
use std::fmt;
10+
use std::str::FromStr;
1011

1112
impl_enum_type!(
1213
WebhookDeliveryStateEnum:
@@ -57,3 +58,10 @@ impl From<views::WebhookDeliveryState> for WebhookDeliveryState {
5758
}
5859
}
5960
}
61+
62+
impl FromStr for WebhookDeliveryState {
63+
type Err = omicron_common::api::external::Error;
64+
fn from_str(s: &str) -> Result<Self, Self::Err> {
65+
views::WebhookDeliveryState::from_str(s).map(Into::into)
66+
}
67+
}

nexus/db-model/src/webhook_delivery_trigger.rs

+8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use nexus_types::external_api::views;
77
use serde::Deserialize;
88
use serde::Serialize;
99
use std::fmt;
10+
use std::str::FromStr;
1011

1112
impl_enum_type!(
1213
WebhookDeliveryTriggerEnum:
@@ -61,3 +62,10 @@ impl From<views::WebhookDeliveryTrigger> for WebhookDeliveryTrigger {
6162
}
6263
}
6364
}
65+
66+
impl FromStr for WebhookDeliveryTrigger {
67+
type Err = omicron_common::api::external::Error;
68+
fn from_str(s: &str) -> Result<Self, Self::Err> {
69+
views::WebhookDeliveryTrigger::from_str(s).map(Into::into)
70+
}
71+
}

0 commit comments

Comments
 (0)