meta_srv/service/admin/
maintenance.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::HashMap;
16
17use axum::extract::State;
18use axum::response::{IntoResponse, Response};
19use axum::Json;
20use common_meta::key::runtime_switch::RuntimeSwitchManagerRef;
21use common_telemetry::{info, warn};
22use serde::{Deserialize, Serialize};
23use snafu::{OptionExt, ResultExt};
24use tonic::codegen::http;
25
26use crate::error::{
27    self, MissingRequiredParameterSnafu, ParseBoolSnafu, Result, RuntimeSwitchManagerSnafu,
28    UnsupportedSnafu,
29};
30use crate::service::admin::util::{to_json_response, to_not_found_response, ErrorHandler};
31use crate::service::admin::HttpHandler;
32
33#[derive(Clone)]
34pub struct MaintenanceHandler {
35    pub manager: RuntimeSwitchManagerRef,
36}
37
38#[derive(Debug, Serialize, Deserialize)]
39pub(crate) struct MaintenanceResponse {
40    enabled: bool,
41}
42
43/// Get the maintenance mode.
44#[axum_macros::debug_handler]
45pub(crate) async fn status(State(handler): State<MaintenanceHandler>) -> Response {
46    handler
47        .get_maintenance()
48        .await
49        .map(Json)
50        .map_err(ErrorHandler::new)
51        .into_response()
52}
53
54/// Set the maintenance mode.
55#[axum_macros::debug_handler]
56pub(crate) async fn set(State(handler): State<MaintenanceHandler>) -> Response {
57    handler
58        .set_maintenance()
59        .await
60        .map(Json)
61        .map_err(ErrorHandler::new)
62        .into_response()
63}
64
65/// Unset the maintenance mode.
66#[axum_macros::debug_handler]
67pub(crate) async fn unset(State(handler): State<MaintenanceHandler>) -> Response {
68    handler
69        .unset_maintenance()
70        .await
71        .map(Json)
72        .map_err(ErrorHandler::new)
73        .into_response()
74}
75
76impl TryFrom<MaintenanceResponse> for String {
77    type Error = error::Error;
78
79    fn try_from(response: MaintenanceResponse) -> Result<Self> {
80        serde_json::to_string(&response).context(error::SerializeToJsonSnafu {
81            input: format!("{response:?}"),
82        })
83    }
84}
85
86impl MaintenanceHandler {
87    pub(crate) async fn get_maintenance(&self) -> crate::Result<MaintenanceResponse> {
88        let enabled = self
89            .manager
90            .maintenance_mode()
91            .await
92            .context(RuntimeSwitchManagerSnafu)?;
93        Ok(MaintenanceResponse { enabled })
94    }
95
96    pub(crate) async fn set_maintenance(&self) -> crate::Result<MaintenanceResponse> {
97        self.manager
98            .set_maintenance_mode()
99            .await
100            .context(RuntimeSwitchManagerSnafu)?;
101        // TODO(weny): Add a record to the system events.
102        info!("Enable the maintenance mode.");
103        Ok(MaintenanceResponse { enabled: true })
104    }
105
106    pub(crate) async fn unset_maintenance(&self) -> crate::Result<MaintenanceResponse> {
107        self.manager
108            .unset_maintenance_mode()
109            .await
110            .context(RuntimeSwitchManagerSnafu)?;
111        // TODO(weny): Add a record to the system events.
112        info!("Disable the maintenance mode.");
113        Ok(MaintenanceResponse { enabled: false })
114    }
115
116    async fn handle_legacy_maintenance(
117        &self,
118        params: &HashMap<String, String>,
119    ) -> crate::Result<MaintenanceResponse> {
120        let enable = get_enable_from_params(params)?;
121        if enable {
122            self.set_maintenance().await
123        } else {
124            self.unset_maintenance().await
125        }
126    }
127}
128
129fn get_enable_from_params(params: &HashMap<String, String>) -> crate::Result<bool> {
130    params
131        .get("enable")
132        .map(|v| v.parse::<bool>())
133        .context(MissingRequiredParameterSnafu { param: "enable" })?
134        .context(ParseBoolSnafu {
135            err_msg: "'enable' must be 'true' or 'false'",
136        })
137}
138
139const MAINTENANCE_PATH: &str = "maintenance";
140const ENABLE_SUFFIX: &str = "enable";
141const DISABLE_SUFFIX: &str = "disable";
142const STATUS_SUFFIX: &str = "status";
143
144#[async_trait::async_trait]
145impl HttpHandler for MaintenanceHandler {
146    // TODO(weny): Remove the legacy version of the maintenance API.
147    // However, we need to keep the legacy version for a while to avoid breaking the existing operators.
148    async fn handle(
149        &self,
150        path: &str,
151        method: http::Method,
152        params: &HashMap<String, String>,
153    ) -> crate::Result<http::Response<String>> {
154        match method {
155            http::Method::GET => {
156                if path.ends_with(STATUS_SUFFIX) {
157                    // Handle GET request to '/admin/maintenance/status'
158                    let response = self.get_maintenance().await?;
159                    to_json_response(response)
160                } else if path.ends_with(MAINTENANCE_PATH) && params.is_empty() {
161                    // Handle GET request to '/admin/maintenance'. (The legacy version)
162                    let response = self.get_maintenance().await?;
163                    to_json_response(response)
164                } else if path.ends_with(MAINTENANCE_PATH) {
165                    // Handle GET request to '/admin/maintenance' with URL parameters. (The legacy version)
166                    warn!(
167                        "Found URL parameters in '/admin/maintenance' request, it's deprecated, will be removed in the future"
168                    );
169                    // The old version operator will send GET request with URL parameters,
170                    // so we need to support it.
171                    let response = self.handle_legacy_maintenance(params).await?;
172                    to_json_response(response)
173                } else {
174                    to_not_found_response()
175                }
176            }
177            http::Method::PUT => {
178                // Handle PUT request to '/admin/maintenance' with URL parameters. (The legacy version)
179                if path.ends_with(MAINTENANCE_PATH) {
180                    warn!("Found PUT request to '/admin/maintenance', it's deprecated, will be removed in the future");
181                    let response = self.handle_legacy_maintenance(params).await?;
182                    to_json_response(response)
183                } else {
184                    to_not_found_response()
185                }
186            }
187            http::Method::POST => {
188                // Handle POST request to '/admin/maintenance/enable'
189                if path.ends_with(ENABLE_SUFFIX) {
190                    let response = self.set_maintenance().await?;
191                    to_json_response(response)
192                } else if path.ends_with(DISABLE_SUFFIX) {
193                    // Handle POST request to '/admin/maintenance/disable'
194                    let response = self.unset_maintenance().await?;
195                    to_json_response(response)
196                } else if path.ends_with(MAINTENANCE_PATH) {
197                    // Handle POST request to '/admin/maintenance' with URL parameters. (The legacy version)
198                    warn!("Found PUT request to '/admin/maintenance', it's deprecated, will be removed in the future");
199                    let response = self.handle_legacy_maintenance(params).await?;
200                    to_json_response(response)
201                } else {
202                    to_not_found_response()
203                }
204            }
205            _ => UnsupportedSnafu {
206                operation: format!("http method {method}"),
207            }
208            .fail(),
209        }
210    }
211}