diff --git a/go.mod b/go.mod index 726f0a1ba74..da28679ae58 100644 --- a/go.mod +++ b/go.mod @@ -68,6 +68,7 @@ require ( github.com/hashicorp/go-hclog v1.5.0 // @grafana/plugins-platform-backend github.com/hashicorp/go-plugin v1.4.9 // @grafana/plugins-platform-backend github.com/hashicorp/go-version v1.6.0 // @grafana/backend-platform + github.com/hashicorp/hcl/v2 v2.17.0 // @grafana/alerting-squad-backend github.com/influxdata/influxdb-client-go/v2 v2.12.3 // @grafana/observability-metrics github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 // @grafana/grafana-app-platform-squad github.com/jmespath/go-jmespath v0.4.0 // @grafana/backend-platform @@ -290,10 +291,12 @@ require ( github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect + github.com/agext/levenshtein v1.2.1 // indirect github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect github.com/apache/thrift v0.18.1 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/bmatcuk/doublestar v1.1.1 // indirect github.com/buildkite/yaml v2.1.0+incompatible // indirect @@ -391,6 +394,7 @@ require ( github.com/weaveworks/promrus v1.2.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/yuin/gopher-lua v1.1.0 // indirect + github.com/zclconf/go-cty v1.13.0 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.etcd.io/etcd/api/v3 v3.5.7 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.7 // indirect diff --git a/go.sum b/go.sum index 207ec645a83..4a0c2dcd2d1 100644 --- a/go.sum +++ b/go.sum @@ -672,6 +672,8 @@ github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f/go.mod h1:f3H github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= @@ -711,6 +713,8 @@ github.com/apache/thrift v0.18.1 h1:lNhK/1nqjbwbiOPDBPFJVKxgDEGSepKuTh6OLiXW8kg= github.com/apache/thrift v0.18.1/go.mod h1:rdQn/dCcDKEWjjylUeueum4vQEjG2v8v2PqriUnbr+I= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -1904,6 +1908,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.2 h1:Dwmkdr5Nc/oBiXgJS3CDHNhJtIHkuZ3DZF5 github.com/hashicorp/golang-lru/v2 v2.0.2/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY= +github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= @@ -2906,6 +2912,8 @@ github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE= github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= +github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 11e353dbaf0..73b45156f18 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/ngalert/api/hcl" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" alerting_models "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" @@ -498,7 +499,7 @@ func extractExportRequest(c *contextmodel.ReqContext) definitions.ExportQueryPar } queryFormat := c.Query("format") - if queryFormat == "yaml" || queryFormat == "json" { + if queryFormat == "yaml" || queryFormat == "json" || queryFormat == "hcl" { format = queryFormat } @@ -512,6 +513,10 @@ func extractExportRequest(c *contextmodel.ReqContext) definitions.ExportQueryPar func exportResponse(c *contextmodel.ReqContext, body definitions.AlertingFileExport) response.Response { params := extractExportRequest(c) + if params.Format == "hcl" { + return exportHcl(params.Download, body) + } + if params.Download { r := response.JSONDownload if params.Format == "yaml" { @@ -526,3 +531,43 @@ func exportResponse(c *contextmodel.ReqContext, body definitions.AlertingFileExp } return r(http.StatusOK, body) } + +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, + }) + } + + // TODO implement support. + // for idx, cp := range ex.ContactPoints { + // resources = append(resources, resourceBlock{ + // Type: "grafana_contact_point", + // Name: fmt.Sprintf("contact_point_%d", idx), + // Body: &cp, + // }) + // } + // for idx, cp := range ex.Policies { + // resources = append(resources, resourceBlock{ + // Type: "grafana_notification_policy", + // Name: fmt.Sprintf("notification_policy_%d", idx), + // Body: &cp, + // }) + // + + hclBody, err := hcl.Encode(resources...) + if err != nil { + return response.Error(500, "body hcl encode", err) + } + resp := response.Respond(http.StatusOK, hclBody) + if download { + return resp. + SetHeader("Content-Type", "application/terraform+hcl"). + SetHeader("Content-Disposition", `attachment;filename=export.tf`) + } + return resp.SetHeader("Content-Type", "text/hcl") +} diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index 245e80ba0f6..467fbc9340d 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -566,6 +566,106 @@ func TestProvisioningApi(t *testing.T) { require.Equal(t, 200, response.Status()) require.Equal(t, expectedResponse, string(response.Body())) }) + + t.Run("hcl body content is as expected", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rule1 := createTestAlertRule("rule1", 1) + rule1.Labels = map[string]string{ + "test": "label", + } + rule1.Annotations = map[string]string{ + "test": "annotation", + } + rule1.NoDataState = definitions.Alerting + rule1.ExecErrState = definitions.ErrorErrState + insertRule(t, sut, rule1) + insertRule(t, sut, createTestAlertRule("rule2", 1)) + + expectedResponse := `resource "grafana_rule_group" "rule_group_0000" { + org_id = 1 + name = "my-cool-group" + folder_uid = "folder-uid" + interval_seconds = 60 + + rule { + name = "rule1" + condition = "A" + + data { + ref_id = "A" + query_type = "" + + relative_time_range { + from = 0 + to = 0 + } + + datasource_uid = "" + model = "{\"conditions\":[{\"evaluator\":{\"params\":[3],\"type\":\"gt\"},\"operator\":{\"type\":\"and\"},\"query\":{\"params\":[\"A\"]},\"reducer\":{\"type\":\"last\"},\"type\":\"query\"}],\"datasource\":{\"type\":\"__expr__\",\"uid\":\"__expr__\"},\"expression\":\"1==0\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}" + } + + no_data_state = "Alerting" + exec_err_state = "Error" + for = 0 + annotations = { + test = "annotation" + } + labels = { + test = "label" + } + is_paused = false + } + rule { + name = "rule2" + condition = "A" + + data { + ref_id = "A" + query_type = "" + + relative_time_range { + from = 0 + to = 0 + } + + datasource_uid = "" + model = "{\"conditions\":[{\"evaluator\":{\"params\":[3],\"type\":\"gt\"},\"operator\":{\"type\":\"and\"},\"query\":{\"params\":[\"A\"]},\"reducer\":{\"type\":\"last\"},\"type\":\"query\"}],\"datasource\":{\"type\":\"__expr__\",\"uid\":\"__expr__\"},\"expression\":\"1==0\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}" + } + + no_data_state = "OK" + exec_err_state = "OK" + for = 0 + annotations = null + labels = null + is_paused = false + } +} +` + rc := createTestRequestCtx() + rc.Context.Req.Form.Set("format", "hcl") + rc.Context.Req.Form.Set("download", "false") + + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedResponse, string(response.Body())) + require.Equal(t, "text/hcl", rc.Resp.Header().Get("Content-Type")) + + t.Run("and add specific headers if download=true", func(t *testing.T) { + rc := createTestRequestCtx() + rc.Context.Req.Form.Set("format", "hcl") + rc.Context.Req.Form.Set("download", "true") + + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedResponse, string(response.Body())) + require.Equal(t, "application/terraform+hcl", rc.Resp.Header().Get("Content-Type")) + require.Equal(t, `attachment;filename=export.tf`, rc.Resp.Header().Get("Content-Disposition")) + }) + }) }) t.Run("alert rule", func(t *testing.T) { diff --git a/pkg/services/ngalert/api/compat.go b/pkg/services/ngalert/api/compat.go index 697e693346b..f1938cba94c 100644 --- a/pkg/services/ngalert/api/compat.go +++ b/pkg/services/ngalert/api/compat.go @@ -151,11 +151,13 @@ func AlertRuleGroupExportFromAlertRuleGroupWithFolderTitle(d models.AlertRuleGro rules = append(rules, alert) } return definitions.AlertRuleGroupExport{ - OrgID: d.OrgID, - Name: d.Title, - Folder: d.FolderTitle, - Interval: model.Duration(time.Duration(d.Interval) * time.Second), - Rules: rules, + OrgID: d.OrgID, + Name: d.Title, + Folder: d.FolderTitle, + FolderUID: d.FolderUID, + Interval: model.Duration(time.Duration(d.Interval) * time.Second), + IntervalSeconds: d.Interval, + Rules: rules, }, nil } @@ -184,6 +186,7 @@ func AlertRuleExportFromAlertRule(rule models.AlertRule) (definitions.AlertRuleE UID: rule.UID, Title: rule.Title, For: model.Duration(rule.For), + ForSeconds: int64(rule.For.Seconds()), Condition: rule.Condition, Data: data, DashboardUID: dashboardUID, @@ -207,12 +210,13 @@ func AlertQueryExportFromAlertQuery(query models.AlertQuery) (definitions.AlertQ return definitions.AlertQueryExport{ RefID: query.RefID, QueryType: query.QueryType, - RelativeTimeRange: definitions.RelativeTimeRange{ - From: definitions.Duration(query.RelativeTimeRange.From), - To: definitions.Duration(query.RelativeTimeRange.To), + RelativeTimeRange: definitions.RelativeTimeRangeExport{ + FromSeconds: int64(time.Duration(query.RelativeTimeRange.From).Seconds()), + ToSeconds: int64(time.Duration(query.RelativeTimeRange.To).Seconds()), }, DatasourceUID: query.DatasourceUID, Model: mdl, + ModelString: string(query.Model), }, nil } diff --git a/pkg/services/ngalert/api/hcl/hcl.go b/pkg/services/ngalert/api/hcl/hcl.go new file mode 100644 index 00000000000..fb4f1c35012 --- /dev/null +++ b/pkg/services/ngalert/api/hcl/hcl.go @@ -0,0 +1,30 @@ +package hcl + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/hashicorp/hcl/v2/hclwrite" +) + +type Resource struct { + Type string `hcl:"type,label"` + Name string `hcl:"name,label"` + Body interface{} `hcl:",block"` +} + +func Encode(resources ...Resource) (data []byte, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("failed to encode struct to HCL: %v", r) + } + }() + f := hclwrite.NewEmptyFile() + + for _, resource := range resources { + blk := gohcl.EncodeAsBlock(resource.Body, "resource") + blk.SetLabels([]string{resource.Type, resource.Name}) + f.Body().AppendBlock(blk) + } + return f.Bytes(), nil +} diff --git a/pkg/services/ngalert/api/hcl/hcl_test.go b/pkg/services/ngalert/api/hcl/hcl_test.go new file mode 100644 index 00000000000..b1832cf880c --- /dev/null +++ b/pkg/services/ngalert/api/hcl/hcl_test.go @@ -0,0 +1,75 @@ +package hcl + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEncode(t *testing.T) { + type data struct { + Name string `hcl:"name"` + Number float64 `hcl:"number"` + NumberRef *float64 `hcl:"numberRef"` + Bool bool `hcl:"bul"` + BoolRef *bool `hcl:"bulRef"` + Ignored string + Blocks []data `hcl:"blocks,block"` + SubData *data `hcl:"sub,block"` + } + + encoded, err := Encode(Resource{ + Type: "grafana_test", + Name: "test-01", + Body: &data{ + Name: "test", + Number: 123, + NumberRef: func(f float64) *float64 { return &f }(1333), + Bool: false, + BoolRef: func(f bool) *bool { return &f }(true), + Ignored: "Ignore me", + Blocks: []data{ + { + Name: "el-0", + Number: 1, + }, + { + Name: "el-1", + Number: 2, + Bool: true, + }, + }, + SubData: &data{ + Name: "sub-data", + Number: 123123, + }, + }, + }) + require.NoError(t, err) + t.Log(string(encoded)) + require.Equal(t, `resource "grafana_test" "test-01" { + name = "test" + number = 123 + numberRef = 1333 + bul = false + bulRef = true + + blocks { + name = "el-0" + number = 1 + bul = false + } + blocks { + name = "el-1" + number = 2 + bul = true + } + + sub { + name = "sub-data" + number = 123123 + bul = false + } +} +`, string(encoded)) +} diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go index efcf09d233b..e34e6d32134 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go @@ -218,34 +218,43 @@ type AlertRuleGroup struct { // AlertRuleGroupExport is the provisioned file export of AlertRuleGroupV1. type AlertRuleGroupExport struct { - OrgID int64 `json:"orgId" yaml:"orgId"` - Name string `json:"name" yaml:"name"` - Folder string `json:"folder" yaml:"folder"` - Interval model.Duration `json:"interval" yaml:"interval"` - Rules []AlertRuleExport `json:"rules" yaml:"rules"` + OrgID int64 `json:"orgId" yaml:"orgId" hcl:"org_id"` + Name string `json:"name" yaml:"name" hcl:"name"` + Folder string `json:"folder" yaml:"folder"` + FolderUID string `json:"-" yaml:"-" hcl:"folder_uid"` + Interval model.Duration `json:"interval" yaml:"interval"` + IntervalSeconds int64 `json:"-" yaml:"-" hcl:"interval_seconds"` + Rules []AlertRuleExport `json:"rules" yaml:"rules" hcl:"rule,block"` } // AlertRuleExport is the provisioned file export of models.AlertRule. type AlertRuleExport struct { UID string `json:"uid" yaml:"uid"` - Title string `json:"title" yaml:"title"` - Condition string `json:"condition" yaml:"condition"` - Data []AlertQueryExport `json:"data" yaml:"data"` + Title string `json:"title" yaml:"title" hcl:"name"` + Condition string `json:"condition" yaml:"condition" hcl:"condition"` + Data []AlertQueryExport `json:"data" yaml:"data" hcl:"data,block"` DashboardUID string `json:"dasboardUid,omitempty" yaml:"dashboardUid,omitempty"` PanelID int64 `json:"panelId,omitempty" yaml:"panelId,omitempty"` - NoDataState NoDataState `json:"noDataState" yaml:"noDataState"` - ExecErrState ExecutionErrorState `json:"execErrState" yaml:"execErrState"` + NoDataState NoDataState `json:"noDataState" yaml:"noDataState" hcl:"no_data_state"` + ExecErrState ExecutionErrorState `json:"execErrState" yaml:"execErrState" hcl:"exec_err_state"` For model.Duration `json:"for" yaml:"for"` - Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"` - Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` - IsPaused bool `json:"isPaused" yaml:"isPaused"` + ForSeconds int64 `json:"-" yaml:"-" hcl:"for"` + Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty" hcl:"annotations"` + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty" hcl:"labels"` + IsPaused bool `json:"isPaused" yaml:"isPaused" hcl:"is_paused"` } // AlertQueryExport is the provisioned export of models.AlertQuery. type AlertQueryExport struct { - RefID string `json:"refId" yaml:"refId"` - QueryType string `json:"queryType,omitempty" yaml:"queryType,omitempty"` - RelativeTimeRange RelativeTimeRange `json:"relativeTimeRange,omitempty" yaml:"relativeTimeRange,omitempty"` - DatasourceUID string `json:"datasourceUid" yaml:"datasourceUid"` - Model map[string]any `json:"model" yaml:"model"` + RefID string `json:"refId" yaml:"refId" hcl:"ref_id"` + QueryType string `json:"queryType,omitempty" yaml:"queryType,omitempty" hcl:"query_type"` + RelativeTimeRange RelativeTimeRangeExport `json:"relativeTimeRange,omitempty" yaml:"relativeTimeRange,omitempty" hcl:"relative_time_range,block"` + DatasourceUID string `json:"datasourceUid" yaml:"datasourceUid" hcl:"datasource_uid"` + Model map[string]any `json:"model" yaml:"model"` + ModelString string `json:"-" yaml:"-" hcl:"model"` +} + +type RelativeTimeRangeExport struct { + FromSeconds int64 `json:"from" yaml:"from" hcl:"from"` + ToSeconds int64 `json:"to" yaml:"to" hcl:"to"` } diff --git a/public/app/features/alerting/unified/api/alertRuleApi.ts b/public/app/features/alerting/unified/api/alertRuleApi.ts index b7a5cc98467..cd0983c7eb6 100644 --- a/public/app/features/alerting/unified/api/alertRuleApi.ts +++ b/public/app/features/alerting/unified/api/alertRuleApi.ts @@ -40,7 +40,7 @@ export interface Datasource { export const PREVIEW_URL = '/api/v1/rule/test/grafana'; export const PROM_RULES_URL = 'api/prometheus/grafana/api/v1/rules'; -function getProvisioningUrl(ruleUid: string, format: 'yaml' | 'json' = 'yaml') { +function getProvisioningUrl(ruleUid: string, format: RuleExportFormats = 'yaml') { return `/api/v1/provisioning/alert-rules/${ruleUid}/export?format=${format}`; } diff --git a/public/app/features/alerting/unified/components/export/providers.ts b/public/app/features/alerting/unified/components/export/providers.ts index 72aadc9a9dc..48b7f7ec505 100644 --- a/public/app/features/alerting/unified/components/export/providers.ts +++ b/public/app/features/alerting/unified/components/export/providers.ts @@ -21,16 +21,15 @@ const YamlRuleExportProvider: RuleExportProvider<'yaml'> = { exportFormat: 'yaml', }; -// TODO Waiting for BE changes -// const HclRuleExportProvider: RuleExportProvider<'hcl'> = { -// name: 'HCL', -// exportFormat: 'hcl', -// }; +const HclRuleExportProvider: RuleExportProvider<'hcl'> = { + name: 'Terraform (HCL)', + exportFormat: 'hcl', +}; export const grafanaRuleExportProviders = { [JsonRuleExportProvider.exportFormat]: JsonRuleExportProvider, [YamlRuleExportProvider.exportFormat]: YamlRuleExportProvider, - // [HclRuleExportProvider.exportFormat]: HclRuleExportProvider, + [HclRuleExportProvider.exportFormat]: HclRuleExportProvider, } as const; export type RuleExportFormats = keyof typeof grafanaRuleExportProviders; diff --git a/public/app/features/alerting/unified/components/rule-editor/GrafanaRuleInspector.tsx b/public/app/features/alerting/unified/components/rule-editor/GrafanaRuleInspector.tsx new file mode 100644 index 00000000000..e69de29bb2d