diff --git a/rs/boundary_node/certificate_issuance/certificate_orchestrator/interface.did b/rs/boundary_node/certificate_issuance/certificate_orchestrator/interface.did index 9eb3dc446aa..d174c535f90 100644 --- a/rs/boundary_node/certificate_issuance/certificate_orchestrator/interface.did +++ b/rs/boundary_node/certificate_issuance/certificate_orchestrator/interface.did @@ -88,6 +88,16 @@ type RemoveRegistrationResponse = variant { Err: RemoveRegistrationError; }; +type ListRegistrationsError = variant { + Unauthorized; + UnexpectedError: text; +}; + +type ListRegistrationsResponse = variant { + Ok: vec record { text; Registration }; + Err: ListRegistrationsError; +}; + type GetCertificateError = variant { NotFound; Unauthorized; @@ -155,6 +165,16 @@ type PeekTaskResponse = variant { Err: PeekTaskError; }; +type ListTasksError = variant { + Unauthorized; + UnexpectedError: text; +}; + +type ListTasksResponse = variant { + Ok: vec record { text; nat64; Registration }; + Err: ListTasksError; +}; + type DispenseTaskError = variant { NoTasksAvailable; Unauthorized; @@ -166,6 +186,17 @@ type DispenseTaskResponse = variant { Err: DispenseTaskError; }; +type RemoveTaskError = variant { + NotFound; + Unauthorized; + UnexpectedError: text; +}; + +type RemoveTaskResponse = variant { + Ok; + Err: RemoveTaskError; +}; + type ModifyAllowedPrincipalError = variant { Unauthorized; UnexpectedError: text; @@ -207,6 +238,7 @@ service: (InitArg) -> { getRegistration: (Id) -> (GetRegistrationResponse) query; updateRegistration: (Id, UpdateType) -> (UpdateRegistrationResponse); removeRegistration: (Id) -> (RemoveRegistrationResponse); + listRegistrations: () -> (ListRegistrationsResponse) query; // Certificates getCertificate: (Id) -> (GetCertificateResponse) query; @@ -218,7 +250,9 @@ service: (InitArg) -> { // Tasks queueTask: (Id, Timestamp) -> (QueueTaskResponse); dispenseTask: () -> (DispenseTaskResponse); + removeTask: (Id) -> (RemoveTaskResponse); peekTask: () -> (PeekTaskResponse) query; + listTasks: () -> (ListTasksResponse) query; // Metrics (Http Interface) http_request: (HttpRequest) -> (HttpResponse) query; diff --git a/rs/boundary_node/certificate_issuance/certificate_orchestrator/src/main.rs b/rs/boundary_node/certificate_issuance/certificate_orchestrator/src/main.rs index ef900305af4..aeb81222fe4 100644 --- a/rs/boundary_node/certificate_issuance/certificate_orchestrator/src/main.rs +++ b/rs/boundary_node/certificate_issuance/certificate_orchestrator/src/main.rs @@ -7,10 +7,12 @@ use certificate_orchestrator_interface::{ ExportCertificatesError, ExportCertificatesResponse, ExportPackage, GetCertificateError, GetCertificateResponse, GetRegistrationError, GetRegistrationResponse, HeaderField, HttpRequest, HttpResponse, Id, InitArg, ListAllowedPrincipalsError, - ListAllowedPrincipalsResponse, ModifyAllowedPrincipalError, ModifyAllowedPrincipalResponse, + ListAllowedPrincipalsResponse, ListRegistrationsError, ListRegistrationsResponse, + ListTasksError, ListTasksResponse, ModifyAllowedPrincipalError, ModifyAllowedPrincipalResponse, Name, PeekTaskError, PeekTaskResponse, QueueTaskError, QueueTaskResponse, Registration, - RemoveRegistrationError, RemoveRegistrationResponse, State, UpdateRegistrationError, - UpdateRegistrationResponse, UpdateType, UploadCertificateError, UploadCertificateResponse, + RemoveRegistrationError, RemoveRegistrationResponse, RemoveTaskError, RemoveTaskResponse, + State, UpdateRegistrationError, UpdateRegistrationResponse, UpdateType, UploadCertificateError, + UploadCertificateResponse, }; use ic_cdk::{ api::{id, time}, @@ -24,7 +26,7 @@ use ic_stable_structures::{ }; use priority_queue::PriorityQueue; use prometheus::{CounterVec, Encoder, Gauge, GaugeVec, Opts, Registry, TextEncoder}; -use work::{Peek, PeekError}; +use work::{Peek, PeekError, TaskRemover}; use crate::{ acl::{Authorize, AuthorizeError, Authorizer, WithAuthorize}, @@ -148,6 +150,13 @@ thread_local! { ), &["status"]).unwrap() }); + static COUNTER_REMOVE_TASK_TOTAL: RefCell = RefCell::new({ + CounterVec::new(Opts::new( + format!("{SERVICE_NAME}_remove_task_total"), // name + "number of times remove_task was called", // help + ), &["status"]).unwrap() + }); + static GAUGE_REGISTRATIONS_TOTAL: RefCell = RefCell::new({ GaugeVec::new(Opts::new( format!("{SERVICE_NAME}_registrations_total"), // name @@ -214,6 +223,11 @@ thread_local! { r.register(c).unwrap(); }); + COUNTER_REMOVE_TASK_TOTAL.with(|c| { + let c = Box::new(c.borrow().to_owned()); + r.register(c).unwrap(); + }); + GAUGE_REGISTRATIONS_TOTAL.with(|g| { let g = Box::new(g.borrow().to_owned()); r.register(g).unwrap(); @@ -344,6 +358,12 @@ thread_local! { let r = WithMetrics(r, &COUNTER_REMOVE_REGISTRATION_TOTAL); Box::new(r) }); + + static REGISTRATION_LISTER: RefCell> = RefCell::new({ + let v = registration::Lister::new(®ISTRATIONS); + let v = WithAuthorize(v, &ROOT_AUTHORIZER); + Box::new(v) + }); } // Certificates @@ -386,12 +406,25 @@ thread_local! { Box::new(d) }); + static TASK_LISTER: RefCell> = RefCell::new({ + let v = work::Lister::new(&TASKS, ®ISTRATIONS); + let v = WithAuthorize(v, &ROOT_AUTHORIZER); + Box::new(v) + }); + static DISPENSER: RefCell> = RefCell::new({ let d = Dispenser::new(&TASKS, &RETRIES); let d = WithAuthorize(d, &MAIN_AUTHORIZER); let d = WithMetrics(d, &COUNTER_DISPENSE_TASK_TOTAL); Box::new(d) }); + + static TASK_REMOVER: RefCell> = RefCell::new({ + let v = TaskRemover::new(&TASKS); + let v = WithAuthorize(v, &ROOT_AUTHORIZER); + let v = WithMetrics(v, &COUNTER_REMOVE_TASK_TOTAL); + Box::new(v) + }); } // Expirations and retries @@ -678,6 +711,20 @@ fn remove_registration(id: Id) -> RemoveRegistrationResponse { } } +#[query(name = "listRegistrations")] +#[candid_method(query, rename = "listRegistrations")] +fn list_registrations() -> ListRegistrationsResponse { + match REGISTRATION_LISTER.with(|v| v.borrow().list()) { + Ok(rs) => ListRegistrationsResponse::Ok(rs), + Err(err) => ListRegistrationsResponse::Err(match err { + registration::ListError::Unauthorized => ListRegistrationsError::Unauthorized, + registration::ListError::UnexpectedError(err) => { + ListRegistrationsError::UnexpectedError(err.to_string()) + } + }), + } +} + // Certificates #[query(name = "getCertificate")] @@ -798,6 +845,35 @@ fn dispense_task() -> DispenseTaskResponse { } } +#[update(name = "removeTask")] +#[candid_method(update, rename = "removeTask")] +fn remove_task(id: Id) -> RemoveTaskResponse { + match TASK_REMOVER.with(|v| v.borrow().remove(&id)) { + Ok(()) => RemoveTaskResponse::Ok, + Err(err) => RemoveTaskResponse::Err(match err { + work::RemoveError::NotFound => RemoveTaskError::NotFound, + work::RemoveError::Unauthorized => RemoveTaskError::Unauthorized, + work::RemoveError::UnexpectedError(err) => { + RemoveTaskError::UnexpectedError(err.to_string()) + } + }), + } +} + +#[query(name = "listTasks")] +#[candid_method(query, rename = "listTasks")] +fn list_tasks() -> ListTasksResponse { + match TASK_LISTER.with(|v| v.borrow().list()) { + Ok(ts) => ListTasksResponse::Ok(ts), + Err(err) => ListTasksResponse::Err(match err { + work::ListError::Unauthorized => ListTasksError::Unauthorized, + work::ListError::UnexpectedError(err) => { + ListTasksError::UnexpectedError(err.to_string()) + } + }), + } +} + // Metrics #[query(name = "http_request")] diff --git a/rs/boundary_node/certificate_issuance/certificate_orchestrator/src/registration.rs b/rs/boundary_node/certificate_issuance/certificate_orchestrator/src/registration.rs index 48458454d0b..1edb97e61d5 100644 --- a/rs/boundary_node/certificate_issuance/certificate_orchestrator/src/registration.rs +++ b/rs/boundary_node/certificate_issuance/certificate_orchestrator/src/registration.rs @@ -380,6 +380,53 @@ impl Update for WithMetrics { } } +#[derive(Debug, thiserror::Error)] +pub enum ListError { + #[error("Unauthorized")] + Unauthorized, + + #[error(transparent)] + UnexpectedError(#[from] anyhow::Error), +} + +pub trait List { + fn list(&self) -> Result, ListError>; +} + +pub struct Lister { + registrations: LocalRef>, +} + +impl Lister { + pub fn new(registrations: LocalRef>) -> Self { + Self { registrations } + } +} + +impl List for Lister { + fn list(&self) -> Result, ListError> { + Ok(self.registrations.with(|rs| { + rs.borrow() + .iter() + .map(|(id, r)| (id.to_string(), r)) + .collect() + })) + } +} + +impl List for WithAuthorize { + fn list(&self) -> Result, ListError> { + if let Err(err) = self.1.authorize(&caller()) { + return Err(match err { + AuthorizeError::Unauthorized => ListError::Unauthorized, + AuthorizeError::UnexpectedError(err) => ListError::UnexpectedError(err), + }); + }; + + self.0.list() + } +} + #[derive(Debug, thiserror::Error)] pub enum RemoveError { #[error("Not found")] diff --git a/rs/boundary_node/certificate_issuance/certificate_orchestrator/src/work.rs b/rs/boundary_node/certificate_issuance/certificate_orchestrator/src/work.rs index a91771c7c44..df4d7897a89 100644 --- a/rs/boundary_node/certificate_issuance/certificate_orchestrator/src/work.rs +++ b/rs/boundary_node/certificate_issuance/certificate_orchestrator/src/work.rs @@ -1,5 +1,6 @@ use std::{cmp::Reverse, time::Duration}; +use anyhow::anyhow; use certificate_orchestrator_interface::{Id, Registration}; use ic_cdk::caller; use priority_queue::PriorityQueue; @@ -178,6 +179,72 @@ impl Peek for WithMetrics { } } +#[derive(Debug, thiserror::Error)] +pub enum ListError { + #[error("Unauthorized")] + Unauthorized, + + #[error(transparent)] + UnexpectedError(#[from] anyhow::Error), +} + +pub trait List { + fn list(&self) -> Result, ListError>; +} + +pub struct Lister { + tasks: LocalRef>>, + registrations: LocalRef>, +} + +impl Lister { + pub fn new( + tasks: LocalRef>>, + registrations: LocalRef>, + ) -> Self { + Self { + tasks, + registrations, + } + } +} + +impl List for Lister { + fn list(&self) -> Result, ListError> { + self.tasks.with(|tasks| { + tasks + .borrow() + .iter() + .map(|(id, Reverse(timestamp))| { + match self + .registrations + .with(|rs| rs.borrow().get(&id.to_owned().into())) + { + Some(r) => Ok((id.to_owned(), timestamp.to_owned(), r)), + None => Err(anyhow!( + "invalid state: task id {id} not found in registrations" + ) + .into()), + } + }) + .collect() + }) + } +} + +impl List for WithAuthorize { + fn list(&self) -> Result, ListError> { + if let Err(err) = self.1.authorize(&caller()) { + return Err(match err { + AuthorizeError::Unauthorized => ListError::Unauthorized, + AuthorizeError::UnexpectedError(err) => ListError::UnexpectedError(err), + }); + }; + + self.0.list() + } +} + #[derive(Debug, thiserror::Error)] pub enum DispenseError { #[error("No tasks available")] @@ -277,6 +344,75 @@ impl Dispense for WithMetrics { } } +#[derive(Debug, thiserror::Error)] +pub enum RemoveError { + #[error("Not found")] + NotFound, + #[error("Unauthorized")] + Unauthorized, + #[error(transparent)] + UnexpectedError(#[from] anyhow::Error), +} + +pub trait Remove { + fn remove(&self, id: &str) -> Result<(), RemoveError>; +} + +pub struct TaskRemover { + tasks: LocalRef>>, +} + +impl TaskRemover { + pub fn new(tasks: LocalRef>>) -> Self { + Self { tasks } + } +} + +impl Remove for TaskRemover { + fn remove(&self, id: &str) -> Result<(), RemoveError> { + self.tasks.with(|ts| match ts.borrow_mut().remove(id) { + Some(_) => Ok(()), + None => Err(RemoveError::NotFound), + }) + } +} + +impl Remove for WithAuthorize { + fn remove(&self, id: &str) -> Result<(), RemoveError> { + if let Err(err) = self.1.authorize(&caller()) { + return Err(match err { + AuthorizeError::Unauthorized => RemoveError::Unauthorized, + AuthorizeError::UnexpectedError(err) => RemoveError::UnexpectedError(err), + }); + }; + + self.0.remove(id) + } +} + +impl Remove for WithMetrics { + fn remove(&self, id: &str) -> Result<(), RemoveError> { + let out = self.0.remove(id); + + self.1.with(|c| { + c.borrow() + .with(&labels! { + "status" => match &out { + Ok(_) => "ok", + Err(err) => match err { + RemoveError::NotFound => "not-found", + RemoveError::Unauthorized => "unauthorized", + RemoveError::UnexpectedError(_) => "fail", + }, + }, + }) + .inc() + }); + + out + } +} + #[derive(Debug, thiserror::Error)] pub enum RetryError { #[error(transparent)] diff --git a/rs/boundary_node/certificate_issuance/certificate_orchestrator_interface/src/lib.rs b/rs/boundary_node/certificate_issuance/certificate_orchestrator_interface/src/lib.rs index cbe979420e2..814c0b4886e 100644 --- a/rs/boundary_node/certificate_issuance/certificate_orchestrator_interface/src/lib.rs +++ b/rs/boundary_node/certificate_issuance/certificate_orchestrator_interface/src/lib.rs @@ -280,6 +280,18 @@ pub enum RemoveRegistrationResponse { Err(RemoveRegistrationError), } +#[derive(Clone, Debug, CandidType, Deserialize)] +pub enum ListRegistrationsError { + Unauthorized, + UnexpectedError(String), +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub enum ListRegistrationsResponse { + Ok(Vec<(String, Registration)>), + Err(ListRegistrationsError), +} + #[derive(Clone, Debug, CandidType, Deserialize)] pub enum GetCertificateError { NotFound, @@ -371,6 +383,31 @@ pub enum DispenseTaskResponse { Err(DispenseTaskError), } +#[derive(Clone, Debug, CandidType, Deserialize)] +pub enum RemoveTaskError { + NotFound, + Unauthorized, + UnexpectedError(String), +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub enum RemoveTaskResponse { + Ok, + Err(RemoveTaskError), +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub enum ListTasksError { + Unauthorized, + UnexpectedError(String), +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub enum ListTasksResponse { + Ok(Vec<(String, u64, Registration)>), + Err(ListTasksError), +} + #[derive(Clone, Debug, CandidType, Deserialize)] pub enum ModifyAllowedPrincipalError { Unauthorized,