From a72f1e820e47eea88b89d452a56ddd47c1683631 Mon Sep 17 00:00:00 2001 From: Martin Raszyk Date: Thu, 5 Sep 2024 17:59:36 +0200 Subject: [PATCH 1/8] feat(station): serve metrics as properly certified HTTP asset --- Cargo.lock | 35 ++-- Cargo.toml | 7 +- core/station/impl/Cargo.toml | 4 + core/station/impl/src/controllers/http.rs | 169 ++++++++++++++------ core/station/impl/src/controllers/system.rs | 12 +- core/station/impl/src/core/middlewares.rs | 2 + tests/integration/src/http.rs | 15 +- 7 files changed, 169 insertions(+), 75 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b50cd51b..84fa96d2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2134,6 +2134,17 @@ dependencies = [ "walkdir", ] +[[package]] +name = "ic-asset-certification" +version = "2.6.0" +source = "git+https://github.com/dfinity/response-verification?rev=eefc99e2a0041ae46873864e6e2f7aa8506a361f#eefc99e2a0041ae46873864e6e2f7aa8506a361f" +dependencies = [ + "globset", + "ic-certification 2.6.0 (git+https://github.com/dfinity/response-verification?rev=eefc99e2a0041ae46873864e6e2f7aa8506a361f)", + "ic-http-certification 2.6.0 (git+https://github.com/dfinity/response-verification?rev=eefc99e2a0041ae46873864e6e2f7aa8506a361f)", + "thiserror", +] + [[package]] name = "ic-cbor" version = "2.6.0" @@ -2263,7 +2274,7 @@ dependencies = [ [[package]] name = "ic-certification" version = "2.6.0" -source = "git+https://github.com/dfinity/response-verification?rev=da70db93832f88ecc556ae082612aedec47d3816#da70db93832f88ecc556ae082612aedec47d3816" +source = "git+https://github.com/dfinity/response-verification?rev=eefc99e2a0041ae46873864e6e2f7aa8506a361f#eefc99e2a0041ae46873864e6e2f7aa8506a361f" dependencies = [ "hex", "serde", @@ -2310,12 +2321,12 @@ dependencies = [ [[package]] name = "ic-http-certification" version = "2.6.0" -source = "git+https://github.com/dfinity/response-verification?rev=da70db93832f88ecc556ae082612aedec47d3816#da70db93832f88ecc556ae082612aedec47d3816" +source = "git+https://github.com/dfinity/response-verification?rev=eefc99e2a0041ae46873864e6e2f7aa8506a361f#eefc99e2a0041ae46873864e6e2f7aa8506a361f" dependencies = [ "candid", "http 0.2.12", - "ic-certification 2.6.0 (git+https://github.com/dfinity/response-verification?rev=da70db93832f88ecc556ae082612aedec47d3816)", - "ic-representation-independent-hash 2.6.0 (git+https://github.com/dfinity/response-verification?rev=da70db93832f88ecc556ae082612aedec47d3816)", + "ic-certification 2.6.0 (git+https://github.com/dfinity/response-verification?rev=eefc99e2a0041ae46873864e6e2f7aa8506a361f)", + "ic-representation-independent-hash 2.6.0 (git+https://github.com/dfinity/response-verification?rev=eefc99e2a0041ae46873864e6e2f7aa8506a361f)", "serde", "thiserror", "urlencoding", @@ -2362,7 +2373,7 @@ dependencies = [ [[package]] name = "ic-representation-independent-hash" version = "2.6.0" -source = "git+https://github.com/dfinity/response-verification?rev=da70db93832f88ecc556ae082612aedec47d3816#da70db93832f88ecc556ae082612aedec47d3816" +source = "git+https://github.com/dfinity/response-verification?rev=eefc99e2a0041ae46873864e6e2f7aa8506a361f#eefc99e2a0041ae46873864e6e2f7aa8506a361f" dependencies = [ "leb128", "sha2 0.10.8", @@ -3131,9 +3142,9 @@ dependencies = [ "hex", "ic-cdk 0.13.5", "ic-cdk-timers", - "ic-certification 2.6.0 (git+https://github.com/dfinity/response-verification?rev=da70db93832f88ecc556ae082612aedec47d3816)", - "ic-http-certification 2.6.0 (git+https://github.com/dfinity/response-verification?rev=da70db93832f88ecc556ae082612aedec47d3816)", - "ic-representation-independent-hash 2.6.0 (git+https://github.com/dfinity/response-verification?rev=da70db93832f88ecc556ae082612aedec47d3816)", + "ic-certification 2.6.0 (git+https://github.com/dfinity/response-verification?rev=eefc99e2a0041ae46873864e6e2f7aa8506a361f)", + "ic-http-certification 2.6.0 (git+https://github.com/dfinity/response-verification?rev=eefc99e2a0041ae46873864e6e2f7aa8506a361f)", + "ic-representation-independent-hash 2.6.0 (git+https://github.com/dfinity/response-verification?rev=eefc99e2a0041ae46873864e6e2f7aa8506a361f)", "ic-stable-structures", "orbit-essentials-macros", "prometheus", @@ -4543,7 +4554,7 @@ dependencies = [ "cfg-if", "libc", "psm", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4558,6 +4569,7 @@ version = "0.0.2-alpha.6" dependencies = [ "anyhow", "async-trait", + "base64 0.22.1", "byteorder", "canbench-rs", "candid", @@ -4566,8 +4578,11 @@ dependencies = [ "deunicode", "futures", "hex", + "ic-asset-certification", "ic-cdk 0.13.5", "ic-cdk-macros 0.9.0", + "ic-certification 2.6.0 (git+https://github.com/dfinity/response-verification?rev=eefc99e2a0041ae46873864e6e2f7aa8506a361f)", + "ic-http-certification 2.6.0 (git+https://github.com/dfinity/response-verification?rev=eefc99e2a0041ae46873864e6e2f7aa8506a361f)", "ic-ledger-types", "ic-stable-structures", "lazy_static", @@ -5458,7 +5473,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b7e0186fa..c8014e700 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,10 +48,11 @@ hex = "0.4" # The ic-agent matches the one sed by bthe ic-agent = { git = "https://github.com/dfinity/agent-rs.git", rev = "be929fd7967249c879f48f2f494cbfc5805a7d98" } ic-asset = { git = "https://github.com/dfinity/sdk.git", rev = "75c080ebae22a70578c06ddf1eda0b18ef091845" } -ic-certification = { git = "https://github.com/dfinity/response-verification", rev = "da70db93832f88ecc556ae082612aedec47d3816" } +ic-asset-certification = { git = "https://github.com/dfinity/response-verification", rev = "eefc99e2a0041ae46873864e6e2f7aa8506a361f" } +ic-certification = { git = "https://github.com/dfinity/response-verification", rev = "eefc99e2a0041ae46873864e6e2f7aa8506a361f" } ic-certified-assets = { git = "https://github.com/dfinity/sdk.git", rev = "75c080ebae22a70578c06ddf1eda0b18ef091845" } -ic-http-certification = { git = "https://github.com/dfinity/response-verification", rev = "da70db93832f88ecc556ae082612aedec47d3816" } -ic-representation-independent-hash = { git = "https://github.com/dfinity/response-verification", rev = "da70db93832f88ecc556ae082612aedec47d3816" } +ic-http-certification = { git = "https://github.com/dfinity/response-verification", rev = "eefc99e2a0041ae46873864e6e2f7aa8506a361f" } +ic-representation-independent-hash = { git = "https://github.com/dfinity/response-verification", rev = "eefc99e2a0041ae46873864e6e2f7aa8506a361f" } ic-cdk = "0.13.2" ic-cdk-macros = "0.9" ic-cdk-timers = "0.7.0" diff --git a/core/station/impl/Cargo.toml b/core/station/impl/Cargo.toml index 70d2ebb36..10a28fb85 100644 --- a/core/station/impl/Cargo.toml +++ b/core/station/impl/Cargo.toml @@ -18,6 +18,7 @@ canbench = ['canbench-rs'] [dependencies] anyhow = { workspace = true } +base64 = { workspace = true } deunicode = { workspace = true } async-trait = { workspace = true } byteorder = { workspace = true } @@ -27,8 +28,11 @@ canfund = { path = '../../../libs/canfund', version = '0.0.2-alpha.3' } futures = { workspace = true } hex = { workspace = true } orbit-essentials = { path = '../../../libs/orbit-essentials', version = '0.0.2-alpha.4' } +ic-asset-certification = { workspace = true } ic-cdk = { workspace = true } ic-cdk-macros = { workspace = true } +ic-certification = { workspace = true } +ic-http-certification = { workspace = true } ic-ledger-types = { workspace = true } ic-stable-structures = { workspace = true } lazy_static = { workspace = true } diff --git a/core/station/impl/src/controllers/http.rs b/core/station/impl/src/controllers/http.rs index a296b51af..5435479bb 100644 --- a/core/station/impl/src/controllers/http.rs +++ b/core/station/impl/src/controllers/http.rs @@ -1,64 +1,139 @@ -use crate::{core::ic_cdk::api::canister_balance, SERVICE_NAME}; +use crate::core::ic_cdk::api::{ + canister_balance, data_certificate, print, set_certified_data, time, trap, +}; +use crate::SERVICE_NAME; +use base64::engine::general_purpose::STANDARD as BASE64; +use base64::Engine; +use ic_asset_certification::{Asset, AssetConfig, AssetEncoding, AssetRouter}; use ic_cdk_macros::query; -use lazy_static::lazy_static; -use orbit_essentials::api::{HeaderField, HttpRequest, HttpResponse}; -use orbit_essentials::http::add_skip_certification_headers; +use ic_certification::HashTree; +use ic_http_certification::{HeaderField, HttpCertificationTree, HttpRequest, HttpResponse}; use orbit_essentials::metrics::with_metrics_registry; +use serde::Serialize; +use std::{cell::RefCell, rc::Rc}; -// Canister entrypoints for the controller. -#[query(name = "http_request", decoding_quota = 10000)] -async fn http_request(request: HttpRequest) -> HttpResponse { - let mut resp = CONTROLLER.router(request).await; - add_skip_certification_headers(&mut resp); - resp +#[query(decoding_quota = 10000)] +fn http_request(req: HttpRequest) -> HttpResponse { + serve_asset(&req) } -// Controller initialization and implementation. -lazy_static! { - static ref CONTROLLER: HttpController = HttpController::new(); +thread_local! { + static HTTP_TREE: Rc> = Default::default(); + static ASSET_ROUTER: RefCell> = RefCell::new(AssetRouter::with_tree(HTTP_TREE.with(|tree| tree.clone()))); } -#[derive(Debug)] -pub struct HttpController {} +// Certification +pub fn certify_metrics() { + // 1. Define the asset certification configurations. + let encodings = vec![ + AssetEncoding::Brotli.default_config(), + AssetEncoding::Gzip.default_config(), + ]; -impl HttpController { - fn new() -> Self { - Self {} - } + let asset_configs = vec![AssetConfig::File { + path: "metrics".to_string(), + content_type: Some("text/plain".to_string()), + headers: get_asset_headers(vec![( + "cache-control".to_string(), + "public, no-cache, no-store".to_string(), + )]), + fallback_for: vec![], + aliased_by: vec![], + encodings: encodings.clone(), + }]; + + // 2. Collect all assets from the frontend build directory. + let mut assets = Vec::new(); + with_metrics_registry(SERVICE_NAME, |registry| { + registry + .gauge_mut( + "canister_cycles_balance", + "cycles balance available to the canister", + ) + .set(canister_balance() as f64); + }); + with_metrics_registry(SERVICE_NAME, |registry| { + registry + .gauge_mut( + "metrics_timestamp", + "UNIX timestamp in nanoseconds when the metrics were exported", + ) + .set(time() as f64); + }); + let metrics_contents = + with_metrics_registry(SERVICE_NAME, |registry| registry.export_metrics()); + assets.push(Asset::new( + "/metrics", + metrics_contents.unwrap_or_else(|e| e.to_string().as_bytes().to_vec()), + )); - async fn router(&self, request: HttpRequest) -> HttpResponse { - if request.url == "/metrics" || request.url == "/metrics/" { - return self.metrics(request).await; + ASSET_ROUTER.with_borrow_mut(|asset_router| { + // 3. Certify the assets using the `certify_assets` function from the `ic-asset-certification` crate. + if let Err(err) = asset_router.certify_assets(assets, asset_configs) { + print(format!("Failed to certify assets: {}", err)); + } else { + // 4. Set the canister's certified data. + set_certified_data(&asset_router.root_hash()); } + }); +} - return HttpResponse { - status_code: 404, - headers: vec![HeaderField("Content-Type".into(), "text/plain".into())], - body: "404 Not Found".as_bytes().to_owned(), - }; - } +// Handlers +fn serve_asset(req: &HttpRequest) -> HttpResponse<'static> { + ASSET_ROUTER.with_borrow(|asset_router| { + if let Ok((mut response, witness, expr_path)) = asset_router.serve_asset(req) { + add_certificate_header(&mut response, &witness, &expr_path); - async fn metrics(&self, request: HttpRequest) -> HttpResponse { - if request.method.to_lowercase() != "get" { - return HttpResponse { - status_code: 405, - headers: vec![HeaderField("Allow".into(), "GET".into())], - body: "405 Method Not Allowed".as_bytes().to_owned(), - }; + response + } else { + trap("Failed to serve asset"); } + }) +} - // Add dynamic metrics, dropped after the request since query calls don't save state changes. - with_metrics_registry(SERVICE_NAME, |registry| { - registry - .gauge_mut( - "canister_cycles_balance", - "cycles balance available to the canister", - ) - .set(canister_balance() as f64); - }); +fn get_asset_headers(additional_headers: Vec) -> Vec { + // set up the default headers and include additional headers provided by the caller + let mut headers = vec![ + ("strict-transport-security".to_string(), "max-age=31536000; includeSubDomains".to_string()), + ("x-frame-options".to_string(), "DENY".to_string()), + ("x-content-type-options".to_string(), "nosniff".to_string()), + ("content-security-policy".to_string(), "default-src 'self'; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content".to_string()), + ("referrer-policy".to_string(), "no-referrer".to_string()), + ("permissions-policy".to_string(), "accelerometer=(),ambient-light-sensor=(),autoplay=(),battery=(),camera=(),display-capture=(),document-domain=(),encrypted-media=(),fullscreen=(),gamepad=(),geolocation=(),gyroscope=(),layout-animations=(self),legacy-image-formats=(self),magnetometer=(),microphone=(),midi=(),oversized-images=(self),payment=(),picture-in-picture=(),publickey-credentials-get=(),speaker-selection=(),sync-xhr=(self),unoptimized-images=(self),unsized-media=(self),usb=(),screen-wake-lock=(),web-share=(),xr-spatial-tracking=()".to_string()), + ("cross-origin-embedder-policy".to_string(), "require-corp".to_string()), + ("cross-origin-opener-policy".to_string(), "same-origin".to_string()), + ]; + headers.extend(additional_headers); - with_metrics_registry(SERVICE_NAME, |registry| { - registry.export_metrics_as_http_response() - }) + headers +} + +const IC_CERTIFICATE_HEADER: &str = "IC-Certificate"; +fn add_certificate_header(response: &mut HttpResponse, witness: &HashTree, expr_path: &[String]) { + if let Some(certified_data) = data_certificate() { + let witness = cbor_encode(witness); + let expr_path = cbor_encode(&expr_path); + + response.add_header(( + IC_CERTIFICATE_HEADER.to_string(), + format!( + "certificate=:{}:, tree=:{}:, expr_path=:{}:, version=2", + BASE64.encode(certified_data), + BASE64.encode(witness), + BASE64.encode(expr_path) + ), + )); } } + +// Encoding +fn cbor_encode(value: &impl Serialize) -> Vec { + let mut serializer = serde_cbor::Serializer::new(Vec::new()); + serializer + .self_describe() + .expect("Failed to self describe CBOR"); + value + .serialize(&mut serializer) + .expect("Failed to serialize value"); + serializer.into_inner() +} diff --git a/core/station/impl/src/controllers/system.rs b/core/station/impl/src/controllers/system.rs index d66cfa7bf..6de395113 100644 --- a/core/station/impl/src/controllers/system.rs +++ b/core/station/impl/src/controllers/system.rs @@ -1,6 +1,7 @@ use crate::{ + controllers::certify_metrics, core::{ - ic_cdk::api::{canister_balance, set_certified_data, trap}, + ic_cdk::api::{canister_balance, trap}, middlewares::{authorize, call_context}, }, errors::AuthorizationError, @@ -12,26 +13,21 @@ use crate::{ use ic_cdk_macros::{post_upgrade, query, update}; use lazy_static::lazy_static; use orbit_essentials::api::ApiResult; -use orbit_essentials::http::certified_data_for_skip_certification; use orbit_essentials::with_middleware; use station_api::{ HealthStatus, NotifyFailedStationUpgradeInput, SystemInfoResponse, SystemInstall, SystemUpgrade, }; use std::sync::Arc; -fn set_certified_data_for_skip_certification() { - set_certified_data(&certified_data_for_skip_certification()); -} - // Canister entrypoints for the controller. #[cfg(any(not(feature = "canbench"), test))] #[ic_cdk_macros::init] async fn initialize(input: Option) { - set_certified_data_for_skip_certification(); match input { Some(SystemInstall::Init(input)) => CONTROLLER.initialize(input).await, Some(SystemInstall::Upgrade(_)) | None => trap("Invalid args to initialize canister"), } + certify_metrics(); } /// The init is overriden for benchmarking purposes. @@ -64,12 +60,12 @@ async fn post_upgrade(input: Option) { // datatype from the one that was initially stored. migration::MigrationHandler::run(); - set_certified_data_for_skip_certification(); match input { None => CONTROLLER.post_upgrade(None).await, Some(SystemInstall::Upgrade(input)) => CONTROLLER.post_upgrade(Some(input)).await, Some(SystemInstall::Init(_)) => trap("Invalid args to upgrade canister"), } + certify_metrics(); } #[query(name = "health_status")] diff --git a/core/station/impl/src/core/middlewares.rs b/core/station/impl/src/core/middlewares.rs index 46ca206b2..7f21adbe7 100644 --- a/core/station/impl/src/core/middlewares.rs +++ b/core/station/impl/src/core/middlewares.rs @@ -1,5 +1,6 @@ use super::authorization::Authorization; use super::CallContext; +use crate::controllers::certify_metrics; use crate::core::ic_cdk::api::trap; use crate::models::resource::Resource; use crate::services::SYSTEM_SERVICE; @@ -65,4 +66,5 @@ where .with(&labels! { "status" => status, "method" => called_method }) .inc(); }); + certify_metrics(); } diff --git a/tests/integration/src/http.rs b/tests/integration/src/http.rs index db7a78934..01bd2fb09 100644 --- a/tests/integration/src/http.rs +++ b/tests/integration/src/http.rs @@ -39,12 +39,11 @@ fn test_candid_decoding_quota(env: &PocketIc, canister_id: Principal) { large_http_request_bytes, ) .unwrap_err(); - println!("desc: {}", err.description); assert!(err.description.contains("Decoding cost exceeds the limit")); } #[test] -fn test_http_request_deconding_quota() { +fn test_http_request_decoding_quota() { let TestEnv { env, canister_ids, .. } = setup_new_env(); @@ -53,16 +52,18 @@ fn test_http_request_deconding_quota() { test_candid_decoding_quota(&env, canister_ids.control_panel); } -fn fetch_asset(canister_id: Principal, port: u16, path: &str, expected: &str) { +fn fetch_asset(canister_id: Principal, port: u16, path: &str, expected: Vec<&str>) { let client = reqwest::blocking::Client::new(); let url = format!("http://{}.localhost:{}{}", canister_id, port, path); let res = client.get(url).send().unwrap(); let page = String::from_utf8(res.bytes().unwrap().to_vec()).unwrap(); - assert!(page.contains(expected)); + for exp in expected { + assert!(page.contains(exp)); + } } #[test] -fn test_skip_asset_certification() { +fn test_asset_certification() { let TestEnv { mut env, canister_ids, @@ -75,7 +76,7 @@ fn test_skip_asset_certification() { canister_ids.station, port, "/metrics", - "# HELP station_total_policies The total number of policies that are available.", + vec!["# HELP station_total_users The total number of users that are registered, labeled by their status.", "# HELP station_metrics_timestamp UNIX timestamp in nanoseconds when the metrics were exported"], ); - fetch_asset(canister_ids.control_panel, port, "/metrics", "# HELP control_panel_active_users Total number of active users in the system, labeled by the time interval."); + fetch_asset(canister_ids.control_panel, port, "/metrics", vec!["# HELP control_panel_active_users Total number of active users in the system, labeled by the time interval."]); } From a927f1cff4377787da5ec83126766d47de1b8246 Mon Sep 17 00:00:00 2001 From: Martin Raszyk Date: Thu, 5 Sep 2024 22:23:02 +0200 Subject: [PATCH 2/8] fix tests --- apps/wallet/src/generated/station/station.did | 2 ++ .../src/generated/station/station.did.d.ts | 1 + .../wallet/src/generated/station/station.did.js | 1 + core/station/api/spec.did | 2 ++ core/station/impl/src/controllers/http.rs | 17 ++++++++++++++++- libs/canfund/src/operations/fetch.rs | 3 +++ tests/integration/src/migration_tests.rs | 3 +++ 7 files changed, 28 insertions(+), 1 deletion(-) diff --git a/apps/wallet/src/generated/station/station.did b/apps/wallet/src/generated/station/station.did index 8ff92f846..2e17b531d 100644 --- a/apps/wallet/src/generated/station/station.did +++ b/apps/wallet/src/generated/station/station.did @@ -2565,4 +2565,6 @@ service : (opt SystemInstall) -> { http_request : (HttpRequest) -> (HttpResponse) query; // Internal endpoint used by the upgrader canister to notify the station about a failed station upgrade request. notify_failed_station_upgrade : (NotifyFailedStationUpgradeInput) -> (NotifyFailedStationUpgradeResult); + // no-op endpoint to refresh cycles balance in metrics + ping : () -> (); }; diff --git a/apps/wallet/src/generated/station/station.did.d.ts b/apps/wallet/src/generated/station/station.did.d.ts index 2688a6241..5d59d6890 100644 --- a/apps/wallet/src/generated/station/station.did.d.ts +++ b/apps/wallet/src/generated/station/station.did.d.ts @@ -1237,6 +1237,7 @@ export interface _SERVICE { [NotifyFailedStationUpgradeInput], NotifyFailedStationUpgradeResult >, + 'ping' : ActorMethod<[], undefined>, 'submit_request_approval' : ActorMethod< [SubmitRequestApprovalInput], SubmitRequestApprovalResult diff --git a/apps/wallet/src/generated/station/station.did.js b/apps/wallet/src/generated/station/station.did.js index 79c2da5b0..1bb46e982 100644 --- a/apps/wallet/src/generated/station/station.did.js +++ b/apps/wallet/src/generated/station/station.did.js @@ -1404,6 +1404,7 @@ export const idlFactory = ({ IDL }) => { [NotifyFailedStationUpgradeResult], [], ), + 'ping' : IDL.Func([], [], []), 'submit_request_approval' : IDL.Func( [SubmitRequestApprovalInput], [SubmitRequestApprovalResult], diff --git a/core/station/api/spec.did b/core/station/api/spec.did index 8ff92f846..2e17b531d 100644 --- a/core/station/api/spec.did +++ b/core/station/api/spec.did @@ -2565,4 +2565,6 @@ service : (opt SystemInstall) -> { http_request : (HttpRequest) -> (HttpResponse) query; // Internal endpoint used by the upgrader canister to notify the station about a failed station upgrade request. notify_failed_station_upgrade : (NotifyFailedStationUpgradeInput) -> (NotifyFailedStationUpgradeResult); + // no-op endpoint to refresh cycles balance in metrics + ping : () -> (); }; diff --git a/core/station/impl/src/controllers/http.rs b/core/station/impl/src/controllers/http.rs index 5435479bb..0c164380f 100644 --- a/core/station/impl/src/controllers/http.rs +++ b/core/station/impl/src/controllers/http.rs @@ -1,17 +1,32 @@ use crate::core::ic_cdk::api::{ canister_balance, data_certificate, print, set_certified_data, time, trap, }; +use crate::core::middlewares::use_canister_call_metric; use crate::SERVICE_NAME; use base64::engine::general_purpose::STANDARD as BASE64; use base64::Engine; use ic_asset_certification::{Asset, AssetConfig, AssetEncoding, AssetRouter}; -use ic_cdk_macros::query; +use ic_cdk_macros::{query, update}; use ic_certification::HashTree; use ic_http_certification::{HeaderField, HttpCertificationTree, HttpRequest, HttpResponse}; +use orbit_essentials::api::ApiResult; use orbit_essentials::metrics::with_metrics_registry; +use orbit_essentials::with_middleware; use serde::Serialize; use std::{cell::RefCell, rc::Rc}; +// no-op endpoint to refresh cycles balance in metrics +#[update] +async fn ping() { + let _ = do_ping().await; +} + +// it is important to collect metrics here to refresh cycles balance in metrics +#[with_middleware(tail = use_canister_call_metric("ping", &result))] +async fn do_ping() -> ApiResult<()> { + Ok(()) +} + #[query(decoding_quota = 10000)] fn http_request(req: HttpRequest) -> HttpResponse { serve_asset(&req) diff --git a/libs/canfund/src/operations/fetch.rs b/libs/canfund/src/operations/fetch.rs index 6199bca40..13309b4e7 100644 --- a/libs/canfund/src/operations/fetch.rs +++ b/libs/canfund/src/operations/fetch.rs @@ -114,6 +114,9 @@ impl FetchCyclesBalanceFromPrometheusMetrics { #[async_trait::async_trait] impl FetchCyclesBalance for FetchCyclesBalanceFromPrometheusMetrics { async fn fetch_cycles_balance(&self, canister_id: CanisterId) -> Result { + // ping to refresh cycles balance in metrics + let _ = call::<_, ()>(canister_id, "ping", ((),)).await; + // Send the HTTP request to fetch the prometheus metrics. let response: Result<(HttpResponse,), _> = call( canister_id, diff --git a/tests/integration/src/migration_tests.rs b/tests/integration/src/migration_tests.rs index dfaa93cf2..959581bfc 100644 --- a/tests/integration/src/migration_tests.rs +++ b/tests/integration/src/migration_tests.rs @@ -134,6 +134,9 @@ fn test_canister_migration_path_with_previous_wasm_memory_version() { pocket_ic::common::rest::BlobCompression::Gzip, ); + // execute a round to avoid canister upgrade rate-limiting + env.tick(); + // Then upgrade the canister to trigger the migration path env.upgrade_canister( canister_ids.station, From eae68d10027b1442da3bcdab7ee2dad9f6007d73 Mon Sep 17 00:00:00 2001 From: Martin Raszyk Date: Thu, 5 Sep 2024 22:32:48 +0200 Subject: [PATCH 3/8] comment --- core/station/impl/src/controllers/http.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/station/impl/src/controllers/http.rs b/core/station/impl/src/controllers/http.rs index 0c164380f..97867b129 100644 --- a/core/station/impl/src/controllers/http.rs +++ b/core/station/impl/src/controllers/http.rs @@ -57,7 +57,7 @@ pub fn certify_metrics() { encodings: encodings.clone(), }]; - // 2. Collect all assets from the frontend build directory. + // 2. Collect all assets. let mut assets = Vec::new(); with_metrics_registry(SERVICE_NAME, |registry| { registry From fd13e5207cd3a0ff125b7bdfcc0636339969f8f5 Mon Sep 17 00:00:00 2001 From: Martin Raszyk Date: Fri, 6 Sep 2024 08:38:21 +0200 Subject: [PATCH 4/8] certify assets in control-panel --- Cargo.lock | 2 + core/control-panel/api/spec.did | 2 + core/control-panel/impl/Cargo.toml | 1 + .../impl/src/controllers/canister.rs | 14 +- .../impl/src/controllers/http.rs | 199 ++++++++---------- .../impl/src/core/middlewares.rs | 2 + core/station/impl/src/controllers/http.rs | 118 ++--------- libs/orbit-essentials/Cargo.toml | 1 + libs/orbit-essentials/src/http.rs | 144 +++++++++---- 9 files changed, 218 insertions(+), 265 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 84fa96d2d..bd5afcb45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -876,6 +876,7 @@ dependencies = [ "ic-cdk 0.13.5", "ic-cdk-macros 0.9.0", "ic-cdk-timers", + "ic-http-certification 2.6.0 (git+https://github.com/dfinity/response-verification?rev=eefc99e2a0041ae46873864e6e2f7aa8506a361f)", "ic-stable-structures", "lazy_static", "orbit-essentials", @@ -3140,6 +3141,7 @@ dependencies = [ "convert_case", "getrandom", "hex", + "ic-asset-certification", "ic-cdk 0.13.5", "ic-cdk-timers", "ic-certification 2.6.0 (git+https://github.com/dfinity/response-verification?rev=eefc99e2a0041ae46873864e6e2f7aa8506a361f)", diff --git a/core/control-panel/api/spec.did b/core/control-panel/api/spec.did index 5c08f3e7e..0b75583fb 100644 --- a/core/control-panel/api/spec.did +++ b/core/control-panel/api/spec.did @@ -668,4 +668,6 @@ service : () -> { can_deploy_station : () -> (CanDeployStationResult) query; // HTTP Protocol interface. http_request : (HttpRequest) -> (HttpResponse) query; + // no-op endpoint to refresh cycles balance in metrics + ping : () -> (); }; diff --git a/core/control-panel/impl/Cargo.toml b/core/control-panel/impl/Cargo.toml index 58224a280..a66178ef2 100644 --- a/core/control-panel/impl/Cargo.toml +++ b/core/control-panel/impl/Cargo.toml @@ -21,6 +21,7 @@ hex = { workspace = true } ic-cdk = { workspace = true } ic-cdk-macros = { workspace = true } ic-cdk-timers = { workspace = true } +ic-http-certification = { workspace = true } ic-stable-structures = { workspace = true } lazy_static = { workspace = true } serde = { workspace = true, features = ['derive'] } diff --git a/core/control-panel/impl/src/controllers/canister.rs b/core/control-panel/impl/src/controllers/canister.rs index 3fa6336dd..be7b454a0 100644 --- a/core/control-panel/impl/src/controllers/canister.rs +++ b/core/control-panel/impl/src/controllers/canister.rs @@ -1,6 +1,7 @@ //! Canister lifecycle hooks. use super::AVAILABLE_TOKENS_USER_REGISTRATION; -use crate::core::ic_cdk::{api::set_certified_data, spawn}; +use crate::controllers::http::certify_metrics; +use crate::core::ic_cdk::spawn; use crate::core::metrics::recompute_all_metrics; use crate::services::CANISTER_SERVICE; use control_panel_api::UploadCanisterModulesInput; @@ -8,7 +9,6 @@ use ic_cdk_macros::{init, post_upgrade}; use ic_cdk_timers::{set_timer, set_timer_interval}; use orbit_essentials::api::ApiResult; use orbit_essentials::cdk::update; -use orbit_essentials::http::certified_data_for_skip_certification; use std::time::Duration; pub const MINUTE: u64 = 60; @@ -23,10 +23,6 @@ async fn upload_canister_modules(input: UploadCanisterModulesInput) -> ApiResult CANISTER_SERVICE.upload_canister_modules(input).await } -fn set_certified_data_for_skip_certification() { - set_certified_data(&certified_data_for_skip_certification()); -} - fn init_timers_fn() { async fn initialize_rng_timer() { use orbit_essentials::utils::initialize_rng; @@ -58,18 +54,18 @@ fn init_timers_fn() { #[init] async fn initialize() { - set_certified_data_for_skip_certification(); init_timers_fn(); CANISTER_SERVICE .init_canister() .await .expect("failed to initialize canister"); + + certify_metrics(); } #[post_upgrade] async fn post_upgrade() { - set_certified_data_for_skip_certification(); recompute_all_metrics(); init_timers_fn(); @@ -77,4 +73,6 @@ async fn post_upgrade() { .init_canister() .await .expect("failed to upgrade canister"); + + certify_metrics(); } diff --git a/core/control-panel/impl/src/controllers/http.rs b/core/control-panel/impl/src/controllers/http.rs index f49eaaba7..3f7f44ef7 100644 --- a/core/control-panel/impl/src/controllers/http.rs +++ b/core/control-panel/impl/src/controllers/http.rs @@ -1,115 +1,97 @@ use crate::core::metrics::METRIC_ACTIVE_USERS; -use crate::services::{UserService, USER_SERVICE}; +use crate::core::middlewares::use_canister_call_metric; +use crate::services::USER_SERVICE; use crate::{ - core::ic_cdk::api::{canister_balance, time}, + core::ic_cdk::api::{ + canister_balance, data_certificate, print, set_certified_data, time, trap, + }, SERVICE_NAME, }; -use ic_cdk_macros::query; -use lazy_static::lazy_static; -use orbit_essentials::api::{HeaderField, HttpRequest, HttpResponse}; -use orbit_essentials::http::add_skip_certification_headers; +use ic_cdk_macros::{query, update}; +use ic_http_certification::{HttpRequest, HttpResponse}; +use orbit_essentials::api::ApiResult; +use orbit_essentials::http::{certify_assets, serve_asset}; use orbit_essentials::metrics::with_metrics_registry; -use std::sync::Arc; +use orbit_essentials::with_middleware; -// Canister entrypoints for the controller. -#[query(name = "http_request", decoding_quota = 10000)] -async fn http_request(request: HttpRequest) -> HttpResponse { - let mut resp = CONTROLLER.router(request).await; - add_skip_certification_headers(&mut resp); - resp +// no-op endpoint to refresh cycles balance in metrics +#[update] +async fn ping() { + let _ = do_ping().await; } -// Controller initialization and implementation. -lazy_static! { - static ref CONTROLLER: HttpController = HttpController::new(Arc::clone(&USER_SERVICE)); +// it is important to collect metrics here to refresh cycles balance in metrics +#[with_middleware(tail = use_canister_call_metric("ping", &result))] +async fn do_ping() -> ApiResult<()> { + Ok(()) } -#[derive(Debug)] -pub struct HttpController { - user_service: Arc, -} - -impl HttpController { - fn new(user_service: Arc) -> Self { - Self { user_service } +#[query(decoding_quota = 10000)] +fn http_request(req: HttpRequest) -> HttpResponse { + let res = serve_asset(&req, data_certificate()); + match res { + Ok(response) => response, + Err(err) => trap(err), } +} - async fn router(&self, request: HttpRequest) -> HttpResponse { - if request.url == "/metrics" || request.url == "/metrics/" { - return self.metrics(request).await; +// Certification +pub fn certify_metrics() { + // Trigger active users metric update. + METRIC_ACTIVE_USERS.with(|metric| metric.borrow_mut().refresh(time())); + + // Add dynamic metrics, dropped after the request since query calls don't save state changes. + with_metrics_registry(SERVICE_NAME, |registry| { + registry + .gauge_mut( + "canister_cycles_balance", + "cycles balance available to the canister", + ) + .set(canister_balance() as f64); + }); + with_metrics_registry(SERVICE_NAME, |registry| { + registry + .gauge_mut( + "metrics_timestamp", + "UNIX timestamp in nanoseconds when the metrics were exported", + ) + .set(time() as f64); + }); + let metrics_contents = + with_metrics_registry(SERVICE_NAME, |registry| registry.export_metrics()); + let res = certify_assets(vec![ + ( + "/metrics".to_string(), + metrics_contents.unwrap_or_else(|e| e.to_string().as_bytes().to_vec()), + ), + ("/metrics/sd".to_string(), metrics_service_discovery()), + ]); + match res { + Ok(certified_data) => { + set_certified_data(&certified_data); } - - if request.url == "/metrics/sd" || request.url == "/metrics/sd/" { - return self.metrics_service_discovery(request).await; + Err(err) => { + print(err); } - - return HttpResponse { - status_code: 404, - headers: vec![HeaderField("Content-Type".into(), "text/plain".into())], - body: "404 Not Found".as_bytes().to_owned(), - }; } +} - /// Returns all deployed station hosts for Prometheus service discovery. - /// - /// As defined by https://prometheus.io/docs/prometheus/latest/configuration/configuration/#http_sd_config - async fn metrics_service_discovery(&self, request: HttpRequest) -> HttpResponse { - if request.method.to_lowercase() != "get" { - return HttpResponse { - status_code: 405, - headers: vec![HeaderField("Allow".into(), "GET".into())], - body: "405 Method Not Allowed".as_bytes().to_owned(), - }; - } - - let station_hosts = self - .user_service - .get_all_deployed_stations() - .iter() - .map(|station| format!("{}.raw.icp0.io", station.to_text())) - .collect::>(); - - let body = format!( - r#"[{{"targets": ["{}"],"labels": {{"__metrics_path__":"/metrics","dapp":"orbit"}}}}]"#, - station_hosts.join("\", \"") - ); - - HttpResponse { - status_code: 200, - headers: vec![HeaderField( - "Content-Type".into(), - "application/json".into(), - )], - body: body.as_bytes().to_owned(), - } - } - - async fn metrics(&self, request: HttpRequest) -> HttpResponse { - if request.method.to_lowercase() != "get" { - return HttpResponse { - status_code: 405, - headers: vec![HeaderField("Allow".into(), "GET".into())], - body: "405 Method Not Allowed".as_bytes().to_owned(), - }; - } - - // Trigger active users metric update. - METRIC_ACTIVE_USERS.with(|metric| metric.borrow_mut().refresh(time())); - - // Add dynamic metrics, dropped after the request since query calls don't save state changes. - with_metrics_registry(SERVICE_NAME, |registry| { - registry - .gauge_mut( - "canister_cycles_balance", - "cycles balance available to the canister", - ) - .set(canister_balance() as f64); - }); - - with_metrics_registry(SERVICE_NAME, |registry| { - registry.export_metrics_as_http_response() - }) - } +/// Returns all deployed station hosts for Prometheus service discovery. +/// +/// As defined by https://prometheus.io/docs/prometheus/latest/configuration/configuration/#http_sd_config +fn metrics_service_discovery() -> Vec { + let station_hosts = USER_SERVICE + .get_all_deployed_stations() + .iter() + .map(|station| format!("{}.raw.icp0.io", station.to_text())) + .collect::>(); + + format!( + r#"[{{"targets": ["{}"],"labels": {{"__metrics_path__":"/metrics","dapp":"orbit"}}}}]"#, + station_hosts.join("\", \"") + ) + .as_bytes() + .to_owned() } #[cfg(test)] @@ -119,35 +101,18 @@ mod tests { use candid::Principal; use orbit_essentials::repository::Repository; - #[tokio::test] - async fn test_service_discovery() { + #[test] + fn test_service_discovery() { let mut user = mock_user(); user.deployed_stations = vec![Principal::from_slice(&[0; 29])]; let station_host = format!("{}.raw.icp0.io", user.deployed_stations[0].to_text()); USER_REPOSITORY.insert(user.to_key(), user.clone()); - let controller = HttpController::new(Arc::new(UserService::default())); - - let request = HttpRequest { - method: "GET".into(), - url: "/metrics/sd".into(), - headers: vec![], - body: vec![], - }; + let response = metrics_service_discovery(); - let response = controller.metrics_service_discovery(request).await; - - assert_eq!(response.status_code, 200); - assert_eq!( - response.headers, - vec![HeaderField( - "Content-Type".into(), - "application/json".into() - )] - ); assert_eq!( - response.body, + response, format!( r#"[{{"targets": ["{}"],"labels": {{"__metrics_path__":"/metrics","dapp":"orbit"}}}}]"#, station_host diff --git a/core/control-panel/impl/src/core/middlewares.rs b/core/control-panel/impl/src/core/middlewares.rs index 85299e2a1..7797836a7 100644 --- a/core/control-panel/impl/src/core/middlewares.rs +++ b/core/control-panel/impl/src/core/middlewares.rs @@ -1,4 +1,5 @@ use super::CallContext; +use crate::controllers::certify_metrics; use crate::{core::ic_cdk, SERVICE_NAME}; use orbit_essentials::{ api::ApiResult, @@ -67,6 +68,7 @@ where .with(&labels! { "status" => status, "method" => called_method }) .inc(); }); + certify_metrics(); } /// Trap the execution of the canister call if the caller is not an authorized admin diff --git a/core/station/impl/src/controllers/http.rs b/core/station/impl/src/controllers/http.rs index 97867b129..b46183e4c 100644 --- a/core/station/impl/src/controllers/http.rs +++ b/core/station/impl/src/controllers/http.rs @@ -3,17 +3,12 @@ use crate::core::ic_cdk::api::{ }; use crate::core::middlewares::use_canister_call_metric; use crate::SERVICE_NAME; -use base64::engine::general_purpose::STANDARD as BASE64; -use base64::Engine; -use ic_asset_certification::{Asset, AssetConfig, AssetEncoding, AssetRouter}; use ic_cdk_macros::{query, update}; -use ic_certification::HashTree; -use ic_http_certification::{HeaderField, HttpCertificationTree, HttpRequest, HttpResponse}; +use ic_http_certification::{HttpRequest, HttpResponse}; use orbit_essentials::api::ApiResult; +use orbit_essentials::http::{certify_assets, serve_asset}; use orbit_essentials::metrics::with_metrics_registry; use orbit_essentials::with_middleware; -use serde::Serialize; -use std::{cell::RefCell, rc::Rc}; // no-op endpoint to refresh cycles balance in metrics #[update] @@ -29,36 +24,15 @@ async fn do_ping() -> ApiResult<()> { #[query(decoding_quota = 10000)] fn http_request(req: HttpRequest) -> HttpResponse { - serve_asset(&req) -} - -thread_local! { - static HTTP_TREE: Rc> = Default::default(); - static ASSET_ROUTER: RefCell> = RefCell::new(AssetRouter::with_tree(HTTP_TREE.with(|tree| tree.clone()))); + let res = serve_asset(&req, data_certificate()); + match res { + Ok(response) => response, + Err(err) => trap(err), + } } // Certification pub fn certify_metrics() { - // 1. Define the asset certification configurations. - let encodings = vec![ - AssetEncoding::Brotli.default_config(), - AssetEncoding::Gzip.default_config(), - ]; - - let asset_configs = vec![AssetConfig::File { - path: "metrics".to_string(), - content_type: Some("text/plain".to_string()), - headers: get_asset_headers(vec![( - "cache-control".to_string(), - "public, no-cache, no-store".to_string(), - )]), - fallback_for: vec![], - aliased_by: vec![], - encodings: encodings.clone(), - }]; - - // 2. Collect all assets. - let mut assets = Vec::new(); with_metrics_registry(SERVICE_NAME, |registry| { registry .gauge_mut( @@ -77,78 +51,16 @@ pub fn certify_metrics() { }); let metrics_contents = with_metrics_registry(SERVICE_NAME, |registry| registry.export_metrics()); - assets.push(Asset::new( - "/metrics", + let res = certify_assets(vec![( + "/metrics".to_string(), metrics_contents.unwrap_or_else(|e| e.to_string().as_bytes().to_vec()), - )); - - ASSET_ROUTER.with_borrow_mut(|asset_router| { - // 3. Certify the assets using the `certify_assets` function from the `ic-asset-certification` crate. - if let Err(err) = asset_router.certify_assets(assets, asset_configs) { - print(format!("Failed to certify assets: {}", err)); - } else { - // 4. Set the canister's certified data. - set_certified_data(&asset_router.root_hash()); + )]); + match res { + Ok(certified_data) => { + set_certified_data(&certified_data); } - }); -} - -// Handlers -fn serve_asset(req: &HttpRequest) -> HttpResponse<'static> { - ASSET_ROUTER.with_borrow(|asset_router| { - if let Ok((mut response, witness, expr_path)) = asset_router.serve_asset(req) { - add_certificate_header(&mut response, &witness, &expr_path); - - response - } else { - trap("Failed to serve asset"); + Err(err) => { + print(err); } - }) -} - -fn get_asset_headers(additional_headers: Vec) -> Vec { - // set up the default headers and include additional headers provided by the caller - let mut headers = vec![ - ("strict-transport-security".to_string(), "max-age=31536000; includeSubDomains".to_string()), - ("x-frame-options".to_string(), "DENY".to_string()), - ("x-content-type-options".to_string(), "nosniff".to_string()), - ("content-security-policy".to_string(), "default-src 'self'; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content".to_string()), - ("referrer-policy".to_string(), "no-referrer".to_string()), - ("permissions-policy".to_string(), "accelerometer=(),ambient-light-sensor=(),autoplay=(),battery=(),camera=(),display-capture=(),document-domain=(),encrypted-media=(),fullscreen=(),gamepad=(),geolocation=(),gyroscope=(),layout-animations=(self),legacy-image-formats=(self),magnetometer=(),microphone=(),midi=(),oversized-images=(self),payment=(),picture-in-picture=(),publickey-credentials-get=(),speaker-selection=(),sync-xhr=(self),unoptimized-images=(self),unsized-media=(self),usb=(),screen-wake-lock=(),web-share=(),xr-spatial-tracking=()".to_string()), - ("cross-origin-embedder-policy".to_string(), "require-corp".to_string()), - ("cross-origin-opener-policy".to_string(), "same-origin".to_string()), - ]; - headers.extend(additional_headers); - - headers -} - -const IC_CERTIFICATE_HEADER: &str = "IC-Certificate"; -fn add_certificate_header(response: &mut HttpResponse, witness: &HashTree, expr_path: &[String]) { - if let Some(certified_data) = data_certificate() { - let witness = cbor_encode(witness); - let expr_path = cbor_encode(&expr_path); - - response.add_header(( - IC_CERTIFICATE_HEADER.to_string(), - format!( - "certificate=:{}:, tree=:{}:, expr_path=:{}:, version=2", - BASE64.encode(certified_data), - BASE64.encode(witness), - BASE64.encode(expr_path) - ), - )); } } - -// Encoding -fn cbor_encode(value: &impl Serialize) -> Vec { - let mut serializer = serde_cbor::Serializer::new(Vec::new()); - serializer - .self_describe() - .expect("Failed to self describe CBOR"); - value - .serialize(&mut serializer) - .expect("Failed to serialize value"); - serializer.into_inner() -} diff --git a/libs/orbit-essentials/Cargo.toml b/libs/orbit-essentials/Cargo.toml index bea8b3c48..4c75c28dd 100644 --- a/libs/orbit-essentials/Cargo.toml +++ b/libs/orbit-essentials/Cargo.toml @@ -18,6 +18,7 @@ base64 = { workspace = true } candid = { workspace = true } convert_case = { workspace = true } getrandom = { workspace = true, features = ['custom'] } +ic-asset-certification = { workspace = true } ic-cdk = { workspace = true } ic-certification = { workspace = true } ic-http-certification = { workspace = true } diff --git a/libs/orbit-essentials/src/http.rs b/libs/orbit-essentials/src/http.rs index 7904d126d..9cd279a53 100644 --- a/libs/orbit-essentials/src/http.rs +++ b/libs/orbit-essentials/src/http.rs @@ -1,40 +1,62 @@ -use crate::api::{HeaderField, HttpResponse}; -use crate::cdk::api::data_certificate; use base64::engine::general_purpose::STANDARD as BASE64; use base64::Engine; -use ic_certification::{labeled, leaf, HashTree}; -use ic_http_certification::DefaultCelBuilder; -use ic_representation_independent_hash::hash; +use ic_asset_certification::{Asset, AssetConfig, AssetEncoding, AssetRouter}; +use ic_certification::HashTree; +use ic_http_certification::{HeaderField, HttpCertificationTree, HttpRequest, HttpResponse}; use serde::Serialize; - -// Certify that frontend asset certification is skipped for this canister. +use std::cell::RefCell; +use std::rc::Rc; const IC_CERTIFICATE_HEADER: &str = "IC-Certificate"; -const IC_CERTIFICATE_EXPRESSION_HEADER: &str = "IC-CertificateExpression"; -fn skip_certification_cel_expr() -> String { - DefaultCelBuilder::skip_certification().to_string() +// Helper functions + +pub fn cbor_encode(value: &impl Serialize) -> Vec { + let mut serializer = serde_cbor::Serializer::new(Vec::new()); + serializer + .self_describe() + .expect("Failed to self describe CBOR"); + value + .serialize(&mut serializer) + .expect("Failed to serialize value"); + serializer.into_inner() +} + +// Certify static frontend assets + +thread_local! { + static HTTP_TREE: Rc> = Default::default(); + static ASSET_ROUTER: RefCell> = RefCell::new(AssetRouter::with_tree(HTTP_TREE.with(|tree| tree.clone()))); } -fn skip_certification_asset_tree() -> HashTree { - let cel_expr_hash = hash(skip_certification_cel_expr().as_bytes()); - labeled( - "http_expr", - labeled("<*>", labeled(cel_expr_hash, leaf(vec![]))), - ) +fn get_asset_headers(additional_headers: Vec) -> Vec { + // set up the default headers and include additional headers provided by the caller + let mut headers = vec![ + ("strict-transport-security".to_string(), "max-age=31536000; includeSubDomains".to_string()), + ("x-frame-options".to_string(), "DENY".to_string()), + ("x-content-type-options".to_string(), "nosniff".to_string()), + ("content-security-policy".to_string(), "default-src 'self'; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content".to_string()), + ("referrer-policy".to_string(), "no-referrer".to_string()), + ("permissions-policy".to_string(), "accelerometer=(),ambient-light-sensor=(),autoplay=(),battery=(),camera=(),display-capture=(),document-domain=(),encrypted-media=(),fullscreen=(),gamepad=(),geolocation=(),gyroscope=(),layout-animations=(self),legacy-image-formats=(self),magnetometer=(),microphone=(),midi=(),oversized-images=(self),payment=(),picture-in-picture=(),publickey-credentials-get=(),speaker-selection=(),sync-xhr=(self),unoptimized-images=(self),unsized-media=(self),usb=(),screen-wake-lock=(),web-share=(),xr-spatial-tracking=()".to_string()), + ("cross-origin-embedder-policy".to_string(), "require-corp".to_string()), + ("cross-origin-opener-policy".to_string(), "same-origin".to_string()), + ]; + headers.extend(additional_headers); + + headers } -pub fn add_skip_certification_headers(response: &mut HttpResponse) { - if let Some(certified_data) = data_certificate() { - let witness = cbor_encode(&skip_certification_asset_tree()); - let expr_path = ["http_expr", "<*>"]; +fn add_certificate_header( + response: &mut HttpResponse, + data_certificate: Option>, + witness: &HashTree, + expr_path: &[String], +) { + if let Some(certified_data) = data_certificate { + let witness = cbor_encode(witness); let expr_path = cbor_encode(&expr_path); - response.headers.push(HeaderField( - IC_CERTIFICATE_EXPRESSION_HEADER.to_string(), - skip_certification_cel_expr(), - )); - response.headers.push(HeaderField( + response.add_header(( IC_CERTIFICATE_HEADER.to_string(), format!( "certificate=:{}:, tree=:{}:, expr_path=:{}:, version=2", @@ -46,18 +68,66 @@ pub fn add_skip_certification_headers(response: &mut HttpResponse) { } } -// Encoding -fn cbor_encode(value: &impl Serialize) -> Vec { - let mut serializer = serde_cbor::Serializer::new(Vec::new()); - serializer - .self_describe() - .expect("Failed to self describe CBOR"); - value - .serialize(&mut serializer) - .expect("Failed to serialize value"); - serializer.into_inner() +pub fn certify_assets(static_assets: Vec<(String, Vec)>) -> Result, String> { + // 1. Define the asset certification configurations. + let encodings = vec![ + AssetEncoding::Brotli.default_config(), + AssetEncoding::Gzip.default_config(), + ]; + + let asset_configs = vec![ + AssetConfig::File { + path: "/metrics".to_string(), + content_type: Some("text/plain".to_string()), + headers: get_asset_headers(vec![( + "cache-control".to_string(), + "public, no-cache, no-store".to_string(), + )]), + fallback_for: vec![], + aliased_by: vec![], + encodings: encodings.clone(), + }, + AssetConfig::File { + path: "/metrics/sd".to_string(), + content_type: Some("application/json".to_string()), + headers: get_asset_headers(vec![( + "cache-control".to_string(), + "public, no-cache, no-store".to_string(), + )]), + fallback_for: vec![], + aliased_by: vec![], + encodings: encodings.clone(), + }, + ]; + + // 2. Collect all assets. + let mut assets = Vec::new(); + for (path, contents) in static_assets { + assets.push(Asset::new(path, contents)); + } + + ASSET_ROUTER.with_borrow_mut(|asset_router| { + // 3. Certify the assets using the `certify_assets` function from the `ic-asset-certification` crate. + if let Err(err) = asset_router.certify_assets(assets, asset_configs) { + Err(format!("Failed to certify assets: {}", err)) + } else { + // 4. Return the canister's certified data to be set. + Ok(asset_router.root_hash().to_vec()) + } + }) } -pub fn certified_data_for_skip_certification() -> [u8; 32] { - skip_certification_asset_tree().digest() +pub fn serve_asset( + req: &HttpRequest, + data_certificate: Option>, +) -> Result, &'static str> { + ASSET_ROUTER.with_borrow(|asset_router| { + if let Ok((mut response, witness, expr_path)) = asset_router.serve_asset(req) { + add_certificate_header(&mut response, data_certificate, &witness, &expr_path); + + Ok(response) + } else { + Err("Failed to serve asset") + } + }) } From 5f3d2c608960aee97cb9a024ad6862a4e5cd58a0 Mon Sep 17 00:00:00 2001 From: Martin Raszyk Date: Fri, 6 Sep 2024 09:20:01 +0200 Subject: [PATCH 5/8] tests --- libs/orbit-essentials/src/http.rs | 16 ++++++++++++++- tests/integration/src/http.rs | 33 +++++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/libs/orbit-essentials/src/http.rs b/libs/orbit-essentials/src/http.rs index 9cd279a53..50c99e452 100644 --- a/libs/orbit-essentials/src/http.rs +++ b/libs/orbit-essentials/src/http.rs @@ -1,6 +1,6 @@ use base64::engine::general_purpose::STANDARD as BASE64; use base64::Engine; -use ic_asset_certification::{Asset, AssetConfig, AssetEncoding, AssetRouter}; +use ic_asset_certification::{Asset, AssetConfig, AssetEncoding, AssetFallbackConfig, AssetRouter}; use ic_certification::HashTree; use ic_http_certification::{HeaderField, HttpCertificationTree, HttpRequest, HttpResponse}; use serde::Serialize; @@ -98,10 +98,24 @@ pub fn certify_assets(static_assets: Vec<(String, Vec)>) -> Result, aliased_by: vec![], encodings: encodings.clone(), }, + AssetConfig::File { + path: "/".to_string(), + content_type: Some("text/plain".to_string()), + headers: get_asset_headers(vec![( + "cache-control".to_string(), + "public, no-cache, no-store".to_string(), + )]), + fallback_for: vec![AssetFallbackConfig { + scope: "/".to_string(), + }], + aliased_by: vec![], + encodings: encodings.clone(), + }, ]; // 2. Collect all assets. let mut assets = Vec::new(); + assets.push(Asset::new("/", "404 Not Found".as_bytes().to_owned())); for (path, contents) in static_assets { assets.push(Asset::new(path, contents)); } diff --git a/tests/integration/src/http.rs b/tests/integration/src/http.rs index 01bd2fb09..71f6146f1 100644 --- a/tests/integration/src/http.rs +++ b/tests/integration/src/http.rs @@ -52,13 +52,22 @@ fn test_http_request_decoding_quota() { test_candid_decoding_quota(&env, canister_ids.control_panel); } -fn fetch_asset(canister_id: Principal, port: u16, path: &str, expected: Vec<&str>) { +fn fetch_asset( + canister_id: Principal, + port: u16, + path: &str, + expected_headers: Vec<(&str, &str)>, + expected_body_chunks: Vec<&str>, +) { let client = reqwest::blocking::Client::new(); let url = format!("http://{}.localhost:{}{}", canister_id, port, path); let res = client.get(url).send().unwrap(); + for (name, value) in expected_headers { + assert_eq!(res.headers().get(name).unwrap(), value); + } let page = String::from_utf8(res.bytes().unwrap().to_vec()).unwrap(); - for exp in expected { - assert!(page.contains(exp)); + for chunk in expected_body_chunks { + assert!(page.contains(chunk)); } } @@ -72,11 +81,27 @@ fn test_asset_certification() { let port = env.make_live(None).port_or_known_default().unwrap(); + fetch_asset( + canister_ids.station, + port, + "/foo", + vec![("content-type", "text/plain")], + vec!["404 Not Found"], + ); fetch_asset( canister_ids.station, port, "/metrics", + vec![("content-type", "text/plain")], vec!["# HELP station_total_users The total number of users that are registered, labeled by their status.", "# HELP station_metrics_timestamp UNIX timestamp in nanoseconds when the metrics were exported"], ); - fetch_asset(canister_ids.control_panel, port, "/metrics", vec!["# HELP control_panel_active_users Total number of active users in the system, labeled by the time interval."]); + fetch_asset( + canister_ids.control_panel, + port, + "/foo", + vec![("content-type", "text/plain")], + vec!["404 Not Found"], + ); + fetch_asset(canister_ids.control_panel, port, "/metrics", vec![("content-type", "text/plain")], vec!["# HELP control_panel_active_users Total number of active users in the system, labeled by the time interval."]); + fetch_asset(canister_ids.control_panel, port, "/metrics/sd", vec![("content-type", "application/json")], vec!["[{\"targets\": [\"\"],\"labels\": {\"__metrics_path__\":\"/metrics\",\"dapp\":\"orbit\"}}]"]); } From 752d0177125bedef7c6f4e526f45dcc902d857df Mon Sep 17 00:00:00 2001 From: Martin Raszyk Date: Fri, 6 Sep 2024 10:16:53 +0200 Subject: [PATCH 6/8] deps --- Cargo.lock | 4 ---- Cargo.toml | 1 - core/station/impl/Cargo.toml | 3 --- libs/orbit-essentials/Cargo.toml | 1 - 4 files changed, 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d1b88b13d..0798bcf28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3146,7 +3146,6 @@ dependencies = [ "ic-cdk-timers", "ic-certification 2.6.0 (git+https://github.com/dfinity/response-verification?rev=eefc99e2a0041ae46873864e6e2f7aa8506a361f)", "ic-http-certification 2.6.0 (git+https://github.com/dfinity/response-verification?rev=eefc99e2a0041ae46873864e6e2f7aa8506a361f)", - "ic-representation-independent-hash 2.6.0 (git+https://github.com/dfinity/response-verification?rev=eefc99e2a0041ae46873864e6e2f7aa8506a361f)", "ic-stable-structures", "orbit-essentials-macros", "prometheus", @@ -4571,7 +4570,6 @@ version = "0.0.2-alpha.6" dependencies = [ "anyhow", "async-trait", - "base64 0.22.1", "byteorder", "canbench-rs", "candid", @@ -4580,10 +4578,8 @@ dependencies = [ "deunicode", "futures", "hex", - "ic-asset-certification", "ic-cdk 0.13.5", "ic-cdk-macros 0.9.0", - "ic-certification 2.6.0 (git+https://github.com/dfinity/response-verification?rev=eefc99e2a0041ae46873864e6e2f7aa8506a361f)", "ic-http-certification 2.6.0 (git+https://github.com/dfinity/response-verification?rev=eefc99e2a0041ae46873864e6e2f7aa8506a361f)", "ic-ledger-types", "ic-stable-structures", diff --git a/Cargo.toml b/Cargo.toml index c8014e700..2ed9d2d55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,6 @@ ic-asset-certification = { git = "https://github.com/dfinity/response-verificati ic-certification = { git = "https://github.com/dfinity/response-verification", rev = "eefc99e2a0041ae46873864e6e2f7aa8506a361f" } ic-certified-assets = { git = "https://github.com/dfinity/sdk.git", rev = "75c080ebae22a70578c06ddf1eda0b18ef091845" } ic-http-certification = { git = "https://github.com/dfinity/response-verification", rev = "eefc99e2a0041ae46873864e6e2f7aa8506a361f" } -ic-representation-independent-hash = { git = "https://github.com/dfinity/response-verification", rev = "eefc99e2a0041ae46873864e6e2f7aa8506a361f" } ic-cdk = "0.13.2" ic-cdk-macros = "0.9" ic-cdk-timers = "0.7.0" diff --git a/core/station/impl/Cargo.toml b/core/station/impl/Cargo.toml index 10a28fb85..6769b1d6f 100644 --- a/core/station/impl/Cargo.toml +++ b/core/station/impl/Cargo.toml @@ -18,7 +18,6 @@ canbench = ['canbench-rs'] [dependencies] anyhow = { workspace = true } -base64 = { workspace = true } deunicode = { workspace = true } async-trait = { workspace = true } byteorder = { workspace = true } @@ -28,10 +27,8 @@ canfund = { path = '../../../libs/canfund', version = '0.0.2-alpha.3' } futures = { workspace = true } hex = { workspace = true } orbit-essentials = { path = '../../../libs/orbit-essentials', version = '0.0.2-alpha.4' } -ic-asset-certification = { workspace = true } ic-cdk = { workspace = true } ic-cdk-macros = { workspace = true } -ic-certification = { workspace = true } ic-http-certification = { workspace = true } ic-ledger-types = { workspace = true } ic-stable-structures = { workspace = true } diff --git a/libs/orbit-essentials/Cargo.toml b/libs/orbit-essentials/Cargo.toml index 4c75c28dd..d2f5c349a 100644 --- a/libs/orbit-essentials/Cargo.toml +++ b/libs/orbit-essentials/Cargo.toml @@ -22,7 +22,6 @@ ic-asset-certification = { workspace = true } ic-cdk = { workspace = true } ic-certification = { workspace = true } ic-http-certification = { workspace = true } -ic-representation-independent-hash = { workspace = true } ic-stable-structures = { workspace = true } orbit-essentials-macros = { path = '../orbit-essentials-macros', version = '0.0.2-alpha.3' } prometheus = { workspace = true } From a30c2891b76e5effd837a7326aacc987868c51c7 Mon Sep 17 00:00:00 2001 From: Martin Raszyk Date: Fri, 6 Sep 2024 10:17:42 +0200 Subject: [PATCH 7/8] dfx generate --- apps/wallet/src/generated/control-panel/control_panel.did | 2 ++ apps/wallet/src/generated/control-panel/control_panel.did.d.ts | 1 + apps/wallet/src/generated/control-panel/control_panel.did.js | 1 + 3 files changed, 4 insertions(+) diff --git a/apps/wallet/src/generated/control-panel/control_panel.did b/apps/wallet/src/generated/control-panel/control_panel.did index 6fd377537..34837a277 100644 --- a/apps/wallet/src/generated/control-panel/control_panel.did +++ b/apps/wallet/src/generated/control-panel/control_panel.did @@ -668,4 +668,6 @@ service : () -> { can_deploy_station : () -> (CanDeployStationResult) query; // HTTP Protocol interface. http_request : (HttpRequest) -> (HttpResponse) query; + // no-op endpoint to refresh cycles balance in metrics + ping : () -> (); }; diff --git a/apps/wallet/src/generated/control-panel/control_panel.did.d.ts b/apps/wallet/src/generated/control-panel/control_panel.did.d.ts index 21ee568d4..db413ec4e 100644 --- a/apps/wallet/src/generated/control-panel/control_panel.did.d.ts +++ b/apps/wallet/src/generated/control-panel/control_panel.did.d.ts @@ -249,6 +249,7 @@ export interface _SERVICE { [NextWasmModuleVersionInput], NextWasmModuleVersionResult >, + 'ping' : ActorMethod<[], undefined>, 'register_user' : ActorMethod<[RegisterUserInput], RegisterUserResult>, 'search_registry' : ActorMethod<[SearchRegistryInput], SearchRegistryResult>, 'set_user_active' : ActorMethod<[], SetUserActiveResult>, diff --git a/apps/wallet/src/generated/control-panel/control_panel.did.js b/apps/wallet/src/generated/control-panel/control_panel.did.js index 6d27b6f55..3c337af6e 100644 --- a/apps/wallet/src/generated/control-panel/control_panel.did.js +++ b/apps/wallet/src/generated/control-panel/control_panel.did.js @@ -304,6 +304,7 @@ export const idlFactory = ({ IDL }) => { [NextWasmModuleVersionResult], ['query'], ), + 'ping' : IDL.Func([], [], []), 'register_user' : IDL.Func([RegisterUserInput], [RegisterUserResult], []), 'search_registry' : IDL.Func( [SearchRegistryInput], From 803df267b4a953d7c5e204e90e2dc4e8c25fd3a8 Mon Sep 17 00:00:00 2001 From: Martin Raszyk Date: Wed, 11 Sep 2024 22:33:04 +0200 Subject: [PATCH 8/8] no ping endpoint --- .../generated/control-panel/control_panel.did | 2 - .../control-panel/control_panel.did.d.ts | 1 - .../control-panel/control_panel.did.js | 1 - apps/wallet/src/generated/station/station.did | 2 - .../src/generated/station/station.did.d.ts | 1 - .../src/generated/station/station.did.js | 1 - core/control-panel/api/spec.did | 2 - .../impl/src/controllers/http.rs | 39 +++++++++---------- core/station/api/spec.did | 2 - core/station/impl/src/controllers/http.rs | 39 +++++++++---------- libs/canfund/src/operations/fetch.rs | 3 -- 11 files changed, 36 insertions(+), 57 deletions(-) diff --git a/apps/wallet/src/generated/control-panel/control_panel.did b/apps/wallet/src/generated/control-panel/control_panel.did index 34837a277..6fd377537 100644 --- a/apps/wallet/src/generated/control-panel/control_panel.did +++ b/apps/wallet/src/generated/control-panel/control_panel.did @@ -668,6 +668,4 @@ service : () -> { can_deploy_station : () -> (CanDeployStationResult) query; // HTTP Protocol interface. http_request : (HttpRequest) -> (HttpResponse) query; - // no-op endpoint to refresh cycles balance in metrics - ping : () -> (); }; diff --git a/apps/wallet/src/generated/control-panel/control_panel.did.d.ts b/apps/wallet/src/generated/control-panel/control_panel.did.d.ts index db413ec4e..21ee568d4 100644 --- a/apps/wallet/src/generated/control-panel/control_panel.did.d.ts +++ b/apps/wallet/src/generated/control-panel/control_panel.did.d.ts @@ -249,7 +249,6 @@ export interface _SERVICE { [NextWasmModuleVersionInput], NextWasmModuleVersionResult >, - 'ping' : ActorMethod<[], undefined>, 'register_user' : ActorMethod<[RegisterUserInput], RegisterUserResult>, 'search_registry' : ActorMethod<[SearchRegistryInput], SearchRegistryResult>, 'set_user_active' : ActorMethod<[], SetUserActiveResult>, diff --git a/apps/wallet/src/generated/control-panel/control_panel.did.js b/apps/wallet/src/generated/control-panel/control_panel.did.js index 3c337af6e..6d27b6f55 100644 --- a/apps/wallet/src/generated/control-panel/control_panel.did.js +++ b/apps/wallet/src/generated/control-panel/control_panel.did.js @@ -304,7 +304,6 @@ export const idlFactory = ({ IDL }) => { [NextWasmModuleVersionResult], ['query'], ), - 'ping' : IDL.Func([], [], []), 'register_user' : IDL.Func([RegisterUserInput], [RegisterUserResult], []), 'search_registry' : IDL.Func( [SearchRegistryInput], diff --git a/apps/wallet/src/generated/station/station.did b/apps/wallet/src/generated/station/station.did index 2e17b531d..8ff92f846 100644 --- a/apps/wallet/src/generated/station/station.did +++ b/apps/wallet/src/generated/station/station.did @@ -2565,6 +2565,4 @@ service : (opt SystemInstall) -> { http_request : (HttpRequest) -> (HttpResponse) query; // Internal endpoint used by the upgrader canister to notify the station about a failed station upgrade request. notify_failed_station_upgrade : (NotifyFailedStationUpgradeInput) -> (NotifyFailedStationUpgradeResult); - // no-op endpoint to refresh cycles balance in metrics - ping : () -> (); }; diff --git a/apps/wallet/src/generated/station/station.did.d.ts b/apps/wallet/src/generated/station/station.did.d.ts index 5d59d6890..2688a6241 100644 --- a/apps/wallet/src/generated/station/station.did.d.ts +++ b/apps/wallet/src/generated/station/station.did.d.ts @@ -1237,7 +1237,6 @@ export interface _SERVICE { [NotifyFailedStationUpgradeInput], NotifyFailedStationUpgradeResult >, - 'ping' : ActorMethod<[], undefined>, 'submit_request_approval' : ActorMethod< [SubmitRequestApprovalInput], SubmitRequestApprovalResult diff --git a/apps/wallet/src/generated/station/station.did.js b/apps/wallet/src/generated/station/station.did.js index 1bb46e982..79c2da5b0 100644 --- a/apps/wallet/src/generated/station/station.did.js +++ b/apps/wallet/src/generated/station/station.did.js @@ -1404,7 +1404,6 @@ export const idlFactory = ({ IDL }) => { [NotifyFailedStationUpgradeResult], [], ), - 'ping' : IDL.Func([], [], []), 'submit_request_approval' : IDL.Func( [SubmitRequestApprovalInput], [SubmitRequestApprovalResult], diff --git a/core/control-panel/api/spec.did b/core/control-panel/api/spec.did index 34837a277..6fd377537 100644 --- a/core/control-panel/api/spec.did +++ b/core/control-panel/api/spec.did @@ -668,6 +668,4 @@ service : () -> { can_deploy_station : () -> (CanDeployStationResult) query; // HTTP Protocol interface. http_request : (HttpRequest) -> (HttpResponse) query; - // no-op endpoint to refresh cycles balance in metrics - ping : () -> (); }; diff --git a/core/control-panel/impl/src/controllers/http.rs b/core/control-panel/impl/src/controllers/http.rs index 3f7f44ef7..509da73d8 100644 --- a/core/control-panel/impl/src/controllers/http.rs +++ b/core/control-panel/impl/src/controllers/http.rs @@ -1,5 +1,4 @@ use crate::core::metrics::METRIC_ACTIVE_USERS; -use crate::core::middlewares::use_canister_call_metric; use crate::services::USER_SERVICE; use crate::{ core::ic_cdk::api::{ @@ -7,27 +6,22 @@ use crate::{ }, SERVICE_NAME, }; -use ic_cdk_macros::{query, update}; +use ic_cdk_macros::query; use ic_http_certification::{HttpRequest, HttpResponse}; -use orbit_essentials::api::ApiResult; use orbit_essentials::http::{certify_assets, serve_asset}; use orbit_essentials::metrics::with_metrics_registry; -use orbit_essentials::with_middleware; - -// no-op endpoint to refresh cycles balance in metrics -#[update] -async fn ping() { - let _ = do_ping().await; -} - -// it is important to collect metrics here to refresh cycles balance in metrics -#[with_middleware(tail = use_canister_call_metric("ping", &result))] -async fn do_ping() -> ApiResult<()> { - Ok(()) -} #[query(decoding_quota = 10000)] fn http_request(req: HttpRequest) -> HttpResponse { + // If no data certificate is available (in an update call), + // then we can refresh the metrics (note that this does not invalidate + // the certificate since any state changes in an update call + // to a query method are discarded at the end). + if data_certificate().is_none() { + if let Err(err) = refresh_metrics() { + print(format!("Failed to refresh metrics: {err}")); + } + } let res = serve_asset(&req, data_certificate()); match res { Ok(response) => response, @@ -36,7 +30,7 @@ fn http_request(req: HttpRequest) -> HttpResponse { } // Certification -pub fn certify_metrics() { +fn refresh_metrics() -> Result, String> { // Trigger active users metric update. METRIC_ACTIVE_USERS.with(|metric| metric.borrow_mut().refresh(time())); @@ -59,19 +53,22 @@ pub fn certify_metrics() { }); let metrics_contents = with_metrics_registry(SERVICE_NAME, |registry| registry.export_metrics()); - let res = certify_assets(vec![ + certify_assets(vec![ ( "/metrics".to_string(), metrics_contents.unwrap_or_else(|e| e.to_string().as_bytes().to_vec()), ), ("/metrics/sd".to_string(), metrics_service_discovery()), - ]); - match res { + ]) +} + +pub fn certify_metrics() { + match refresh_metrics() { Ok(certified_data) => { set_certified_data(&certified_data); } Err(err) => { - print(err); + print(format!("Failed to refresh metrics: {err}")); } } } diff --git a/core/station/api/spec.did b/core/station/api/spec.did index 2e17b531d..8ff92f846 100644 --- a/core/station/api/spec.did +++ b/core/station/api/spec.did @@ -2565,6 +2565,4 @@ service : (opt SystemInstall) -> { http_request : (HttpRequest) -> (HttpResponse) query; // Internal endpoint used by the upgrader canister to notify the station about a failed station upgrade request. notify_failed_station_upgrade : (NotifyFailedStationUpgradeInput) -> (NotifyFailedStationUpgradeResult); - // no-op endpoint to refresh cycles balance in metrics - ping : () -> (); }; diff --git a/core/station/impl/src/controllers/http.rs b/core/station/impl/src/controllers/http.rs index b46183e4c..042a1d31c 100644 --- a/core/station/impl/src/controllers/http.rs +++ b/core/station/impl/src/controllers/http.rs @@ -1,29 +1,23 @@ use crate::core::ic_cdk::api::{ canister_balance, data_certificate, print, set_certified_data, time, trap, }; -use crate::core::middlewares::use_canister_call_metric; use crate::SERVICE_NAME; -use ic_cdk_macros::{query, update}; +use ic_cdk_macros::query; use ic_http_certification::{HttpRequest, HttpResponse}; -use orbit_essentials::api::ApiResult; use orbit_essentials::http::{certify_assets, serve_asset}; use orbit_essentials::metrics::with_metrics_registry; -use orbit_essentials::with_middleware; - -// no-op endpoint to refresh cycles balance in metrics -#[update] -async fn ping() { - let _ = do_ping().await; -} - -// it is important to collect metrics here to refresh cycles balance in metrics -#[with_middleware(tail = use_canister_call_metric("ping", &result))] -async fn do_ping() -> ApiResult<()> { - Ok(()) -} #[query(decoding_quota = 10000)] fn http_request(req: HttpRequest) -> HttpResponse { + // If no data certificate is available (in an update call), + // then we can refresh the metrics (note that this does not invalidate + // the certificate since any state changes in an update call + // to a query method are discarded at the end). + if data_certificate().is_none() { + if let Err(err) = refresh_metrics() { + print(format!("Failed to refresh metrics: {err}")); + } + } let res = serve_asset(&req, data_certificate()); match res { Ok(response) => response, @@ -32,7 +26,7 @@ fn http_request(req: HttpRequest) -> HttpResponse { } // Certification -pub fn certify_metrics() { +fn refresh_metrics() -> Result, String> { with_metrics_registry(SERVICE_NAME, |registry| { registry .gauge_mut( @@ -51,16 +45,19 @@ pub fn certify_metrics() { }); let metrics_contents = with_metrics_registry(SERVICE_NAME, |registry| registry.export_metrics()); - let res = certify_assets(vec![( + certify_assets(vec![( "/metrics".to_string(), metrics_contents.unwrap_or_else(|e| e.to_string().as_bytes().to_vec()), - )]); - match res { + )]) +} + +pub fn certify_metrics() { + match refresh_metrics() { Ok(certified_data) => { set_certified_data(&certified_data); } Err(err) => { - print(err); + print(format!("Failed to refresh metrics: {err}")); } } } diff --git a/libs/canfund/src/operations/fetch.rs b/libs/canfund/src/operations/fetch.rs index 13309b4e7..6199bca40 100644 --- a/libs/canfund/src/operations/fetch.rs +++ b/libs/canfund/src/operations/fetch.rs @@ -114,9 +114,6 @@ impl FetchCyclesBalanceFromPrometheusMetrics { #[async_trait::async_trait] impl FetchCyclesBalance for FetchCyclesBalanceFromPrometheusMetrics { async fn fetch_cycles_balance(&self, canister_id: CanisterId) -> Result { - // ping to refresh cycles balance in metrics - let _ = call::<_, ()>(canister_id, "ping", ((),)).await; - // Send the HTTP request to fetch the prometheus metrics. let response: Result<(HttpResponse,), _> = call( canister_id,