diff --git a/api/config.go b/api/config.go index 96443818d..c239b306d 100644 --- a/api/config.go +++ b/api/config.go @@ -1,6 +1,31 @@ package api -import "time" +import ( + "net/http" + "time" +) + +// WebContact is container for web ui contact validation. +type WebContact struct { + ContactType string `json:"type" example:"webhook"` + ContactLabel string `json:"label" example:"Webhook"` + ValidationRegex string `json:"validation,omitempty" example:"^(http|https):\\/\\/.*(moira.ru)(:[0-9]{2,5})?\\/"` + Placeholder string `json:"placeholder,omitempty" example:"https://moira.ru/webhooks"` + Help string `json:"help,omitempty" example:"### Domains whitelist:\n - moira.ru\n"` +} + +// FeatureFlags is struct to manage feature flags. +type FeatureFlags struct { + IsPlottingDefaultOn bool `json:"isPlottingDefaultOn" example:"false"` + IsPlottingAvailable bool `json:"isPlottingAvailable" example:"true"` + IsSubscriptionToAllTagsAvailable bool `json:"isSubscriptionToAllTagsAvailable" example:"false"` + IsReadonlyEnabled bool `json:"isReadonlyEnabled" example:"false"` +} + +// Sentry - config for sentry settings +type Sentry struct { + DSN string `json:"dsn,omitempty" example:"https://secret@sentry.host"` +} // Config for api configuration variables. type Config struct { @@ -14,25 +39,13 @@ type Config struct { // WebConfig is container for web ui configuration parameters. type WebConfig struct { - SupportEmail string `json:"supportEmail,omitempty"` - RemoteAllowed bool `json:"remoteAllowed"` + SupportEmail string `json:"supportEmail,omitempty" example:"opensource@skbkontur.com"` + RemoteAllowed bool `json:"remoteAllowed" example:"true"` Contacts []WebContact `json:"contacts"` FeatureFlags FeatureFlags `json:"featureFlags"` + Sentry Sentry `json:"sentry"` } -// WebContact is container for web ui contact validation. -type WebContact struct { - ContactType string `json:"type"` - ContactLabel string `json:"label"` - ValidationRegex string `json:"validation,omitempty"` - Placeholder string `json:"placeholder,omitempty"` - Help string `json:"help,omitempty"` -} - -// FeatureFlags is struct to manage feature flags. -type FeatureFlags struct { - IsPlottingDefaultOn bool `json:"isPlottingDefaultOn"` - IsPlottingAvailable bool `json:"isPlottingAvailable"` - IsSubscriptionToAllTagsAvailable bool `json:"isSubscriptionToAllTagsAvailable"` - IsReadonlyEnabled bool `json:"isReadonlyEnabled"` +func (WebConfig) Render(w http.ResponseWriter, r *http.Request) error { + return nil } diff --git a/api/handler/config.go b/api/handler/config.go index bfa6a84f1..780d3509a 100644 --- a/api/handler/config.go +++ b/api/handler/config.go @@ -1,31 +1,26 @@ package handler -import "net/http" +import ( + "net/http" -type ContactExample struct { - Type string `json:"type" example:"webhook kontur"` - Label string `json:"label" example:"Webhook Kontur"` - Validation string `json:"validation" example:"^(http|https):\\/\\/.*(moira.ru)(:[0-9]{2,5})?\\/"` - Placeholder string `json:"placeholder" example:"https://moira.ru/webhooks/moira"` - Help string `json:"help" example:"### Domains whitelist:\n - moira.ru\n"` -} - -type ConfigurationResponse struct { - RemoteAllowed bool `json:"remoteAllowed" example:"false"` - Contacts []ContactExample `json:"contacts"` -} + "github.com/go-chi/render" + "github.com/moira-alert/moira/api" +) // nolint: gofmt,goimports // -// @summary Get available configuration +// @summary Get web configuration // @id get-web-config // @tags config // @produce json -// @success 200 {object} ConfigurationResponse "Configuration fetched successfully" +// @success 200 {object} api.WebConfig "Configuration fetched successfully" +// @failure 422 {object} api.ErrorRenderExample "Render error" // @router /config [get] -func getWebConfig(configContent []byte) http.HandlerFunc { +func getWebConfig(webConfig *api.WebConfig) http.HandlerFunc { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - writer.Header().Set("Content-Type", "application/json") - writer.Write(configContent) //nolint + if err := render.Render(writer, request, webConfig); err != nil { + render.Render(writer, request, api.ErrorRender(err)) //nolint + return + } }) } diff --git a/api/handler/handler.go b/api/handler/handler.go index 0ceb343fd..d62f4d1ab 100644 --- a/api/handler/handler.go +++ b/api/handler/handler.go @@ -28,9 +28,9 @@ func NewHandler( db moira.Database, log moira.Logger, index moira.Searcher, - config *api.Config, + apiConfig *api.Config, metricSourceProvider *metricSource.SourceProvider, - webConfigContent []byte, + webConfig *api.WebConfig, ) http.Handler { database = db searchIndex = index @@ -94,13 +94,13 @@ func NewHandler( router.Use(moiramiddle.DatabaseContext(database)) router.Route("/health", health) router.Route("/", func(router chi.Router) { - router.Use(moiramiddle.ReadOnlyMiddleware(config)) - router.Get("/config", getWebConfig(webConfigContent)) + router.Use(moiramiddle.ReadOnlyMiddleware(apiConfig)) + router.Get("/config", getWebConfig(webConfig)) router.Route("/user", user) router.With(moiramiddle.Triggers( - config.GraphiteLocalMetricTTL, - config.GraphiteRemoteMetricTTL, - config.PrometheusRemoteMetricTTL, + apiConfig.GraphiteLocalMetricTTL, + apiConfig.GraphiteRemoteMetricTTL, + apiConfig.PrometheusRemoteMetricTTL, )).Route("/trigger", triggers(metricSourceProvider, searchIndex)) router.Route("/tag", tag) router.Route("/pattern", pattern) @@ -118,7 +118,7 @@ func NewHandler( }) }) - if config.EnableCORS { + if apiConfig.EnableCORS { return cors.AllowAll().Handler(router) } return router diff --git a/api/handler/handler_test.go b/api/handler/handler_test.go index 5512f3448..43415a2d9 100644 --- a/api/handler/handler_test.go +++ b/api/handler/handler_test.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "net/http/httptest" + "strings" "testing" "github.com/golang/mock/gomock" @@ -27,8 +28,11 @@ func TestReadonlyMode(t *testing.T) { logger, _ := zerolog_adapter.GetLogger("Test") config := &api.Config{Flags: api.FeatureFlags{IsReadonlyEnabled: true}} - expectedConfig := []byte("Expected config") - handler := NewHandler(mockDb, logger, nil, config, nil, expectedConfig) + webConfig := &api.WebConfig{ + SupportEmail: "test", + Contacts: []api.WebContact{}, + } + handler := NewHandler(mockDb, logger, nil, config, nil, webConfig) Convey("Get notifier health", func() { mockDb.EXPECT().GetNotifierState().Return("OK", nil).Times(1) @@ -92,10 +96,16 @@ func TestReadonlyMode(t *testing.T) { response := responseWriter.Result() defer response.Body.Close() - actual, _ := io.ReadAll(response.Body) + actual, err := io.ReadAll(response.Body) + So(err, ShouldBeNil) + actualStr := strings.TrimSpace(string(actual)) + + expected, err := json.Marshal(webConfig) + So(err, ShouldBeNil) + expectedStr := strings.TrimSpace(string(expected)) So(response.StatusCode, ShouldEqual, http.StatusOK) - So(actual, ShouldResemble, expectedConfig) + So(actualStr, ShouldResemble, expectedStr) }) }) } diff --git a/cmd/api/config.go b/cmd/api/config.go index 0a4995735..7ef257d13 100644 --- a/cmd/api/config.go +++ b/cmd/api/config.go @@ -1,9 +1,6 @@ package main import ( - "encoding/json" - "fmt" - "github.com/moira-alert/moira/notifier" "github.com/xiam/to" @@ -30,15 +27,27 @@ type apiConfig struct { EnableCORS bool `yaml:"enable_cors"` } +type sentryConfig struct { + DSN string `yaml:"dsn"` +} + +func (config *sentryConfig) getSettings() api.Sentry { + return api.Sentry{ + DSN: config.DSN, + } +} + type webConfig struct { - // Moira administrator email address. + // Moira administrator email address SupportEmail string `yaml:"supportEmail"` // If true, users will be able to choose Graphite as trigger metrics data source RemoteAllowed bool // List of enabled contact types Contacts []webContact `yaml:"contacts"` - // struct to manage feature flags. + // Struct to manage feature flags FeatureFlags featureFlags `yaml:"feature_flags"` + // Returns the sentry configuration for the frontend + Sentry sentryConfig `yaml:"sentry"` } type webContact struct { @@ -75,7 +84,7 @@ func (config *apiConfig) getSettings( } } -func (config *webConfig) getSettings(isRemoteEnabled bool) ([]byte, error) { +func (config *webConfig) getSettings(isRemoteEnabled bool) *api.WebConfig { webContacts := make([]api.WebContact, 0, len(config.Contacts)) for _, configContact := range config.Contacts { contact := api.WebContact{ @@ -87,16 +96,14 @@ func (config *webConfig) getSettings(isRemoteEnabled bool) ([]byte, error) { } webContacts = append(webContacts, contact) } - configContent, err := json.Marshal(api.WebConfig{ + + return &api.WebConfig{ SupportEmail: config.SupportEmail, RemoteAllowed: isRemoteEnabled, Contacts: webContacts, FeatureFlags: config.getFeatureFlags(), - }) - if err != nil { - return make([]byte, 0), fmt.Errorf("failed to parse web config: %s", err.Error()) + Sentry: config.Sentry.getSettings(), } - return configContent, nil } func (config *webConfig) getFeatureFlags() api.FeatureFlags { diff --git a/cmd/api/config_test.go b/cmd/api/config_test.go index 0b24df01f..2e0a4fb2d 100644 --- a/cmd/api/config_test.go +++ b/cmd/api/config_test.go @@ -112,23 +112,32 @@ func Test_webConfig_getDefault(t *testing.T) { func Test_webConfig_getSettings(t *testing.T) { Convey("Empty config, fill it", t, func() { - wC := webConfig{} + config := webConfig{} - result, err := wC.getSettings(true) - So(err, ShouldBeEmpty) - So(string(result), ShouldResemble, `{"remoteAllowed":true,"contacts":[],"featureFlags":{"isPlottingDefaultOn":false,"isPlottingAvailable":false,"isSubscriptionToAllTagsAvailable":false,"isReadonlyEnabled":false}}`) + settings := config.getSettings(true) + So(settings, ShouldResemble, &api.WebConfig{ + RemoteAllowed: true, + Contacts: []api.WebContact{}, + }) }) Convey("Default config, fill it", t, func() { config := getDefault() - result, err := config.Web.getSettings(true) - So(err, ShouldBeEmpty) - So(string(result), ShouldResemble, `{"remoteAllowed":true,"contacts":[],"featureFlags":{"isPlottingDefaultOn":true,"isPlottingAvailable":true,"isSubscriptionToAllTagsAvailable":true,"isReadonlyEnabled":false}}`) + settings := config.Web.getSettings(true) + So(settings, ShouldResemble, &api.WebConfig{ + RemoteAllowed: true, + Contacts: []api.WebContact{}, + FeatureFlags: api.FeatureFlags{ + IsPlottingDefaultOn: true, + IsPlottingAvailable: true, + IsSubscriptionToAllTagsAvailable: true, + }, + }) }) Convey("Not empty config, fill it", t, func() { - wC := webConfig{ + config := webConfig{ SupportEmail: "lalal@mail.la", RemoteAllowed: false, Contacts: []webContact{ @@ -142,13 +151,36 @@ func Test_webConfig_getSettings(t *testing.T) { }, FeatureFlags: featureFlags{ IsPlottingDefaultOn: true, - IsPlottingAvailable: false, + IsPlottingAvailable: true, IsSubscriptionToAllTagsAvailable: true, + IsReadonlyEnabled: false, + }, + Sentry: sentryConfig{ + DSN: "test dsn", }, } - result, err := wC.getSettings(true) - So(err, ShouldBeEmpty) - So(string(result), ShouldResemble, `{"supportEmail":"lalal@mail.la","remoteAllowed":true,"contacts":[{"type":"slack","label":"label","validation":"t(\\d+)","help":"help"}],"featureFlags":{"isPlottingDefaultOn":true,"isPlottingAvailable":false,"isSubscriptionToAllTagsAvailable":true,"isReadonlyEnabled":false}}`) + settings := config.getSettings(true) + So(settings, ShouldResemble, &api.WebConfig{ + SupportEmail: "lalal@mail.la", + RemoteAllowed: true, + Contacts: []api.WebContact{ + { + ContactType: "slack", + ContactLabel: "label", + ValidationRegex: "t(\\d+)", + Help: "help", + }, + }, + FeatureFlags: api.FeatureFlags{ + IsPlottingDefaultOn: true, + IsPlottingAvailable: true, + IsSubscriptionToAllTagsAvailable: true, + IsReadonlyEnabled: false, + }, + Sentry: api.Sentry{ + DSN: "test dsn", + }, + }) }) } diff --git a/cmd/api/main.go b/cmd/api/main.go index 17aff7865..8d282177d 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -141,12 +141,7 @@ func main() { prometheusSource, ) - webConfigContent, err := applicationConfig.Web.getSettings(remoteConfig.Enabled || prometheusConfig.Enabled) - if err != nil { - logger.Fatal(). - Error(err). - Msg("Failed to get web applicationConfig content ") - } + webConfig := applicationConfig.Web.getSettings(remoteConfig.Enabled || prometheusConfig.Enabled) httpHandler := handler.NewHandler( database, @@ -154,7 +149,7 @@ func main() { searchIndex, apiConfig, metricSourceProvider, - webConfigContent, + webConfig, ) server := &http.Server{