@@ -25,22 +25,29 @@ use diesel::query_dsl::QueryDsl;
2525use nexus_db_lookup:: LookupPath ;
2626use nexus_db_model:: Alert ;
2727use nexus_db_model:: AlertClass ;
28+ use nexus_db_model:: AlertDeliveryState ;
29+ use nexus_db_model:: AlertDeliveryTrigger ;
2830use nexus_db_model:: AlertReceiver ;
2931use nexus_db_model:: WebhookDelivery ;
3032use nexus_db_model:: fm:: RendezvousAlertCreated ;
33+ use nexus_db_queries:: authz;
3134use nexus_db_queries:: context:: OpContext ;
3235use nexus_db_queries:: db;
3336use nexus_db_queries:: db:: DataStore ;
37+ use nexus_db_queries:: db:: datastore:: WebhookDeliveryFilters ;
3438use nexus_db_schema:: schema:: webhook_delivery:: dsl as delivery_dsl;
3539use nexus_db_schema:: schema:: webhook_delivery_attempt:: dsl as attempt_dsl;
40+ use nexus_types:: external_api:: alert as types;
3641use nexus_types:: identity:: Resource ;
3742use omicron_common:: api:: external:: DataPageParams ;
3843use omicron_common:: api:: external:: Generation ;
3944use omicron_common:: api:: external:: NameOrId ;
4045use omicron_common:: api:: external:: http_pagination:: PaginatedBy ;
46+ use omicron_uuid_kinds:: AlertReceiverUuid ;
4147use omicron_uuid_kinds:: AlertUuid ;
4248use omicron_uuid_kinds:: CaseUuid ;
4349use omicron_uuid_kinds:: GenericUuid ;
50+ use std:: sync:: LazyLock ;
4451use tabled:: Tabled ;
4552use uuid:: Uuid ;
4653
@@ -126,8 +133,8 @@ struct ClapAlertClass(nexus_types::alert::AlertClass);
126133
127134impl clap:: ValueEnum for ClapAlertClass {
128135 fn value_variants < ' a > ( ) -> & ' a [ Self ] {
129- static VALUE_VARIANTS : std :: sync :: LazyLock < Vec < ClapAlertClass > > =
130- std :: sync :: LazyLock :: new ( || {
136+ static VALUE_VARIANTS : LazyLock < Vec < ClapAlertClass > > =
137+ LazyLock :: new ( || {
131138 nexus_types:: alert:: AlertClass :: ALL_CLASSES
132139 . iter ( )
133140 . copied ( )
@@ -224,17 +231,26 @@ struct WebhookDeliveryListArgs {
224231 #[ clap( long, short, alias = "rx" ) ]
225232 receiver : Option < NameOrId > ,
226233
227- /// If present, select only deliveries for the given event.
228- #[ clap( long, short) ]
229- event : Option < AlertUuid > ,
234+ /// If present, select only deliveries for the given alert.
235+ #[ clap(
236+ long, short = 'A' ,
237+ alias = "event" , // backwards compatibility
238+ ) ]
239+ alert : Option < AlertUuid > ,
230240
231241 /// If present, select only deliveries in the provided state(s)
232- #[ clap( long = "state" , short) ]
233- states : Vec < db:: model:: AlertDeliveryState > ,
242+ ///
243+ /// If multiple values are provided, deliveries in any of those states will
244+ /// be included in the output.
245+ #[ clap( long = "state" , short, num_args( 1 ..) , value_enum) ]
246+ states : Vec < ClapAlertDeliveryState > ,
234247
235248 /// If present, select only deliveries with the provided trigger(s)
236- #[ clap( long = "trigger" , short) ]
237- triggers : Vec < db:: model:: AlertDeliveryTrigger > ,
249+ ///
250+ /// If multiple values are provided, deliveries with any of those triggers
251+ /// will be included in the output.
252+ #[ clap( long = "trigger" , short, num_args( 1 ..) , value_enum) ]
253+ triggers : Vec < ClapAlertDeliveryTrigger > ,
238254
239255 /// Include only delivery entries created before this timestamp
240256 #[ clap( long, short) ]
@@ -245,6 +261,78 @@ struct WebhookDeliveryListArgs {
245261 after : Option < DateTime < Utc > > ,
246262}
247263
264+ // Wrapper for implementing `clap::ValueEnum` for `nexus_types`'s version of
265+ // `AlertDeliveryState`.
266+ #[ derive( Debug , Copy , Clone ) ]
267+ struct ClapAlertDeliveryState ( types:: AlertDeliveryState ) ;
268+
269+ impl clap:: ValueEnum for ClapAlertDeliveryState {
270+ fn value_variants < ' a > ( ) -> & ' a [ Self ] {
271+ static VALUE_VARIANTS : LazyLock < Vec < ClapAlertDeliveryState > > =
272+ LazyLock :: new ( || {
273+ types:: AlertDeliveryState :: ALL
274+ . iter ( )
275+ . copied ( )
276+ . map ( ClapAlertDeliveryState )
277+ . collect ( )
278+ } ) ;
279+ & VALUE_VARIANTS
280+ }
281+
282+ fn to_possible_value ( & self ) -> Option < clap:: builder:: PossibleValue > {
283+ Some ( clap:: builder:: PossibleValue :: new ( self . 0 . as_str ( ) ) )
284+ }
285+
286+ fn from_str ( input : & str , _ignore_case : bool ) -> Result < Self , String > {
287+ input
288+ . parse :: < types:: AlertDeliveryState > ( )
289+ . map ( ClapAlertDeliveryState )
290+ . map_err ( |e| e. to_string ( ) )
291+ }
292+ }
293+
294+ impl From < & ' _ ClapAlertDeliveryState > for AlertDeliveryState {
295+ fn from ( & ClapAlertDeliveryState ( state) : & ClapAlertDeliveryState ) -> Self {
296+ state. into ( )
297+ }
298+ }
299+
300+ #[ derive( Debug , Copy , Clone ) ]
301+ struct ClapAlertDeliveryTrigger ( types:: AlertDeliveryTrigger ) ;
302+
303+ impl clap:: ValueEnum for ClapAlertDeliveryTrigger {
304+ fn value_variants < ' a > ( ) -> & ' a [ Self ] {
305+ static VALUE_VARIANTS : LazyLock < Vec < ClapAlertDeliveryTrigger > > =
306+ LazyLock :: new ( || {
307+ types:: AlertDeliveryTrigger :: ALL
308+ . iter ( )
309+ . copied ( )
310+ . map ( ClapAlertDeliveryTrigger )
311+ . collect ( )
312+ } ) ;
313+ & VALUE_VARIANTS
314+ }
315+
316+ fn to_possible_value ( & self ) -> Option < clap:: builder:: PossibleValue > {
317+ Some ( clap:: builder:: PossibleValue :: new ( self . 0 . as_str ( ) ) )
318+ }
319+
320+ fn from_str ( input : & str , _ignore_case : bool ) -> Result < Self , String > {
321+ input
322+ . parse :: < types:: AlertDeliveryTrigger > ( )
323+ . map ( ClapAlertDeliveryTrigger )
324+ . map_err ( |e| e. to_string ( ) )
325+ }
326+ }
327+
328+ impl From < & ' _ ClapAlertDeliveryTrigger > for AlertDeliveryTrigger {
329+ fn from (
330+ & ClapAlertDeliveryTrigger ( trigger) : & ClapAlertDeliveryTrigger ,
331+ ) -> Self {
332+ trigger. into ( )
333+ }
334+ }
335+
248336#[ derive( Debug , Args , Clone ) ]
249337struct WebhookDeliveryInfoArgs {
250338 /// The ID of the delivery to show.
@@ -285,7 +373,10 @@ async fn cmd_db_webhook(
285373 } ) => cmd_db_webhook_rx_list ( opctx, datastore, fetch_opts, args) . await ,
286374 WebhookCommands :: Delivery ( WebhookDeliveryArgs {
287375 command : WebhookDeliveryCommands :: List ( args) ,
288- } ) => cmd_db_webhook_delivery_list ( datastore, fetch_opts, args) . await ,
376+ } ) => {
377+ cmd_db_webhook_delivery_list ( opctx, datastore, fetch_opts, args)
378+ . await
379+ }
289380 WebhookCommands :: Delivery ( WebhookDeliveryArgs {
290381 command : WebhookDeliveryCommands :: Info ( args) ,
291382 } ) => cmd_db_webhook_delivery_info ( datastore, fetch_opts, args) . await ,
@@ -558,6 +649,7 @@ async fn cmd_db_webhook_rx_info(
558649}
559650
560651async fn cmd_db_webhook_delivery_list (
652+ opctx : & OpContext ,
561653 datastore : & DataStore ,
562654 fetch_opts : & DbFetchOptions ,
563655 args : & WebhookDeliveryListArgs ,
@@ -568,56 +660,63 @@ async fn cmd_db_webhook_delivery_list(
568660 receiver,
569661 states,
570662 triggers,
571- event ,
663+ alert ,
572664 } = args;
573- let conn = datastore. pool_connection_for_tests ( ) . await ?;
574- let mut query = delivery_dsl:: webhook_delivery
575- . limit ( fetch_opts. fetch_limit . get ( ) . into ( ) )
576- . order_by ( delivery_dsl:: time_created. desc ( ) )
577- . into_boxed ( ) ;
578-
579- if let ( Some ( before) , Some ( after) ) = ( before, after) {
580- anyhow:: ensure!(
581- after < before,
582- "if both `--after` and `--before` are included, after must be
583- earlier than before"
584- ) ;
585- }
586-
587- if let Some ( before) = * before {
588- query = query. filter ( delivery_dsl:: time_created. lt ( before) ) ;
589- }
590-
591- if let Some ( after) = * after {
592- query = query. filter ( delivery_dsl:: time_created. gt ( after) ) ;
593- }
594-
595- if let Some ( receiver) = receiver {
596- let rx =
597- lookup_webhook_rx ( datastore, receiver) . await ?. ok_or_else ( || {
598- anyhow:: anyhow!( "no webhook receiver {receiver} found" )
599- } ) ?;
600- query = query. filter ( delivery_dsl:: rx_id. eq ( rx. identity . id ) ) ;
601- }
602-
603- if !states. is_empty ( ) {
604- query = query. filter ( delivery_dsl:: state. eq_any ( states. clone ( ) ) ) ;
605- }
606-
607- if !triggers. is_empty ( ) {
608- query =
609- query. filter ( delivery_dsl:: triggered_by. eq_any ( triggers. clone ( ) ) ) ;
610- }
611-
612- if let Some ( id) = event {
613- query = query. filter ( delivery_dsl:: alert_id. eq ( id. into_untyped_uuid ( ) ) ) ;
614- }
665+ let filters = {
666+ let mut filters = WebhookDeliveryFilters :: new ( ) ;
667+ if let & Some ( before) = before {
668+ filters = filters. before ( before) ?;
669+ }
670+ if let & Some ( after) = after {
671+ filters = filters. after ( after) ?;
672+ }
673+ if let Some ( receiver) = receiver {
674+ // Resolve (and authorize access to) the receiver before filtering
675+ // deliveries by it; `for_receiver` requires an `authz` resource.
676+ let lookup = LookupPath :: new ( opctx, datastore) ;
677+ let rx_lookup = match receiver {
678+ NameOrId :: Id ( id) => lookup. alert_receiver_id (
679+ AlertReceiverUuid :: from_untyped_uuid ( * id) ,
680+ ) ,
681+ NameOrId :: Name ( name) => {
682+ lookup. alert_receiver_name_owned ( name. clone ( ) . into ( ) )
683+ }
684+ } ;
685+ let ( authz_rx, ) =
686+ rx_lookup. lookup_for ( authz:: Action :: Read ) . await . with_context (
687+ || format ! ( "looking up alert receiver {receiver} failed" ) ,
688+ ) ?;
689+ filters = filters. for_receiver ( & authz_rx) ;
690+ }
691+ if let & Some ( alert) = alert {
692+ let ( authz_alert, ) = LookupPath :: new ( opctx, datastore)
693+ . alert_id ( alert)
694+ . lookup_for ( authz:: Action :: Read )
695+ . await
696+ . with_context ( || format ! ( "looking up alert {alert} failed" ) ) ?;
697+ filters = filters. for_alert ( & authz_alert) ;
698+ }
699+ if !states. is_empty ( ) {
700+ filters = filters
701+ . with_states ( states. iter ( ) . map ( AlertDeliveryState :: from) ) ;
702+ }
703+ if !triggers. is_empty ( ) {
704+ filters = filters
705+ . with_triggers ( triggers. iter ( ) . map ( AlertDeliveryTrigger :: from) ) ;
706+ }
707+ filters
708+ } ;
615709
616710 let ctx = || "listing webhook deliveries" ;
617711
618- let deliveries = query
619- . select ( WebhookDelivery :: as_select ( ) )
620- . load_async ( & * conn)
712+ let pagparams = DataPageParams {
713+ // Descending order shows the most recent delivery first
714+ direction : dropshot:: PaginationOrder :: Descending ,
715+ ..first_page ( fetch_opts. fetch_limit )
716+ } ;
717+
718+ let deliveries = datastore
719+ . webhook_delivery_list ( opctx, & filters, & pagparams)
621720 . await
622721 . with_context ( ctx) ?;
623722
@@ -640,7 +739,7 @@ async fn cmd_db_webhook_delivery_list(
640739 }
641740 }
642741
643- let mut table = match ( args. receiver . as_ref ( ) , args. event ) {
742+ let mut table = match ( args. receiver . as_ref ( ) , args. alert ) {
644743 // Filtered by both receiver and event, so don't display either.
645744 ( Some ( _) , Some ( _) ) => {
646745 tabled:: Table :: new ( deliveries. iter ( ) . map ( DeliveryRow :: from) )
@@ -693,8 +792,8 @@ impl From<&'_ WebhookDelivery> for DeliveryRow {
693792 fn from ( d : & WebhookDelivery ) -> Self {
694793 let WebhookDelivery {
695794 id,
696- // event and receiver UUIDs are toggled on and off based on
697- // whether or not we are filtering by receiver and event , so
795+ // alert and receiver UUIDs are toggled on and off based on
796+ // whether or not we are filtering by receiver and alert , so
698797 // ignore them here.
699798 alert_id : _,
700799 rx_id : _,
@@ -1170,7 +1269,11 @@ async fn cmd_db_alert_info(
11701269 ..first_page ( fetch_opts. fetch_limit )
11711270 } ;
11721271 let deliveries = datastore
1173- . alert_list_webhook_deliveries ( opctx, & authz_alert, & pagparams)
1272+ . webhook_delivery_list (
1273+ opctx,
1274+ & WebhookDeliveryFilters :: default ( ) . for_alert ( & authz_alert) ,
1275+ & pagparams,
1276+ )
11741277 . await
11751278 . with_context ( ctx) ?;
11761279
0 commit comments