@@ -9,15 +9,19 @@ use crate::integration_tests::instances::{
9
9
} ;
10
10
use chrono:: Utc ;
11
11
use dropshot:: test_util:: ClientTestContext ;
12
- use dropshot:: ResultsPage ;
12
+ use dropshot:: { HttpErrorResponseBody , ResultsPage } ;
13
13
use http:: { Method , StatusCode } ;
14
+ use nexus_auth:: authn:: USER_TEST_UNPRIVILEGED ;
15
+ use nexus_db_queries:: db:: identity:: Asset ;
16
+ use nexus_test_utils:: background:: activate_background_task;
14
17
use nexus_test_utils:: http_testing:: { AuthnMode , NexusRequest , RequestBuilder } ;
15
18
use nexus_test_utils:: resource_helpers:: {
16
19
create_default_ip_pool, create_disk, create_instance, create_project,
17
- objects_list_page_authz, DiskTest ,
20
+ grant_iam , object_create_error , objects_list_page_authz, DiskTest ,
18
21
} ;
19
22
use nexus_test_utils:: ControlPlaneTestContext ;
20
23
use nexus_test_utils_macros:: nexus_test;
24
+ use nexus_types:: external_api:: shared:: ProjectRole ;
21
25
use nexus_types:: external_api:: views:: OxqlQueryResult ;
22
26
use nexus_types:: silo:: DEFAULT_SILO_ID ;
23
27
use omicron_test_utils:: dev:: poll:: { wait_for_condition, CondCheckError } ;
@@ -287,6 +291,28 @@ async fn test_timeseries_schema_list(
287
291
pub async fn timeseries_query (
288
292
cptestctx : & ControlPlaneTestContext < omicron_nexus:: Server > ,
289
293
query : impl ToString ,
294
+ ) -> Vec < oxql_types:: Table > {
295
+ execute_timeseries_query ( cptestctx, "/v1/system/timeseries/query" , query)
296
+ . await
297
+ }
298
+
299
+ pub async fn project_timeseries_query (
300
+ cptestctx : & ControlPlaneTestContext < omicron_nexus:: Server > ,
301
+ project : & str ,
302
+ query : impl ToString ,
303
+ ) -> Vec < oxql_types:: Table > {
304
+ execute_timeseries_query (
305
+ cptestctx,
306
+ & format ! ( "/v1/timeseries/query?project={}" , project) ,
307
+ query,
308
+ )
309
+ . await
310
+ }
311
+
312
+ async fn execute_timeseries_query (
313
+ cptestctx : & ControlPlaneTestContext < omicron_nexus:: Server > ,
314
+ endpoint : & str ,
315
+ query : impl ToString ,
290
316
) -> Vec < oxql_types:: Table > {
291
317
// first, make sure the latest timeseries have been collected.
292
318
cptestctx. oximeter . force_collect ( ) . await ;
@@ -300,7 +326,7 @@ pub async fn timeseries_query(
300
326
nexus_test_utils:: http_testing:: RequestBuilder :: new (
301
327
& cptestctx. external_client ,
302
328
http:: Method :: POST ,
303
- "/v1/system/timeseries/query" ,
329
+ endpoint ,
304
330
)
305
331
. body ( Some ( & body) ) ,
306
332
)
@@ -527,6 +553,134 @@ async fn test_instance_watcher_metrics(
527
553
assert_gte ! ( ts2_running, 2 ) ;
528
554
}
529
555
556
+ #[ nexus_test]
557
+ async fn test_project_timeseries_query (
558
+ cptestctx : & ControlPlaneTestContext < omicron_nexus:: Server > ,
559
+ ) {
560
+ let client = & cptestctx. external_client ;
561
+
562
+ create_default_ip_pool ( & client) . await ; // needed for instance create to work
563
+
564
+ // Create two projects
565
+ let p1 = create_project ( & client, "project1" ) . await ;
566
+ let _p2 = create_project ( & client, "project2" ) . await ;
567
+
568
+ // Create resources in each project
569
+ let i1 = create_instance ( & client, "project1" , "instance1" ) . await ;
570
+ let _i2 = create_instance ( & client, "project2" , "instance2" ) . await ;
571
+
572
+ let internal_client = & cptestctx. internal_client ;
573
+
574
+ // get the instance metrics to show up
575
+ let _ =
576
+ activate_background_task ( & internal_client, "instance_watcher" ) . await ;
577
+
578
+ // Query with no project specified
579
+ let q1 = "get virtual_machine:check" ;
580
+
581
+ let result = project_timeseries_query ( & cptestctx, "project1" , q1) . await ;
582
+ assert_eq ! ( result. len( ) , 1 ) ;
583
+ assert ! ( result[ 0 ] . timeseries( ) . len( ) > 0 ) ;
584
+
585
+ // also works with project ID
586
+ let result =
587
+ project_timeseries_query ( & cptestctx, & p1. identity . id . to_string ( ) , q1)
588
+ . await ;
589
+ assert_eq ! ( result. len( ) , 1 ) ;
590
+ assert ! ( result[ 0 ] . timeseries( ) . len( ) > 0 ) ;
591
+
592
+ let result = project_timeseries_query ( & cptestctx, "project2" , q1) . await ;
593
+ assert_eq ! ( result. len( ) , 1 ) ;
594
+ assert ! ( result[ 0 ] . timeseries( ) . len( ) > 0 ) ;
595
+
596
+ // with project specified
597
+ let q2 = & format ! ( "{} | filter project_id == \" {}\" " , q1, p1. identity. id) ;
598
+
599
+ let result = project_timeseries_query ( & cptestctx, "project1" , q2) . await ;
600
+ assert_eq ! ( result. len( ) , 1 ) ;
601
+ assert ! ( result[ 0 ] . timeseries( ) . len( ) > 0 ) ;
602
+
603
+ let result = project_timeseries_query ( & cptestctx, "project2" , q2) . await ;
604
+ assert_eq ! ( result. len( ) , 1 ) ;
605
+ assert_eq ! ( result[ 0 ] . timeseries( ) . len( ) , 0 ) ;
606
+
607
+ // with instance specified
608
+ let q3 = & format ! ( "{} | filter instance_id == \" {}\" " , q1, i1. identity. id) ;
609
+
610
+ // project containing instance gives me something
611
+ let result = project_timeseries_query ( & cptestctx, "project1" , q3) . await ;
612
+ assert_eq ! ( result. len( ) , 1 ) ;
613
+ assert_eq ! ( result[ 0 ] . timeseries( ) . len( ) , 1 ) ;
614
+
615
+ // should be empty or error
616
+ let result = project_timeseries_query ( & cptestctx, "project2" , q3) . await ;
617
+ assert_eq ! ( result. len( ) , 1 ) ;
618
+ assert_eq ! ( result[ 0 ] . timeseries( ) . len( ) , 0 ) ;
619
+
620
+ // expect error when querying a metric that has no project_id on it
621
+ let q4 = "get integration_target:integration_metric" ;
622
+ let url = "/v1/timeseries/query?project=project1" ;
623
+ let body = nexus_types:: external_api:: params:: TimeseriesQuery {
624
+ query : q4. to_string ( ) ,
625
+ } ;
626
+ let result =
627
+ object_create_error ( client, url, & body, StatusCode :: BAD_REQUEST ) . await ;
628
+ assert_eq ! ( result. error_code. unwrap( ) , "InvalidRequest" ) ;
629
+ // Notable that the error confirms that the metric exists and says what the
630
+ // fields are. This is helpful generally, but here it would be better if
631
+ // we could say something more like "you can't query this timeseries from
632
+ // this endpoint"
633
+ assert_eq ! ( result. message, "The filter expression contains identifiers that are not valid for its input timeseries. Invalid identifiers: [\" project_id\" , \" silo_id\" ], timeseries fields: {\" datum\" , \" metric_name\" , \" target_name\" , \" timestamp\" }" ) ;
634
+
635
+ // nonexistent project
636
+ let url = "/v1/timeseries/query?project=nonexistent" ;
637
+ let body = nexus_types:: external_api:: params:: TimeseriesQuery {
638
+ query : q4. to_string ( ) ,
639
+ } ;
640
+ let result =
641
+ object_create_error ( client, url, & body, StatusCode :: NOT_FOUND ) . await ;
642
+ assert_eq ! ( result. message, "not found: project with name \" nonexistent\" " ) ;
643
+
644
+ // unprivileged user gets 404 on project that exists, but which they can't read
645
+ let url = "/v1/timeseries/query?project=project1" ;
646
+ let body = nexus_types:: external_api:: params:: TimeseriesQuery {
647
+ query : q1. to_string ( ) ,
648
+ } ;
649
+
650
+ let request = RequestBuilder :: new ( client, Method :: POST , url)
651
+ . body ( Some ( & body) )
652
+ . expect_status ( Some ( StatusCode :: NOT_FOUND ) ) ;
653
+ let result = NexusRequest :: new ( request)
654
+ . authn_as ( AuthnMode :: UnprivilegedUser )
655
+ . execute ( )
656
+ . await
657
+ . unwrap ( )
658
+ . parsed_body :: < HttpErrorResponseBody > ( )
659
+ . unwrap ( ) ;
660
+ assert_eq ! ( result. message, "not found: project with name \" project1\" " ) ;
661
+
662
+ // now grant the user access to that project only
663
+ grant_iam (
664
+ client,
665
+ "/v1/projects/project1" ,
666
+ ProjectRole :: Viewer ,
667
+ USER_TEST_UNPRIVILEGED . id ( ) ,
668
+ AuthnMode :: PrivilegedUser ,
669
+ )
670
+ . await ;
671
+
672
+ // now they can access the timeseries. how cool is that
673
+ let request = RequestBuilder :: new ( client, Method :: POST , url)
674
+ . body ( Some ( & body) )
675
+ . expect_status ( Some ( StatusCode :: OK ) ) ;
676
+ let result = NexusRequest :: new ( request)
677
+ . authn_as ( AuthnMode :: UnprivilegedUser )
678
+ . execute_and_parse_unwrap :: < OxqlQueryResult > ( )
679
+ . await ;
680
+ assert_eq ! ( result. tables. len( ) , 1 ) ;
681
+ assert_eq ! ( result. tables[ 0 ] . timeseries( ) . len( ) , 1 ) ;
682
+ }
683
+
530
684
#[ nexus_test]
531
685
async fn test_mgs_metrics (
532
686
cptestctx : & ControlPlaneTestContext < omicron_nexus:: Server > ,
0 commit comments