LCOV - code coverage report
Current view: top level - proxy/src/console - provider.rs (source / functions) Coverage Total Hit
Test: 8ac049b474321fdc72ddcb56d7165153a1a900e8.info Lines: 7.7 % 52 4
Test Date: 2023-09-06 10:18:01 Functions: 3.8 % 26 1

            Line data    Source code
       1              : pub mod mock;
       2              : pub mod neon;
       3              : 
       4              : use super::messages::MetricsAuxInfo;
       5              : use crate::{
       6              :     auth::ClientCredentials,
       7              :     cache::{timed_lru, TimedLru},
       8              :     compute, scram,
       9              : };
      10              : use async_trait::async_trait;
      11              : use std::sync::Arc;
      12              : 
      13              : pub mod errors {
      14              :     use crate::{
      15              :         error::{io_error, UserFacingError},
      16              :         http,
      17              :         proxy::ShouldRetry,
      18              :     };
      19              :     use thiserror::Error;
      20              : 
      21              :     /// A go-to error message which doesn't leak any detail.
      22              :     const REQUEST_FAILED: &str = "Console request failed";
      23              : 
      24              :     /// Common console API error.
      25            0 :     #[derive(Debug, Error)]
      26              :     pub enum ApiError {
      27              :         /// Error returned by the console itself.
      28              :         #[error("{REQUEST_FAILED} with {}: {}", .status, .text)]
      29              :         Console {
      30              :             status: http::StatusCode,
      31              :             text: Box<str>,
      32              :         },
      33              : 
      34              :         /// Various IO errors like broken pipe or malformed payload.
      35              :         #[error("{REQUEST_FAILED}: {0}")]
      36              :         Transport(#[from] std::io::Error),
      37              :     }
      38              : 
      39              :     impl ApiError {
      40              :         /// Returns HTTP status code if it's the reason for failure.
      41            0 :         pub fn http_status_code(&self) -> Option<http::StatusCode> {
      42            0 :             use ApiError::*;
      43            0 :             match self {
      44            0 :                 Console { status, .. } => Some(*status),
      45            0 :                 _ => None,
      46              :             }
      47            0 :         }
      48              :     }
      49              : 
      50              :     impl UserFacingError for ApiError {
      51            0 :         fn to_string_client(&self) -> String {
      52            0 :             use ApiError::*;
      53            0 :             match self {
      54              :                 // To minimize risks, only select errors are forwarded to users.
      55              :                 // Ask @neondatabase/control-plane for review before adding more.
      56            0 :                 Console { status, .. } => match *status {
      57              :                     http::StatusCode::NOT_FOUND => {
      58              :                         // Status 404: failed to get a project-related resource.
      59            0 :                         format!("{REQUEST_FAILED}: endpoint cannot be found")
      60              :                     }
      61              :                     http::StatusCode::NOT_ACCEPTABLE => {
      62              :                         // Status 406: endpoint is disabled (we don't allow connections).
      63            0 :                         format!("{REQUEST_FAILED}: endpoint is disabled")
      64              :                     }
      65              :                     http::StatusCode::LOCKED => {
      66              :                         // Status 423: project might be in maintenance mode (or bad state), or quotas exceeded.
      67            0 :                         format!("{REQUEST_FAILED}: endpoint is temporary unavailable. check your quotas and/or contact our support")
      68              :                     }
      69            0 :                     _ => REQUEST_FAILED.to_owned(),
      70              :                 },
      71            0 :                 _ => REQUEST_FAILED.to_owned(),
      72              :             }
      73            0 :         }
      74              :     }
      75              : 
      76              :     impl ShouldRetry for ApiError {
      77              :         fn could_retry(&self) -> bool {
      78            4 :             match self {
      79              :                 // retry some transport errors
      80            0 :                 Self::Transport(io) => io.could_retry(),
      81              :                 // retry some temporary failures because the compute was in a bad state
      82              :                 // (bad request can be returned when the endpoint was in transition)
      83              :                 Self::Console {
      84              :                     status: http::StatusCode::BAD_REQUEST,
      85              :                     ..
      86            0 :                 } => true,
      87              :                 // locked can be returned when the endpoint was in transition
      88              :                 // or when quotas are exceeded. don't retry when quotas are exceeded
      89              :                 Self::Console {
      90              :                     status: http::StatusCode::LOCKED,
      91            0 :                     ref text,
      92            0 :                 } => !text.contains("quota"),
      93              :                 // retry server errors
      94            4 :                 Self::Console { status, .. } if status.is_server_error() => true,
      95            2 :                 _ => false,
      96              :             }
      97            4 :         }
      98              :     }
      99              : 
     100              :     impl From<reqwest::Error> for ApiError {
     101            0 :         fn from(e: reqwest::Error) -> Self {
     102            0 :             io_error(e).into()
     103            0 :         }
     104              :     }
     105              : 
     106              :     impl From<reqwest_middleware::Error> for ApiError {
     107            0 :         fn from(e: reqwest_middleware::Error) -> Self {
     108            0 :             io_error(e).into()
     109            0 :         }
     110              :     }
     111              : 
     112            0 :     #[derive(Debug, Error)]
     113              :     pub enum GetAuthInfoError {
     114              :         // We shouldn't include the actual secret here.
     115              :         #[error("Console responded with a malformed auth secret")]
     116              :         BadSecret,
     117              : 
     118              :         #[error(transparent)]
     119              :         ApiError(ApiError),
     120              :     }
     121              : 
     122              :     // This allows more useful interactions than `#[from]`.
     123              :     impl<E: Into<ApiError>> From<E> for GetAuthInfoError {
     124            0 :         fn from(e: E) -> Self {
     125            0 :             Self::ApiError(e.into())
     126            0 :         }
     127              :     }
     128              : 
     129              :     impl UserFacingError for GetAuthInfoError {
     130            0 :         fn to_string_client(&self) -> String {
     131            0 :             use GetAuthInfoError::*;
     132            0 :             match self {
     133              :                 // We absolutely should not leak any secrets!
     134            0 :                 BadSecret => REQUEST_FAILED.to_owned(),
     135              :                 // However, API might return a meaningful error.
     136            0 :                 ApiError(e) => e.to_string_client(),
     137              :             }
     138            0 :         }
     139              :     }
     140            0 :     #[derive(Debug, Error)]
     141              :     pub enum WakeComputeError {
     142              :         #[error("Console responded with a malformed compute address: {0}")]
     143              :         BadComputeAddress(Box<str>),
     144              : 
     145              :         #[error(transparent)]
     146              :         ApiError(ApiError),
     147              :     }
     148              : 
     149              :     // This allows more useful interactions than `#[from]`.
     150              :     impl<E: Into<ApiError>> From<E> for WakeComputeError {
     151            0 :         fn from(e: E) -> Self {
     152            0 :             Self::ApiError(e.into())
     153            0 :         }
     154              :     }
     155              : 
     156              :     impl UserFacingError for WakeComputeError {
     157            0 :         fn to_string_client(&self) -> String {
     158            0 :             use WakeComputeError::*;
     159            0 :             match self {
     160              :                 // We shouldn't show user the address even if it's broken.
     161              :                 // Besides, user is unlikely to care about this detail.
     162            0 :                 BadComputeAddress(_) => REQUEST_FAILED.to_owned(),
     163              :                 // However, API might return a meaningful error.
     164            0 :                 ApiError(e) => e.to_string_client(),
     165              :             }
     166            0 :         }
     167              :     }
     168              : }
     169              : 
     170              : /// Extra query params we'd like to pass to the console.
     171              : pub struct ConsoleReqExtra<'a> {
     172              :     /// A unique identifier for a connection.
     173              :     pub session_id: uuid::Uuid,
     174              :     /// Name of client application, if set.
     175              :     pub application_name: Option<&'a str>,
     176              : }
     177              : 
     178              : /// Auth secret which is managed by the cloud.
     179              : pub enum AuthInfo {
     180              :     /// Md5 hash of user's password.
     181              :     Md5([u8; 16]),
     182              : 
     183              :     /// [SCRAM](crate::scram) authentication info.
     184              :     Scram(scram::ServerSecret),
     185              : }
     186              : 
     187              : /// Info for establishing a connection to a compute node.
     188              : /// This is what we get after auth succeeded, but not before!
     189            0 : #[derive(Clone)]
     190              : pub struct NodeInfo {
     191              :     /// Compute node connection params.
     192              :     /// It's sad that we have to clone this, but this will improve
     193              :     /// once we migrate to a bespoke connection logic.
     194              :     pub config: compute::ConnCfg,
     195              : 
     196              :     /// Labels for proxy's metrics.
     197              :     pub aux: Arc<MetricsAuxInfo>,
     198              : 
     199              :     /// Whether we should accept self-signed certificates (for testing)
     200              :     pub allow_self_signed_compute: bool,
     201              : }
     202              : 
     203              : pub type NodeInfoCache = TimedLru<Arc<str>, NodeInfo>;
     204              : pub type CachedNodeInfo = timed_lru::Cached<&'static NodeInfoCache>;
     205              : 
     206              : /// This will allocate per each call, but the http requests alone
     207              : /// already require a few allocations, so it should be fine.
     208              : #[async_trait]
     209              : pub trait Api {
     210              :     /// Get the client's auth secret for authentication.
     211              :     async fn get_auth_info(
     212              :         &self,
     213              :         extra: &ConsoleReqExtra<'_>,
     214              :         creds: &ClientCredentials,
     215              :     ) -> Result<Option<AuthInfo>, errors::GetAuthInfoError>;
     216              : 
     217              :     /// Wake up the compute node and return the corresponding connection info.
     218              :     async fn wake_compute(
     219              :         &self,
     220              :         extra: &ConsoleReqExtra<'_>,
     221              :         creds: &ClientCredentials,
     222              :     ) -> Result<CachedNodeInfo, errors::WakeComputeError>;
     223              : }
     224              : 
     225              : /// Various caches for [`console`](super).
     226              : pub struct ApiCaches {
     227              :     /// Cache for the `wake_compute` API method.
     228              :     pub node_info: NodeInfoCache,
     229              : }
        

Generated by: LCOV version 2.1-beta