@@ -9,15 +9,19 @@ use crate::integration_tests::instances::{
99} ;
1010use chrono:: Utc ;
1111use dropshot:: test_util:: ClientTestContext ;
12- use dropshot:: ResultsPage ;
12+ use dropshot:: { HttpErrorResponseBody , ResultsPage } ;
1313use 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;
1417use nexus_test_utils:: http_testing:: { AuthnMode , NexusRequest , RequestBuilder } ;
1518use nexus_test_utils:: resource_helpers:: {
1619 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 ,
1821} ;
1922use nexus_test_utils:: ControlPlaneTestContext ;
2023use nexus_test_utils_macros:: nexus_test;
24+ use nexus_types:: external_api:: shared:: ProjectRole ;
2125use nexus_types:: external_api:: views:: OxqlQueryResult ;
2226use nexus_types:: silo:: DEFAULT_SILO_ID ;
2327use omicron_test_utils:: dev:: poll:: { wait_for_condition, CondCheckError } ;
@@ -287,6 +291,28 @@ async fn test_timeseries_schema_list(
287291pub async fn timeseries_query (
288292 cptestctx : & ControlPlaneTestContext < omicron_nexus:: Server > ,
289293 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 ,
290316) -> Vec < oxql_types:: Table > {
291317 // first, make sure the latest timeseries have been collected.
292318 cptestctx. oximeter . force_collect ( ) . await ;
@@ -300,7 +326,7 @@ pub async fn timeseries_query(
300326 nexus_test_utils:: http_testing:: RequestBuilder :: new (
301327 & cptestctx. external_client ,
302328 http:: Method :: POST ,
303- "/v1/system/timeseries/query" ,
329+ endpoint ,
304330 )
305331 . body ( Some ( & body) ) ,
306332 )
@@ -527,6 +553,134 @@ async fn test_instance_watcher_metrics(
527553 assert_gte ! ( ts2_running, 2 ) ;
528554}
529555
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+
530684#[ nexus_test]
531685async fn test_mgs_metrics (
532686 cptestctx : & ControlPlaneTestContext < omicron_nexus:: Server > ,
0 commit comments