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  }