From 8af08d0df29cadeb058cd5623e2c7418a9a7127d Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Mon, 11 Dec 2023 21:36:51 -0500 Subject: [PATCH] Alerting: Add export of mute timings to file provisioning formats (#79225) * add export of mute timings to file provisioning formats * support export of mute timings to HCL --- pkg/services/ngalert/api/api_provisioning.go | 94 +++++++--- .../ngalert/api/api_provisioning_test.go | 160 +++++++++++++++++- pkg/services/ngalert/api/authorization.go | 4 +- .../ngalert/api/authorization_test.go | 2 +- pkg/services/ngalert/api/compat.go | 31 ++++ .../api/generated_base_api_provisioning.go | 34 ++++ pkg/services/ngalert/api/provisioning.go | 8 + ...lertmanager_default_mutetimings-export.hcl | 20 +++ ...ertmanager_default_mutetimings-export.json | 1 + ...ertmanager_default_mutetimings-export.yaml | 16 ++ pkg/services/ngalert/api/tooling/api.json | 110 +++++++++++- .../api/tooling/definitions/provisioning.go | 3 +- .../definitions/provisioning_mute_timings.go | 45 ++++- pkg/services/ngalert/api/tooling/post.json | 115 ++++++++++++- pkg/services/ngalert/api/tooling/spec.json | 117 ++++++++++++- public/api-merged.json | 110 +++++++++++- public/openapi3.json | 136 ++++++++++++++- 17 files changed, 965 insertions(+), 41 deletions(-) create mode 100644 pkg/services/ngalert/api/test-data/alertmanager_default_mutetimings-export.hcl create mode 100644 pkg/services/ngalert/api/test-data/alertmanager_default_mutetimings-export.json create mode 100644 pkg/services/ngalert/api/test-data/alertmanager_default_mutetimings-export.yaml diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 68a83f7ad92..c8e67e7cf05 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -255,6 +255,20 @@ func (srv *ProvisioningSrv) RouteGetMuteTiming(c *contextmodel.ReqContext, name return response.Empty(http.StatusNotFound) } +func (srv *ProvisioningSrv) RouteGetMuteTimingExport(c *contextmodel.ReqContext, name string) response.Response { + timings, err := srv.muteTimings.GetMuteTimings(c.Req.Context(), c.SignedInUser.GetOrgID()) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + for _, timing := range timings { + if name == timing.Name { + e := AlertingFileExportFromMuteTimings(c.SignedInUser.GetOrgID(), []definitions.MuteTimeInterval{timing}) + return exportResponse(c, e) + } + } + return response.Empty(http.StatusNotFound) +} + func (srv *ProvisioningSrv) RouteGetMuteTimings(c *contextmodel.ReqContext) response.Response { timings, err := srv.muteTimings.GetMuteTimings(c.Req.Context(), c.SignedInUser.GetOrgID()) if err != nil { @@ -263,6 +277,15 @@ func (srv *ProvisioningSrv) RouteGetMuteTimings(c *contextmodel.ReqContext) resp return response.JSON(http.StatusOK, timings) } +func (srv *ProvisioningSrv) RouteGetMuteTimingsExport(c *contextmodel.ReqContext) response.Response { + timings, err := srv.muteTimings.GetMuteTimings(c.Req.Context(), c.SignedInUser.GetOrgID()) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + e := AlertingFileExportFromMuteTimings(c.SignedInUser.GetOrgID(), timings) + return exportResponse(c, e) +} + func (srv *ProvisioningSrv) RoutePostMuteTiming(c *contextmodel.ReqContext, mt definitions.MuteTimeInterval) response.Response { mt.Provenance = determineProvenance(c) created, err := srv.muteTimings.CreateMuteTiming(c.Req.Context(), mt, c.SignedInUser.GetOrgID()) @@ -551,36 +574,53 @@ func exportResponse(c *contextmodel.ReqContext, body definitions.AlertingFileExp } func exportHcl(download bool, body definitions.AlertingFileExport) response.Response { - resources := make([]hcl.Resource, 0, len(body.Groups)+len(body.ContactPoints)+len(body.Policies)) - for idx, group := range body.Groups { - gr := group - resources = append(resources, hcl.Resource{ - Type: "grafana_rule_group", - Name: fmt.Sprintf("rule_group_%04d", idx), - Body: &gr, - }) - } - for idx, cp := range body.ContactPoints { - upd, err := ContactPointFromContactPointExport(cp) - if err != nil { - return response.Error(http.StatusInternalServerError, "failed to convert contact points to HCL", err) + resources := make([]hcl.Resource, 0, len(body.Groups)+len(body.ContactPoints)+len(body.Policies)+len(body.MuteTimings)) + convertToResources := func() error { + for idx, group := range body.Groups { + gr := group + resources = append(resources, hcl.Resource{ + Type: "grafana_rule_group", + Name: fmt.Sprintf("rule_group_%04d", idx), + Body: &gr, + }) + } + for idx, cp := range body.ContactPoints { + upd, err := ContactPointFromContactPointExport(cp) + if err != nil { + return fmt.Errorf("failed to convert contact points to HCL:%w", err) + } + resources = append(resources, hcl.Resource{ + Type: "grafana_contact_point", + Name: fmt.Sprintf("contact_point_%d", idx), + Body: &upd, + }) } - resources = append(resources, hcl.Resource{ - Type: "grafana_contact_point", - Name: fmt.Sprintf("contact_point_%d", idx), - Body: &upd, - }) - } - for idx, cp := range body.Policies { - policy := cp.RouteExport - resources = append(resources, hcl.Resource{ - Type: "grafana_notification_policy", - Name: fmt.Sprintf("notification_policy_%d", idx+1), - Body: policy, - }) - } + for idx, cp := range body.Policies { + policy := cp.RouteExport + resources = append(resources, hcl.Resource{ + Type: "grafana_notification_policy", + Name: fmt.Sprintf("notification_policy_%d", idx+1), + Body: policy, + }) + } + for idx, mt := range body.MuteTimings { + mthcl, err := MuteTimingIntervalToMuteTimeIntervalHclExport(mt) + if err != nil { + return fmt.Errorf("failed to convert mute timing [%s] to HCL:%w", mt.Name, err) + } + resources = append(resources, hcl.Resource{ + Type: "grafana_mute_timing", + Name: fmt.Sprintf("mute_timing_%d", idx+1), + Body: mthcl, + }) + } + return nil + } + if err := convertToResources(); err != nil { + return response.Error(500, "failed to convert to HCL resources", err) + } hclBody, err := hcl.Encode(resources...) if err != nil { return response.Error(500, "body hcl encode", err) diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index 7062bcc1a09..ded0b5196d6 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "path" "testing" "time" @@ -1150,6 +1151,132 @@ func TestProvisioningApi(t *testing.T) { require.Equal(t, expectedResponse, string(response.Body())) }) }) + + t.Run("mute timings", func(t *testing.T) { + t.Run("are present, GET returns 200", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + + response := sut.RouteGetMuteTimingsExport(&rc) + + require.Equal(t, 200, response.Status()) + }) + + t.Run("accept header contains yaml, GET returns text yaml", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + + rc.Context.Req.Header.Add("Accept", "application/yaml") + response := sut.RouteGetMuteTimingsExport(&rc) + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "text/yaml", rc.Context.Resp.Header().Get("Content-Type")) + }) + + t.Run("accept header contains json, GET returns json", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + + rc.Context.Req.Header.Add("Accept", "application/json") + response := sut.RouteGetMuteTimingsExport(&rc) + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type")) + }) + + t.Run("accept header contains json and yaml, GET returns json", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + + rc.Context.Req.Header.Add("Accept", "application/json, application/yaml") + response := sut.RouteGetMuteTimingsExport(&rc) + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type")) + }) + + t.Run("query param download=true, GET returns content disposition attachment", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + + rc.Context.Req.Form.Set("download", "true") + response := sut.RouteGetMuteTimingsExport(&rc) + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Contains(t, rc.Context.Resp.Header().Get("Content-Disposition"), "attachment") + }) + + t.Run("query param download=false, GET returns empty content disposition", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + + rc.Context.Req.Form.Set("download", "false") + response := sut.RouteGetMuteTimingsExport(&rc) + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition")) + }) + + t.Run("query param download not set, GET returns empty content disposition", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + + response := sut.RouteGetMuteTimingsExport(&rc) + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition")) + }) + + t.Run("json body content is as expected", func(t *testing.T) { + expectedResponse, err := testData.ReadFile(path.Join("test-data", "alertmanager_default_mutetimings-export.json")) + require.NoError(t, err) + sut := createProvisioningSrvSut(t) + sut.policies = createFakeNotificationPolicyService() + rc := createTestRequestCtx() + + rc.Context.Req.Header.Add("Accept", "application/json") + response := sut.RouteGetMuteTimingsExport(&rc) + + require.Equal(t, 200, response.Status()) + require.JSONEq(t, string(expectedResponse), string(response.Body())) + }) + + t.Run("yaml body content is as expected", func(t *testing.T) { + expectedResponse, err := testData.ReadFile(path.Join("test-data", "alertmanager_default_mutetimings-export.yaml")) + require.NoError(t, err) + sut := createProvisioningSrvSut(t) + sut.policies = createFakeNotificationPolicyService() + rc := createTestRequestCtx() + + rc.Context.Req.Header.Add("Accept", "application/yaml") + + response := sut.RouteGetMuteTimingsExport(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, string(expectedResponse), string(response.Body())) + }) + + t.Run("hcl body content is as expected", func(t *testing.T) { + expectedResponse, err := testData.ReadFile(path.Join("test-data", "alertmanager_default_mutetimings-export.hcl")) + require.NoError(t, err) + sut := createProvisioningSrvSut(t) + sut.policies = createFakeNotificationPolicyService() + rc := createTestRequestCtx() + + rc.Context.Req.Form.Add("format", "hcl") + + response := sut.RouteGetMuteTimingsExport(&rc) + t.Log(string(response.Body())) + require.Equal(t, 200, response.Status()) + require.Equal(t, string(expectedResponse), string(response.Body())) + }) + }) }) } @@ -1765,7 +1892,38 @@ var testConfig = ` "mute_time_intervals": [{ "name": "interval", "time_intervals": [] - }] + }, { + "name": "full-interval", + "time_intervals": [ + { + "times": [ + { + "start_time": "10:00", + "end_time": "12:00" + } + ], + "weekdays": [ + "monday", + "wednesday", + "friday" + ], + "days_of_month": [ + "1", + "14:16", + "20" + ], + "months": [ + "1:3", + "7", + "12" + ], + "years": [ + "2023:2025" + ], + "location": "America/New_York" + } + ] + }] } } ` diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index 5b5a44a151d..92a131721e5 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -182,7 +182,9 @@ func (api *API) authorize(method, path string) web.Handler { // Grafana-only Provisioning Read Paths case http.MethodGet + "/api/v1/provisioning/policies/export", - http.MethodGet + "/api/v1/provisioning/contact-points/export": + http.MethodGet + "/api/v1/provisioning/contact-points/export", + http.MethodGet + "/api/v1/provisioning/mute-timings/export", + http.MethodGet + "/api/v1/provisioning/mute-timings/{name}/export": eval = ac.EvalAny( ac.EvalPermission(ac.ActionAlertingNotificationsRead), // organization scope ac.EvalPermission(ac.ActionAlertingProvisioningRead), // organization scope diff --git a/pkg/services/ngalert/api/authorization_test.go b/pkg/services/ngalert/api/authorization_test.go index 213be17fd70..2502ea3e092 100644 --- a/pkg/services/ngalert/api/authorization_test.go +++ b/pkg/services/ngalert/api/authorization_test.go @@ -40,7 +40,7 @@ func TestAuthorize(t *testing.T) { } paths[p] = methods } - require.Len(t, paths, 52) + require.Len(t, paths, 54) ac := acmock.New() api := &API{AccessControl: ac} diff --git a/pkg/services/ngalert/api/compat.go b/pkg/services/ngalert/api/compat.go index aefa0ae6a30..d27980d0968 100644 --- a/pkg/services/ngalert/api/compat.go +++ b/pkg/services/ngalert/api/compat.go @@ -4,6 +4,7 @@ import ( "encoding/json" "time" + jsoniter "github.com/json-iterator/go" "github.com/prometheus/common/model" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -342,3 +343,33 @@ func NilIfEmpty[T any](v *[]T) *[]T { } return v } + +func AlertingFileExportFromMuteTimings(orgID int64, m []definitions.MuteTimeInterval) definitions.AlertingFileExport { + f := definitions.AlertingFileExport{ + APIVersion: 1, + MuteTimings: make([]definitions.MuteTimeIntervalExport, 0, len(m)), + } + for _, mi := range m { + f.MuteTimings = append(f.MuteTimings, MuteTimeIntervalExportFromMuteTiming(orgID, mi)) + } + return f +} + +func MuteTimeIntervalExportFromMuteTiming(orgID int64, m definitions.MuteTimeInterval) definitions.MuteTimeIntervalExport { + return definitions.MuteTimeIntervalExport{ + OrgID: orgID, + MuteTimeInterval: m.MuteTimeInterval, + } +} + +// Converts definitions.MuteTimeIntervalExport to definitions.MuteTimeIntervalExportHcl using JSON marshalling. Returns error if structure could not be marshalled\unmarshalled +func MuteTimingIntervalToMuteTimeIntervalHclExport(m definitions.MuteTimeIntervalExport) (definitions.MuteTimeIntervalExportHcl, error) { + result := definitions.MuteTimeIntervalExportHcl{} + j := jsoniter.ConfigCompatibleWithStandardLibrary + mdata, err := j.Marshal(m) + if err != nil { + return result, err + } + err = j.Unmarshal(mdata, &result) + return result, err +} diff --git a/pkg/services/ngalert/api/generated_base_api_provisioning.go b/pkg/services/ngalert/api/generated_base_api_provisioning.go index e931284ea13..453d151101e 100644 --- a/pkg/services/ngalert/api/generated_base_api_provisioning.go +++ b/pkg/services/ngalert/api/generated_base_api_provisioning.go @@ -24,6 +24,8 @@ type ProvisioningApi interface { RouteDeleteContactpoints(*contextmodel.ReqContext) response.Response RouteDeleteMuteTiming(*contextmodel.ReqContext) response.Response RouteDeleteTemplate(*contextmodel.ReqContext) response.Response + RouteExportMuteTiming(*contextmodel.ReqContext) response.Response + RouteExportMuteTimings(*contextmodel.ReqContext) response.Response RouteGetAlertRule(*contextmodel.ReqContext) response.Response RouteGetAlertRuleExport(*contextmodel.ReqContext) response.Response RouteGetAlertRuleGroup(*contextmodel.ReqContext) response.Response @@ -70,6 +72,14 @@ func (f *ProvisioningApiHandler) RouteDeleteTemplate(ctx *contextmodel.ReqContex nameParam := web.Params(ctx.Req)[":name"] return f.handleRouteDeleteTemplate(ctx, nameParam) } +func (f *ProvisioningApiHandler) RouteExportMuteTiming(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + nameParam := web.Params(ctx.Req)[":name"] + return f.handleRouteExportMuteTiming(ctx, nameParam) +} +func (f *ProvisioningApiHandler) RouteExportMuteTimings(ctx *contextmodel.ReqContext) response.Response { + return f.handleRouteExportMuteTimings(ctx) +} func (f *ProvisioningApiHandler) RouteGetAlertRule(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters uIDParam := web.Params(ctx.Req)[":UID"] @@ -263,6 +273,30 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApi, m *metrics m, ), ) + group.Get( + toMacaronPath("/api/v1/provisioning/mute-timings/{name}/export"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodGet, "/api/v1/provisioning/mute-timings/{name}/export"), + metrics.Instrument( + http.MethodGet, + "/api/v1/provisioning/mute-timings/{name}/export", + api.Hooks.Wrap(srv.RouteExportMuteTiming), + m, + ), + ) + group.Get( + toMacaronPath("/api/v1/provisioning/mute-timings/export"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodGet, "/api/v1/provisioning/mute-timings/export"), + metrics.Instrument( + http.MethodGet, + "/api/v1/provisioning/mute-timings/export", + api.Hooks.Wrap(srv.RouteExportMuteTimings), + m, + ), + ) group.Get( toMacaronPath("/api/v1/provisioning/alert-rules/{UID}"), requestmeta.SetOwner(requestmeta.TeamAlerting), diff --git a/pkg/services/ngalert/api/provisioning.go b/pkg/services/ngalert/api/provisioning.go index 7430b74d7d3..f43e9bb8f7c 100644 --- a/pkg/services/ngalert/api/provisioning.go +++ b/pkg/services/ngalert/api/provisioning.go @@ -127,3 +127,11 @@ func (f *ProvisioningApiHandler) handleRouteGetAlertRuleGroupExport(ctx *context func (f *ProvisioningApiHandler) handleRoutePutAlertRuleGroup(ctx *contextmodel.ReqContext, ag apimodels.AlertRuleGroup, folder, group string) response.Response { return f.svc.RoutePutAlertRuleGroup(ctx, ag, folder, group) } + +func (f *ProvisioningApiHandler) handleRouteExportMuteTiming(ctx *contextmodel.ReqContext, name string) response.Response { + return f.svc.RouteGetMuteTimingExport(ctx, name) +} + +func (f *ProvisioningApiHandler) handleRouteExportMuteTimings(ctx *contextmodel.ReqContext) response.Response { + return f.svc.RouteGetMuteTimingsExport(ctx) +} diff --git a/pkg/services/ngalert/api/test-data/alertmanager_default_mutetimings-export.hcl b/pkg/services/ngalert/api/test-data/alertmanager_default_mutetimings-export.hcl new file mode 100644 index 00000000000..0b949503cf6 --- /dev/null +++ b/pkg/services/ngalert/api/test-data/alertmanager_default_mutetimings-export.hcl @@ -0,0 +1,20 @@ +resource "grafana_mute_timing" "mute_timing_1" { + name = "interval" +} +resource "grafana_mute_timing" "mute_timing_2" { + name = "full-interval" + + intervals { + + times { + start = "10:00" + end = "12:00" + } + + weekdays = ["monday", "wednesday", "friday"] + days_of_month = ["1", "14:16", "20"] + months = ["1:3", "7", "12"] + years = ["2023:2025"] + location = "America/New_York" + } +} diff --git a/pkg/services/ngalert/api/test-data/alertmanager_default_mutetimings-export.json b/pkg/services/ngalert/api/test-data/alertmanager_default_mutetimings-export.json new file mode 100644 index 00000000000..4b62851e1a6 --- /dev/null +++ b/pkg/services/ngalert/api/test-data/alertmanager_default_mutetimings-export.json @@ -0,0 +1 @@ +{"apiVersion":1,"muteTimes":[{"orgId":1,"name":"interval","time_intervals":[]},{"orgId":1,"name":"full-interval","time_intervals":[{"times":[{"start_time":"10:00","end_time":"12:00"}],"weekdays":["monday","wednesday","friday"],"days_of_month":["1","14:16","20"],"months":["1:3","7","12"],"years":["2023:2025"],"location":"America/New_York"}]}]} \ No newline at end of file diff --git a/pkg/services/ngalert/api/test-data/alertmanager_default_mutetimings-export.yaml b/pkg/services/ngalert/api/test-data/alertmanager_default_mutetimings-export.yaml new file mode 100644 index 00000000000..2abc990a44f --- /dev/null +++ b/pkg/services/ngalert/api/test-data/alertmanager_default_mutetimings-export.yaml @@ -0,0 +1,16 @@ +apiVersion: 1 +muteTimes: + - orgId: 1 + name: interval + time_intervals: [] + - orgId: 1 + name: full-interval + time_intervals: + - times: + - start_time: "10:00" + end_time: "12:00" + weekdays: [monday, wednesday, friday] + days_of_month: ["1", "14:16", "20"] + months: ["1:3", "7", "12"] + years: ['2023:2025'] + location: America/New_York diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 8d1f829aeac..da46e288d21 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -298,6 +298,12 @@ }, "type": "array" }, + "muteTimes": { + "items": { + "$ref": "#/definitions/MuteTimeIntervalExport" + }, + "type": "array" + }, "policies": { "items": { "$ref": "#/definitions/NotificationPolicyExport" @@ -1920,6 +1926,24 @@ "title": "MuteTimeInterval represents a named set of time intervals for which a route should be muted.", "type": "object" }, + "MuteTimeIntervalExport": { + "properties": { + "OrgID": { + "format": "int64", + "type": "integer" + }, + "name": { + "type": "string" + }, + "time_intervals": { + "items": { + "$ref": "#/definitions/TimeInterval" + }, + "type": "array" + } + }, + "type": "object" + }, "MuteTimings": { "items": { "$ref": "#/definitions/MuteTimeInterval" @@ -4228,7 +4252,6 @@ "type": "object" }, "alertGroups": { - "description": "AlertGroups alert groups", "items": { "$ref": "#/definitions/alertGroup" }, @@ -5365,6 +5388,45 @@ ] } }, + "/api/v1/provisioning/mute-timings/export": { + "get": { + "operationId": "RouteExportMuteTimings", + "parameters": [ + { + "default": false, + "description": "Whether to initiate a download of the file or not.", + "in": "query", + "name": "download", + "type": "boolean" + }, + { + "default": "yaml", + "description": "Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence.", + "in": "query", + "name": "format", + "type": "string" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "403": { + "description": "PermissionDenied", + "schema": { + "$ref": "#/definitions/PermissionDenied" + } + } + }, + "summary": "Export all mute timings in provisioning format.", + "tags": [ + "provisioning" + ] + } + }, "/api/v1/provisioning/mute-timings/{name}": { "delete": { "operationId": "RouteDeleteMuteTiming", @@ -5455,6 +5517,52 @@ ] } }, + "/api/v1/provisioning/mute-timings/{name}/export": { + "get": { + "operationId": "RouteExportMuteTiming", + "parameters": [ + { + "default": false, + "description": "Whether to initiate a download of the file or not.", + "in": "query", + "name": "download", + "type": "boolean" + }, + { + "default": "yaml", + "description": "Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence.", + "in": "query", + "name": "format", + "type": "string" + }, + { + "description": "Mute timing name", + "in": "path", + "name": "name", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "403": { + "description": "PermissionDenied", + "schema": { + "$ref": "#/definitions/PermissionDenied" + } + } + }, + "summary": "Export a mute timing in provisioning format.", + "tags": [ + "provisioning" + ] + } + }, "/api/v1/provisioning/policies": { "delete": { "consumes": [ diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning.go b/pkg/services/ngalert/api/tooling/definitions/provisioning.go index 0833e2c4ee3..e51cfe349c4 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning.go @@ -7,9 +7,10 @@ type AlertingFileExport struct { Groups []AlertRuleGroupExport `json:"groups,omitempty" yaml:"groups,omitempty"` ContactPoints []ContactPointExport `json:"contactPoints,omitempty" yaml:"contactPoints,omitempty"` Policies []NotificationPolicyExport `json:"policies,omitempty" yaml:"policies,omitempty"` + MuteTimings []MuteTimeIntervalExport `json:"muteTimes,omitempty" yaml:"muteTimes,omitempty"` } -// swagger:parameters RouteGetAlertRuleGroupExport RouteGetAlertRuleExport RouteGetContactpointsExport RouteGetContactpointExport RoutePostRulesGroupForExport +// swagger:parameters RouteGetAlertRuleGroupExport RouteGetAlertRuleExport RouteGetContactpointsExport RouteGetContactpointExport RoutePostRulesGroupForExport RouteExportMuteTimings RouteExportMuteTiming type ExportQueryParams struct { // Whether to initiate a download of the file or not. // in: query diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_mute_timings.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_mute_timings.go index ae61889a0d8..77a7444cc9c 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_mute_timings.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_mute_timings.go @@ -11,6 +11,14 @@ import ( // Responses: // 200: MuteTimings +// swagger:route GET /api/v1/provisioning/mute-timings/export provisioning stable RouteExportMuteTimings +// +// Export all mute timings in provisioning format. +// +// Responses: +// 200: AlertingFileExport +// 403: PermissionDenied + // swagger:route GET /api/v1/provisioning/mute-timings/{name} provisioning stable RouteGetMuteTiming // // Get a mute timing. @@ -19,6 +27,14 @@ import ( // 200: MuteTimeInterval // 404: description: Not found. +// swagger:route GET /api/v1/provisioning/mute-timings/{name}/export provisioning stable RouteExportMuteTiming +// +// Export a mute timing in provisioning format. +// +// Responses: +// 200: AlertingFileExport +// 403: PermissionDenied + // swagger:route POST /api/v1/provisioning/mute-timings provisioning stable RoutePostMuteTiming // // Create a new mute timing. @@ -53,7 +69,7 @@ import ( // swagger:model type MuteTimings []MuteTimeInterval -// swagger:parameters RouteGetTemplate RouteGetMuteTiming RoutePutMuteTiming stable RouteDeleteMuteTiming +// swagger:parameters RouteGetTemplate RouteGetMuteTiming RoutePutMuteTiming stable RouteDeleteMuteTiming RouteExportMuteTiming type RouteGetMuteTimingParam struct { // Mute timing name // in:path @@ -79,3 +95,30 @@ func (mt *MuteTimeInterval) ResourceType() string { func (mt *MuteTimeInterval) ResourceID() string { return mt.MuteTimeInterval.Name } + +type MuteTimeIntervalExport struct { + OrgID int64 `json:"orgId" yaml:"orgId"` + config.MuteTimeInterval `json:",inline" yaml:",inline"` +} + +// MuteTimeIntervalExportHcl is a representation of the MuteTimeInterval in HCL +type MuteTimeIntervalExportHcl struct { + Name string `json:"name" hcl:"name"` + TimeIntervals []TimeIntervalExportHcl `json:"time_intervals" hcl:"intervals,block"` +} + +// TimeIntervalExportHcl is a representation of the timeinterval.TimeInterval in HCL +type TimeIntervalExportHcl struct { + Times []TimeRangeExportHcl `json:"times,omitempty" hcl:"times,block"` + Weekdays *[]string `json:"weekdays,omitempty" hcl:"weekdays"` + DaysOfMonth *[]string `json:"days_of_month,omitempty" hcl:"days_of_month"` + Months *[]string `json:"months,omitempty" hcl:"months"` + Years *[]string `json:"years,omitempty" hcl:"years"` + Location *string `json:"location,omitempty" hcl:"location"` +} + +// TimeRangeExportHcl is a representation of the timeinterval.TimeRange in HCL +type TimeRangeExportHcl struct { + StartMinute string `json:"start_time" hcl:"start"` + EndMinute string `json:"end_time" hcl:"end"` +} diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 7819457c12f..8ae6df6a8f5 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -298,6 +298,12 @@ }, "type": "array" }, + "muteTimes": { + "items": { + "$ref": "#/definitions/MuteTimeIntervalExport" + }, + "type": "array" + }, "policies": { "items": { "$ref": "#/definitions/NotificationPolicyExport" @@ -1920,6 +1926,24 @@ "title": "MuteTimeInterval represents a named set of time intervals for which a route should be muted.", "type": "object" }, + "MuteTimeIntervalExport": { + "properties": { + "OrgID": { + "format": "int64", + "type": "integer" + }, + "name": { + "type": "string" + }, + "time_intervals": { + "items": { + "$ref": "#/definitions/TimeInterval" + }, + "type": "array" + } + }, + "type": "object" + }, "MuteTimings": { "items": { "$ref": "#/definitions/MuteTimeInterval" @@ -4205,6 +4229,7 @@ "type": "object" }, "alertGroup": { + "description": "AlertGroup alert group", "properties": { "alerts": { "description": "alerts", @@ -4228,7 +4253,6 @@ "type": "object" }, "alertGroups": { - "description": "AlertGroups alert groups", "items": { "$ref": "#/definitions/alertGroup" }, @@ -4333,7 +4357,6 @@ "type": "object" }, "gettableAlert": { - "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/definitions/labelSet" @@ -4396,6 +4419,7 @@ "type": "array" }, "gettableSilence": { + "description": "GettableSilence gettable silence", "properties": { "comment": { "description": "comment", @@ -4444,7 +4468,6 @@ "type": "object" }, "gettableSilences": { - "description": "GettableSilences gettable silences", "items": { "$ref": "#/definitions/gettableSilence" }, @@ -4595,6 +4618,7 @@ "type": "array" }, "postableSilence": { + "description": "PostableSilence postable silence", "properties": { "comment": { "description": "comment", @@ -7344,6 +7368,45 @@ ] } }, + "/api/v1/provisioning/mute-timings/export": { + "get": { + "operationId": "RouteExportMuteTimings", + "parameters": [ + { + "default": false, + "description": "Whether to initiate a download of the file or not.", + "in": "query", + "name": "download", + "type": "boolean" + }, + { + "default": "yaml", + "description": "Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence.", + "in": "query", + "name": "format", + "type": "string" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "403": { + "description": "PermissionDenied", + "schema": { + "$ref": "#/definitions/PermissionDenied" + } + } + }, + "summary": "Export all mute timings in provisioning format.", + "tags": [ + "provisioning" + ] + } + }, "/api/v1/provisioning/mute-timings/{name}": { "delete": { "operationId": "RouteDeleteMuteTiming", @@ -7434,6 +7497,52 @@ ] } }, + "/api/v1/provisioning/mute-timings/{name}/export": { + "get": { + "operationId": "RouteExportMuteTiming", + "parameters": [ + { + "default": false, + "description": "Whether to initiate a download of the file or not.", + "in": "query", + "name": "download", + "type": "boolean" + }, + { + "default": "yaml", + "description": "Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence.", + "in": "query", + "name": "format", + "type": "string" + }, + { + "description": "Mute timing name", + "in": "path", + "name": "name", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "403": { + "description": "PermissionDenied", + "schema": { + "$ref": "#/definitions/PermissionDenied" + } + } + }, + "summary": "Export a mute timing in provisioning format.", + "tags": [ + "provisioning" + ] + } + }, "/api/v1/provisioning/policies": { "delete": { "consumes": [ diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 8c22d4431e9..05195a1a1c5 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -2626,6 +2626,46 @@ } } }, + "/api/v1/provisioning/mute-timings/export": { + "get": { + "tags": [ + "provisioning", + "stable" + ], + "summary": "Export all mute timings in provisioning format.", + "operationId": "RouteExportMuteTimings", + "parameters": [ + { + "type": "boolean", + "default": false, + "description": "Whether to initiate a download of the file or not.", + "name": "download", + "in": "query" + }, + { + "type": "string", + "default": "yaml", + "description": "Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence.", + "name": "format", + "in": "query" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "403": { + "description": "PermissionDenied", + "schema": { + "$ref": "#/definitions/PermissionDenied" + } + } + } + } + }, "/api/v1/provisioning/mute-timings/{name}": { "get": { "tags": [ @@ -2719,6 +2759,53 @@ } } }, + "/api/v1/provisioning/mute-timings/{name}/export": { + "get": { + "tags": [ + "provisioning", + "stable" + ], + "summary": "Export a mute timing in provisioning format.", + "operationId": "RouteExportMuteTiming", + "parameters": [ + { + "type": "boolean", + "default": false, + "description": "Whether to initiate a download of the file or not.", + "name": "download", + "in": "query" + }, + { + "type": "string", + "default": "yaml", + "description": "Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence.", + "name": "format", + "in": "query" + }, + { + "type": "string", + "description": "Mute timing name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "403": { + "description": "PermissionDenied", + "schema": { + "$ref": "#/definitions/PermissionDenied" + } + } + } + } + }, "/api/v1/provisioning/policies": { "get": { "tags": [ @@ -3362,6 +3449,12 @@ "$ref": "#/definitions/AlertRuleGroupExport" } }, + "muteTimes": { + "type": "array", + "items": { + "$ref": "#/definitions/MuteTimeIntervalExport" + } + }, "policies": { "type": "array", "items": { @@ -4986,6 +5079,24 @@ } } }, + "MuteTimeIntervalExport": { + "type": "object", + "properties": { + "OrgID": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "time_intervals": { + "type": "array", + "items": { + "$ref": "#/definitions/TimeInterval" + } + } + } + }, "MuteTimings": { "type": "array", "items": { @@ -7271,6 +7382,7 @@ } }, "alertGroup": { + "description": "AlertGroup alert group", "type": "object", "required": [ "alerts", @@ -7295,7 +7407,6 @@ "$ref": "#/definitions/alertGroup" }, "alertGroups": { - "description": "AlertGroups alert groups", "type": "array", "items": { "$ref": "#/definitions/alertGroup" @@ -7401,7 +7512,6 @@ } }, "gettableAlert": { - "description": "GettableAlert gettable alert", "type": "object", "required": [ "labels", @@ -7466,6 +7576,7 @@ "$ref": "#/definitions/gettableAlerts" }, "gettableSilence": { + "description": "GettableSilence gettable silence", "type": "object", "required": [ "comment", @@ -7515,7 +7626,6 @@ "$ref": "#/definitions/gettableSilence" }, "gettableSilences": { - "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" @@ -7668,6 +7778,7 @@ } }, "postableSilence": { + "description": "PostableSilence postable silence", "type": "object", "required": [ "comment", diff --git a/public/api-merged.json b/public/api-merged.json index c7aa973609b..e646ad46456 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -3079,6 +3079,45 @@ } } }, + "/api/v1/provisioning/mute-timings/export": { + "get": { + "tags": [ + "provisioning" + ], + "summary": "Export all mute timings in provisioning format.", + "operationId": "RouteExportMuteTimings", + "parameters": [ + { + "type": "boolean", + "default": false, + "description": "Whether to initiate a download of the file or not.", + "name": "download", + "in": "query" + }, + { + "type": "string", + "default": "yaml", + "description": "Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence.", + "name": "format", + "in": "query" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "403": { + "description": "PermissionDenied", + "schema": { + "$ref": "#/definitions/PermissionDenied" + } + } + } + } + }, "/api/v1/provisioning/mute-timings/{name}": { "get": { "tags": [ @@ -3169,6 +3208,52 @@ } } }, + "/api/v1/provisioning/mute-timings/{name}/export": { + "get": { + "tags": [ + "provisioning" + ], + "summary": "Export a mute timing in provisioning format.", + "operationId": "RouteExportMuteTiming", + "parameters": [ + { + "type": "boolean", + "default": false, + "description": "Whether to initiate a download of the file or not.", + "name": "download", + "in": "query" + }, + { + "type": "string", + "default": "yaml", + "description": "Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence.", + "name": "format", + "in": "query" + }, + { + "type": "string", + "description": "Mute timing name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "403": { + "description": "PermissionDenied", + "schema": { + "$ref": "#/definitions/PermissionDenied" + } + } + } + } + }, "/api/v1/provisioning/policies": { "get": { "tags": [ @@ -11776,6 +11861,12 @@ "$ref": "#/definitions/AlertRuleGroupExport" } }, + "muteTimes": { + "type": "array", + "items": { + "$ref": "#/definitions/MuteTimeIntervalExport" + } + }, "policies": { "type": "array", "items": { @@ -15691,6 +15782,24 @@ } } }, + "MuteTimeIntervalExport": { + "type": "object", + "properties": { + "OrgID": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "time_intervals": { + "type": "array", + "items": { + "$ref": "#/definitions/TimeInterval" + } + } + } + }, "MuteTimings": { "type": "array", "items": { @@ -20559,7 +20668,6 @@ } }, "alertGroups": { - "description": "AlertGroups alert groups", "type": "array", "items": { "$ref": "#/definitions/alertGroup" diff --git a/public/openapi3.json b/public/openapi3.json index 86a0ec2a222..caa6c0b601d 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -2792,6 +2792,12 @@ }, "type": "array" }, + "muteTimes": { + "items": { + "$ref": "#/components/schemas/MuteTimeIntervalExport" + }, + "type": "array" + }, "policies": { "items": { "$ref": "#/components/schemas/NotificationPolicyExport" @@ -6709,6 +6715,24 @@ "title": "MuteTimeInterval represents a named set of time intervals for which a route should be muted.", "type": "object" }, + "MuteTimeIntervalExport": { + "properties": { + "OrgID": { + "format": "int64", + "type": "integer" + }, + "name": { + "type": "string" + }, + "time_intervals": { + "items": { + "$ref": "#/components/schemas/TimeInterval" + }, + "type": "array" + } + }, + "type": "object" + }, "MuteTimings": { "items": { "$ref": "#/components/schemas/MuteTimeInterval" @@ -11575,7 +11599,6 @@ "type": "object" }, "alertGroups": { - "description": "AlertGroups alert groups", "items": { "$ref": "#/components/schemas/alertGroup" }, @@ -15557,6 +15580,57 @@ ] } }, + "/api/v1/provisioning/mute-timings/export": { + "get": { + "operationId": "RouteExportMuteTimings", + "parameters": [ + { + "description": "Whether to initiate a download of the file or not.", + "in": "query", + "name": "download", + "schema": { + "default": false, + "type": "boolean" + } + }, + { + "description": "Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence.", + "in": "query", + "name": "format", + "schema": { + "default": "yaml", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertingFileExport" + } + } + }, + "description": "AlertingFileExport" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PermissionDenied" + } + } + }, + "description": "PermissionDenied" + } + }, + "summary": "Export all mute timings in provisioning format.", + "tags": [ + "provisioning" + ] + } + }, "/api/v1/provisioning/mute-timings/{name}": { "delete": { "operationId": "RouteDeleteMuteTiming", @@ -15665,6 +15739,66 @@ ] } }, + "/api/v1/provisioning/mute-timings/{name}/export": { + "get": { + "operationId": "RouteExportMuteTiming", + "parameters": [ + { + "description": "Whether to initiate a download of the file or not.", + "in": "query", + "name": "download", + "schema": { + "default": false, + "type": "boolean" + } + }, + { + "description": "Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence.", + "in": "query", + "name": "format", + "schema": { + "default": "yaml", + "type": "string" + } + }, + { + "description": "Mute timing name", + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertingFileExport" + } + } + }, + "description": "AlertingFileExport" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PermissionDenied" + } + } + }, + "description": "PermissionDenied" + } + }, + "summary": "Export a mute timing in provisioning format.", + "tags": [ + "provisioning" + ] + } + }, "/api/v1/provisioning/policies": { "delete": { "operationId": "RouteResetPolicyTree",