github.com/muhammadn/cortex@v1.9.1-0.20220510110439-46bb7000d03d/pkg/configs/api/api.go (about) 1 package api 2 3 import ( 4 "database/sql" 5 "encoding/json" 6 "errors" 7 "flag" 8 "fmt" 9 "html/template" 10 "io/ioutil" 11 "mime" 12 "net/http" 13 "strconv" 14 "strings" 15 16 "gopkg.in/yaml.v2" 17 18 "github.com/go-kit/log/level" 19 "github.com/gorilla/mux" 20 amconfig "github.com/prometheus/alertmanager/config" 21 amtemplate "github.com/prometheus/alertmanager/template" 22 23 "github.com/cortexproject/cortex/pkg/configs/db" 24 "github.com/cortexproject/cortex/pkg/configs/userconfig" 25 "github.com/cortexproject/cortex/pkg/tenant" 26 "github.com/cortexproject/cortex/pkg/util" 27 util_log "github.com/cortexproject/cortex/pkg/util/log" 28 ) 29 30 var ( 31 ErrEmailNotificationsAreDisabled = errors.New("email notifications are disabled") 32 ErrWebhookNotificationsAreDisabled = errors.New("webhook notifications are disabled") 33 ) 34 35 // Config configures Configs API 36 type Config struct { 37 Notifications NotificationsConfig `yaml:"notifications"` 38 } 39 40 // NotificationsConfig configures Alertmanager notifications method. 41 type NotificationsConfig struct { 42 DisableEmail bool `yaml:"disable_email"` 43 DisableWebHook bool `yaml:"disable_webhook"` 44 } 45 46 // RegisterFlags adds the flags required to configure this to the given FlagSet. 47 func (cfg *Config) RegisterFlags(f *flag.FlagSet) { 48 f.BoolVar(&cfg.Notifications.DisableEmail, "configs.notifications.disable-email", false, "Disable Email notifications for Alertmanager.") 49 f.BoolVar(&cfg.Notifications.DisableWebHook, "configs.notifications.disable-webhook", false, "Disable WebHook notifications for Alertmanager.") 50 } 51 52 // API implements the configs api. 53 type API struct { 54 http.Handler 55 db db.DB 56 cfg Config 57 } 58 59 // New creates a new API 60 func New(database db.DB, cfg Config) *API { 61 a := &API{ 62 db: database, 63 cfg: cfg, 64 } 65 r := mux.NewRouter() 66 a.RegisterRoutes(r) 67 a.Handler = r 68 return a 69 } 70 71 func (a *API) admin(w http.ResponseWriter, r *http.Request) { 72 w.Header().Add("Content-Type", "text/html") 73 fmt.Fprintf(w, ` 74 <!doctype html> 75 <html> 76 <head><title>configs :: configuration service</title></head> 77 <body> 78 <h1>configs :: configuration service</h1> 79 </body> 80 </html> 81 `) 82 } 83 84 // RegisterRoutes registers the configs API HTTP routes with the provided Router. 85 func (a *API) RegisterRoutes(r *mux.Router) { 86 for _, route := range []struct { 87 name, method, path string 88 handler http.HandlerFunc 89 }{ 90 {"root", "GET", "/", a.admin}, 91 // Dedicated APIs for updating rules config. In the future, these *must* 92 // be used. 93 {"get_rules", "GET", "/api/prom/configs/rules", a.getConfig}, 94 {"set_rules", "POST", "/api/prom/configs/rules", a.setConfig}, 95 {"get_templates", "GET", "/api/prom/configs/templates", a.getConfig}, 96 {"set_templates", "POST", "/api/prom/configs/templates", a.setConfig}, 97 {"get_alertmanager_config", "GET", "/api/prom/configs/alertmanager", a.getConfig}, 98 {"set_alertmanager_config", "POST", "/api/prom/configs/alertmanager", a.setConfig}, 99 {"validate_alertmanager_config", "POST", "/api/prom/configs/alertmanager/validate", a.validateAlertmanagerConfig}, 100 {"deactivate_config", "DELETE", "/api/prom/configs/deactivate", a.deactivateConfig}, 101 {"restore_config", "POST", "/api/prom/configs/restore", a.restoreConfig}, 102 // Internal APIs. 103 {"private_get_rules", "GET", "/private/api/prom/configs/rules", a.getConfigs}, 104 {"private_get_alertmanager_config", "GET", "/private/api/prom/configs/alertmanager", a.getConfigs}, 105 } { 106 r.Handle(route.path, route.handler).Methods(route.method).Name(route.name) 107 } 108 } 109 110 // getConfig returns the request configuration. 111 func (a *API) getConfig(w http.ResponseWriter, r *http.Request) { 112 userID, _, err := tenant.ExtractTenantIDFromHTTPRequest(r) 113 if err != nil { 114 http.Error(w, err.Error(), http.StatusUnauthorized) 115 return 116 } 117 logger := util_log.WithContext(r.Context(), util_log.Logger) 118 119 cfg, err := a.db.GetConfig(r.Context(), userID) 120 if err == sql.ErrNoRows { 121 http.Error(w, "No configuration", http.StatusNotFound) 122 return 123 } else if err != nil { 124 // XXX: Untested 125 level.Error(logger).Log("msg", "error getting config", "err", err) 126 http.Error(w, err.Error(), http.StatusInternalServerError) 127 return 128 } 129 130 switch parseConfigFormat(r.Header.Get("Accept"), FormatJSON) { 131 case FormatJSON: 132 w.Header().Set("Content-Type", "application/json") 133 err = json.NewEncoder(w).Encode(cfg) 134 case FormatYAML: 135 w.Header().Set("Content-Type", "application/yaml") 136 err = yaml.NewEncoder(w).Encode(cfg) 137 default: 138 // should never reach this point 139 level.Error(logger).Log("msg", "unexpected error detecting the config format") 140 http.Error(w, err.Error(), http.StatusInternalServerError) 141 } 142 if err != nil { 143 // XXX: Untested 144 level.Error(logger).Log("msg", "error encoding config", "err", err) 145 http.Error(w, err.Error(), http.StatusInternalServerError) 146 } 147 } 148 149 func (a *API) setConfig(w http.ResponseWriter, r *http.Request) { 150 userID, _, err := tenant.ExtractTenantIDFromHTTPRequest(r) 151 if err != nil { 152 http.Error(w, err.Error(), http.StatusUnauthorized) 153 return 154 } 155 logger := util_log.WithContext(r.Context(), util_log.Logger) 156 157 var cfg userconfig.Config 158 switch parseConfigFormat(r.Header.Get("Content-Type"), FormatJSON) { 159 case FormatJSON: 160 if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil { 161 // XXX: Untested 162 level.Error(logger).Log("msg", "error decoding json body", "err", err) 163 http.Error(w, err.Error(), http.StatusBadRequest) 164 return 165 } 166 case FormatYAML: 167 if err := yaml.NewDecoder(r.Body).Decode(&cfg); err != nil { 168 // XXX: Untested 169 level.Error(logger).Log("msg", "error decoding yaml body", "err", err) 170 http.Error(w, err.Error(), http.StatusBadRequest) 171 return 172 } 173 default: 174 // should never reach this point 175 level.Error(logger).Log("msg", "unexpected error detecting the config format") 176 http.Error(w, err.Error(), http.StatusInternalServerError) 177 return 178 } 179 180 if err := validateAlertmanagerConfig(cfg.AlertmanagerConfig, a.cfg.Notifications); err != nil && cfg.AlertmanagerConfig != "" { 181 level.Error(logger).Log("msg", "invalid Alertmanager config", "err", err) 182 http.Error(w, fmt.Sprintf("Invalid Alertmanager config: %v", err), http.StatusBadRequest) 183 return 184 } 185 if err := validateRulesFiles(cfg); err != nil { 186 level.Error(logger).Log("msg", "invalid rules", "err", err) 187 http.Error(w, fmt.Sprintf("Invalid rules: %v", err), http.StatusBadRequest) 188 return 189 } 190 if err := validateTemplateFiles(cfg); err != nil { 191 level.Error(logger).Log("msg", "invalid templates", "err", err) 192 http.Error(w, fmt.Sprintf("Invalid templates: %v", err), http.StatusBadRequest) 193 return 194 } 195 if err := a.db.SetConfig(r.Context(), userID, cfg); err != nil { 196 // XXX: Untested 197 level.Error(logger).Log("msg", "error storing config", "err", err) 198 http.Error(w, err.Error(), http.StatusInternalServerError) 199 return 200 } 201 w.WriteHeader(http.StatusNoContent) 202 } 203 204 func (a *API) validateAlertmanagerConfig(w http.ResponseWriter, r *http.Request) { 205 logger := util_log.WithContext(r.Context(), util_log.Logger) 206 cfg, err := ioutil.ReadAll(r.Body) 207 if err != nil { 208 level.Error(logger).Log("msg", "error reading request body", "err", err) 209 http.Error(w, err.Error(), http.StatusInternalServerError) 210 return 211 } 212 213 if err = validateAlertmanagerConfig(string(cfg), a.cfg.Notifications); err != nil { 214 w.WriteHeader(http.StatusBadRequest) 215 util.WriteJSONResponse(w, map[string]string{ 216 "status": "error", 217 "error": err.Error(), 218 }) 219 return 220 } 221 222 util.WriteJSONResponse(w, map[string]string{ 223 "status": "success", 224 }) 225 } 226 227 func validateAlertmanagerConfig(cfg string, noCfg NotificationsConfig) error { 228 amCfg, err := amconfig.Load(cfg) 229 if err != nil { 230 return err 231 } 232 233 for _, recv := range amCfg.Receivers { 234 if noCfg.DisableEmail && len(recv.EmailConfigs) > 0 { 235 return ErrEmailNotificationsAreDisabled 236 } 237 if noCfg.DisableWebHook && len(recv.WebhookConfigs) > 0 { 238 return ErrWebhookNotificationsAreDisabled 239 } 240 } 241 242 return nil 243 } 244 245 func validateRulesFiles(c userconfig.Config) error { 246 _, err := c.RulesConfig.Parse() 247 return err 248 } 249 250 func validateTemplateFiles(c userconfig.Config) error { 251 for fn, content := range c.TemplateFiles { 252 if _, err := template.New(fn).Funcs(template.FuncMap(amtemplate.DefaultFuncs)).Parse(content); err != nil { 253 return err 254 } 255 } 256 257 return nil 258 } 259 260 // ConfigsView renders multiple configurations, mapping userID to userconfig.View. 261 // Exposed only for tests. 262 type ConfigsView struct { 263 Configs map[string]userconfig.View `json:"configs"` 264 } 265 266 func (a *API) getConfigs(w http.ResponseWriter, r *http.Request) { 267 var cfgs map[string]userconfig.View 268 var cfgErr error 269 logger := util_log.WithContext(r.Context(), util_log.Logger) 270 rawSince := r.FormValue("since") 271 if rawSince == "" { 272 cfgs, cfgErr = a.db.GetAllConfigs(r.Context()) 273 } else { 274 since, err := strconv.ParseUint(rawSince, 10, 0) 275 if err != nil { 276 level.Info(logger).Log("msg", "invalid config ID", "err", err) 277 http.Error(w, err.Error(), http.StatusBadRequest) 278 return 279 } 280 cfgs, cfgErr = a.db.GetConfigs(r.Context(), userconfig.ID(since)) 281 } 282 283 if cfgErr != nil { 284 // XXX: Untested 285 level.Error(logger).Log("msg", "error getting configs", "err", cfgErr) 286 http.Error(w, cfgErr.Error(), http.StatusInternalServerError) 287 return 288 } 289 290 view := ConfigsView{Configs: cfgs} 291 w.Header().Set("Content-Type", "application/json") 292 if err := json.NewEncoder(w).Encode(view); err != nil { 293 // XXX: Untested 294 http.Error(w, err.Error(), http.StatusInternalServerError) 295 return 296 } 297 } 298 299 func (a *API) deactivateConfig(w http.ResponseWriter, r *http.Request) { 300 userID, _, err := tenant.ExtractTenantIDFromHTTPRequest(r) 301 if err != nil { 302 http.Error(w, err.Error(), http.StatusUnauthorized) 303 return 304 } 305 logger := util_log.WithContext(r.Context(), util_log.Logger) 306 307 if err := a.db.DeactivateConfig(r.Context(), userID); err != nil { 308 if err == sql.ErrNoRows { 309 level.Info(logger).Log("msg", "deactivate config - no configuration", "userID", userID) 310 http.Error(w, "No configuration", http.StatusNotFound) 311 return 312 } 313 level.Error(logger).Log("msg", "error deactivating config", "err", err) 314 http.Error(w, err.Error(), http.StatusInternalServerError) 315 return 316 } 317 level.Info(logger).Log("msg", "config deactivated", "userID", userID) 318 w.WriteHeader(http.StatusOK) 319 } 320 321 func (a *API) restoreConfig(w http.ResponseWriter, r *http.Request) { 322 userID, _, err := tenant.ExtractTenantIDFromHTTPRequest(r) 323 if err != nil { 324 http.Error(w, err.Error(), http.StatusUnauthorized) 325 return 326 } 327 logger := util_log.WithContext(r.Context(), util_log.Logger) 328 329 if err := a.db.RestoreConfig(r.Context(), userID); err != nil { 330 if err == sql.ErrNoRows { 331 level.Info(logger).Log("msg", "restore config - no configuration", "userID", userID) 332 http.Error(w, "No configuration", http.StatusNotFound) 333 return 334 } 335 level.Error(logger).Log("msg", "error restoring config", "err", err) 336 http.Error(w, err.Error(), http.StatusInternalServerError) 337 return 338 } 339 level.Info(logger).Log("msg", "config restored", "userID", userID) 340 w.WriteHeader(http.StatusOK) 341 } 342 343 const ( 344 FormatInvalid = "invalid" 345 FormatJSON = "json" 346 FormatYAML = "yaml" 347 ) 348 349 func parseConfigFormat(v string, defaultFormat string) string { 350 if v == "" { 351 return defaultFormat 352 } 353 parts := strings.Split(v, ",") 354 for _, part := range parts { 355 mimeType, _, err := mime.ParseMediaType(part) 356 if err != nil { 357 continue 358 } 359 switch mimeType { 360 case "application/json": 361 return FormatJSON 362 case "text/yaml", "text/x-yaml", "application/yaml", "application/x-yaml": 363 return FormatYAML 364 } 365 } 366 return defaultFormat 367 }