TLA 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 UBC 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 CBC 4 : match self {
79 : // retry some transport errors
80 UBC 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 : } => {
93 0 : !text.contains("written data quota exceeded")
94 0 : && !text.contains("the limit for current plan reached")
95 : }
96 : // retry server errors
97 CBC 4 : Self::Console { status, .. } if status.is_server_error() => true,
98 2 : _ => false,
99 : }
100 4 : }
101 : }
102 :
103 : impl From<reqwest::Error> for ApiError {
104 UBC 0 : fn from(e: reqwest::Error) -> Self {
105 0 : io_error(e).into()
106 0 : }
107 : }
108 :
109 : impl From<reqwest_middleware::Error> for ApiError {
110 0 : fn from(e: reqwest_middleware::Error) -> Self {
111 0 : io_error(e).into()
112 0 : }
113 : }
114 :
115 0 : #[derive(Debug, Error)]
116 : pub enum GetAuthInfoError {
117 : // We shouldn't include the actual secret here.
118 : #[error("Console responded with a malformed auth secret")]
119 : BadSecret,
120 :
121 : #[error(transparent)]
122 : ApiError(ApiError),
123 : }
124 :
125 : // This allows more useful interactions than `#[from]`.
126 : impl<E: Into<ApiError>> From<E> for GetAuthInfoError {
127 0 : fn from(e: E) -> Self {
128 0 : Self::ApiError(e.into())
129 0 : }
130 : }
131 :
132 : impl UserFacingError for GetAuthInfoError {
133 0 : fn to_string_client(&self) -> String {
134 0 : use GetAuthInfoError::*;
135 0 : match self {
136 : // We absolutely should not leak any secrets!
137 0 : BadSecret => REQUEST_FAILED.to_owned(),
138 : // However, API might return a meaningful error.
139 0 : ApiError(e) => e.to_string_client(),
140 : }
141 0 : }
142 : }
143 0 : #[derive(Debug, Error)]
144 : pub enum WakeComputeError {
145 : #[error("Console responded with a malformed compute address: {0}")]
146 : BadComputeAddress(Box<str>),
147 :
148 : #[error(transparent)]
149 : ApiError(ApiError),
150 : }
151 :
152 : // This allows more useful interactions than `#[from]`.
153 : impl<E: Into<ApiError>> From<E> for WakeComputeError {
154 0 : fn from(e: E) -> Self {
155 0 : Self::ApiError(e.into())
156 0 : }
157 : }
158 :
159 : impl UserFacingError for WakeComputeError {
160 0 : fn to_string_client(&self) -> String {
161 0 : use WakeComputeError::*;
162 0 : match self {
163 : // We shouldn't show user the address even if it's broken.
164 : // Besides, user is unlikely to care about this detail.
165 0 : BadComputeAddress(_) => REQUEST_FAILED.to_owned(),
166 : // However, API might return a meaningful error.
167 0 : ApiError(e) => e.to_string_client(),
168 : }
169 0 : }
170 : }
171 : }
172 :
173 : /// Extra query params we'd like to pass to the console.
174 : pub struct ConsoleReqExtra<'a> {
175 : /// A unique identifier for a connection.
176 : pub session_id: uuid::Uuid,
177 : /// Name of client application, if set.
178 : pub application_name: Option<&'a str>,
179 : }
180 :
181 : /// Auth secret which is managed by the cloud.
182 : pub enum AuthInfo {
183 : /// Md5 hash of user's password.
184 : Md5([u8; 16]),
185 :
186 : /// [SCRAM](crate::scram) authentication info.
187 : Scram(scram::ServerSecret),
188 : }
189 :
190 : /// Info for establishing a connection to a compute node.
191 : /// This is what we get after auth succeeded, but not before!
192 0 : #[derive(Clone)]
193 : pub struct NodeInfo {
194 : /// Compute node connection params.
195 : /// It's sad that we have to clone this, but this will improve
196 : /// once we migrate to a bespoke connection logic.
197 : pub config: compute::ConnCfg,
198 :
199 : /// Labels for proxy's metrics.
200 : pub aux: Arc<MetricsAuxInfo>,
201 :
202 : /// Whether we should accept self-signed certificates (for testing)
203 : pub allow_self_signed_compute: bool,
204 : }
205 :
206 : pub type NodeInfoCache = TimedLru<Arc<str>, NodeInfo>;
207 : pub type CachedNodeInfo = timed_lru::Cached<&'static NodeInfoCache>;
208 :
209 : /// This will allocate per each call, but the http requests alone
210 : /// already require a few allocations, so it should be fine.
211 : #[async_trait]
212 : pub trait Api {
213 : /// Get the client's auth secret for authentication.
214 : async fn get_auth_info(
215 : &self,
216 : extra: &ConsoleReqExtra<'_>,
217 : creds: &ClientCredentials,
218 : ) -> Result<Option<AuthInfo>, errors::GetAuthInfoError>;
219 :
220 : /// Wake up the compute node and return the corresponding connection info.
221 : async fn wake_compute(
222 : &self,
223 : extra: &ConsoleReqExtra<'_>,
224 : creds: &ClientCredentials,
225 : ) -> Result<CachedNodeInfo, errors::WakeComputeError>;
226 : }
227 :
228 : /// Various caches for [`console`](super).
229 : pub struct ApiCaches {
230 : /// Cache for the `wake_compute` API method.
231 : pub node_info: NodeInfoCache,
232 : }
|