diff --git a/slo.go b/slo.go new file mode 100644 index 00000000..1ee4f03d --- /dev/null +++ b/slo.go @@ -0,0 +1,177 @@ +// Slo types lifted from github.com/grafana/slo/pkg/generated/models/slo/slo_type_gen.go +package gapi + +import ( + "bytes" + "encoding/json" + "fmt" +) + +var sloPath string = "/api/plugins/grafana-slo-app/resources/v1/slo" + +type Slos struct { + Slos []Slo `json:"slos"` +} + +const ( + QueryTypeFreeform QueryType = "freeform" + QueryTypeHistogram QueryType = "histogram" + QueryTypeRatio QueryType = "ratio" + QueryTypeThreshold QueryType = "threshold" +) + +const ( + ThresholdOperatorEmpty ThresholdOperator = "<" + ThresholdOperatorEqualEqual ThresholdOperator = "==" + ThresholdOperatorN1 ThresholdOperator = "<=" + ThresholdOperatorN2 ThresholdOperator = ">=" + ThresholdOperatorN3 ThresholdOperator = ">" +) + +type Alerting struct { + Annotations []Label `json:"annotations,omitempty"` + FastBurn *AlertingMetadata `json:"fastBurn,omitempty"` + Labels []Label `json:"labels,omitempty"` + SlowBurn *AlertingMetadata `json:"slowBurn,omitempty"` +} + +type AlertingMetadata struct { + Annotations []Label `json:"annotations,omitempty"` + Labels []Label `json:"labels,omitempty"` +} + +type DashboardRef struct { + UID string `json:"UID"` +} + +type FreeformQuery struct { + Query string `json:"query"` +} + +type HistogramQuery struct { + GroupByLabels []string `json:"groupByLabels,omitempty"` + Metric MetricDef `json:"metric"` + Percentile float64 `json:"percentile"` + Threshold Threshold `json:"threshold"` +} + +type Label struct { + Key string `json:"key"` + Value string `json:"value"` +} + +type MetricDef struct { + PrometheusMetric string `json:"prometheusMetric"` + Type *string `json:"type,omitempty"` +} + +type Objective struct { + Value float64 `json:"value"` + Window string `json:"window"` +} + +type Query struct { + Freeform *FreeformQuery `json:"freeform,omitempty"` + Histogram *HistogramQuery `json:"histogram,omitempty"` + Ratio *RatioQuery `json:"ratio,omitempty"` + Threshold *ThresholdQuery `json:"threshold,omitempty"` + Type QueryType `json:"type"` +} + +type QueryType string + +type RatioQuery struct { + GroupByLabels []string `json:"groupByLabels,omitempty"` + SuccessMetric MetricDef `json:"successMetric"` + TotalMetric MetricDef `json:"totalMetric"` +} + +type Slo struct { + Alerting *Alerting `json:"alerting,omitempty"` + Description string `json:"description"` + DrillDownDashboardRef *DashboardRef `json:"drillDownDashboardRef,omitempty"` + Labels []Label `json:"labels,omitempty"` + Name string `json:"name"` + Objectives []Objective `json:"objectives"` + Query Query `json:"query"` + UUID string `json:"uuid"` +} + +type Threshold struct { + Operator ThresholdOperator `json:"operator"` + Value float64 `json:"value"` +} + +type ThresholdOperator string + +type ThresholdQuery struct { + GroupByLabels []string `json:"groupByLabels,omitempty"` + Metric MetricDef `json:"metric"` + Threshold Threshold `json:"threshold"` +} + +type CreateSLOResponse struct { + Message string `json:"message,omitempty"` + UUID string `json:"uuid,omitempty"` +} + +// ListSlos retrieves a list of all Slos +func (c *Client) ListSlos() (Slos, error) { + var slos Slos + + if err := c.request("GET", sloPath, nil, nil, &slos); err != nil { + return Slos{}, err + } + + return slos, nil +} + +// GetSLO returns a single Slo based on its uuid +func (c *Client) GetSlo(uuid string) (Slo, error) { + var slo Slo + path := fmt.Sprintf("%s/%s", sloPath, uuid) + + if err := c.request("GET", path, nil, nil, &slo); err != nil { + return Slo{}, err + } + + return slo, nil +} + +// CreateSLO creates a single Slo +func (c *Client) CreateSlo(slo Slo) (CreateSLOResponse, error) { + response := CreateSLOResponse{} + + data, err := json.Marshal(slo) + if err != nil { + return response, err + } + + if err := c.request("POST", sloPath, nil, bytes.NewBuffer(data), &response); err != nil { + return CreateSLOResponse{}, err + } + + return response, err +} + +// DeleteSLO deletes the Slo with the passed in UUID +func (c *Client) DeleteSlo(uuid string) error { + path := fmt.Sprintf("%s/%s", sloPath, uuid) + return c.request("DELETE", path, nil, nil, nil) +} + +// UpdateSLO updates the Slo with the passed in UUID and Slo +func (c *Client) UpdateSlo(uuid string, slo Slo) error { + path := fmt.Sprintf("%s/%s", sloPath, uuid) + + data, err := json.Marshal(slo) + if err != nil { + return err + } + + if err := c.request("PUT", path, nil, bytes.NewBuffer(data), nil); err != nil { + return err + } + + return nil +} diff --git a/slo_test.go b/slo_test.go new file mode 100644 index 00000000..9c1df685 --- /dev/null +++ b/slo_test.go @@ -0,0 +1,180 @@ +package gapi + +import ( + "testing" + + "github.com/gobs/pretty" +) + +func TestSLOs(t *testing.T) { + t.Run("list all SLOs succeeds", func(t *testing.T) { + client := gapiTestTools(t, 200, getSlosJSON) + + resp, err := client.ListSlos() + + slos := resp.Slos + + if err != nil { + t.Error(err) + } + t.Log(pretty.PrettyFormat(slos)) + if len(slos) != 1 { + t.Errorf("wrong number of contact points returned, got %d", len(slos)) + } + if slos[0].Name != "list-slos" { + t.Errorf("incorrect name - expected Name-Test, got %s", slos[0].Name) + } + }) + + t.Run("get individual SLO succeeds", func(t *testing.T) { + client := gapiTestTools(t, 200, getSloJSON) + + slo, err := client.GetSlo("qkkrknp12w6tmsdcrfkdf") + + t.Log(pretty.PrettyFormat(slo)) + if err != nil { + t.Error(err) + } + if slo.UUID != "qkkrknp12w6tmsdcrfkdf" { + t.Errorf("incorrect UID - expected qkkrknp12w6tmsdcrfkdf, got %s", slo.UUID) + } + }) + + t.Run("get non-existent SLOs fails", func(t *testing.T) { + client := gapiTestTools(t, 404, getSlosJSON) + + slo, err := client.GetSlo("qkkrknp12w6tmsdcrfkdf") + + if err == nil { + t.Log(pretty.PrettyFormat(slo)) + t.Error("expected error but got nil") + } + }) + + t.Run("create SLO succeeds", func(t *testing.T) { + client := gapiTestTools(t, 201, createSloJSON) + slo := generateSlo() + + resp, err := client.CreateSlo(slo) + + if err != nil { + t.Error(err) + } + if resp.UUID != "sjnp8wobcbs3eit28n8yb" { + t.Errorf("unexpected UID returned, got %s", resp.UUID) + } + }) + + t.Run("update SLO succeeds", func(t *testing.T) { + client := gapiTestTools(t, 200, createSloJSON) + slo := generateSlo() + slo.Description = "Updated Description" + + err := client.UpdateSlo(slo.UUID, slo) + + if err != nil { + t.Error(err) + } + }) + + t.Run("delete SLO succeeds", func(t *testing.T) { + client := gapiTestTools(t, 204, "") + + err := client.DeleteSlo("qkkrknp12w6tmsdcrfkdf") + + if err != nil { + t.Log(err) + t.Error(err) + } + }) +} + +const getSlosJSON = ` +{ + "slos": [ + { + "uuid": "qkkrknp12w6tmsdcrfkdf", + "name": "list-slos", + "description": "list-slos-description", + "query": { + "freeform": { + "query": "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))" + }, + "type": "freeform" + }, + "objectives": [ + { + "value": 0.995, + "window": "28d" + } + ], + "drillDownDashboardRef": { + "uid": "5IkqX6P4k" + } + } + ] +}` + +const getSloJSON = ` +{ + "uuid": "qkkrknp12w6tmsdcrfkdf", + "name": "Name-Test", + "description": "Description-Test", + "query": { + "freeform": { + "query": "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))" + }, + "type": "freeform" + }, + "objectives": [ + { + "value": 0.995, + "window": "28d" + } + ], + "drillDownDashboardRef": { + "uid": "5IkqX6P4k" + } +}` + +const createSloJSON = ` +{ + "uuid": "sjnp8wobcbs3eit28n8yb", + "name": "test-name", + "description": "test-description", + "query": { + "freeform": { + "query": "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))" + }, + "type": "freeform" + }, + "objectives": [ + { + "value": 0.995, + "window": "30d" + } + ], + "drillDownDashboardRef": { + "uid": "zz5giRyVk" + } +} +` + +func generateSlo() Slo { + objective := []Objective{{Value: 0.995, Window: "30d"}} + query := Query{ + Freeform: &FreeformQuery{ + Query: "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))", + }, + Type: QueryTypeFreeform, + } + + slo := Slo{ + Name: "test-name", + Description: "test-description", + Objectives: objective, + Query: query, + } + + return slo +}