Skip to content
This repository has been archived by the owner on Jan 15, 2024. It is now read-only.

SLO: CRUD API Functionality #145

Merged
merged 15 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions slo.go
Original file line number Diff line number Diff line change
@@ -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
}
180 changes: 180 additions & 0 deletions slo_test.go
Original file line number Diff line number Diff line change
@@ -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
}